package cards

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/niels/banking-app/backend/internal/audit"
	"github.com/niels/banking-app/backend/internal/domain"
	"github.com/niels/banking-app/backend/internal/httpapi/middleware"
	"github.com/niels/banking-app/backend/internal/integrations/cardissuer"
	"github.com/niels/banking-app/backend/internal/platform/httputil"
	"github.com/niels/banking-app/backend/internal/providers"
	"github.com/niels/banking-app/backend/internal/respond"
)

type Handler struct {
	repo       *Repository
	audit      *audit.Repository
	issuer     cardissuer.Issuer
	compliance interface {
		RequireClear(ctx context.Context, userID string) error
	}
	webhookSecret    []byte
	providerWebhooks providers.WebhookRecorder
	providerName     string
	webhookTolerance time.Duration
	log              *slog.Logger
}

type createVirtualCardRequest struct {
	AccountID          string `json:"account_id"`
	Nickname           string `json:"nickname"`
	CardholderName     string `json:"cardholder_name"`
	Network            string `json:"network"`
	CardType           string `json:"card_type"`
	SpendingLimitCents int64  `json:"spending_limit_cents"`
}

type createVirtualCardResponse struct {
	Card      domain.VirtualCard              `json:"card"`
	Sensitive cardissuer.OneTimeSensitiveCard `json:"sensitive"`
}

type updateSpendingLimitRequest struct {
	SpendingLimitCents int64 `json:"spending_limit_cents"`
}

func NewHandler(repo *Repository, auditRepo *audit.Repository, issuer cardissuer.Issuer, compliance interface {
	RequireClear(ctx context.Context, userID string) error
}, webhookSecret string, log *slog.Logger) *Handler {
	return &Handler{
		repo:          repo,
		audit:         auditRepo,
		issuer:        issuer,
		compliance:    compliance,
		webhookSecret: []byte(webhookSecret),
		log:           log,
	}
}

func (h *Handler) WithProviderWebhooks(recorder providers.WebhookRecorder, providerName string, tolerance time.Duration) *Handler {
	h.providerWebhooks = recorder
	h.providerName = strings.TrimSpace(providerName)
	if h.providerName == "" {
		h.providerName = "local_card_issuer"
	}
	h.webhookTolerance = tolerance
	return h
}

func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	var req createVirtualCardRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}

	accountID := strings.TrimSpace(req.AccountID)
	if err := domain.ValidateUUID("account_id", accountID); err != nil {
		respond.Error(w, err)
		return
	}

	cardholderName := strings.TrimSpace(req.CardholderName)
	if cardholderName == "" {
		respond.Error(w, fmt.Errorf("%w: cardholder_name is required", domain.ErrValidation))
		return
	}

	network, err := NormalizeNetwork(req.Network)
	if err != nil {
		respond.Error(w, err)
		return
	}
	cardType, err := NormalizeCardType(req.CardType)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if req.SpendingLimitCents < 0 {
		respond.Error(w, fmt.Errorf("%w: spending_limit_cents cannot be negative", domain.ErrValidation))
		return
	}
	if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
		respond.Error(w, err)
		return
	}

	var card domain.VirtualCard
	var issued cardissuer.CreatedVirtualCard
	for attempt := 0; attempt < 5; attempt++ {
		issued, err = h.issuer.CreateVirtualCard(r.Context(), cardissuer.CreateVirtualCardParams{
			UserID:             claims.Subject,
			AccountID:          accountID,
			CardholderName:     cardholderName,
			Network:            network,
			CardType:           cardType,
			SpendingLimitCents: req.SpendingLimitCents,
		})
		if err != nil {
			respond.Error(w, err)
			return
		}

		card, err = h.repo.Create(r.Context(), CreateParams{
			UserID:             claims.Subject,
			AccountID:          accountID,
			Nickname:           strings.TrimSpace(req.Nickname),
			CardholderName:     cardholderName,
			Network:            network,
			CardType:           cardType,
			ExternalCardID:     issued.ExternalCardID,
			PANFingerprint:     issued.PANFingerprint,
			Last4:              issued.Last4,
			ExpMonth:           issued.ExpMonth,
			ExpYear:            issued.ExpYear,
			SpendingLimitCents: req.SpendingLimitCents,
		})
		if !errors.Is(err, domain.ErrConflict) {
			break
		}
	}
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "virtual_card.created", "virtual_card", card.ID, map[string]any{
		"account_id":              card.AccountID,
		"last4":                   card.Last4,
		"network":                 card.Network,
		"card_type":               card.CardType,
		"spending_limit_cents":    card.SpendingLimitCents,
		"sensitive_returned_once": true,
	})

	respond.Created(w, createVirtualCardResponse{Card: card, Sensitive: issued.Sensitive.OneTimeDisclosure()})
}

