package storage

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

// The equip queue: the one adventure feature that carries intent *back* to the
// game box, and it does it the same way mischief does — no new network route.
//
// A signed-in owner, on their own detail page, asks to wear an item they own or
// take one off. Pete records only the *intent*; it never touches the game's
// equipment tables. gogobee's poll loop drains the pending orders, runs the real
// equip through its own rules (slot eviction, the 3-bond attunement cap,
// reconcile), and hands back a verdict Pete files against the order. The guid is
// the idempotency key end to end.
//
// Unlike mischief the underlying game action is NOT naturally idempotent —
// equipping consumes an inventory row and regenerates it on unequip, so replaying
// the flow would double-move items. gogobee therefore short-circuits on the order
// guid before it mutates anything (see its poller). On Pete's side the mechanic
// is mischief's exactly: a verdict only moves a still-pending order, so a retried
// verdict is a no-op.

// EquipOrder is one wear/remove request and its current standing.
type EquipOrder struct {
	GUID           string `json:"guid"`
	OwnerSub       string `json:"-"`                        // OIDC subject; keys "my orders", never sent to gogobee
	OwnerLocalpart string `json:"owner_localpart"`          // Matrix localpart gogobee turns into an MXID — the character to dress
	CharacterName  string `json:"character_name,omitempty"` // display copy, frozen at order time; gogobee ignores it
	ItemID         int64  `json:"item_id,omitempty"`        // adventure_inventory row id, for an equip; unused for unequip
	ItemName       string `json:"item_name"`                // display copy
	Slot           string `json:"slot"`                     // the magic-item slot to fill or clear
	Action         string `json:"action"`                   // equip / unequip
	Status         string `json:"status"`
	Detail         string `json:"detail,omitempty"`
	CreatedAt      int64  `json:"created_at"`
	UpdatedAt      int64  `json:"updated_at,omitempty"`
}

// Actions. These cross the wire to gogobee, so they are part of the contract.
const (
	EquipActionEquip   = "equip"
	EquipActionUnequip = "unequip"
)

// Order states. Terminal states are enumerated, not free-text, so the page can
// say something specific; detail carries the prose. The rejection set is honest
// to what gogobee's equip path can actually return: it auto-evicts a slot's
// current occupant (so there is no "slot taken") and equips over the bond cap as
// inert rather than refusing (so there is no "requirements" bounce). What is left
// is the item having moved out from under the order, or not being wearable.
const (
	EquipPending           = "pending"                 // placed; gogobee hasn't acted yet
	EquipApplied           = "applied"                 // worn/removed; detail says how (bonded, inert, ...)
	EquipRejectedNotOwned  = "rejected_not_owned"      // the item is no longer in the pack (stale page)
	EquipRejectedNotWorn   = "rejected_not_worn"       // unequip of a slot that's already empty
	EquipRejectedNotEquipp = "rejected_not_equippable" // the item has no slot to fill
)

// validEquipVerdict is the set of terminal states gogobee may hand back.
func validEquipVerdict(status string) bool {
	switch status {
	case EquipApplied, EquipRejectedNotOwned, EquipRejectedNotWorn, EquipRejectedNotEquipp:
		return true
	}
	return false
}

func validEquipAction(action string) bool {
	return action == EquipActionEquip || action == EquipActionUnequip
}

var ErrNoSuchEquipOrder = errors.New("equip: no such order")

