package server

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"html/template"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"scrabble/backend/internal/adminconsole"
	"scrabble/backend/internal/payments"
)

// ledgerPageSize is how many operations one ledger page shows.
const ledgerPageSize = 100

// ledgerDefaultDays is how far back the ledger looks when the operator has not chosen a range. A
// month is the reporting period that matters (it is what a tax filing covers) and it keeps the
// default view bounded on a ledger that only ever grows.
const ledgerDefaultDays = 30

// ledgerDateFormat is the date form the filter accepts and renders (an <input type="date"> value).
const ledgerDateFormat = "2006-01-02"

// ledgerKinds, ledgerWallets and ledgerProviders are the filter's fixed option lists. They are
// spelled out rather than derived from the data so the form is stable on an empty ledger and an
// operator can see what exists at all.
var (
	ledgerKinds     = []string{"fund", "spend", "admin_grant", "refund"}
	ledgerWallets   = []string{string(payments.SourceDirect), string(payments.SourceVK), string(payments.SourceTelegram)}
	ledgerProviders = []string{providerYooKassa, providerRobokassa, providerVK, providerTelegram, "vk_ads", "admin"}
)

// ledgerFilterFrom reads the ledger filter out of the query string, defaulting the range to the last
// ledgerDefaultDays. An unparseable date or id is treated as unset rather than as an error: a
// hand-edited URL should narrow nothing, not break the page.
func ledgerFilterFrom(c *gin.Context, now time.Time) (payments.LedgerFilter, adminconsole.LedgerView) {
	view := adminconsole.LedgerView{
		Kinds:     ledgerKinds,
		Wallets:   ledgerWallets,
		Providers: ledgerProviders,
	}
	f := payments.LedgerFilter{}

	from, to := strings.TrimSpace(c.Query("from")), strings.TrimSpace(c.Query("to"))
	if from == "" && to == "" {
		from = now.AddDate(0, 0, -ledgerDefaultDays).Format(ledgerDateFormat)
	}
	if t, err := time.Parse(ledgerDateFormat, from); err == nil {
		f.From, view.From = t, from
	}
	if t, err := time.Parse(ledgerDateFormat, to); err == nil {
		// The range is inclusive of the chosen end date, so it runs to the start of the next day.
		f.To, view.To = t.AddDate(0, 0, 1), to
	}
	if id, err := uuid.Parse(strings.TrimSpace(c.Query("user"))); err == nil {
		f.AccountID, view.UserID = id, id.String()
	}
	if v := c.Query("kind"); slicesHas(ledgerKinds, v) {
		f.Kind, view.Kind = v, v
	}
	if v := c.Query("wallet"); slicesHas(ledgerWallets, v) {
		f.Wallet, view.Wallet = v, v
	}
	if v := c.Query("provider"); slicesHas(ledgerProviders, v) {
		f.Provider, view.Provider = v, v
	}
	view.FilterQuery = template.URL(ledgerFilterQuery(view))
	return f, view
}

// slicesHas reports whether v is one of the allowed options. The filter only ever accepts a value
// from its own list, so a crafted query string cannot reach the store with something unexpected.
func slicesHas(allowed []string, v string) bool {
	for _, a := range allowed {
		if a == v {
			return true
		}
	}
	return false
}

// ledgerFilterQuery renders the active filters as an escaped query fragment. Every link that must
// stay on the same slice of the ledger — the pager, the CSV export, a refund's way back — is built
// from it, which is what keeps them in step with the form.
func ledgerFilterQuery(v adminconsole.LedgerView) string {
	q := url.Values{}
	for key, val := range map[string]string{
		"from": v.From, "to": v.To, "user": v.UserID,
		"kind": v.Kind, "wallet": v.Wallet, "provider": v.Provider,
	} {
		if val != "" {
			q.Set(key, val)
		}
	}
	return q.Encode()
}

