package restapi

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/mail"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

var errPublicAuthIdentityNotApplicable = errors.New("public auth identity does not apply to this route")

type malformedJSONRequestError struct {
	message string
	reason  PublicMalformedRequestReason
}

func (e *malformedJSONRequestError) Error() string {
	if e == nil {
		return ""
	}

	return e.message
}

type publicAuthIdentity struct {
	kind  string
	value string
}

// AuthServiceClient defines the consumer-side contract used by public auth
// REST handlers to delegate unauthenticated authentication commands to the
// Auth / Session Service.
type AuthServiceClient interface {
	// SendEmailCode starts a login challenge for input.Email and returns the
	// challenge identifier that the client must later confirm.
	SendEmailCode(ctx context.Context, input SendEmailCodeInput) (SendEmailCodeResult, error)

	// ConfirmEmailCode completes a previously issued challenge, registers
	// input.ClientPublicKey for the new device session, and returns the created
	// device session identifier.
	ConfirmEmailCode(ctx context.Context, input ConfirmEmailCodeInput) (ConfirmEmailCodeResult, error)
}

// SendEmailCodeInput describes the public REST and adapter payload used to
// request a login code for a single e-mail address.
type SendEmailCodeInput struct {
	// Email is the single client e-mail address that should receive the login
	// code challenge.
	Email string `json:"email"`

	// Locale is the optional BCP 47 language tag the caller wants the
	// auth-mail in. The body field is the canonical channel because Safari
	// silently drops JS-set Accept-Language headers; when set, it overrides
	// the request Accept-Language for preferred-language resolution.
	Locale string `json:"locale,omitempty"`

	// PreferredLanguage stores the canonical BCP 47 language tag derived
	// from Locale (preferred) or the Accept-Language header (fallback) for
	// upstream auth-mail localization and create-only user registration
	// context.
	PreferredLanguage string `json:"-"`
}

// SendEmailCodeResult describes the public REST and adapter payload returned
// after the Auth / Session Service creates a login challenge.
type SendEmailCodeResult struct {
	// ChallengeID identifies the issued challenge that must be confirmed by the
	// client in the next public auth step.
	ChallengeID string `json:"challenge_id"`
}

// ConfirmEmailCodeInput describes the public REST and adapter payload used to
// complete a previously issued login challenge.
type ConfirmEmailCodeInput struct {
	// ChallengeID identifies the challenge previously returned by
	// SendEmailCode.
	ChallengeID string `json:"challenge_id"`

	// Code is the verification code delivered to the client by the Auth /
	// Session Service.
	Code string `json:"code"`

	// ClientPublicKey is the standard base64-encoded raw 32-byte Ed25519 public
	// key that should be registered for the created device session.
	ClientPublicKey string `json:"client_public_key"`

	// TimeZone is the client-selected IANA time zone name forwarded to the
	// Auth / Session Service as registration context for first-time user
	// creation.
	TimeZone string `json:"time_zone"`
}

// ConfirmEmailCodeResult describes the public REST and adapter payload
// returned after the Auth / Session Service creates a device session.
type ConfirmEmailCodeResult struct {
	// DeviceSessionID is the stable identifier of the created device session.
	DeviceSessionID string `json:"device_session_id"`
}

// AuthServiceError allows an auth adapter to project a stable public REST
// error without teaching the gateway transport layer about upstream business
// rules.
type AuthServiceError struct {
	// StatusCode is the HTTP status that the public REST handler should expose.
	StatusCode int

	// Code is the stable edge-level error code written into the JSON envelope.
	Code string

	// Message is the human-readable client-safe error description.
	Message string
}

// Error returns a readable representation of the projected auth service error.
func (e *AuthServiceError) Error() string {
	if e == nil {
		return ""
	}

	switch {
	case strings.TrimSpace(e.Code) == "" && strings.TrimSpace(e.Message) == "":
		return http.StatusText(e.normalizedStatusCode())
	case strings.TrimSpace(e.Code) == "":
		return e.Message
	case strings.TrimSpace(e.Message) == "":
		return e.Code
	default:
		return e.Code + ": " + e.Message
	}
}

func (e *AuthServiceError) normalizedStatusCode() int {
	if e == nil || e.StatusCode < 400 || e.StatusCode > 599 {
		return http.StatusInternalServerError
	}

	return e.StatusCode
}

func (e *AuthServiceError) normalizedCode() string {
	if e == nil {
		return errorCodeInternalError
	}

	code := strings.TrimSpace(e.Code)
	if code == "" {
		switch e.normalizedStatusCode() {
		case http.StatusServiceUnavailable:
			return errorCodeServiceUnavailable
		case http.StatusBadRequest:
			return errorCodeInvalidRequest
		default:
			return errorCodeInternalError
		}
	}

	return code
}

func (e *AuthServiceError) normalizedMessage() string {
	if e == nil {
		return "internal server error"
	}

	message := strings.TrimSpace(e.Message)
	if message == "" {
		switch e.normalizedStatusCode() {
		case http.StatusServiceUnavailable:
			return "auth service is unavailable"
		case http.StatusBadRequest:
			return "request is invalid"
		default:
			return "internal server error"
		}
	}

	return message
}

