package main

// Phase-5b extras: recipes, loot tables, dedi-stats sparkline, chat-log
// viewer, faction alliance dashboard, blueprint browser, PDA missions.
//
// Recipes + loot are static catalogs (read from embedded ECFs). The other
// four are runtime / DB-backed: blueprints come from the agent's filesystem,
// dedi-stats / chat / missions come from the empyrion_events table the
// SSE subscriber populates.

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"sync"

	"github.com/dbledeez/panel/pkg/empyrionitems"
)

//go:embed empyrion-data/Templates.ecf
var empyrionTemplatesECF []byte

//go:embed empyrion-data/LootGroups.ecf
var empyrionLootECF []byte

var (
	empyrionTemplatesOnce sync.Once
	empyrionTemplatesIdx  *empyrionitems.TemplateIndex
	empyrionTemplatesErr  error

	empyrionLootOnce sync.Once
	empyrionLootIdx  *empyrionitems.LootIndex
	empyrionLootErr  error
)

func loadEmpyrionTemplates() (*empyrionitems.TemplateIndex, error) {
	empyrionTemplatesOnce.Do(func() {
		empyrionTemplatesIdx, empyrionTemplatesErr = empyrionitems.LoadTemplatesFromReader(bytes.NewReader(empyrionTemplatesECF))
	})
	return empyrionTemplatesIdx, empyrionTemplatesErr
}

func loadEmpyrionLoot() (*empyrionitems.LootIndex, error) {
	empyrionLootOnce.Do(func() {
		empyrionLootIdx, empyrionLootErr = empyrionitems.LoadLootFromReader(bytes.NewReader(empyrionLootECF))
	})
	return empyrionLootIdx, empyrionLootErr
}

// GET /api/empyrion/recipe/{name} — crafting template for an item.
// Returns 404 if no recipe exists (BaseItem ingredients have no recipe).
func (h *httpServer) handleEmpyrionRecipe(w http.ResponseWriter, r *http.Request) {
	idx, err := loadEmpyrionTemplates()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "load_failed", err.Error())
		return
	}
	t := idx.ByName(r.PathValue("name"))
	if t == nil {
		writeError(w, http.StatusNotFound, "no_recipe", "no template for that item")
		return
	}
	used := idx.UsedIn(r.PathValue("name"))
	writeJSON(w, http.StatusOK, map[string]any{
		"recipe":  t,
		"usedIn":  used, // templates that consume this item as input
	})
}

// GET /api/empyrion/loot/{name} — loot group contents.
func (h *httpServer) handleEmpyrionLootGroup(w http.ResponseWriter, r *http.Request) {
	idx, err := loadEmpyrionLoot()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "load_failed", err.Error())
		return
	}
	g := idx.ByName(r.PathValue("name"))
	if g == nil {
		writeError(w, http.StatusNotFound, "no_group", "no loot group with that name")
		return
	}
	writeJSON(w, http.StatusOK, g)
}

// GET /api/empyrion/loot — list all groups.
func (h *httpServer) handleEmpyrionLootList(w http.ResponseWriter, r *http.Request) {
	idx, err := loadEmpyrionLoot()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "load_failed", err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"groups": idx.Groups, "count": len(idx.Groups)})
}

// GET /api/empyrion/items/{id}/dropped-by — reverse loot lookup by item ID.
func (h *httpServer) handleEmpyrionItemDroppedBy(w http.ResponseWriter, r *http.Request) {
	itemIdx, err := loadEmpyrionItems()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "load_failed", err.Error())
		return
	}
	id, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		writeError(w, http.StatusBadRequest, "bad_id", "id must be integer")
		return
	}
	it := itemIdx.ByID(id)
	if it == nil {
		writeError(w, http.StatusNotFound, "no_item", "item not found")
		return
	}
	lootIdx, err := loadEmpyrionLoot()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "load_failed", err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"itemId":   id,
		"itemName": it.Name,
		"groups":   lootIdx.DroppedBy(it.Name),
	})
}

