package errors

import (
	"errors"
	"fmt"
	"net/http"
	"sort"
	"strings"

	appErrors "github.com/nexuscrm/shared/pkg/errors"
)

// AppError is the base interface for all application errors
type AppError interface {
	error
	HTTPStatus() int
	Code() string
}

// NotFoundError represents a resource that was not found
type NotFoundError struct {
	Resource string
	ID       string
}

func (e *NotFoundError) Error() string {
	if e.ID != "" {
		return fmt.Sprintf("%s with ID '%s' not found", e.Resource, e.ID)
	}
	return fmt.Sprintf("%s not found", e.Resource)
}

func (e *NotFoundError) HTTPStatus() int {
	return http.StatusNotFound
}

func (e *NotFoundError) Code() string {
	return string(appErrors.ErrNotFound)
}

// NewNotFoundError creates a new NotFoundError
func NewNotFoundError(resource, id string) *NotFoundError {
	return &NotFoundError{Resource: resource, ID: id}
}

// ValidationError represents invalid input
type ValidationError struct {
	Field   string
	Message string
	Value   interface{}
}

func (e *ValidationError) Error() string {
	if e.Field != "" {
		return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
	}
	return fmt.Sprintf("validation error: %s", e.Message)
}

func (e *ValidationError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *ValidationError) Code() string {
	return string(appErrors.ErrValidation)
}

// NewValidationError creates a new ValidationError
func NewValidationError(field, message string) *ValidationError {
	return &ValidationError{Field: field, Message: message}
}

// FLSDeniedError represents a write-path rejection caused by one or more
// fields the caller may not create/update — in practice, **user-level FLS
// Edit denial** and a handful of **explicit immutable-field rejections**
// (e.g. CreatedDate on update). Intrinsically non-writable fields
// (formula, auto-number, system-managed) are **silently dropped** by the
// current write paths and are NOT surfaced as FLSDeniedError — callers
// should not rely on this type to detect intrinsic-capability failures.
//
// Salesforce returns this as HTTP 400 with errorCode
// INVALID_FIELD_FOR_INSERT_UPDATE and a fields array — the handler layer
// unwraps this type via errors.As and emits the SF shape.
type FLSDeniedError struct {
	Fields  []string
	Message string
}

func (e *FLSDeniedError) Error() string { return e.Message }

func (e *FLSDeniedError) HTTPStatus() int { return http.StatusBadRequest }

func (e *FLSDeniedError) Code() string {
	return string(appErrors.ErrValidation)
}

// NewFLSDeniedError creates a new FLSDeniedError. The message SHOULD match
// SF's verbatim response so clients that parse it continue to work.
func NewFLSDeniedError(fields []string, message string) *FLSDeniedError {
	return &FLSDeniedError{Fields: fields, Message: message}
}

// NewFLSDeniedErrorForFields builds the Salesforce-verbatim FLS response
// (errorCode INVALID_FIELD_FOR_INSERT_UPDATE) for one or more denied
// fields. SF v63.0 lists ALL denied fields in a single error per record,
// comma-separated in the message and enumerated in the fields array —
// verified against sandbox for both PATCH and POST. Callers should
// accumulate every denied field in one pass and invoke this helper once,
// rather than failing on the first denial.
//
// The caller's slice is NOT mutated — we sort a copy so the `fields`
// array and message are deterministic across runs. Callers typically
// populate `fields` by ranging a Go map, which has non-deterministic
// iteration order; without the sort, the parity claim
// ("byte-for-byte matches SF") would vary across invocations, making
// tests flaky and client-side caching inconsistent.
func NewFLSDeniedErrorForFields(fields []string) *FLSDeniedError {
	if len(fields) == 0 {
		return nil
	}
	// Copy to avoid mutating caller's slice, then sort for determinism.
	sorted := append([]string(nil), fields...)
	sort.Strings(sorted)
	return &FLSDeniedError{
		Fields: sorted,
		Message: fmt.Sprintf(
			"Unable to create/update fields: %s. Please check the security settings of this field and verify that it is read/write for your profile or permission set.",
			strings.Join(sorted, ", "),
		),
	}
}

// PermissionError represents insufficient permissions
type PermissionError struct {
	Action   string
	Resource string
	UserID   string
}

func (e *PermissionError) Error() string {
	return fmt.Sprintf("permission denied: cannot %s %s", e.Action, e.Resource)
}

func (e *PermissionError) HTTPStatus() int {
	return http.StatusForbidden
}

func (e *PermissionError) Code() string {
	return string(appErrors.ErrPermissionDenied)
}

// NewPermissionError creates a new PermissionError
func NewPermissionError(action, resource string) *PermissionError {
	return &PermissionError{Action: action, Resource: resource}
}

// UnauthorizedError represents authentication failures
type UnauthorizedError struct {
	Reason string
}

func (e *UnauthorizedError) Error() string {
	if e.Reason != "" {
		return fmt.Sprintf("unauthorized: %s", e.Reason)
	}
	return "unauthorized"
}

func (e *UnauthorizedError) HTTPStatus() int {
	return http.StatusUnauthorized
}

func (e *UnauthorizedError) Code() string {
	return string(appErrors.ErrUnauthorized)
}

// NewUnauthorizedError creates a new UnauthorizedError
func NewUnauthorizedError(reason string) *UnauthorizedError {
	return &UnauthorizedError{Reason: reason}
}

// ConflictError represents a conflict with existing data
type ConflictError struct {
	Resource string
	Field    string
	Value    string
}

func (e *ConflictError) Error() string {
	if e.Field != "" && e.Value != "" {
		return fmt.Sprintf("%s already exists with %s='%s'", e.Resource, e.Field, e.Value)
	}
	return fmt.Sprintf("%s already exists", e.Resource)
}

