package storage

import (
	"database/sql"
	"errors"
	"fmt"
)

// The mischief storefront's half of the euro border.
//
// A buyer signs in, picks a mark off the anonymous roster board, and places a
// hit. Pete records only the *intent*: it never moves money and never runs a
// single game rule. gogobee's poll loop reads the pending orders, does the real
// work against its own ledger (debit, eligibility, open the contract), and hands
// back a verdict Pete files against the order. The guid is the idempotency key
// end to end — gogobee passes it to DebitIdem and stamps it on the contract — so
// a verdict whose ack is lost on the wire can be retried without the buyer paying
// twice or the mark catching two hits for one order.

// MischiefOrder is one storefront order and its current standing.
type MischiefOrder struct {
	GUID          string `json:"guid"`
	BuyerSub      string `json:"-"`             // OIDC subject; keys "my orders", never sent to gogobee
	BuyerUsername string `json:"buyer_username"` // localpart gogobee turns into @username:server
	TargetToken   string `json:"target_token"`
	TargetName    string `json:"target_name"`
	Tier          string `json:"tier"`
	Signed        bool   `json:"signed"`
	Status        string `json:"status"`
	Detail        string `json:"detail,omitempty"`
	CreatedAt     int64  `json:"created_at"`
	UpdatedAt     int64  `json:"updated_at,omitempty"`
}

// Order states. These strings cross the wire to gogobee (it POSTs the verdict),
// so they are part of the contract — see the schema and gogobee_mischief_plan.md.
const (
	MischiefPending           = "pending"            // placed; gogobee hasn't acted yet
	MischiefPlaced            = "placed"             // gogobee debited the buyer and opened a contract
	MischiefBouncedFunds      = "bounced_funds"      // buyer couldn't afford it after all
	MischiefBouncedIneligible = "bounced_ineligible" // target no longer a valid mark
)

// validMischiefVerdict is the set of terminal states gogobee is allowed to hand
// back. An unknown verdict is a contract mismatch, not something to file blindly.
func validMischiefVerdict(status string) bool {
	switch status {
	case MischiefPlaced, MischiefBouncedFunds, MischiefBouncedIneligible:
		return true
	}
	return false
}

var ErrNoSuchOrder = errors.New("mischief: no such order")

