package web

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"

	"pete/internal/storage"
)

// The adventurer detail page.
//
// A click-through from the live board: anyone may see a mark's current stats and
// equipped gear (the same anonymity model as the board — a character name and a
// sheet, never a Matrix handle). The signed-in owner sees the same page enriched
// with their private inventory, vault, house, and pets.
//
// Both halves are still gogobee → Pete: the public detail rides the roster
// snapshot, the private detail rides its own push. Pete renders what it was
// given; it never reaches back into the game box.

// whoDetail is the public sheet as gogobee pushed it (RosterEntry.Detail).
type whoDetail struct {
	HPCurrent   int       `json:"hp_current"`
	HPMax       int       `json:"hp_max"`
	TempHP      int       `json:"temp_hp"`
	ArmorClass  int       `json:"armor_class"`
	Abilities   [6]int    `json:"abilities"`
	Modifiers   [6]int    `json:"modifiers"`
	Gear        []whoGear `json:"gear"`
	Supplies    int       `json:"supplies"`
	ThreatLevel int       `json:"threat_level"`
	Room        string    `json:"room"`
}

type whoGear struct {
	Slot       string `json:"slot"`
	Name       string `json:"name"`
	Tier       int    `json:"tier"`
	Condition  int    `json:"condition"`
	Masterwork bool   `json:"masterwork"`
}

// abilityRow is one ability line, pre-formatted for the template.
type abilityRow struct {
	Label  string
	Score  int
	ModStr string // "+2", "-1"
}

var abilityLabels = [6]string{"STR", "DEX", "CON", "INT", "WIS", "CHA"}

type whoPage struct {
	pageData
	Mark      RosterView
	Stale     bool
	HasDetail bool
	Detail    whoDetail
	Abilities []abilityRow
	HasSelf   bool
	Self      storage.PlayerDetail
}

// handleAdventureWho serves one adventurer's detail page. Public; 404s when the
// token isn't on the current board — the same liveness gate the storefront uses,
// so a stale or guessed token never resolves to a page.
func (s *Server) handleAdventureWho(w http.ResponseWriter, r *http.Request) {
	if !s.adv.Enabled {
		http.NotFound(w, r)
		return
	}
	token := r.PathValue("token")
	entry, ok, err := storage.RosterEntryByToken(token)
	if err != nil {
		slog.Error("who: roster lookup failed", "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if !ok {
		http.NotFound(w, r)
		return
	}
	s.track(r, "adventure")

	base := s.base(r)
	base.Active = "adventure"
	base.NoIndex = true // names a player character; keep out of search indexes

	page := whoPage{
		pageData: base,
		Mark:     toRosterView(entry),
		Stale:    storage.RosterSnapshotAt() == 0,
	}
	if d, abil, ok := decodeWhoDetail(entry.Detail); ok {
		page.HasDetail = true
		page.Detail = d
		page.Abilities = abil
	}

	// Owner enrichment: only when the signed-in user's localpart owns this exact
	// page token. The ownership is proven by a row gogobee pushed, never by
	// reversing the token, so no visitor can unlock another player's self extras.
	if s.auth != nil {
		if u := s.auth.userFromRequest(r); u != nil {
			if self, ok, err := storage.PlayerDetailByOwner(buyerLocalpart(u), token); err == nil && ok {
				page.HasSelf = true
				page.Self = self
			}
		}
	}

	s.render(w, "who", page)
}

// handleAdventureWhoAPI serves the public detail as JSON for the page's live
// re-poll, so an open tab tracks HP / room / supplies as they move. Public — the
// same exposure as the rendered page. The private self extras are not included:
// inventory changes rarely and stays server-rendered behind the ownership check.
func (s *Server) handleAdventureWhoAPI(w http.ResponseWriter, r *http.Request) {
	if !s.adv.Enabled {
		http.NotFound(w, r)
		return
	}
	token := r.PathValue("token")
	entry, ok, err := storage.RosterEntryByToken(token)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	if !ok {
		// Gone from the board (expedition ended, opted out): tell the poller so it
		// can stop, rather than 404-ing an open tab into an error.
		_ = json.NewEncoder(w).Encode(map[string]any{"live": false})
		return
	}
	d, abil, hasDetail := decodeWhoDetail(entry.Detail)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"live":       true,
		"mark":       toRosterView(entry),
		"has_detail": hasDetail,
		"detail":     d,
		"abilities":  abil,
	})
}

// decodeWhoDetail unpacks the public detail blob and builds the ability rows.
// Returns ok=false when there is no detail (a snapshot from before the detail
// push), so the page can fall back to the summary the board already has.
func decodeWhoDetail(raw json.RawMessage) (whoDetail, []abilityRow, bool) {
	if len(raw) == 0 {
		return whoDetail{}, nil, false
	}
	var d whoDetail
	if err := json.Unmarshal(raw, &d); err != nil {
		return whoDetail{}, nil, false
	}
	rows := make([]abilityRow, 0, 6)
	for i := 0; i < 6; i++ {
		rows = append(rows, abilityRow{
			Label:  abilityLabels[i],
			Score:  d.Abilities[i],
			ModStr: fmt.Sprintf("%+d", d.Modifiers[i]),
		})
	}
	return d, rows, true
}
