package accounts

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

	"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"
	"github.com/niels/banking-app/backend/internal/security"
)

type Handler struct {
	repo  *Repository
	audit *audit.Repository
	log   *slog.Logger
}

type createAccountRequest struct {
	Currency string `json:"currency"`
	WalletID string `json:"wallet_id"`
}

func NewHandler(repo *Repository, auditRepo *audit.Repository, log *slog.Logger) *Handler {
	return &Handler{repo: repo, audit: auditRepo, 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
	}

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

	currency := domain.NormalizeCurrency(req.Currency)
	if err := domain.ValidateCurrency(currency); err != nil {
		respond.Error(w, err)
		return
	}

	walletID := req.WalletID
	if walletID != "" {
		if err := domain.ValidateUUID("wallet_id", walletID); err != nil {
			respond.Error(w, err)
			return
		}
	}

	var account domain.Account
	var err error
	for attempt := 0; attempt < 5; attempt++ {
		accountNumber, numberErr := security.AccountNumber()
		if numberErr != nil {
			respond.Error(w, numberErr)
			return
		}

		account, err = h.repo.Create(r.Context(), CreateParams{
			UserID:        claims.Subject,
			WalletID:      walletID,
			AccountNumber: accountNumber,
			Currency:      currency,
		})
		if !errors.Is(err, domain.ErrConflict) {
			break
		}
	}
	if err != nil {
		if errors.Is(err, domain.ErrConflict) {
			err = fmt.Errorf("%w: could not allocate account number", domain.ErrConflict)
		}
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, claims.Subject, "account.created", "account", account.ID, map[string]string{
		"currency":  account.Currency,
		"wallet_id": account.WalletID,
	})
	respond.Created(w, account)
}

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

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

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

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

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

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

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

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