package main

// Galaxy / star-map view. Reads the running savegame's `Sectors.yaml`
// (Eleon's per-scenario star catalog) via the agent's FsRead RPC, parses
// the YAML on the controller, and returns a flattened solar-system list
// ready for SVG rendering on the dashboard.
//
// Sectors.yaml lives at:
//   /game-saves/Saves/Games/<gameName>/Sectors/Sectors.yaml
//
// The default game name is "DediGame" but operators can change it via
// dedicated.yaml `GameName:`. We auto-discover by listing /game-saves/Saves/Games/
// and grabbing the first dir with a Sectors/Sectors.yaml.

import (
	"context"
	"errors"
	"net/http"
	"path"
	"strings"
	"time"

	"gopkg.in/yaml.v3"

	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// sectorsYAML is the subset of Empyrion's Sectors.yaml that drives the
// galaxy view. We don't unmarshal everything — just stars + sectors +
// playfields with their coordinates.
type sectorsYAML struct {
	GalaxyMode   bool                   `yaml:"GalaxyMode"`
	SolarSystems []sectorsSolarSystem   `yaml:"SolarSystems"`
}
type sectorsSolarSystem struct {
	Name        string         `yaml:"Name"`
	Coordinates []float64      `yaml:"Coordinates"`
	StarClass   string         `yaml:"StarClass"`
	Sectors     []sectorsEntry `yaml:"Sectors"`
}
type sectorsEntry struct {
	Coordinates    []float64       `yaml:"Coordinates"`
	SectorMapType  string          `yaml:"SectorMapType"`
	Color          string          `yaml:"Color"`
	Playfields     [][]interface{} `yaml:"Playfields"`
	AllowFactions  string          `yaml:"AllowFactions"`
	DenyFactions   string          `yaml:"DenyFactions"`
}

// GET /api/instances/{id}/empyrion/galaxy
// Returns: { stars: [{ name, x, y, z, starClass, sectorCount, playfieldCount }],
//            sectors: [{ star, x, y, z, type, color, playfields: [{ name, type }] }] }
func (h *httpServer) handleEmpyrionGalaxy(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
	}

	// Discover the savegame folder. Empyrion default is DediGame but it can
	// be renamed via dedicated.yaml.GameName, so we list /game-saves/Saves/Games/.
	root := "/game-saves/Saves/Games"
	games, err := h.fsListAt(r.Context(), conn, id, root)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "fs_list_failed", err.Error())
		return
	}
	var sectorsPath string
	for _, g := range games {
		if !g.IsDir {
			continue
		}
		candidate := path.Join(root, g.Name, "Sectors", "Sectors.yaml")
		// Probe by attempting a read.
		if data, err := h.empyrionReadFile(r.Context(), conn, id, candidate); err == nil && len(data) > 0 {
			sectorsPath = candidate
			break
		} else {
			_ = data
		}
	}
	if sectorsPath == "" {
		writeError(w, http.StatusNotFound, "no_sectors", "no Sectors.yaml in /game-saves/Saves/Games/*/Sectors/")
		return
	}

	data, err := h.empyrionReadFile(r.Context(), conn, id, sectorsPath)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "read_failed", err.Error())
		return
	}

	var doc sectorsYAML
	if err := yaml.Unmarshal(data, &doc); err != nil {
		writeError(w, http.StatusInternalServerError, "yaml_parse", err.Error())
		return
	}

	type starOut struct {
		Name           string  `json:"name"`
		X              float64 `json:"x"`
		Y              float64 `json:"y"`
		Z              float64 `json:"z"`
		StarClass      string  `json:"starClass"`
		SectorCount    int     `json:"sectorCount"`
		PlayfieldCount int     `json:"playfieldCount"`
	}
	type pfOut struct {
		Name string `json:"name"`
		Type string `json:"type"`
	}
	type sectorOut struct {
		Star       string    `json:"star"`
		X          float64   `json:"x"`
		Y          float64   `json:"y"`
		Z          float64   `json:"z"`
		Type       string    `json:"type"`
		Color      string    `json:"color,omitempty"`
		Playfields []pfOut   `json:"playfields,omitempty"`
	}
	stars := make([]starOut, 0, len(doc.SolarSystems))
	sectors := make([]sectorOut, 0, 64)
	for _, s := range doc.SolarSystems {
		x, y, z := pickXYZ(s.Coordinates)
		so := starOut{
			Name:        s.Name,
			X:           x, Y: y, Z: z,
			StarClass:   s.StarClass,
			SectorCount: len(s.Sectors),
		}
		for _, sec := range s.Sectors {
			sx, sy, sz := pickXYZ(sec.Coordinates)
			pfs := make([]pfOut, 0, len(sec.Playfields))
			for _, p := range sec.Playfields {
				if len(p) >= 3 {
					name, _ := p[1].(string)
					typ, _ := p[2].(string)
					pfs = append(pfs, pfOut{Name: strings.TrimSpace(name), Type: strings.TrimSpace(typ)})
				}
			}
			so.PlayfieldCount += len(pfs)
			sectors = append(sectors, sectorOut{
				Star: s.Name, X: sx, Y: sy, Z: sz,
				Type: sec.SectorMapType, Color: sec.Color,
				Playfields: pfs,
			})
		}
		stars = append(stars, so)
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"stars":   stars,
		"sectors": sectors,
		"path":    sectorsPath,
		"galaxyMode": doc.GalaxyMode,
	})
}

func pickXYZ(coords []float64) (x, y, z float64) {
	for i, v := range coords {
		switch i {
		case 0:
			x = v
		case 1:
			y = v
		case 2:
			z = v
		}
	}
	return
}

// empyrionReadFile is a typed wrapper around the FsRead RPC matching the
// dayzXmlReadFile pattern; returns just the bytes (or an error).
func (h *httpServer) empyrionReadFile(ctx context.Context, conn *agentConn, instanceID, absPath string) ([]byte, error) {
	corrID := newCorrelationID("emp_read")
	if err := conn.Send(&panelv1.ControllerEnvelope{
		CorrelationId: corrID,
		SentAt:        timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_FsRead{
			FsRead: &panelv1.FsReadRequest{InstanceId: instanceID, Path: absPath},
		},
	}); err != nil {
		return nil, err
	}
	env, err := h.pending.Await(ctx, corrID, 15*time.Second)
	if err != nil {
		return nil, err
	}
	res := env.GetFsReadResult()
	if res == nil {
		return nil, errors.New("no fs read reply")
	}
	if res.Error != "" {
		return nil, errors.New(res.Error)
	}
	return res.Content, nil
}
