package savings

import (
	"context"
	"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/platform/httputil"
	"github.com/niels/banking-app/backend/internal/respond"
)

type Handler struct {
	repo       *Repository
	audit      *audit.Repository
	compliance interface {
		RequireClear(ctx context.Context, userID string) error
	}
	log *slog.Logger
}

type createGoalRequest struct {
	AccountID         string `json:"account_id"`
	Name              string `json:"name"`
	TargetAmountCents int64  `json:"target_amount_cents"`
	TargetDate        string `json:"target_date"`
}

type updateGoalRequest struct {
	Name              *string `json:"name"`
	TargetAmountCents *int64  `json:"target_amount_cents"`
	TargetDate        *string `json:"target_date"`
}

type movementRequest struct {
	AmountCents int64  `json:"amount_cents"`
	Description string `json:"description"`
}

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

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

	var req createGoalRequest
	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
	}
	name, err := normalizeName(req.Name)
	if err != nil {
		respond.Error(w, err)
		return
	}
	targetDate, err := normalizeTargetDate(req.TargetDate)
	if err != nil {
		respond.Error(w, err)
		return
	}

	goal, err := h.repo.Create(r.Context(), CreateGoalParams{
		UserID:            claims.Subject,
		AccountID:         accountID,
		Name:              name,
		TargetAmountCents: req.TargetAmountCents,
		TargetDate:        targetDate,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "savings_goal.created", "savings_goal", goal.ID, map[string]any{
		"target_amount_cents": goal.TargetAmountCents,
		"currency":            goal.Currency,
	})
	respond.Created(w, goal)
}

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

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

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

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

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

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

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

func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
		respond.Error(w, err)
		return
	}

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

	var req updateGoalRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	if req.Name == nil && req.TargetAmountCents == nil && req.TargetDate == nil {
		respond.Error(w, fmt.Errorf("%w: at least one field is required", domain.ErrValidation))
		return
	}

	var name *string
	if req.Name != nil {
		normalized, err := normalizeName(*req.Name)
		if err != nil {
			respond.Error(w, err)
			return
		}
		name = &normalized
	}
	if req.TargetAmountCents != nil {
		if err := domain.ValidateAmount(*req.TargetAmountCents); err != nil {
			respond.Error(w, err)
			return
		}
	}

	var targetDate *string
	if req.TargetDate != nil {
		normalized, err := normalizeTargetDate(*req.TargetDate)
		if err != nil {
			respond.Error(w, err)
			return
		}
		targetDate = &normalized
	}

	goal, err := h.repo.Update(r.Context(), UpdateGoalParams{
		UserID:            claims.Subject,
		GoalID:            goalID,
		Name:              name,
		TargetAmountCents: req.TargetAmountCents,
		TargetDate:        targetDate,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "savings_goal.updated", "savings_goal", goal.ID, map[string]any{"status": goal.Status})
	respond.JSON(w, http.StatusOK, goal)
}

func (h *Handler) Contribute(w http.ResponseWriter, r *http.Request) {
	h.move(w, r, "contribution")
}

func (h *Handler) Withdraw(w http.ResponseWriter, r *http.Request) {
	h.move(w, r, "withdrawal")
}

func (h *Handler) Pause(w http.ResponseWriter, r *http.Request) {
	h.transition(w, r, "paused")
}

func (h *Handler) Resume(w http.ResponseWriter, r *http.Request) {
	h.transition(w, r, "active")
}

func (h *Handler) Close(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
		respond.Error(w, err)
		return
	}

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

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

	h.recordAudit(r, claims.Subject, "savings_goal.closed", "savings_goal", goal.ID, map[string]string{"status": goal.Status})
	respond.JSON(w, http.StatusOK, goal)
}

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

	goalID := r.PathValue("id")
	if err := domain.ValidateUUID("id", goalID); 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
		}
	}

	transactions, err := h.repo.ListTransactions(r.Context(), claims.Subject, goalID, limit)
	if err != nil {
		respond.Error(w, err)
		return
	}

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

func (h *Handler) move(w http.ResponseWriter, r *http.Request, movementType string) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
		respond.Error(w, err)
		return
	}

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

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

	idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
	if len(idempotencyKey) > 128 {
		respond.Error(w, fmt.Errorf("%w: Idempotency-Key must be 128 characters or fewer", domain.ErrValidation))
		return
	}

	description := strings.TrimSpace(req.Description)
	if len(description) > 280 {
		respond.Error(w, fmt.Errorf("%w: description must be 280 characters or fewer", domain.ErrValidation))
		return
	}

	params := MovementParams{
		UserID:         claims.Subject,
		GoalID:         goalID,
		AmountCents:    req.AmountCents,
		Description:    description,
		IdempotencyKey: idempotencyKey,
	}

	var op Operation
	var err error
	switch movementType {
	case "contribution":
		op, err = h.repo.Contribute(r.Context(), params)
	case "withdrawal":
		op, err = h.repo.Withdraw(r.Context(), params)
	default:
		err = fmt.Errorf("%w: unsupported movement type", domain.ErrValidation)
	}
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "savings_goal."+movementType, "savings_goal", goalID, map[string]any{
		"amount_cents": req.AmountCents,
		"currency":     op.Goal.Currency,
	})
	respond.Created(w, op)
}

func (h *Handler) transition(w http.ResponseWriter, r *http.Request, toStatus string) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	if err := h.requireComplianceClear(r.Context(), claims.Subject); err != nil {
		respond.Error(w, err)
		return
	}

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

	var (
		goal domain.SavingsGoal
		err  error
	)
	switch toStatus {
	case "paused":
		goal, err = h.repo.Pause(r.Context(), claims.Subject, goalID)
	case "active":
		goal, err = h.repo.Resume(r.Context(), claims.Subject, goalID)
	default:
		err = fmt.Errorf("%w: unsupported savings goal status", domain.ErrValidation)
	}
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "savings_goal.status_updated", "savings_goal", goal.ID, map[string]string{"status": goal.Status})
	respond.JSON(w, http.StatusOK, goal)
}

func normalizeName(raw string) (string, error) {
	name := strings.TrimSpace(raw)
	if name == "" {
		return "", fmt.Errorf("%w: name is required", domain.ErrValidation)
	}
	if len(name) > 80 {
		return "", fmt.Errorf("%w: name must be 80 characters or fewer", domain.ErrValidation)
	}
	return name, nil
}

func normalizeTargetDate(raw string) (string, error) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return "", nil
	}
	date, err := time.Parse("2006-01-02", value)
	if err != nil {
		return "", fmt.Errorf("%w: target_date must use YYYY-MM-DD", domain.ErrValidation)
	}
	today := time.Now().UTC().Truncate(24 * time.Hour)
	if date.Before(today) {
		return "", fmt.Errorf("%w: target_date cannot be in the past", domain.ErrValidation)
	}
	return value, nil
}

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)
	}
}