// InsertEquipOrder records a fresh, pending order and returns it with a new guid.
// The guid is minted here so the owner sees a stable reference the instant they
// click, before gogobee has heard of it. The caller has already checked the owner
// is signed in and owns the page; eligibility (still-owned, wearable, bond cap) is
// gogobee's, at verdict time.
func InsertEquipOrder(ownerSub, ownerLocalpart, characterName string, itemID int64, itemName, slot, action string) (EquipOrder, error) {
	if !validEquipAction(action) {
		return EquipOrder{}, fmt.Errorf("equip: bad action %q", action)
	}
	guid, err := newGUID()
	if err != nil {
		return EquipOrder{}, err
	}
	now := nowUnix()
	if _, err := Get().Exec(
		`INSERT INTO equip_orders
		    (guid, owner_sub, owner_localpart, character_name, item_id, item_name, slot, action, status, created_at, updated_at)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		guid, ownerSub, ownerLocalpart, characterName, itemID, itemName, slot, action, EquipPending, now, now,
	); err != nil {
		return EquipOrder{}, fmt.Errorf("equip: insert order: %w", err)
	}
	return EquipOrder{
		GUID: guid, OwnerSub: ownerSub, OwnerLocalpart: ownerLocalpart,
		CharacterName: characterName, ItemID: itemID, ItemName: itemName,
		Slot: slot, Action: action, Status: EquipPending, CreatedAt: now, UpdatedAt: now,
	}, nil
}

// PendingEquipOrders is gogobee's poll: every order still waiting. Like mischief
// there is no claimed-but-stale window — a gogobee that dies mid-apply leaves the
// order pending to be offered again, and gogobee's own guid guard makes the replay
// a no-op.
func PendingEquipOrders(limit int) ([]EquipOrder, error) {
	if limit <= 0 {
		limit = 100
	}
	rows, err := Get().Query(
		`SELECT guid, owner_sub, owner_localpart, character_name, item_id, item_name, slot, action, status, COALESCE(detail, ''), created_at, updated_at
		   FROM equip_orders
		  WHERE status = ?
		  ORDER BY created_at
		  LIMIT ?`,
		EquipPending, limit,
	)
	if err != nil {
		return nil, fmt.Errorf("equip: pending orders: %w", err)
	}
	defer rows.Close()
	return scanEquipOrders(rows)
}

// ResolveEquipOrder files gogobee's verdict against a pending order. Idempotent by
// exactly mischief's mechanic: the UPDATE only moves a still-pending row, and the
// row is read back unconditionally so a first verdict, a retried verdict, and a
// missing row all take one path.
func ResolveEquipOrder(guid, status, detail string) (EquipOrder, error) {
	if !validEquipVerdict(status) {
		return EquipOrder{}, fmt.Errorf("equip: bad verdict %q", status)
	}
	now := nowUnix()
	if _, err := Get().Exec(
		`UPDATE equip_orders SET status = ?, detail = ?, updated_at = ?
		  WHERE guid = ? AND status = ?`,
		status, detail, now, guid, EquipPending,
	); err != nil {
		return EquipOrder{}, fmt.Errorf("equip: resolve order: %w", err)
	}
	return EquipOrderByGUID(guid)
}

// EquipOrderByGUID reads one order.
func EquipOrderByGUID(guid string) (EquipOrder, error) {
	rows, err := Get().Query(
		`SELECT guid, owner_sub, owner_localpart, character_name, item_id, item_name, slot, action, status, COALESCE(detail, ''), created_at, updated_at
		   FROM equip_orders WHERE guid = ?`, guid,
	)
	if err != nil {
		return EquipOrder{}, fmt.Errorf("equip: read order: %w", err)
	}
	defer rows.Close()
	out, err := scanEquipOrders(rows)
	if err != nil {
		return EquipOrder{}, err
	}
	if len(out) == 0 {
		return EquipOrder{}, ErrNoSuchEquipOrder
	}
	return out[0], nil
}

// EquipOrdersByOwner returns an owner's own recent orders, newest first, for the
// status strip on the detail page. Keyed on the OIDC subject so a username change
// doesn't strand history.
func EquipOrdersByOwner(ownerSub string, limit int) ([]EquipOrder, error) {
	if limit <= 0 {
		limit = 20
	}
	rows, err := Get().Query(
		`SELECT guid, owner_sub, owner_localpart, character_name, item_id, item_name, slot, action, status, COALESCE(detail, ''), created_at, updated_at
		   FROM equip_orders
		  WHERE owner_sub = ?
		  ORDER BY created_at DESC
		  LIMIT ?`,
		ownerSub, limit,
	)
	if err != nil {
		return nil, fmt.Errorf("equip: orders by owner: %w", err)
	}
	defer rows.Close()
	return scanEquipOrders(rows)
}

// CountEquipOrdersSince backs the web anti-spam guard — the real eligibility check
// is gogobee's at verdict time; this only blunts a stuck mouse button.
func CountEquipOrdersSince(ownerSub string, since int64) (int, error) {
	var n int
	err := Get().QueryRow(
		`SELECT COUNT(*) FROM equip_orders WHERE owner_sub = ? AND created_at >= ?`,
		ownerSub, since,
	).Scan(&n)
	if err != nil {
		return 0, fmt.Errorf("equip: count recent orders: %w", err)
	}
	return n, nil
}

func scanEquipOrders(rows *sql.Rows) ([]EquipOrder, error) {
	var out []EquipOrder
	for rows.Next() {
		var o EquipOrder
		if err := rows.Scan(&o.GUID, &o.OwnerSub, &o.OwnerLocalpart, &o.CharacterName,
			&o.ItemID, &o.ItemName, &o.Slot, &o.Action, &o.Status, &o.Detail,
			&o.CreatedAt, &o.UpdatedAt); err != nil {
			return nil, fmt.Errorf("equip: scan order: %w", err)
		}
		out = append(out, o)
	}
	return out, rows.Err()
}