// GET /api/instances/{id}/empyrion-history/dedi-stats?window=1h
// Returns an array of {ts, fps, heap, players, pfsRunning, pfsInactive, pfsActive, ticks}
// extracted from the raw JSON stored in empyrion_events for cmd=Event_Dedi_Stats.
func (h *httpServer) handleEmpyrionDediStats(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	window := r.URL.Query().Get("window")
	if window == "" {
		window = "1h"
	}
	rows, err := h.db.Pool().Query(r.Context(), `
		SELECT ts, raw FROM empyrion_events
		WHERE instance_id = $1 AND cmd = 'Event_Dedi_Stats' AND ts > NOW() - $2::interval
		ORDER BY ts ASC LIMIT 5000`, id, window)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	defer rows.Close()
	type pt struct {
		TS          string  `json:"ts"`
		FPS         float64 `json:"fps"`
		HeapMB      int     `json:"heap"`
		Players     int     `json:"players"`
		PfsRunning  int     `json:"pfsRunning"`
		PfsInactive int     `json:"pfsInactive"`
		PfsActive   int     `json:"pfsActive"`
		Ticks       int64   `json:"ticks"`
	}
	out := []pt{}
	for rows.Next() {
		var ts any
		var raw []byte
		if err := rows.Scan(&ts, &raw); err != nil {
			continue
		}
		var d struct {
			Fps          float64 `json:"fps"`
			Heap         int     `json:"heap"`
			NumPlayers   int     `json:"numPlayers"`
			PfsRunning   int     `json:"pfsRunning"`
			PfsInactive  int     `json:"pfsInactive"`
			PfsActive    int     `json:"pfsActive"`
			Ticks        int64   `json:"ticks"`
		}
		_ = json.Unmarshal(raw, &d)
		out = append(out, pt{
			TS: formatTS(ts), FPS: d.Fps, HeapMB: d.Heap,
			Players: d.NumPlayers, PfsRunning: d.PfsRunning,
			PfsInactive: d.PfsInactive, PfsActive: d.PfsActive, Ticks: d.Ticks,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"points": out, "window": window})
}

// GET /api/instances/{id}/empyrion-history/chat?limit=200&channel=&player_id=
// Returns the chat-log slice. channel maps to the ChatInfo.type field
// (0=global, 1=faction, 2=allies, 3=whisper, 4=server). When channel
// is omitted, all channels are returned.
func (h *httpServer) handleEmpyrionChatLog(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	if limit <= 0 || limit > 1000 {
		limit = 200
	}
	q := `SELECT ts, player_id, player_name, msg, raw FROM empyrion_events
	      WHERE instance_id = $1 AND cmd IN ('Event_ChatMessage','Event_ChatMessageEx')`
	args := []any{id}
	if pid := r.URL.Query().Get("player_id"); pid != "" {
		args = append(args, pid)
		q += " AND player_id = $" + strconv.Itoa(len(args))
	}
	channel := r.URL.Query().Get("channel")
	args = append(args, limit)
	q += " ORDER BY ts DESC LIMIT $" + strconv.Itoa(len(args))
	rows, err := h.db.Pool().Query(r.Context(), q, args...)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	defer rows.Close()
	type m struct {
		TS         string `json:"ts"`
		PlayerID   *int   `json:"playerId,omitempty"`
		PlayerName string `json:"playerName,omitempty"`
		Msg        string `json:"msg"`
		Channel    int    `json:"channel"`
	}
	out := []m{}
	for rows.Next() {
		var ts any
		var pid *int
		var name *string
		var msg *string
		var raw []byte
		if err := rows.Scan(&ts, &pid, &name, &msg, &raw); err != nil {
			continue
		}
		mm := m{TS: formatTS(ts), PlayerID: pid}
		if name != nil {
			mm.PlayerName = *name
		}
		if msg != nil {
			mm.Msg = *msg
		}
		var d struct {
			Type int `json:"type"`
		}
		_ = json.Unmarshal(raw, &d)
		mm.Channel = d.Type
		if channel != "" {
			ch, _ := strconv.Atoi(channel)
			if mm.Channel != ch {
				continue
			}
		}
		out = append(out, mm)
	}
	writeJSON(w, http.StatusOK, map[string]any{"messages": out, "count": len(out)})
}

// GET /api/instances/{id}/empyrion/blueprints — list .epb files in the
// instance's Saves/Blueprints/.
func (h *httpServer) handleEmpyrionBlueprints(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	agentID, err := h.lookupInstance(r, id)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", err.Error())
		return
	}
	conn := h.registry.get(agentID)
	if conn == nil {
		writeError(w, http.StatusNotFound, "agent_offline", "agent not connected")
		return
	}
	files := h.empyrionListFiles(r.Context(), conn, id, "/game-saves/Saves/Blueprints", ".epb", 0)
	writeJSON(w, http.StatusOK, map[string]any{"blueprints": files, "count": len(files)})
}

