package kyc

import (
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/niels/banking-app/backend/internal/aml"
	"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/identityprovider"
	"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
	identity         identityprovider.Provider
	aml              *aml.Service
	webhookSecret    []byte
	providerWebhooks providers.WebhookRecorder
	providerName     string
	webhookTolerance time.Duration
	log              *slog.Logger
}

type upsertProfileRequest struct {
	LegalName    string `json:"legal_name"`
	DateOfBirth  string `json:"date_of_birth"`
	Country      string `json:"country"`
	AddressLine1 string `json:"address_line1"`
	City         string `json:"city"`
	PostalCode   string `json:"postal_code"`
}

type addDocumentRequest struct {
	DocumentType   string `json:"document_type"`
	DocumentNumber string `json:"document_number"`
	Country        string `json:"country"`
}

type updateProfileStatusRequest struct {
	Status          string `json:"status"`
	RejectionReason string `json:"rejection_reason"`
}

type webhookEnvelope struct {
	ID   string          `json:"id"`
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}

type verificationCompletedData struct {
	ExternalVerificationID string `json:"external_verification_id"`
	Status                 string `json:"status"`
	RejectionReason        string `json:"rejection_reason"`
	ConfidenceScore        *int   `json:"confidence_score"`
}

func NewHandler(repo *Repository, auditRepo *audit.Repository, identity identityprovider.Provider, amlService *aml.Service, webhookSecret string, log *slog.Logger) *Handler {
	return &Handler{
		repo:          repo,
		audit:         auditRepo,
		identity:      identity,
		aml:           amlService,
		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_identity"
	}
	h.webhookTolerance = tolerance
	return h
}

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

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

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

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

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

	profile, err := h.repo.UpsertProfile(r.Context(), UpsertProfileParams{
		UserID:       claims.Subject,
		LegalName:    strings.TrimSpace(req.LegalName),
		DateOfBirth:  strings.TrimSpace(req.DateOfBirth),
		Country:      strings.ToUpper(strings.TrimSpace(req.Country)),
		AddressLine1: strings.TrimSpace(req.AddressLine1),
		City:         strings.TrimSpace(req.City),
		PostalCode:   strings.TrimSpace(req.PostalCode),
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "kyc.profile_upserted", "kyc_profile", profile.ID, map[string]string{"status": profile.Status})
	respond.JSON(w, http.StatusOK, profile)
}

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

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

	documentType := strings.ToLower(strings.TrimSpace(req.DocumentType))
	if documentType != "passport" && documentType != "national_id" && documentType != "drivers_license" {
		respond.Error(w, fmt.Errorf("%w: document_type must be passport, national_id or drivers_license", domain.ErrValidation))
		return
	}
	country := strings.ToUpper(strings.TrimSpace(req.Country))
	if len(country) != 2 {
		respond.Error(w, fmt.Errorf("%w: country must be an ISO-3166 alpha-2 code", domain.ErrValidation))
		return
	}

	document, err := h.repo.AddDocument(r.Context(), AddDocumentParams{
		UserID:         claims.Subject,
		DocumentType:   documentType,
		DocumentNumber: strings.TrimSpace(req.DocumentNumber),
		Country:        country,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "kyc.document_added", "kyc_document", document.ID, map[string]string{"document_type": document.DocumentType})
	respond.Created(w, document)
}

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

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

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

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

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

	documents, err := h.repo.ListDocuments(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if len(documents) == 0 {
		respond.Error(w, fmt.Errorf("%w: at least one KYC document is required", domain.ErrValidation))
		return
	}

	verification, err := h.identity.StartVerification(r.Context(), identityprovider.StartVerificationParams{
		UserID:       claims.Subject,
		LegalName:    profile.LegalName,
		DateOfBirth:  profile.DateOfBirth,
		Country:      profile.Country,
		DocumentType: documents[0].DocumentType,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	profile, err = h.repo.Submit(r.Context(), claims.Subject, verification.Provider, verification.ExternalVerificationID)
	if err != nil {
		respond.Error(w, err)
		return
	}
	rawVerification, err := json.Marshal(verification)
	if err != nil {
		respond.Error(w, err)
		return
	}
	verificationDecision := normalizeKYCStatus(verification.Status)
	if verificationDecision == "" {
		verificationDecision = "pending"
	}
	if _, err := h.repo.RecordProviderDecision(r.Context(), ProviderDecisionParams{
		UserID:                 claims.Subject,
		KYCProfileID:           profile.ID,
		Provider:               verification.Provider,
		ExternalVerificationID: verification.ExternalVerificationID,
		Decision:               verificationDecision,
		Reason:                 "verification_started",
		RawPayload:             rawVerification,
	}); err != nil {
		respond.Error(w, err)
		return
	}
	for _, document := range documents {
		if _, err := h.repo.RecordEvidence(r.Context(), EvidenceParams{
			UserID:             claims.Subject,
			KYCProfileID:       profile.ID,
			KYCDocumentID:      document.ID,
			Provider:           verification.Provider,
			EvidenceType:       "document",
			ExternalEvidenceID: document.ID,
			Status:             "submitted",
			Metadata: map[string]any{
				"document_type": document.DocumentType,
				"country":       document.Country,
				"source":        "customer_upload_metadata",
			},
		}); err != nil {
			respond.Error(w, err)
			return
		}
	}

	screening, amlCase, err := h.aml.ScreenKYCProfile(r.Context(), profile)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if amlCase != nil {
		profile, err = h.repo.UpdateStatusByExternalID(r.Context(), profile.ExternalVerificationID, "manual_review", "aml_review_required")
		if err != nil {
			respond.Error(w, err)
			return
		}
	}

	h.recordAudit(r, claims.Subject, "kyc.submitted", "kyc_profile", profile.ID, map[string]any{
		"external_verification_id": profile.ExternalVerificationID,
		"aml_screening_id":         screening.ID,
	})
	respond.JSON(w, http.StatusOK, map[string]any{"profile": profile, "aml_screening": screening, "aml_case": amlCase})
}

func (h *Handler) Webhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		respond.Error(w, fmt.Errorf("%w: invalid webhook body", domain.ErrValidation))
		return
	}
	if !h.validWebhookSignature(r.Header.Get("X-KYC-Provider-Signature"), r.Header.Get("X-KYC-Provider-Timestamp"), body) {
		respond.Problem(w, http.StatusUnauthorized, "invalid_signature", "invalid KYC provider signature")
		return
	}

	var event webhookEnvelope
	if err := json.Unmarshal(body, &event); err != nil {
		respond.Error(w, fmt.Errorf("%w: invalid webhook JSON", domain.ErrValidation))
		return
	}
	event.ID = strings.TrimSpace(event.ID)
	event.Type = strings.TrimSpace(event.Type)
	if event.ID == "" || event.Type == "" {
		respond.Error(w, fmt.Errorf("%w: webhook id and type are required", domain.ErrValidation))
		return
	}
	if h.providerWebhooks != nil {
		decision, err := h.providerWebhooks.RecordWebhookEvent(r.Context(), providers.WebhookEventRecord{
			ProviderType:     "identity",
			ProviderName:     h.providerName,
			ExternalEventID:  event.ID,
			EventType:        event.Type,
			SequenceNumber:   providers.WebhookSequence(r.Header.Get("X-KYC-Provider-Sequence")),
			Payload:          body,
			SignatureStatus:  providers.SignatureVerified,
			ProcessingStatus: "processing",
		})
		if err != nil {
			respond.Error(w, err)
			return
		}
		if !decision.Inserted {
			respond.JSON(w, http.StatusOK, map[string]string{"status": "ignored", "reason": "duplicate_event"})
			return
		}
		if decision.OrderingStatus == providers.OrderingGap || decision.OrderingStatus == providers.OrderingRegression {
			respond.JSON(w, http.StatusOK, map[string]string{"status": "quarantined", "reason": decision.OrderingStatus})
			return
		}
	}

	switch event.Type {
	case "kyc.verification.completed":
		h.handleVerificationCompleted(w, r, event)
	default:
		respond.JSON(w, http.StatusOK, map[string]string{"status": "ignored"})
	}
}

