package main

// Empyrion item directory — a panel-side static catalog of every Item / Block /
// Device defined by stock Empyrion's `Content/Configuration/ItemsConfig.ecf` +
// `BlocksConfig.ecf`. Used by the EAH-style give-item picker, ID→Name lookups
// for the inventory view, and category filters.
//
// Stock vanilla totals on Empyrion 1.16.1: ~698 Items + ~2549 Blocks/Devices.
// The catalog is embedded at build time so the panel works without a live
// game install on disk; scenario-specific overrides (Reforged Eden 2 etc.)
// will layer on top in a later sprint.

import (
	"bytes"
	"embed"
	_ "embed"
	"encoding/csv"
	"io/fs"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

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

//go:embed empyrion-data/ItemsConfig.ecf
var empyrionItemsConfigECF []byte

//go:embed empyrion-data/BlocksConfig.ecf
var empyrionBlocksConfigECF []byte

//go:embed empyrion-data/Localization.csv
var empyrionLocalizationCSV []byte

// 1,750 PNG item icons extracted from EmpAdminHelper V01.61.5.0 Full /Items.
// Filename convention is `<ItemName>.png`, sometimes lowercase. We do
// case-insensitive lookups against the parsed item index's Name.
//go:embed empyrion-data/icons/*.png
var empyrionIconFS embed.FS

var (
	empyrionIconOnce  sync.Once
	empyrionIconIndex map[string]string // lowercase-name → embedded path
)

func loadEmpyrionIconIndex() map[string]string {
	empyrionIconOnce.Do(func() {
		empyrionIconIndex = map[string]string{}
		entries, err := fs.ReadDir(empyrionIconFS, "empyrion-data/icons")
		if err != nil {
			return
		}
		for _, e := range entries {
			name := e.Name()
			if !strings.HasSuffix(name, ".png") {
				continue
			}
			stem := strings.TrimSuffix(name, ".png")
			empyrionIconIndex[strings.ToLower(stem)] = "empyrion-data/icons/" + name
		}
	})
	return empyrionIconIndex
}

var (
	empyrionItemsOnce  sync.Once
	empyrionItemsIndex *empyrionitems.Index
	empyrionItemsErr   error

	empyrionLocOnce sync.Once
	empyrionLocMap  map[string]map[string]string // canonicalKey → langName → display
	empyrionLocLangs []string
)

// loadLocalization parses Empyrion's Localization.csv. Header row gives
// language column names; each subsequent row maps a canonical key (the
// same Name token used in ItemsConfig/BlocksConfig) to translations.
func loadLocalization() (map[string]map[string]string, []string) {
	empyrionLocOnce.Do(func() {
		rdr := csv.NewReader(bytes.NewReader(empyrionLocalizationCSV))
		rdr.FieldsPerRecord = -1 // tolerate variable-length rows (some entries have empty trailing langs)
		header, err := rdr.Read()
		if err != nil {
			return
		}
		// Drop the leading "KEY" column from the language list.
		empyrionLocLangs = make([]string, 0, len(header)-1)
		if len(header) > 0 {
			empyrionLocLangs = append(empyrionLocLangs, header[1:]...)
		}
		empyrionLocMap = map[string]map[string]string{}
		for {
			row, err := rdr.Read()
			if err != nil {
				break
			}
			if len(row) < 2 {
				continue
			}
			key := strings.TrimSpace(row[0])
			if key == "" {
				continue
			}
			tr := map[string]string{}
			for i, lang := range empyrionLocLangs {
				if i+1 >= len(row) {
					break
				}
				v := strings.TrimSpace(row[i+1])
				if v != "" {
					tr[lang] = v
				}
			}
			empyrionLocMap[key] = tr
		}
	})
	return empyrionLocMap, empyrionLocLangs
}

// loadEmpyrionItems lazily parses the embedded ECF files into a merged index.
// BlocksConfig is read after ItemsConfig so block IDs win on collisions —
// same precedence Empyrion uses internally.
func loadEmpyrionItems() (*empyrionitems.Index, error) {
	empyrionItemsOnce.Do(func() {
		empyrionItemsIndex, empyrionItemsErr = empyrionitems.LoadFromReaders(
			bytes.NewReader(empyrionItemsConfigECF),
			bytes.NewReader(empyrionBlocksConfigECF),
		)
	})
	return empyrionItemsIndex, empyrionItemsErr
}

// handleEmpyrionItemsList is GET /api/empyrion/items?q=&category=&limit=&lang=English
// Returns a JSON list of items matching the filter. No instance association
// — this is a static catalog, same response for every operator. The optional
// `lang` parameter promotes localized display names from Localization.csv;
// search still matches against the canonical name (lang is purely cosmetic).
func (h *httpServer) handleEmpyrionItemsList(w http.ResponseWriter, r *http.Request) {
	idx, err := loadEmpyrionItems()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "load_failed", err.Error())
		return
	}
	q := r.URL.Query().Get("q")
	cat := r.URL.Query().Get("category")
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	if limit <= 0 {
		limit = 200
	}
	hits := idx.Search(q, cat, limit)

	// If lang param present, fold display names from Localization.csv.
	lang := strings.TrimSpace(r.URL.Query().Get("lang"))
	locMap, langs := loadLocalization()
	if lang != "" && locMap != nil {
		for i := range hits {
			if tr, ok := locMap[hits[i].Name]; ok {
				if v := tr[lang]; v != "" {
					if hits[i].Extras == nil {
						hits[i].Extras = map[string]string{}
					}
					hits[i].Extras["display"] = v
				}
			}
		}
	}

	writeJSON(w, http.StatusOK, map[string]any{
		"total":      len(idx.Items),
		"returned":   len(hits),
		"limit":      limit,
		"q":          q,
		"category":   cat,
		"lang":       lang,
		"langs":      langs,
		"items":      hits,
		"categories": idx.Categories(),
	})
}

// handleEmpyrionItemIcon serves the PNG icon for an item ID. Resolves
// item.Name → empyrion-data/icons/<name>.png case-insensitively. 24-hour
// browser cache + immutable since icons are baked into the binary.
func (h *httpServer) handleEmpyrionItemIcon(w http.ResponseWriter, r *http.Request) {
	idx, 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 := idx.ByID(id)
	if it == nil {
		http.NotFound(w, r)
		return
	}
	icons := loadEmpyrionIconIndex()
	path, ok := icons[strings.ToLower(it.Name)]
	if !ok {
		// Fallback: some icons are keyed off the canonical (lowercased) name
		// only. Try a few common variants before giving up.
		for _, candidate := range []string{strings.ToLower(it.Name), it.Name} {
			if p, ok2 := icons[strings.ToLower(candidate)]; ok2 {
				path = p
				ok = ok2
				break
			}
		}
	}
	if !ok {
		http.NotFound(w, r)
		return
	}
	data, err := empyrionIconFS.ReadFile(path)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "image/png")
	w.Header().Set("Cache-Control", "public, max-age=86400, immutable")
	w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
	_, _ = w.Write(data)
}

// handleEmpyrionItemByID returns a single item by numeric ID.
func (h *httpServer) handleEmpyrionItemByID(w http.ResponseWriter, r *http.Request) {
	idx, 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 := idx.ByID(id)
	if it == nil {
		writeError(w, http.StatusNotFound, "not_found", "no item with that id")
		return
	}
	writeJSON(w, http.StatusOK,it)
}
