package ledger

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"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
	log   *slog.Logger
}

type createReversalRequest struct {
	Reason       string          `json:"reason"`
	ReversalType string          `json:"reversal_type"`
	Metadata     json.RawMessage `json:"metadata"`
}

func NewHandler(repo *Repository, auditRepo *audit.Repository, log *slog.Logger) *Handler {
	return &Handler{repo: repo, audit: auditRepo, log: log}
}

func (h *Handler) ListJournalEntries(w http.ResponseWriter, r *http.Request) {
	limit := 50
	if raw := r.URL.Query().Get("limit"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil {
			limit = parsed
		}
	}

	entries, err := h.repo.ListJournalEntries(r.Context(), limit)
	if err != nil {
		respond.Error(w, err)
		return
	}

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

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

	entry, err := h.repo.FindJournalEntry(r.Context(), entryID)
	if err != nil {
		respond.Error(w, err)
		return
	}

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

func (h *Handler) TrialBalance(w http.ResponseWriter, r *http.Request) {
	asOf, err := parseOptionalTime(r.URL.Query().Get("as_of"))
	if err != nil {
		respond.Error(w, err)
		return
	}
	report, err := h.repo.TrialBalance(r.Context(), asOf)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, report)
}

func (h *Handler) Export(w http.ResponseWriter, r *http.Request) {
	from, err := parseOptionalTime(r.URL.Query().Get("from"))
	if err != nil {
		respond.Error(w, err)
		return
	}
	to, err := parseOptionalTime(r.URL.Query().Get("to"))
	if err != nil {
		respond.Error(w, err)
		return
	}
	rows, err := h.repo.Export(r.Context(), ExportFilters{
		From:       from,
		To:         to,
		Currency:   r.URL.Query().Get("currency"),
		SourceType: r.URL.Query().Get("source_type"),
		Limit:      queryLimit(r, 1000),
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	claims, _ := middleware.CurrentClaims(r)
	h.recordAudit(r, claims.Subject, "admin.ledger.export.created", "ledger_export", "ledger_export", map[string]any{
		"from":        r.URL.Query().Get("from"),
		"to":          r.URL.Query().Get("to"),
		"currency":    r.URL.Query().Get("currency"),
		"source_type": r.URL.Query().Get("source_type"),
		"rows":        len(rows),
	})

	if strings.EqualFold(r.URL.Query().Get("format"), "csv") {
		writeLedgerExportCSV(w, rows)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"rows": rows})
}

func (h *Handler) RoundingPolicies(w http.ResponseWriter, r *http.Request) {
	policies, err := h.repo.ListRoundingPolicies(r.Context())
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"rounding_policies": policies})
}

func (h *Handler) ListReversals(w http.ResponseWriter, r *http.Request) {
	reversals, err := h.repo.ListReversals(r.Context(), queryLimit(r, 100))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"reversals": reversals})
}

func (h *Handler) CreateJournalReversal(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	entryID := r.PathValue("id")
	var req createReversalRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	metadata := any(nil)
	if len(req.Metadata) > 0 {
		metadata = req.Metadata
	}
	reversal, err := h.repo.CreateJournalReversal(r.Context(), ReversalParams{
		OriginalJournalEntryID: entryID,
		AdminUserID:            claims.Subject,
		Reason:                 req.Reason,
		ReversalType:           req.ReversalType,
		Metadata:               metadata,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, claims.Subject, "admin.ledger.reversal.created", "ledger_journal_reversal", reversal.ID, map[string]any{
		"original_journal_entry_id": reversal.OriginalJournalEntryID,
		"reversal_journal_entry_id": reversal.ReversalJournalEntryID,
		"source_type":               reversal.SourceType,
		"movement_type":             reversal.MovementType,
		"reversal_type":             reversal.ReversalType,
		"status":                    reversal.Status,
	})
	respond.Created(w, reversal)
}

func writeLedgerExportCSV(w http.ResponseWriter, rows []ExportRow) {
	var buf bytes.Buffer
	writer := csv.NewWriter(&buf)
	_ = writer.Write([]string{
		"journal_entry_id",
		"journal_line_id",
		"event_type",
		"source_type",
		"source_id",
		"idempotency_key",
		"description",
		"ledger_account_id",
		"ledger_account_owner_user_id",
		"ledger_account_reference_type",
		"ledger_account_reference_id",
		"ledger_account_normal_balance",
		"direction",
		"amount_cents",
		"currency",
		"journal_entry_created_at",
		"journal_line_created_at",
	})
	for _, row := range rows {
		_ = writer.Write([]string{
			row.JournalEntryID,
			row.JournalLineID,
			row.EventType,
			row.SourceType,
			row.SourceID,
			row.IdempotencyKey,
			row.Description,
			row.LedgerAccountID,
			row.LedgerAccountOwnerUserID,
			row.LedgerAccountReferenceType,
			row.LedgerAccountReferenceID,
			row.LedgerAccountNormalBalance,
			row.Direction,
			strconv.FormatInt(row.AmountCents, 10),
			row.Currency,
			row.JournalEntryCreatedAt.Format(time.RFC3339),
			row.JournalLineCreatedAt.Format(time.RFC3339),
		})
	}
	writer.Flush()

	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	w.Header().Set("Content-Disposition", `attachment; filename="ledger-export.csv"`)
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(buf.Bytes())
}

func parseOptionalTime(raw string) (*time.Time, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	parsed, err := time.Parse(time.RFC3339, raw)
	if err == nil {
		return &parsed, nil
	}
	date, err := time.Parse("2006-01-02", raw)
	if err == nil {
		parsed = date
		return &parsed, nil
	}
	return nil, domain.ErrValidation
}

func queryLimit(r *http.Request, fallback int) int {
	limit := fallback
	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) {
	if h.audit == nil || actorID == "" {
		return
	}
	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 != nil {
		h.log.Error("audit record failed", "error", err)
	}
}