func (h *Handler) ListProfiles(w http.ResponseWriter, r *http.Request) {
	profiles, err := h.repo.ListProfiles(r.Context(), strings.TrimSpace(r.URL.Query().Get("status")), queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}

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

func (h *Handler) UpdateProfileStatus(w http.ResponseWriter, r *http.Request) {
	profileID := r.PathValue("id")
	if err := domain.ValidateUUID("id", profileID); err != nil {
		respond.Error(w, err)
		return
	}

	var req updateProfileStatusRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	status := normalizeKYCStatus(req.Status)
	if status == "" {
		respond.Error(w, fmt.Errorf("%w: invalid KYC status", domain.ErrValidation))
		return
	}

	profile, err := h.repo.UpdateStatusByProfileID(r.Context(), profileID, status, strings.TrimSpace(req.RejectionReason))
	if err != nil {
		respond.Error(w, err)
		return
	}

	actorID := ""
	if claims, ok := middleware.CurrentClaims(r); ok {
		actorID = claims.Subject
	}
	h.recordAudit(r, actorID, "kyc.status_updated", "kyc_profile", profile.ID, map[string]string{"status": profile.Status})
	respond.JSON(w, http.StatusOK, profile)
}

func (h *Handler) ProfileEvidence(w http.ResponseWriter, r *http.Request) {
	profileID := r.PathValue("id")
	if err := domain.ValidateUUID("id", profileID); err != nil {
		respond.Error(w, err)
		return
	}
	evidence, err := h.repo.ListEvidence(r.Context(), profileID)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"kyc_evidence": evidence})
}