// unavailableAuthServiceClient keeps the public auth surface mounted until a
// concrete upstream adapter is wired into the gateway process.
type unavailableAuthServiceClient struct{}

func (unavailableAuthServiceClient) SendEmailCode(context.Context, SendEmailCodeInput) (SendEmailCodeResult, error) {
	return SendEmailCodeResult{}, &AuthServiceError{
		StatusCode: http.StatusServiceUnavailable,
		Code:       errorCodeServiceUnavailable,
		Message:    "auth service is unavailable",
	}
}

func (unavailableAuthServiceClient) ConfirmEmailCode(context.Context, ConfirmEmailCodeInput) (ConfirmEmailCodeResult, error) {
	return ConfirmEmailCodeResult{}, &AuthServiceError{
		StatusCode: http.StatusServiceUnavailable,
		Code:       errorCodeServiceUnavailable,
		Message:    "auth service is unavailable",
	}
}

func handleSendEmailCode(authService AuthServiceClient, timeout time.Duration) gin.HandlerFunc {
	return func(c *gin.Context) {
		var input SendEmailCodeInput
		if err := decodeJSONRequest(c.Request, &input); err != nil {
			abortInvalidRequest(c, err.Error())
			return
		}
		if err := validateSendEmailCodeInput(&input); err != nil {
			abortInvalidRequest(c, err.Error())
			return
		}
		// Body locale wins over the request header so Safari clients,
		// which cannot set Accept-Language from JavaScript, can still
		// pick a non-system mail language. Empty / malformed values
		// fall through resolvePreferredLanguage to the default.
		if strings.TrimSpace(input.Locale) != "" {
			input.PreferredLanguage = resolvePreferredLanguage(input.Locale)
		} else {
			input.PreferredLanguage = resolvePreferredLanguage(c.Request.Header.Get("Accept-Language"))
		}

		callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout)
		defer cancel()

		result, err := authService.SendEmailCode(callCtx, input)
		if err != nil {
			if errors.Is(err, context.DeadlineExceeded) {
				abortWithError(c, http.StatusServiceUnavailable, errorCodeServiceUnavailable, "auth service is unavailable")
				return
			}
			abortWithAuthServiceError(c, err)
			return
		}
		if err := validateSendEmailCodeResult(&result); err != nil {
			abortWithError(c, http.StatusInternalServerError, errorCodeInternalError, "internal server error")
			return
		}

		c.JSON(http.StatusOK, result)
	}
}

func handleConfirmEmailCode(authService AuthServiceClient, timeout time.Duration) gin.HandlerFunc {
	return func(c *gin.Context) {
		var input ConfirmEmailCodeInput
		if err := decodeJSONRequest(c.Request, &input); err != nil {
			abortInvalidRequest(c, err.Error())
			return
		}
		if err := validateConfirmEmailCodeInput(&input); err != nil {
			abortInvalidRequest(c, err.Error())
			return
		}

		callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout)
		defer cancel()

		result, err := authService.ConfirmEmailCode(callCtx, input)
		if err != nil {
			if errors.Is(err, context.DeadlineExceeded) {
				abortWithError(c, http.StatusServiceUnavailable, errorCodeServiceUnavailable, "auth service is unavailable")
				return
			}
			abortWithAuthServiceError(c, err)
			return
		}
		if err := validateConfirmEmailCodeResult(&result); err != nil {
			abortWithError(c, http.StatusInternalServerError, errorCodeInternalError, "internal server error")
			return
		}

		c.JSON(http.StatusOK, result)
	}
}

func abortInvalidRequest(c *gin.Context, message string) {
	abortWithError(c, http.StatusBadRequest, errorCodeInvalidRequest, message)
}

func abortWithAuthServiceError(c *gin.Context, err error) {
	var authErr *AuthServiceError
	if errors.As(err, &authErr) {
		abortWithError(c, authErr.normalizedStatusCode(), authErr.normalizedCode(), authErr.normalizedMessage())
		return
	}

	abortWithError(c, http.StatusInternalServerError, errorCodeInternalError, "internal server error")
}

func decodeJSONRequest(r *http.Request, target any) error {
	if r == nil || r.Body == nil {
		return &malformedJSONRequestError{
			message: "request body must not be empty",
			reason:  PublicMalformedRequestReasonEmptyBody,
		}
	}

	return decodeJSONReader(r.Body, target)
}

func decodeJSONBytes(bodyBytes []byte, target any) error {
	return decodeJSONReader(bytes.NewReader(bodyBytes), target)
}

func decodeJSONReader(reader io.Reader, target any) error {
	decoder := json.NewDecoder(reader)
	decoder.DisallowUnknownFields()

	if err := decoder.Decode(target); err != nil {
		return describeJSONDecodeError(err)
	}

	if err := decoder.Decode(&struct{}{}); err != nil {
		if errors.Is(err, io.EOF) {
			return nil
		}

		return &malformedJSONRequestError{
			message: "request body must contain a single JSON object",
			reason:  PublicMalformedRequestReasonMultipleJSONObjects,
		}
	}

	return &malformedJSONRequestError{
		message: "request body must contain a single JSON object",
		reason:  PublicMalformedRequestReasonMultipleJSONObjects,
	}
}