// InsertMischiefOrder records a fresh, pending order and returns it with a new
// guid. Money and eligibility are gogobee's problem; all Pete asserts here is
// that the buyer is signed in and the form was well formed (checked by the
// caller). The guid is minted here so the buyer sees a stable reference the
// instant they submit, before gogobee has ever heard of it.
func InsertMischiefOrder(buyerSub, buyerUsername, targetToken, targetName, tier string, signed bool) (MischiefOrder, error) {
	guid, err := newGUID()
	if err != nil {
		return MischiefOrder{}, err
	}
	now := nowUnix()
	sig := 0
	if signed {
		sig = 1
	}
	if _, err := Get().Exec(
		`INSERT INTO mischief_orders
		    (guid, buyer_sub, buyer_username, target_token, target_name, tier, signed, status, created_at, updated_at)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		guid, buyerSub, buyerUsername, targetToken, targetName, tier, sig, MischiefPending, now, now,
	); err != nil {
		return MischiefOrder{}, fmt.Errorf("mischief: insert order: %w", err)
	}
	return MischiefOrder{
		GUID: guid, BuyerSub: buyerSub, BuyerUsername: buyerUsername,
		TargetToken: targetToken, TargetName: targetName, Tier: tier,
		Signed: signed, Status: MischiefPending, CreatedAt: now, UpdatedAt: now,
	}, nil
}

// PendingMischiefOrders is what gogobee's poll loop reads: every order still
// waiting to be acted on. There is no claimed-but-stale window like escrow has,
// because a pending order carries no intermediate state — if gogobee dies partway
// through, the order simply stays pending and is offered again next poll, and the
// guid makes the replay a no-op.
func PendingMischiefOrders(limit int) ([]MischiefOrder, error) {
	if limit <= 0 {
		limit = 100
	}
	rows, err := Get().Query(
		`SELECT guid, buyer_sub, buyer_username, target_token, target_name, tier, signed, status, COALESCE(detail, ''), created_at, updated_at
		   FROM mischief_orders
		  WHERE status = ?
		  ORDER BY created_at
		  LIMIT ?`,
		MischiefPending, limit,
	)
	if err != nil {
		return nil, fmt.Errorf("mischief: pending orders: %w", err)
	}
	defer rows.Close()
	return scanMischiefOrders(rows)
}

// ResolveMischiefOrder files gogobee's verdict against a pending order. It is
// idempotent: gogobee's poll loop can re-offer and re-resolve the same order, so
// a verdict that arrives twice is a no-op the second time. Only a pending order
// moves; an order already in a terminal state reports what it already decided,
// which is what stops a retried verdict from overwriting the first one.
func ResolveMischiefOrder(guid, status, detail string) (MischiefOrder, error) {
	if !validMischiefVerdict(status) {
		return MischiefOrder{}, fmt.Errorf("mischief: bad verdict %q", status)
	}
	now := nowUnix()
	if _, err := Get().Exec(
		`UPDATE mischief_orders SET status = ?, detail = ?, updated_at = ?
		  WHERE guid = ? AND status = ?`,
		status, detail, now, guid, MischiefPending,
	); err != nil {
		return MischiefOrder{}, fmt.Errorf("mischief: resolve order: %w", err)
	}
	// Whether the update moved a pending order or matched nothing (missing, or
	// already terminal from an earlier verdict), the current row is the
	// authoritative answer — reading it back is the single path either way, and
	// MischiefOrderByGUID turns a missing row into ErrNoSuchOrder for the caller.
	return MischiefOrderByGUID(guid)
}

// MischiefOrderByGUID reads one order.
func MischiefOrderByGUID(guid string) (MischiefOrder, error) {
	rows, err := Get().Query(
		`SELECT guid, buyer_sub, buyer_username, target_token, target_name, tier, signed, status, COALESCE(detail, ''), created_at, updated_at
		   FROM mischief_orders WHERE guid = ?`, guid,
	)
	if err != nil {
		return MischiefOrder{}, fmt.Errorf("mischief: read order: %w", err)
	}
	defer rows.Close()
	out, err := scanMischiefOrders(rows)
	if err != nil {
		return MischiefOrder{}, err
	}
	if len(out) == 0 {
		return MischiefOrder{}, ErrNoSuchOrder
	}
	return out[0], nil
}

// MischiefOrdersByBuyer returns a buyer's own recent orders, newest first, for
// the storefront status panel. Keyed on the OIDC subject so a username change
// doesn't strand a buyer's history.
func MischiefOrdersByBuyer(buyerSub string, limit int) ([]MischiefOrder, error) {
	if limit <= 0 {
		limit = 20
	}
	rows, err := Get().Query(
		`SELECT guid, buyer_sub, buyer_username, target_token, target_name, tier, signed, status, COALESCE(detail, ''), created_at, updated_at
		   FROM mischief_orders
		  WHERE buyer_sub = ?
		  ORDER BY created_at DESC
		  LIMIT ?`,
		buyerSub, limit,
	)
	if err != nil {
		return nil, fmt.Errorf("mischief: orders by buyer: %w", err)
	}
	defer rows.Close()
	return scanMischiefOrders(rows)
}

// CountMischiefOrdersSince counts how many orders a buyer has placed since a unix
// cutoff. It backs the storefront's burst guard — the real economic caps (per
// day, per target, boss weekly) live on gogobee, this only blunts form-spam.
func CountMischiefOrdersSince(buyerSub string, since int64) (int, error) {
	var n int
	err := Get().QueryRow(
		`SELECT COUNT(*) FROM mischief_orders WHERE buyer_sub = ? AND created_at >= ?`,
		buyerSub, since,
	).Scan(&n)
	if err != nil {
		return 0, fmt.Errorf("mischief: count recent orders: %w", err)
	}
	return n, nil
}

func scanMischiefOrders(rows *sql.Rows) ([]MischiefOrder, error) {
	var out []MischiefOrder
	for rows.Next() {
		var o MischiefOrder
		var sig int
		if err := rows.Scan(&o.GUID, &o.BuyerSub, &o.BuyerUsername, &o.TargetToken,
			&o.TargetName, &o.Tier, &sig, &o.Status, &o.Detail, &o.CreatedAt, &o.UpdatedAt); err != nil {
			return nil, fmt.Errorf("mischief: scan order: %w", err)
		}
		o.Signed = sig != 0
		out = append(out, o)
	}
	return out, rows.Err()
}

// ---- user_euro: the buyer's own advisory balance -------------------------------

// ReplaceUserEuro swaps the whole balance table for a new snapshot, in one
// transaction, mirroring the roster: a buyer gogobee stopped reporting (opted
// out, deleted character) must drop out rather than keep a frozen number forever.
// Replace — never merge.
func ReplaceUserEuro(balances []MischiefBalance, snapshotAt int64) error {
	tx, err := Get().Begin()
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := tx.Exec(`DELETE FROM user_euro`); err != nil {
		return err
	}
	stmt, err := tx.Prepare(`INSERT INTO user_euro (username, euro, snapshot_at) VALUES (?, ?, ?)`)
	if err != nil {
		return err
	}
	defer stmt.Close()
	for _, b := range balances {
		if b.Username == "" {
			continue
		}
		if _, err := stmt.Exec(b.Username, b.Euro, snapshotAt); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// MischiefBalance is one buyer's advisory euro balance in the roster push.
type MischiefBalance struct {
	Username string  `json:"username"`
	Euro     float64 `json:"euro"`
}

// ---- mischief_tiers: the price catalog, pushed by gogobee -----------------------

// MischiefTier is one rung of the storefront's price list. gogobee is the sole
// authority on prices — it pushes the whole catalog on the roster tick so a fee
// retune reaches the storefront within a snapshot and Pete never hardcodes a
// number that can silently drift. Pete uses it only to render and to validate a
// submitted tier key; the real debit is always gogobee's, at its own price.
type MischiefTier struct {
	Key       string `json:"key"`
	Display   string `json:"display"`
	Fee       int    `json:"fee"`
	SignedFee int    `json:"signed_fee"`
	Blurb     string `json:"blurb,omitempty"`
}

// ReplaceMischiefTiers swaps the whole catalog for gogobee's latest, preserving
// the push order (grunt→boss) via an ordinal column. Replace, never merge — a
// tier gogobee dropped must disappear from the storefront.
func ReplaceMischiefTiers(tiers []MischiefTier) error {
	tx, err := Get().Begin()
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := tx.Exec(`DELETE FROM mischief_tiers`); err != nil {
		return err
	}
	stmt, err := tx.Prepare(`INSERT INTO mischief_tiers (key, display, fee, signed_fee, blurb, ordinal) VALUES (?, ?, ?, ?, ?, ?)`)
	if err != nil {
		return err
	}
	defer stmt.Close()
	for i, t := range tiers {
		if t.Key == "" {
			continue
		}
		if _, err := stmt.Exec(t.Key, t.Display, t.Fee, t.SignedFee, t.Blurb, i); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// MischiefTiers returns the catalog in push order. Empty (not an error) until
// gogobee has pushed one — the storefront treats that as "catalog not ready yet".
func MischiefTiers() ([]MischiefTier, error) {
	rows, err := Get().Query(
		`SELECT key, display, fee, signed_fee, COALESCE(blurb, '') FROM mischief_tiers ORDER BY ordinal`)
	if err != nil {
		return nil, fmt.Errorf("mischief: read tiers: %w", err)
	}
	defer rows.Close()
	var out []MischiefTier
	for rows.Next() {
		var t MischiefTier
		if err := rows.Scan(&t.Key, &t.Display, &t.Fee, &t.SignedFee, &t.Blurb); err != nil {
			return nil, fmt.Errorf("mischief: scan tier: %w", err)
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

// MischiefTierByKey looks up one tier for validating a submitted order.
func MischiefTierByKey(key string) (MischiefTier, bool, error) {
	tiers, err := MischiefTiers()
	if err != nil {
		return MischiefTier{}, false, err
	}
	for _, t := range tiers {
		if t.Key == key {
			return t, true, nil
		}
	}
	return MischiefTier{}, false, nil
}

// UserEuro reads one buyer's advisory balance. The bool is false when gogobee has
// never reported a balance for them (never played, opted out) — the storefront
// then shows tiers without an affordability hint rather than a misleading €0.
func UserEuro(username string) (float64, bool, error) {
	var euro float64
	err := Get().QueryRow(`SELECT euro FROM user_euro WHERE username = ?`, username).Scan(&euro)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, false, nil
	}
	if err != nil {
		return 0, false, fmt.Errorf("mischief: read user euro: %w", err)
	}
	return euro, true, nil
}
