package errs

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

	"gateway/internal/library/errors/code"
)

// Error is a structured application error with an 8-digit code: SSCCCDDD.
type Error struct {
	scope       code.Scope
	category    code.Category
	detail      code.Detail
	msg         string
	internalErr error
}

// New constructs an Error after validating scope, category, and detail.
func New(scope code.Scope, category code.Category, detail code.Detail, displayMsg string) (*Error, error) {
	if err := validateComponents(scope, category, detail); err != nil {
		return nil, err
	}

	return &Error{
		scope:    scope,
		category: category,
		detail:   detail,
		msg:      displayMsg,
	}, nil
}

// MustNew is like New but panics when components are invalid.
// Intended for compile-time constants (Builder helpers).
func MustNew(scope code.Scope, category code.Category, detail code.Detail, displayMsg string) *Error {
	e, err := New(scope, category, detail, displayMsg)
	if err != nil {
		panic(err)
	}

	return e
}

// For returns a scope-bound builder. Prefer this over package-level globals.
//
//	var appErr = errs.For(code.Facade)
//	return appErr.ResNotFound("user", id).WithCause(err)
func For(scope code.Scope) Builder {
	return Builder{scope: scope}
}

// Error returns the client-facing message.
func (e *Error) Error() string {
	if e == nil {
		return ""
	}

	return e.msg
}

func (e *Error) Scope() code.Scope {
	if e == nil {
		return code.Unset
	}

	return e.scope
}

func (e *Error) Category() code.Category {
	if e == nil {
		return code.DefaultCategory
	}

	return e.category
}

func (e *Error) Detail() code.Detail {
	if e == nil {
		return code.DefaultDetail
	}

	return e.detail
}

// SubCode returns the 6-digit CCCDDD portion.
func (e *Error) SubCode() uint32 {
	if e == nil {
		return code.OK
	}

	return uint32(e.category)*code.CategoryMultiplier + uint32(e.detail)
}

// Code returns the full 8-digit SSCCCDDD code.
func (e *Error) Code() uint32 {
	if e == nil {
		return code.NonCode
	}

	return uint32(e.scope)*code.ScopeMultiplier + e.SubCode()
}

// DisplayCode returns the 8-digit code as a zero-padded string.
func (e *Error) DisplayCode() string {
	if e == nil {
		return "00000000"
	}

	return fmt.Sprintf("%08d", e.Code())
}

// Is implements errors.Is semantics using the full 8-digit code.
func (e *Error) Is(target error) bool {
	var t *Error
	if !errors.As(target, &t) {
		return false
	}

	return e.Code() == t.Code()
}

// Unwrap returns the wrapped cause, if any.
func (e *Error) Unwrap() error {
	if e == nil {
		return nil
	}

	return e.internalErr
}

func (e *Error) clone() *Error {
	if e == nil {
		return nil
	}

	cp := *e

	return &cp
}

// WithCause returns a copy of e with the given cause attached.
func (e *Error) WithCause(cause error) *Error {
	if e == nil {
		return nil
	}

	cp := e.clone()
	cp.internalErr = cause

	return cp
}

// WithScope returns a copy of e using a different scope.
func (e *Error) WithScope(scope code.Scope) (*Error, error) {
	if e == nil {
		return nil, nil
	}
	if !scope.Valid() {
		return nil, fmt.Errorf("%w: scope %d exceeds max %d", ErrInvalidCode, scope, code.MaxScope)
	}

	cp := e.clone()
	cp.scope = scope

	return cp, nil
}

// MustWithScope is like WithScope but panics on invalid scope.
func (e *Error) MustWithScope(scope code.Scope) *Error {
	out, err := e.WithScope(scope)
	if err != nil {
		panic(err)
	}

	return out
}

// WithDetail returns a copy of e with a different detail code.
func (e *Error) WithDetail(detail code.Detail) (*Error, error) {
	if e == nil {
		return nil, nil
	}
	if !detail.Valid() {
		return nil, fmt.Errorf("%w: detail %d exceeds max %d", ErrInvalidCode, detail, code.MaxDetail)
	}

	cp := e.clone()
	cp.detail = detail

	return cp, nil
}

// WithMessage returns a copy of e with a different client-facing message.
func (e *Error) WithMessage(msg string) *Error {
	if e == nil {
		return nil
	}

	cp := e.clone()
	cp.msg = msg

	return cp
}

// HTTPStatus maps the error category to an HTTP status code.
func (e *Error) HTTPStatus() int {
	if e == nil || e.SubCode() == code.OK {
		return http.StatusOK
	}

	switch e.Category() {
	case code.InputInvalidFormat, code.InputMissingRequired:
		return http.StatusBadRequest
	case code.InputNotValidImplementation, code.InputInvalidRange:
		return http.StatusUnprocessableEntity
	case code.InputUnsupportedMedia:
		return http.StatusUnsupportedMediaType
	case code.InputPayloadTooLarge:
		return http.StatusRequestEntityTooLarge

	case code.DBError:
		return http.StatusInternalServerError
	case code.DBDataConvert:
		return http.StatusUnprocessableEntity
	case code.DBDuplicate:
		return http.StatusConflict
	case code.DBUnavailable:
		return http.StatusServiceUnavailable

	case code.ResNotFound:
		return http.StatusNotFound
	case code.ResInvalidFormat:
		return http.StatusUnprocessableEntity
	case code.ResAlreadyExist:
		return http.StatusConflict
	case code.ResInsufficient, code.ResInvalidMeasureID:
		return http.StatusBadRequest
	case code.ResInsufficientPerm:
		return http.StatusForbidden
	case code.ResExpired, code.ResMigrated:
		return http.StatusGone
	case code.ResInvalidState, code.ResMultiOwner:
		return http.StatusConflict
	case code.ResInsufficientQuota:
		return http.StatusTooManyRequests
	case code.ResPreconditionFailed:
		return http.StatusPreconditionFailed
	case code.ResLocked:
		return http.StatusLocked

	case code.AuthUnauthorized, code.AuthExpired, code.AuthInvalidPosixTime, code.AuthSigPayloadMismatch:
		return http.StatusUnauthorized
	case code.AuthForbidden:
		return http.StatusForbidden
	case code.AuthMethodNotAllowed:
		return http.StatusMethodNotAllowed

	case code.SysTooManyRequest:
		return http.StatusTooManyRequests
	case code.SysInternal:
		return http.StatusInternalServerError
	case code.SysMaintain:
		return http.StatusServiceUnavailable
	case code.SysTimeout:
		return http.StatusGatewayTimeout
	case code.SysNotImplemented:
		return http.StatusNotImplemented
	case code.SysClientTimeout:
		return http.StatusRequestTimeout

	case code.PSuPublish, code.PSuConsume:
		return http.StatusBadGateway
	case code.PSuTooLarge:
		return http.StatusRequestEntityTooLarge

	case code.SvcMaintenance:
		return http.StatusServiceUnavailable
	case code.SvcInternal:
		return http.StatusInternalServerError
	case code.SvcThirdParty:
		return http.StatusBadGateway
	case code.SvcHTTP400:
		return http.StatusBadRequest
	case code.SvcRateLimited:
		return http.StatusTooManyRequests

	case code.CatGRPC:
		return httpStatusFromGRPCDetail(e.Detail())
	}

	return http.StatusInternalServerError
}