func describeJSONDecodeError(err error) error {
	var syntaxErr *json.SyntaxError
	var typeErr *json.UnmarshalTypeError

	switch {
	case errors.Is(err, io.EOF):
		return &malformedJSONRequestError{
			message: "request body must not be empty",
			reason:  PublicMalformedRequestReasonEmptyBody,
		}
	case errors.As(err, &syntaxErr):
		return &malformedJSONRequestError{
			message: "request body contains malformed JSON",
			reason:  PublicMalformedRequestReasonMalformedJSON,
		}
	case errors.Is(err, io.ErrUnexpectedEOF):
		return &malformedJSONRequestError{
			message: "request body contains malformed JSON",
			reason:  PublicMalformedRequestReasonMalformedJSON,
		}
	case errors.As(err, &typeErr):
		if strings.TrimSpace(typeErr.Field) != "" {
			return &malformedJSONRequestError{
				message: fmt.Sprintf("request body contains an invalid value for %q", typeErr.Field),
				reason:  PublicMalformedRequestReasonInvalidJSONValue,
			}
		}
		return &malformedJSONRequestError{
			message: "request body contains an invalid JSON value",
			reason:  PublicMalformedRequestReasonInvalidJSONValue,
		}
	case strings.HasPrefix(err.Error(), "json: unknown field "):
		return &malformedJSONRequestError{
			message: fmt.Sprintf("request body contains unknown field %s", strings.TrimPrefix(err.Error(), "json: unknown field ")),
			reason:  PublicMalformedRequestReasonUnknownField,
		}
	default:
		return &malformedJSONRequestError{
			message: "request body contains invalid JSON",
			reason:  PublicMalformedRequestReasonMalformedJSON,
		}
	}
}

func validateSendEmailCodeInput(input *SendEmailCodeInput) error {
	input.Email = strings.TrimSpace(input.Email)
	if input.Email == "" {
		return errors.New("email must not be empty")
	}

	parsedAddress, err := mail.ParseAddress(input.Email)
	if err != nil || parsedAddress.Name != "" || parsedAddress.Address != input.Email {
		return errors.New("email must be a single valid email address")
	}

	return nil
}

func validateSendEmailCodeResult(result *SendEmailCodeResult) error {
	result.ChallengeID = strings.TrimSpace(result.ChallengeID)
	if result.ChallengeID == "" {
		return errors.New("auth service returned an empty challenge_id")
	}

	return nil
}

func validateConfirmEmailCodeInput(input *ConfirmEmailCodeInput) error {
	input.ChallengeID = strings.TrimSpace(input.ChallengeID)
	if input.ChallengeID == "" {
		return errors.New("challenge_id must not be empty")
	}

	input.Code = strings.TrimSpace(input.Code)
	if input.Code == "" {
		return errors.New("code must not be empty")
	}

	input.ClientPublicKey = strings.TrimSpace(input.ClientPublicKey)
	if input.ClientPublicKey == "" {
		return errors.New("client_public_key must not be empty")
	}

	input.TimeZone = strings.TrimSpace(input.TimeZone)
	if input.TimeZone == "" {
		return errors.New("time_zone must not be empty")
	}

	return nil
}

func validateConfirmEmailCodeResult(result *ConfirmEmailCodeResult) error {
	result.DeviceSessionID = strings.TrimSpace(result.DeviceSessionID)
	if result.DeviceSessionID == "" {
		return errors.New("auth service returned an empty device_session_id")
	}

	return nil
}

func malformedRequestReasonFromError(err error) (PublicMalformedRequestReason, bool) {
	var malformedErr *malformedJSONRequestError
	if !errors.As(err, &malformedErr) {
		return "", false
	}

	return malformedErr.reason, true
}

func extractPublicAuthIdentity(requestPath string, bodyBytes []byte) (publicAuthIdentity, error) {
	switch requestPath {
	case "/api/v1/public/auth/send-email-code":
		var input SendEmailCodeInput
		if err := decodeJSONBytes(bodyBytes, &input); err != nil {
			return publicAuthIdentity{}, err
		}
		if err := validateSendEmailCodeInput(&input); err != nil {
			return publicAuthIdentity{}, err
		}

		return publicAuthIdentity{
			kind:  "email",
			value: input.Email,
		}, nil
	case "/api/v1/public/auth/confirm-email-code":
		var input ConfirmEmailCodeInput
		if err := decodeJSONBytes(bodyBytes, &input); err != nil {
			return publicAuthIdentity{}, err
		}
		if err := validateConfirmEmailCodeInput(&input); err != nil {
			return publicAuthIdentity{}, err
		}

		return publicAuthIdentity{
			kind:  "challenge",
			value: input.ChallengeID,
		}, nil
	default:
		return publicAuthIdentity{}, errPublicAuthIdentityNotApplicable
	}
}
