package payments

import (
	"context"
	"encoding/json"
	"time"

	"github.com/google/uuid"
)

// Statement is an account's full financial picture for the admin console: chip balances per funding
// segment, benefits per origin, the recorded refund risk, and the append-only ledger history
// (newest first). Read straight from the materialized tables + the ledger, uncached — an admin-only,
// rare view, not a hot path.
type Statement struct {
	Segments []SegmentChips
	Benefits []OriginBenefit
	Risk     RiskInfo
	Ledger   []LedgerEntry
}

// SegmentChips is one funding segment's chip balance.
type SegmentChips struct {
	Source Source
	Chips  int
}

// OriginBenefit is one origin's benefit state: the hint wallet, the ad-free term (zero AdsPaidUntil
// when none) and the lifetime ad-free flag.
type OriginBenefit struct {
	Origin       Source
	Hints        int
	AdsPaidUntil time.Time
	AdsForever   bool
}

// RiskInfo is the account's recorded refund risk: whether it is abuse-flagged and the unrecoverable
// chip loss accumulated by floor-0 refunds. Present is false when the account has no risk row.
type RiskInfo struct {
	Present   bool
	Abuse     bool
	LossChips int
}

// LedgerEntry is one append-only ledger row projected for the report. The string ids are empty when
// the column is NULL; Shop is the direct-rail merchant channel the referenced order used (empty for
// other rails or order-less rows); Snapshot is the raw purchase/refund JSON (empty when none).
type LedgerEntry struct {
	Kind              string
	Source            string
	Origin            string
	ChipsDelta        int
	ProductID         string
	OrderID           string
	Provider          string
	ProviderPaymentID string
	Shop              string
	Snapshot          string
	CreatedAt         time.Time
}

// AccountStatement assembles the account's financial picture (segments, benefits, risk, full ledger
// history) for the admin console, straight from the materialized tables and the ledger (uncached).
func (s *Service) AccountStatement(ctx context.Context, accountID uuid.UUID) (Statement, error) {
	return s.store.accountStatement(ctx, accountID)
}

// LedgerFilter narrows the operator's ledger report. A zero value matches everything; each set field
// narrows further. From is inclusive and To exclusive, so a day range does not double-count a row on
// the boundary.
//
// Wallet and Provider are deliberately separate axes, because they answer different questions.
// Wallet ("what happened on VK") matches the row's funded segment or the origin a benefit was bought
// in; Provider ("what came through YooKassa") matches the rail that settled it — and a chip spend has
// no rail at all, so a provider filter excludes spends by construction.
type LedgerFilter struct {
	From      time.Time
	To        time.Time
	AccountID uuid.UUID
	Kind      string
	Wallet    string
	Provider  string
}

// LedgerReportRow is one ledger row for the operator report: the entry itself, the account it
// belongs to (the per-account report already knows that, the all-accounts one does not) and the
// money the row moved, recovered from the snapshot — the ledger's own columns count chips.
type LedgerReportRow struct {
	LedgerEntry
	AccountID uuid.UUID
	// Money is what the customer actually paid or was refunded, or the zero Money for a row that
	// moved no money (a spend, an admin grant, a rewarded-video credit).
	Money Money
}

// HasMoney reports whether the row moved real money.
func (r LedgerReportRow) HasMoney() bool { return r.Money.Currency() != "" }

// LedgerTotals sums everything a filter matches, not merely the page on screen. Money is listed per
// currency because the rails settle in different ones (roubles, Votes, Stars) and summing across
// them would be meaningless.
type LedgerTotals struct {
	MoneyIn       []Money
	MoneyRefunded []Money
	ChipsIn       int
	ChipsOut      int
}

// LedgerReport is one page of the filtered ledger plus the totals for the whole filtered range and
// how many rows it matches.
type LedgerReport struct {
	Rows   []LedgerReportRow
	Totals LedgerTotals
	Total  int
}

// exportLimit caps the CSV export. The ledger is append-only and grows forever, so an unbounded
// export would eventually time out; the filter is the way to narrow a real accounting export.
const exportLimit = 100_000

// LedgerReportPage reads one page of the filtered ledger together with the totals for everything the
// filter matches. It is the all-accounts operator view: uncached, admin-only, not a hot path.
func (s *Service) LedgerReportPage(ctx context.Context, f LedgerFilter, limit, offset int) (LedgerReport, error) {
	rows, total, err := s.store.ledgerPage(ctx, f, limit, offset)
	if err != nil {
		return LedgerReport{}, err
	}
	totals, err := s.store.ledgerTotals(ctx, f)
	if err != nil {
		return LedgerReport{}, err
	}
	return LedgerReport{Rows: rows, Totals: totals, Total: total}, nil
}

// FilteredLedgerExport reads the whole filtered ledger for the CSV export, capped at exportLimit.
func (s *Service) FilteredLedgerExport(ctx context.Context, f LedgerFilter) ([]LedgerReportRow, error) {
	return s.store.filteredLedger(ctx, f)
}

// MoneyFromLedgerSnapshot recovers the money a ledger row moved from its snapshot, returning the
// zero Money when the row carries none (a spend, a grant) or the snapshot predates the fields. It is
// exported for callers folding their own totals out of a Statement's history.
func MoneyFromLedgerSnapshot(snapshot string) Money { return moneyFromSnapshot(snapshot) }

// moneyFromSnapshot recovers the money a ledger row moved from its snapshot, returning the zero
// Money when the row carries none (a spend, a grant) or the snapshot predates the fields.
func moneyFromSnapshot(snapshot string) Money {
	if snapshot == "" {
		return Money{}
	}
	var snap struct {
		Amount   int64  `json:"amount_minor"`
		Currency string `json:"currency"`
	}
	if err := json.Unmarshal([]byte(snapshot), &snap); err != nil || snap.Currency == "" {
		return Money{}
	}
	m, err := MoneyFromMinor(snap.Amount, Currency(snap.Currency))
	if err != nil {
		return Money{}
	}
	return m
}