func (h *Handler) ProfileProviderDecisions(w http.ResponseWriter, r *http.Request) {
	profileID := r.PathValue("id")
	if err := domain.ValidateUUID("id", profileID); err != nil {
		respond.Error(w, err)
		return
	}
	decisions, err := h.repo.ListProviderDecisions(r.Context(), profileID, queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"kyc_provider_decisions": decisions})
}

func (h *Handler) handleVerificationCompleted(w http.ResponseWriter, r *http.Request, event webhookEnvelope) {
	var data verificationCompletedData
	if err := json.Unmarshal(event.Data, &data); err != nil {
		respond.Error(w, fmt.Errorf("%w: invalid verification data", domain.ErrValidation))
		return
	}

	status := normalizeKYCStatus(data.Status)
	if status == "" {
		respond.Error(w, fmt.Errorf("%w: invalid KYC status", domain.ErrValidation))
		return
	}

	profile, err := h.repo.UpdateStatusByExternalID(r.Context(), strings.TrimSpace(data.ExternalVerificationID), status, strings.TrimSpace(data.RejectionReason))
	if err != nil {
		respond.Error(w, err)
		return
	}
	if _, err := h.repo.RecordProviderDecision(r.Context(), ProviderDecisionParams{
		UserID:                 profile.UserID,
		KYCProfileID:           profile.ID,
		Provider:               profile.Provider,
		ExternalVerificationID: strings.TrimSpace(data.ExternalVerificationID),
		ProviderEventID:        strings.TrimSpace(event.ID),
		Decision:               status,
		Reason:                 strings.TrimSpace(data.RejectionReason),
		ConfidenceScore:        data.ConfidenceScore,
		RawPayload:             event.Data,
	}); err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, "", "kyc.provider_status_updated", "kyc_profile", profile.ID, map[string]string{"status": profile.Status})
	respond.JSON(w, http.StatusOK, map[string]any{"status": "processed", "profile": profile})
}

func validateProfile(req upsertProfileRequest) error {
	if strings.TrimSpace(req.LegalName) == "" {
		return fmt.Errorf("%w: legal_name is required", domain.ErrValidation)
	}
	if _, err := time.Parse("2006-01-02", strings.TrimSpace(req.DateOfBirth)); err != nil {
		return fmt.Errorf("%w: date_of_birth must use YYYY-MM-DD", domain.ErrValidation)
	}
	if len(strings.TrimSpace(req.Country)) != 2 {
		return fmt.Errorf("%w: country must be an ISO-3166 alpha-2 code", domain.ErrValidation)
	}
	if strings.TrimSpace(req.AddressLine1) == "" || strings.TrimSpace(req.City) == "" || strings.TrimSpace(req.PostalCode) == "" {
		return fmt.Errorf("%w: address_line1, city and postal_code are required", domain.ErrValidation)
	}
	return nil
}

func normalizeKYCStatus(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "pending", "verified", "rejected", "manual_review":
		return strings.ToLower(strings.TrimSpace(status))
	default:
		return ""
	}
}

func (h *Handler) validWebhookSignature(header, timestamp string, body []byte) bool {
	return providers.VerifyWebhookHMAC(providers.WebhookVerification{
		ProviderType: "identity",
		ProviderName: h.providerName,
		Secret:       h.webhookSecret,
		Signature:    header,
		Timestamp:    timestamp,
		Body:         body,
		Tolerance:    h.webhookTolerance,
	}) == nil
}

func queryLimit(r *http.Request) int {
	limit := 50
	if raw := r.URL.Query().Get("limit"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil {
			limit = parsed
		}
	}
	return limit
}

func (h *Handler) recordAudit(r *http.Request, actorID, eventType, targetType, targetID string, metadata any) {
	var actor *string
	if actorID != "" {
		actor = &actorID
	}
	if err := h.audit.Record(r.Context(), audit.Event{
		ActorUserID: actor,
		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)
	}
}