// empyrionListFiles walks a directory tree (up to 3 levels) and returns
// all files with the given suffix. Used for blueprint discovery; the
// canonical Empyrion layout is Blueprints/<steamId>/<bpName>.epb.
func (h *httpServer) empyrionListFiles(ctx context.Context, conn *agentConn, instanceID, root, suffix string, depth int) []map[string]any {
	out := []map[string]any{}
	if depth > 3 {
		return out
	}
	entries, err := h.fsListAt(ctx, conn, instanceID, root)
	if err != nil {
		return out
	}
	for _, e := range entries {
		full := strings.TrimRight(root, "/") + "/" + e.Name
		if e.IsDir {
			out = append(out, h.empyrionListFiles(ctx, conn, instanceID, full, suffix, depth+1)...)
			continue
		}
		if suffix != "" && !strings.HasSuffix(strings.ToLower(e.Name), strings.ToLower(suffix)) {
			continue
		}
		out = append(out, map[string]any{"name": e.Name, "path": full, "size": e.Size})
	}
	return out
}

// POST /api/instances/{id}/empyrion/blueprints/{bpName}/spawn
// Body: {"playerId": 12345}
// Issues a console "spawn" command targeting the player's location.
func (h *httpServer) handleEmpyrionBlueprintSpawn(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	bpName := r.PathValue("bpName")
	var body struct {
		PlayerID int `json:"playerId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if body.PlayerID == 0 {
		writeError(w, http.StatusBadRequest, "missing", "playerId required")
		return
	}
	tgt, err := h.resolveEmpyrionTarget(r.Context(), id)
	if err != nil {
		writeError(w, http.StatusServiceUnavailable, "no_bridge", err.Error())
		return
	}
	bpClean := strings.ReplaceAll(bpName, "'", "")
	cmd := "spawn '" + strings.TrimSpace(bpClean) + "' " + strconv.Itoa(body.PlayerID)
	ok, msg := postBridgeBody(r.Context(), tgt, "/api/v1/console", cmd)
	if !ok {
		writeError(w, http.StatusBadGateway, "console_failed", msg)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"sent": cmd})
}

// GET /api/instances/{id}/empyrion-history/missions — last PDA state per player.
// Each Event_PdaStateChange row replaces the previous state for that
// (player_id, chapterKey) pair.
func (h *httpServer) handleEmpyrionMissions(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	rows, err := h.db.Pool().Query(r.Context(), `
		SELECT DISTINCT ON (player_id, raw->>'chapterKey')
		  ts, player_id, player_name, raw
		FROM empyrion_events
		WHERE instance_id = $1 AND cmd = 'Event_PdaStateChange'
		ORDER BY player_id, raw->>'chapterKey', ts DESC
		LIMIT 1000`, id)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	defer rows.Close()
	type m struct {
		TS         string          `json:"ts"`
		PlayerID   *int            `json:"playerId,omitempty"`
		PlayerName string          `json:"playerName,omitempty"`
		State      json.RawMessage `json:"state"`
	}
	out := []m{}
	for rows.Next() {
		var ts any
		var pid *int
		var name *string
		var raw []byte
		if err := rows.Scan(&ts, &pid, &name, &raw); err != nil {
			continue
		}
		mm := m{TS: formatTS(ts), PlayerID: pid, State: raw}
		if name != nil {
			mm.PlayerName = *name
		}
		out = append(out, mm)
	}
	writeJSON(w, http.StatusOK, map[string]any{"missions": out})
}

// postBridgeBody is a small helper for one-shot text/plain POSTs.
func postBridgeBody(ctx context.Context, tgt *empyrionTarget, path, body string) (bool, string) {
	return postBridgeJSON(ctx, tgt, path, []byte(body))
}