func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	cards, err := h.repo.ListByUser(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}

	respond.JSON(w, http.StatusOK, map[string]any{"virtual_cards": cards})
}

func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	cardID := r.PathValue("id")
	if err := domain.ValidateUUID("id", cardID); err != nil {
		respond.Error(w, err)
		return
	}

	card, err := h.repo.FindOwned(r.Context(), claims.Subject, cardID)
	if err != nil {
		respond.Error(w, err)
		return
	}

	respond.JSON(w, http.StatusOK, card)
}

func (h *Handler) Freeze(w http.ResponseWriter, r *http.Request) {
	h.changeStatus(w, r, "frozen", "virtual_card.frozen")
}

func (h *Handler) Unfreeze(w http.ResponseWriter, r *http.Request) {
	h.changeStatus(w, r, "active", "virtual_card.unfrozen")
}

func (h *Handler) Cancel(w http.ResponseWriter, r *http.Request) {
	h.changeStatus(w, r, "canceled", "virtual_card.canceled")
}

func (h *Handler) changeStatus(w http.ResponseWriter, r *http.Request, nextStatus, eventType string) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	cardID := r.PathValue("id")
	if err := domain.ValidateUUID("id", cardID); err != nil {
		respond.Error(w, err)
		return
	}

	current, err := h.repo.FindOwned(r.Context(), claims.Subject, cardID)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if current.Status == "canceled" && nextStatus != "canceled" {
		respond.Error(w, fmt.Errorf("%w: canceled cards cannot be reactivated", domain.ErrValidation))
		return
	}
	if nextStatus == "active" {
		if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
			respond.Error(w, err)
			return
		}
	}

	if err := h.syncIssuerStatus(r.Context(), current.ExternalCardID, nextStatus); err != nil {
		respond.Error(w, err)
		return
	}

	card, err := h.repo.UpdateStatus(r.Context(), claims.Subject, cardID, nextStatus)
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, eventType, "virtual_card", card.ID, map[string]any{
		"account_id": card.AccountID,
		"last4":      card.Last4,
		"status":     card.Status,
	})

	respond.JSON(w, http.StatusOK, card)
}

func (h *Handler) UpdateSpendingLimit(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	cardID := r.PathValue("id")
	if err := domain.ValidateUUID("id", cardID); err != nil {
		respond.Error(w, err)
		return
	}

	var req updateSpendingLimitRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	if req.SpendingLimitCents < 0 {
		respond.Error(w, fmt.Errorf("%w: spending_limit_cents cannot be negative", domain.ErrValidation))
		return
	}

	card, err := h.repo.FindOwned(r.Context(), claims.Subject, cardID)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if card.Status == "canceled" {
		respond.Error(w, fmt.Errorf("%w: canceled cards cannot sync spending limits", domain.ErrValidation))
		return
	}
	if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
		respond.Error(w, err)
		return
	}

	if _, err := h.issuer.SyncSpendingLimit(r.Context(), card.ExternalCardID, req.SpendingLimitCents); err != nil {
		failed, updateErr := h.repo.UpdateSpendingLimit(r.Context(), claims.Subject, cardID, req.SpendingLimitCents, "failed", err.Error())
		if updateErr == nil {
			h.recordAudit(r, claims.Subject, "virtual_card.limit_sync_failed", "virtual_card", failed.ID, map[string]any{
				"spending_limit_cents": req.SpendingLimitCents,
				"error":                err.Error(),
			})
		}
		respond.Error(w, err)
		return
	}

	updated, err := h.repo.UpdateSpendingLimit(r.Context(), claims.Subject, cardID, req.SpendingLimitCents, "synced", "")
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "virtual_card.limit_synced", "virtual_card", updated.ID, map[string]any{
		"spending_limit_cents": updated.SpendingLimitCents,
	})

	respond.JSON(w, http.StatusOK, updated)
}

