package storage

import (
	"database/sql"
	"encoding/json"
)

// PlayerDetail is one player's private, owner-only expansion — inventory, vault,
// house, pets — pushed by gogobee keyed by localpart. Pete stores it in its own
// keyspace (player_self_detail) and only ever serves it back to the one
// authenticated user it belongs to. Token rides along so the detail page can
// prove owner↔page without ever reversing the anonymous roster token.
type PlayerDetail struct {
	Localpart string     `json:"localpart"`
	Token     string     `json:"token"`
	Inventory []ItemView `json:"inventory,omitempty"`
	Vault     []ItemView `json:"vault,omitempty"`
	Equipped  []ItemView `json:"equipped,omitempty"`
	House     HouseView  `json:"house"`
	Pets      []PetView  `json:"pets,omitempty"`
}

// ItemView is one item in a private panel — backpack, vault, or worn.
//
// Desc and Effect arrive already resolved: gogobee's inventory rows carry no
// description, and the combat delta is computed from the item rather than
// stored. Effect is the game engine's own summary, not Pete's guess at one — if
// it ever disagrees with what the item does in a fight, that is a gogobee bug
// and not something Pete can paper over here.
//
// Attunement means the item wants a bond; Attuned means it has one. Only worn
// items can be Attuned — equipping moves the row out of gogobee's inventory
// table entirely, so a backpack item's bond state isn't false, it's undefined.
type ItemView struct {
	// ID is the adventure_inventory row id, sent only for a backpack item that can
	// be worn through the magic-item path — so a non-zero ID doubles as "this item
	// has an Equip button." Worn items carry none: unequip keys on Slot. The id is
	// the handle an equip order round-trips back to gogobee to name the item.
	ID          int64  `json:"id,omitempty"`
	Name        string `json:"name"`
	Type        string `json:"type"`
	Tier        int    `json:"tier"`
	Value       int64  `json:"value"`
	Temper      int    `json:"temper,omitempty"`
	Slot        string `json:"slot,omitempty"`
	SkillSource string `json:"skill_source,omitempty"`
	Desc        string `json:"desc,omitempty"`
	Effect      string `json:"effect,omitempty"`
	Attunement  bool   `json:"attunement,omitempty"`
	Attuned     bool   `json:"attuned,omitempty"`
	// Compare, set only on backpack magic items (the ones carrying an equip ID),
	// pairs this item against what is worn in the slot it would equip into. gogobee
	// computes the verdict and per-stat deltas (the power math needs tempering and
	// bond state, which live in the engine); Pete only renders it. Owner-private,
	// rides detail_json — no public exposure. Item names here are game-authored, so
	// there is no injection surface like the LLM dispatch prose.
	Compare *ItemCompare `json:"compare,omitempty"`
}

// ItemCompare is gogobee's verdict for equipping a backpack magic item over what
// is currently worn in its slot. Pete renders it verbatim and does no arithmetic.
type ItemCompare struct {
	// Verdict: upgrade, downgrade, sidegrade, same, new, or inert.
	Verdict string `json:"verdict"`
	// VsName is the worn item being replaced; "" when Verdict is new (empty slot).
	VsName string `json:"vs_name,omitempty"`
	// VsSlot is the slot the item would land in (e.g. "ring_1").
	VsSlot string `json:"vs_slot,omitempty"`
	// Deltas is one entry per changed stat, each pre-flagged better/worse.
	Deltas []ItemDelta `json:"deltas,omitempty"`
}

// ItemDelta is one stat's change between the candidate and the worn item.
type ItemDelta struct {
	Label  string `json:"label"`
	Better bool   `json:"better"`
	Text   string `json:"text"`
}

// HouseView is the owner's housing summary.
type HouseView struct {
	Tier        int     `json:"tier"`
	LoanBalance int     `json:"loan_balance,omitempty"`
	Autopay     bool    `json:"autopay,omitempty"`
	Rate        float64 `json:"rate,omitempty"`
}

// PetView is one pet slot.
type PetView struct {
	Type      string `json:"type"`
	Name      string `json:"name"`
	Level     int    `json:"level"`
	XP        int    `json:"xp,omitempty"`
	ArmorTier int    `json:"armor_tier,omitempty"`
}

// ReplacePlayerDetail swaps the whole private-detail set in one transaction —
// replace, never merge, the same contract as the roster: a player who dropped
// out of gogobee's push must lose their stale self-view rather than have it
// linger. localpart is lowercased upstream to match how a session Username reads.
func ReplacePlayerDetail(players []PlayerDetail, snapshotAt int64) error {
	tx, err := Get().Begin()
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	if _, err := tx.Exec(`DELETE FROM player_self_detail`); err != nil {
		return err
	}
	stmt, err := tx.Prepare(`
		INSERT INTO player_self_detail (localpart, token, detail_json, snapshot_at)
		VALUES (?, ?, ?, ?)`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	for _, p := range players {
		if p.Localpart == "" || p.Token == "" {
			continue // a self-view with no owner or no page to hang on is unusable
		}
		body, err := json.Marshal(p)
		if err != nil {
			return err
		}
		if _, err := stmt.Exec(p.Localpart, p.Token, string(body), snapshotAt); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// PlayerDetailByOwner returns the private detail for localpart, but only when it
// owns the given page token. This is the ownership join the detail page needs:
// the signed-in user's localpart is trusted (it comes from their verified
// session), and a row exists only if gogobee pushed that same (localpart, token)
// pair — so a viewer can only ever unlock the self extras on their own page, and
// Pete never has to turn a token back into a handle to decide it.
func PlayerDetailByOwner(localpart, token string) (PlayerDetail, bool, error) {
	if localpart == "" || token == "" {
		return PlayerDetail{}, false, nil
	}
	var storedToken, detailJSON string
	err := Get().QueryRow(
		`SELECT token, detail_json FROM player_self_detail WHERE localpart = ?`, localpart).
		Scan(&storedToken, &detailJSON)
	if err == sql.ErrNoRows {
		return PlayerDetail{}, false, nil
	}
	if err != nil {
		return PlayerDetail{}, false, err
	}
	if storedToken != token {
		return PlayerDetail{}, false, nil // signed in, but not the owner of this page
	}
	var pd PlayerDetail
	if err := json.Unmarshal([]byte(detailJSON), &pd); err != nil {
		return PlayerDetail{}, false, err
	}
	pd.Localpart = localpart
	pd.Token = storedToken
	return pd, true, nil
}

// SelfToken returns the roster token owned by localpart, if gogobee's last push
// carried one. Lets the board mark "your adventurer" without exposing the
// localpart↔token map anywhere public.
func SelfToken(localpart string) (string, bool) {
	if localpart == "" {
		return "", false
	}
	var token string
	err := Get().QueryRow(
		`SELECT token FROM player_self_detail WHERE localpart = ?`, localpart).Scan(&token)
	if err != nil {
		return "", false
	}
	return token, true
}