func (e *ConflictError) HTTPStatus() int {
	return http.StatusConflict
}

func (e *ConflictError) Code() string {
	return string(appErrors.ErrConflict)
}

// NewConflictError creates a new ConflictError
func NewConflictError(resource, field, value string) *ConflictError {
	return &ConflictError{Resource: resource, Field: field, Value: value}
}

// ForbiddenError represents a generic 403 Forbidden error
type ForbiddenError struct {
	Message string
}

func (e *ForbiddenError) Error() string {
	return e.Message
}

func (e *ForbiddenError) HTTPStatus() int {
	return http.StatusForbidden
}

func (e *ForbiddenError) Code() string {
	return string(appErrors.ErrForbidden)
}

// NewForbiddenError creates a new ForbiddenError
func NewForbiddenError(message string) *ForbiddenError {
	return &ForbiddenError{Message: message}
}

// ColumnNotFoundError represents an unknown column in a SQL query (MySQL 1054)
type ColumnNotFoundError struct {
	ColumnName string
}

func (e *ColumnNotFoundError) Error() string {
	return fmt.Sprintf("unknown column: %s", e.ColumnName)
}

func (e *ColumnNotFoundError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e *ColumnNotFoundError) Code() string {
	return string(appErrors.ErrInvalidQuery)
}

// NewColumnNotFoundError creates a new ColumnNotFoundError
func NewColumnNotFoundError(columnName string) *ColumnNotFoundError {
	return &ColumnNotFoundError{ColumnName: columnName}
}

// IsColumnNotFound checks if an error is a ColumnNotFoundError
func IsColumnNotFound(err error) bool {
	var colErr *ColumnNotFoundError
	return errors.As(err, &colErr)
}

// InternalError represents unexpected server errors
type InternalError struct {
	Message string
	Cause   error
}

func (e *InternalError) Error() string {
	if e.Cause != nil {
		return fmt.Sprintf("internal error: %s (caused by: %v)", e.Message, e.Cause)
	}
	return fmt.Sprintf("internal error: %s", e.Message)
}

func (e *InternalError) HTTPStatus() int {
	return http.StatusInternalServerError
}

func (e *InternalError) Code() string {
	return string(appErrors.ErrInternalServerError)
}

func (e *InternalError) Unwrap() error {
	return e.Cause
}

// NewInternalError creates a new InternalError
func NewInternalError(message string, cause error) *InternalError {
	return &InternalError{Message: message, Cause: cause}
}

// Helper functions for error checking

// IsNotFound checks if an error is a NotFoundError
func IsNotFound(err error) bool {
	var notFound *NotFoundError
	return errors.As(err, &notFound)
}

// IsValidation checks if an error is a ValidationError
func IsValidation(err error) bool {
	var validation *ValidationError
	return errors.As(err, &validation)
}

// AsFLSDenied unwraps err into *FLSDeniedError. Callers use this instead of
// importing stdlib errors for errors.As, keeping the write-path abstraction
// local to this package.
func AsFLSDenied(err error) (*FLSDeniedError, bool) {
	var flsErr *FLSDeniedError
	if errors.As(err, &flsErr) {
		return flsErr, true
	}
	return nil, false
}

// IsPermission checks if an error is a PermissionError
func IsPermission(err error) bool {
	var permission *PermissionError
	return errors.As(err, &permission)
}

// IsConflict checks if an error is a ConflictError
func IsConflict(err error) bool {
	var conflict *ConflictError
	return errors.As(err, &conflict)
}

// IsInternal checks if an error is an InternalError (or wraps one).
//
// Callers at system boundaries (REST handlers, bulk orchestrators) MUST
// check this FIRST before classifying an error as a 4xx business failure.
// The transaction manager wraps rollback failures as InternalError(cause=businessErr);
// without this guard, errors.As would unwrap through the InternalError into
// the business cause and misreport a genuine infra failure as a user-facing
// 4xx. Infra problems must surface as 500 so operators see the real signal.
//
// Also used by bulk/allOrNone flows to distinguish true infrastructure
// failures (tx begin/commit/rollback, IO) from business rollbacks, so
// that per-record "rolled back" responses don't mask a dead database.
func IsInternal(err error) bool {
	var internal *InternalError
	return errors.As(err, &internal)
}

// GetHTTPStatus returns the HTTP status code for an error
// Returns 500 if the error doesn't implement AppError
func GetHTTPStatus(err error) int {
	var appErr AppError
	if errors.As(err, &appErr) {
		return appErr.HTTPStatus()
	}
	return http.StatusInternalServerError
}

// GetErrorCode returns the error code for an error
// Returns "UNKNOWN_ERROR" if the error doesn't implement AppError
func GetErrorCode(err error) string {
	var appErr AppError
	if errors.As(err, &appErr) {
		return appErr.Code()
	}
	// Use explicit SERVICE_UNAVAILABLE or similar for unknown system types,
	// or stick to INTERNAL_SERVER_ERROR.
	// Let's use a generic 'INTERNAL_SERVER_ERROR' as default in SSoT spirit.
	return string(appErrors.ErrInternalServerError)
}

// ErrorResponse represents a standardized error response
type ErrorResponse struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	Details   any    `json:"details,omitempty"`
	RequestID string `json:"request_id,omitempty"`
}

// ToResponseWithID converts an error to an ErrorResponse with a request ID
func ToResponseWithID(err error, requestID string) ErrorResponse {
	return ErrorResponse{
		Code:      GetErrorCode(err),
		Message:   err.Error(),
		RequestID: requestID,
	}
}