func (h *Handler) SyncSpendingLimit(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	cardID := r.PathValue("id")
	if err := domain.ValidateUUID("id", cardID); err != nil {
		respond.Error(w, err)
		return
	}

	card, err := h.repo.FindOwned(r.Context(), claims.Subject, cardID)
	if err != nil {
		respond.Error(w, err)
		return
	}

	if _, err := h.issuer.SyncSpendingLimit(r.Context(), card.ExternalCardID, card.SpendingLimitCents); err != nil {
		failed, updateErr := h.repo.UpdateSpendingLimit(r.Context(), claims.Subject, cardID, card.SpendingLimitCents, "failed", err.Error())
		if updateErr == nil {
			h.recordAudit(r, claims.Subject, "virtual_card.limit_sync_failed", "virtual_card", failed.ID, map[string]any{
				"spending_limit_cents": failed.SpendingLimitCents,
				"error":                err.Error(),
			})
		}
		respond.Error(w, err)
		return
	}

	updated, err := h.repo.UpdateSpendingLimit(r.Context(), claims.Subject, cardID, card.SpendingLimitCents, "synced", "")
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "virtual_card.limit_resynced", "virtual_card", updated.ID, map[string]any{
		"spending_limit_cents": updated.SpendingLimitCents,
	})

	respond.JSON(w, http.StatusOK, updated)
}

func (h *Handler) ListAuthorizations(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	cardID := r.PathValue("id")
	if err := domain.ValidateUUID("id", cardID); err != nil {
		respond.Error(w, err)
		return
	}

	limit := 50
	if raw := r.URL.Query().Get("limit"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil {
			limit = parsed
		}
	}

	authorizations, err := h.repo.ListAuthorizations(r.Context(), claims.Subject, cardID, limit)
	if err != nil {
		respond.Error(w, err)
		return
	}

	respond.JSON(w, http.StatusOK, map[string]any{"authorizations": authorizations})
}

func (h *Handler) syncIssuerStatus(ctx context.Context, externalCardID, status string) error {
	switch status {
	case "active":
		return h.issuer.UnfreezeVirtualCard(ctx, externalCardID)
	case "frozen":
		return h.issuer.FreezeVirtualCard(ctx, externalCardID)
	case "canceled":
		return h.issuer.CancelVirtualCard(ctx, externalCardID)
	default:
		return fmt.Errorf("%w: unsupported card status", domain.ErrValidation)
	}
}

func (h *Handler) requireComplianceClear(ctx context.Context, userID string) error {
	if h.compliance == nil {
		return nil
	}
	return h.compliance.RequireClear(ctx, userID)
}

func (h *Handler) recordAudit(r *http.Request, actorID, eventType, targetType, targetID string, metadata any) {
	if err := h.audit.Record(r.Context(), audit.Event{
		ActorUserID: &actorID,
		EventType:   eventType,
		TargetType:  targetType,
		TargetID:    targetID,
		Metadata:    metadata,
		RemoteIP:    httputil.RemoteIP(r),
		UserAgent:   r.UserAgent(),
	}); err != nil {
		h.log.Error("audit record failed", "error", err)
	}
}
