package payments

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
)

// ledgerColumns is the projection every ledger report row is read through, joined to the order so a
// row carries the merchant channel its payment used.
const ledgerColumns = `l.account_id, l.kind, l.source, l.origin, l.chips_delta, l.product_id,
	l.order_id, l.provider, l.provider_payment_id, COALESCE(o.shop, ''), l.snapshot, l.created_at`

// ledgerWhere renders a LedgerFilter as a SQL predicate plus its arguments. Every value is bound as
// a parameter; only the fixed fragments are concatenated.
//
// The wallet filter deliberately matches either side of a row. A row names up to two wallets — the
// segment whose chips moved (source) and, for a benefit purchase, where that benefit was bought
// (origin) — and an operator asking "what happened on VK" means both.
func ledgerWhere(f LedgerFilter) (string, []any) {
	var clauses []string
	var args []any
	add := func(clause string, v any) {
		args = append(args, v)
		clauses = append(clauses, fmt.Sprintf(clause, len(args)))
	}
	if !f.From.IsZero() {
		add("l.created_at >= $%d", f.From)
	}
	if !f.To.IsZero() {
		add("l.created_at < $%d", f.To)
	}
	if f.AccountID != uuid.Nil {
		add("l.account_id = $%d", f.AccountID)
	}
	if f.Kind != "" {
		add("l.kind = $%d", f.Kind)
	}
	if f.Provider != "" {
		add("l.provider = $%d", f.Provider)
	}
	if f.Wallet != "" {
		args = append(args, f.Wallet)
		clauses = append(clauses, fmt.Sprintf("(l.source = $%d OR l.origin = $%d)", len(args), len(args)))
	}
	if len(clauses) == 0 {
		return "", nil
	}
	return " WHERE " + strings.Join(clauses, " AND "), args
}

// ledgerPage reads one page of the filtered ledger, newest first, together with how many rows the
// filter matches in total (which is what the pager needs — the page itself cannot report it).
func (s *Store) ledgerPage(ctx context.Context, f LedgerFilter, limit, offset int) ([]LedgerReportRow, int, error) {
	where, args := ledgerWhere(f)

	var total int
	if err := s.db.QueryRowContext(ctx,
		`SELECT count(*) FROM payments.ledger l`+where, args...).Scan(&total); err != nil {
		return nil, 0, fmt.Errorf("payments: count ledger: %w", err)
	}

	q := `SELECT ` + ledgerColumns + `
		FROM payments.ledger l
		LEFT JOIN payments.orders o ON o.order_id = l.order_id` + where + `
		ORDER BY l.created_at DESC, l.ledger_id DESC
		LIMIT $%d OFFSET $%d`
	args = append(args, limit, offset)
	rows, err := s.db.QueryContext(ctx, fmt.Sprintf(q, len(args)-1, len(args)), args...)
	if err != nil {
		return nil, 0, fmt.Errorf("payments: read ledger page: %w", err)
	}
	defer rows.Close()

	var out []LedgerReportRow
	for rows.Next() {
		var (
			r                                   LedgerReportRow
			accountID                           uuid.UUID
			source, origin, provider, paymentID sql.NullString
			productID, orderID                  sql.NullString
			snapshot                            sql.NullString
			shop                                string
			chipsDelta                          int32
			createdAt                           time.Time
		)
		if err := rows.Scan(&accountID, &r.Kind, &source, &origin, &chipsDelta, &productID,
			&orderID, &provider, &paymentID, &shop, &snapshot, &createdAt); err != nil {
			return nil, 0, fmt.Errorf("payments: scan ledger row: %w", err)
		}
		r.AccountID = accountID
		r.LedgerEntry = LedgerEntry{
			Kind:              r.Kind,
			Source:            source.String,
			Origin:            origin.String,
			ChipsDelta:        int(chipsDelta),
			ProductID:         productID.String,
			OrderID:           orderID.String,
			Provider:          provider.String,
			ProviderPaymentID: paymentID.String,
			Shop:              shop,
			Snapshot:          snapshot.String,
			CreatedAt:         createdAt,
		}
		r.Money = moneyFromSnapshot(snapshot.String)
		out = append(out, r)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, fmt.Errorf("payments: read ledger page: %w", err)
	}
	return out, total, nil
}

// ledgerTotals sums what the filter matches — across every matching row, not just the page on
// screen, which is the point of showing them at all. Money is summed per currency from the row
// snapshot, since the ledger's own columns count chips, not money.
func (s *Store) ledgerTotals(ctx context.Context, f LedgerFilter) (LedgerTotals, error) {
	where, args := ledgerWhere(f)
	out := LedgerTotals{}

	if err := s.db.QueryRowContext(ctx, `
		SELECT COALESCE(SUM(l.chips_delta) FILTER (WHERE l.chips_delta > 0), 0),
		       COALESCE(-SUM(l.chips_delta) FILTER (WHERE l.chips_delta < 0), 0)
		FROM payments.ledger l`+where, args...).Scan(&out.ChipsIn, &out.ChipsOut); err != nil {
		return LedgerTotals{}, fmt.Errorf("payments: sum ledger chips: %w", err)
	}

	// The money predicate is folded into the same WHERE, because the filter's own clause list may be
	// empty and an "AND ..." tacked onto a missing WHERE is a syntax error.
	moneyWhere := where
	const moneyOnly = `l.kind IN ('fund','refund') AND l.snapshot->>'amount_minor' IS NOT NULL`
	if moneyWhere == "" {
		moneyWhere = " WHERE " + moneyOnly
	} else {
		moneyWhere += " AND " + moneyOnly
	}
	rows, err := s.db.QueryContext(ctx, `
		SELECT l.kind, l.snapshot->>'currency',
		       COALESCE(SUM((l.snapshot->>'amount_minor')::bigint), 0)
		FROM payments.ledger l`+moneyWhere+`
		GROUP BY 1, 2`, args...)
	if err != nil {
		return LedgerTotals{}, fmt.Errorf("payments: sum ledger money: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		var kind string
		var currency sql.NullString
		var minor int64
		if err := rows.Scan(&kind, &currency, &minor); err != nil {
			return LedgerTotals{}, fmt.Errorf("payments: scan ledger money: %w", err)
		}
		m, err := MoneyFromMinor(minor, Currency(currency.String))
		if err != nil {
			continue // an unknown currency in an old snapshot must not break the report
		}
		switch kind {
		case "fund":
			out.MoneyIn = append(out.MoneyIn, m)
		case "refund":
			out.MoneyRefunded = append(out.MoneyRefunded, m)
		}
	}
	if err := rows.Err(); err != nil {
		return LedgerTotals{}, fmt.Errorf("payments: sum ledger money: %w", err)
	}
	return out, nil
}

// filteredLedger reads the whole filtered ledger for the CSV export — unpaginated by design, since
// an export of the page on screen would be useless for accounting.
func (s *Store) filteredLedger(ctx context.Context, f LedgerFilter) ([]LedgerReportRow, error) {
	rows, _, err := s.ledgerPage(ctx, f, exportLimit, 0)
	return rows, err
}