// consoleLedger renders the all-accounts financial ledger: one page of operations under the current
// filter, and the totals for everything that filter matches — the picture no per-account card can
// give. The page number rides the same query string as the filters, so paging never silently widens
// the view.
func (s *Server) consoleLedger(c *gin.Context) {
	f, view := ledgerFilterFrom(c, time.Now())
	page, _ := strconv.Atoi(c.Query("page"))
	if page < 1 {
		page = 1
	}
	report, err := s.payments.LedgerReportPage(c.Request.Context(), f, ledgerPageSize, (page-1)*ledgerPageSize)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	view.Pager = adminconsole.NewPager(page, ledgerPageSize, report.Total)
	view.Totals = ledgerTotalsRow(report.Totals)
	for _, r := range report.Rows {
		view.Rows = append(view.Rows, ledgerRow(r))
	}
	s.renderConsole(c, "ledger", "ledger", "Ledger", view)
}

// ledgerRow projects one report row for the logic-free template: times and money pre-formatted, the
// product title lifted out of the snapshot, and the refund affordance decided here rather than in
// the template — only a funded order can be reversed.
func ledgerRow(r payments.LedgerReportRow) adminconsole.LedgerRow {
	row := adminconsole.LedgerRow{
		At:           fmtTime(r.CreatedAt),
		AccountID:    r.AccountID.String(),
		AccountShort: shortID(r.AccountID.String()),
		Kind:         r.Kind,
		Source:       r.Source,
		Origin:       r.Origin,
		ChipsDelta:   r.ChipsDelta,
		Order:        r.OrderID,
		Provider:     r.Provider,
		Shop:         r.Shop,
		Refundable:   r.Kind == "fund" && r.OrderID != "",
		Title:        snapshotTitle(r.Snapshot),
	}
	if r.HasMoney() {
		row.Money = fmtMoney(r.Money)
	}
	return row
}

// ledgerTotalsRow pre-formats the period totals for the template.
func ledgerTotalsRow(t payments.LedgerTotals) adminconsole.LedgerTotalsRow {
	out := adminconsole.LedgerTotalsRow{ChipsIn: t.ChipsIn, ChipsOut: t.ChipsOut}
	for _, m := range t.MoneyIn {
		out.MoneyIn = append(out.MoneyIn, fmtMoney(m))
	}
	for _, m := range t.MoneyRefunded {
		out.MoneyRefunded = append(out.MoneyRefunded, fmtMoney(m))
	}
	return out
}

// fmtMoney renders an amount with its currency, e.g. "149.00 RUB".
func fmtMoney(m payments.Money) string {
	return fmt.Sprintf("%s %s", m.Major(), m.Currency())
}

// snapshotTitle lifts the sold product's title out of a ledger row snapshot, so the table names what
// was bought instead of only its id. It returns "" when the snapshot carries no title.
func snapshotTitle(snapshot string) string {
	if snapshot == "" {
		return ""
	}
	var snap struct {
		Title string `json:"title"`
	}
	if err := json.Unmarshal([]byte(snapshot), &snap); err != nil {
		return ""
	}
	return snap.Title
}

// consoleLedgerExport streams the filtered ledger as a CSV attachment for tax reporting and rail
// reconciliation. It honours the same filters as the page it is linked from — an export that
// silently covered a different range than the screen would be worse than no export.
func (s *Server) consoleLedgerExport(c *gin.Context) {
	f, _ := ledgerFilterFrom(c, time.Now())
	rows, err := s.payments.FilteredLedgerExport(c.Request.Context(), f)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	c.Header("Content-Type", "text/csv; charset=utf-8")
	c.Header("Content-Disposition", `attachment; filename="ledger.csv"`)
	w := csv.NewWriter(c.Writer)
	_ = w.Write([]string{
		"created_at", "account_id", "kind", "source", "origin", "chips_delta",
		"amount_minor", "currency", "product_id", "order_id", "provider", "provider_payment_id", "shop", "snapshot",
	})
	for _, r := range rows {
		amount, currency := "", ""
		if r.HasMoney() {
			amount, currency = strconv.FormatInt(r.Money.Minor(), 10), string(r.Money.Currency())
		}
		_ = w.Write([]string{
			r.CreatedAt.UTC().Format(time.RFC3339), r.AccountID.String(), r.Kind, r.Source, r.Origin,
			strconv.Itoa(r.ChipsDelta), amount, currency,
			r.ProductID, r.OrderID, r.Provider, r.ProviderPaymentID, r.Shop, r.Snapshot,
		})
	}
	w.Flush()
}
