package main

// Per-player POI discovery view. Forwards to the agent that owns the
// instance — the agent has filesystem access to the saves volume and
// runs the SQL query against global.db locally.
//
// Two surfaces:
//   GET /api/instances/{id}/empyrion/discoveries?player=<sid>&mode=<>
//     — session-auth, drives the EAH Discoveries pill
//   GET /admin/v1/empyrion/discoveries?instance=<iid>&player=<sid>&mode=<>
//     — admin-token-auth, used by AMP-Monitor's player /me page proxy

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

	"google.golang.org/protobuf/types/known/timestamppb"

	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

var (
	errAgentOffline  = errors.New("agent not connected")
	errBadAgentReply = errors.New("agent returned an envelope with no discoveries result")
)

// poiDTO is the JSON shape returned to API consumers — flatter than
// the proto, with friendlier field names.
type poiDTO struct {
	SolarSystem       string `json:"solar_system"`
	Playfield         string `json:"playfield"`
	Name              string `json:"name"`
	Type              int32  `json:"type"`
	GameTime          int64  `json:"game_time"`
	DiscovererName    string `json:"discoverer_name,omitempty"`
	DiscovererSteamID string `json:"discoverer_steam_id,omitempty"`
	FactionGroup      int32  `json:"faction_group,omitempty"`
	FactionID         int32  `json:"faction_id,omitempty"`
}

func (h *httpServer) fetchEmpyrionDiscoveries(ctx context.Context, instanceID, agentID, steamID, mode string) (map[string]any, int, error) {
	conn := h.registry.get(agentID)
	if conn == nil {
		return nil, http.StatusServiceUnavailable, errAgentOffline
	}
	corrID := newCorrelationID("emp-disc")
	if err := conn.Send(&panelv1.ControllerEnvelope{
		CorrelationId: corrID,
		SentAt:        timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_EmpyrionDiscoveries{
			EmpyrionDiscoveries: &panelv1.EmpyrionDiscoveriesRequest{
				InstanceId: instanceID,
				SteamId:    steamID,
				Mode:       mode,
			},
		},
	}); err != nil {
		return nil, http.StatusBadGateway, err
	}
	env, err := h.pending.Await(ctx, corrID, 65*time.Second)
	if err != nil {
		return nil, http.StatusGatewayTimeout, err
	}
	res := env.GetEmpyrionDiscoveriesResult()
	if res == nil {
		return nil, http.StatusInternalServerError, errBadAgentReply
	}
	if !res.Ok {
		return map[string]any{
			"error":      res.Error,
			"steam_id":   res.SteamId,
			"mode":       res.Mode,
			"pois":       []poiDTO{},
			"count":      0,
		}, http.StatusOK, nil
	}
	out := make([]poiDTO, 0, len(res.Pois))
	for _, p := range res.Pois {
		out = append(out, poiDTO{
			SolarSystem:       p.SolarSystem,
			Playfield:         p.Playfield,
			Name:              p.PoiName,
			Type:              p.PoiType,
			GameTime:          p.GameTime,
			DiscovererName:    p.DiscovererName,
			DiscovererSteamID: p.DiscovererSteamId,
			FactionGroup:      p.FactionGroup,
			FactionID:         p.FactionId,
		})
	}
	return map[string]any{
		"steam_id":    res.SteamId,
		"player_name": res.PlayerName,
		"mode":        res.Mode,
		"pois":        out,
		"count":       len(out),
	}, http.StatusOK, nil
}

func (h *httpServer) handleEmpyrionDiscoveries(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
	}
	steamID := strings.TrimSpace(r.URL.Query().Get("player"))
	if steamID == "" {
		writeError(w, http.StatusBadRequest, "missing_player", "player query param required (SteamID64)")
		return
	}
	mode := strings.TrimSpace(r.URL.Query().Get("mode"))
	body, code, err := h.fetchEmpyrionDiscoveries(r.Context(), id, agentID, steamID, mode)
	if err != nil {
		writeError(w, code, "fetch_failed", err.Error())
		return
	}
	writeJSON(w, code, body)
}

// fetchEmpyrionPlayerSummary forwards a player-summary request to the
// agent that owns the given instance and packages the result for HTTP.
func (h *httpServer) fetchEmpyrionPlayerSummary(ctx context.Context, instanceID, agentID, steamID string) (map[string]any, int, error) {
	conn := h.registry.get(agentID)
	if conn == nil {
		return nil, http.StatusServiceUnavailable, errAgentOffline
	}
	corrID := newCorrelationID("emp-sum")
	if err := conn.Send(&panelv1.ControllerEnvelope{
		CorrelationId: corrID,
		SentAt:        timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_EmpyrionPlayerSummary{
			EmpyrionPlayerSummary: &panelv1.EmpyrionPlayerSummaryRequest{
				InstanceId: instanceID, SteamId: steamID, MaxLoginRows: 50,
			},
		},
	}); err != nil {
		return nil, http.StatusBadGateway, err
	}
	env, err := h.pending.Await(ctx, corrID, 65*time.Second)
	if err != nil {
		return nil, http.StatusGatewayTimeout, err
	}
	res := env.GetEmpyrionPlayerSummaryResult()
	if res == nil {
		return nil, http.StatusInternalServerError, errBadAgentReply
	}
	if !res.Ok {
		return map[string]any{
			"error":    res.Error,
			"steam_id": res.SteamId,
		}, http.StatusOK, nil
	}
	body := map[string]any{
		"steam_id":      res.SteamId,
		"player_name":   res.PlayerName,
		"entity_id":     res.EntityId,
		"faction_group": res.FactionGroup,
		"faction_id":    res.FactionId,
		"last_position": map[string]any{
			"playfield": res.LastPlayfield,
			"x":         res.LastX,
			"y":         res.LastY,
			"z":         res.LastZ,
		},
		"credits":            res.Credits,
		"inventory_gametime": res.InventoryGametime,
	}
	if res.Stats != nil {
		s := res.Stats
		body["stats"] = map[string]any{
			"killed_enemies":        s.KilledEnemies,
			"killed_allied":         s.KilledAllied,
			"killed_animals":        s.KilledAnimals,
			"killed_drones":         s.KilledDrones,
			"killed_players":        s.KilledPlayers,
			"killed_allied_players": s.KilledAlliedPlayers,
			"died":                  s.Died,
			"score":                 s.Score,
			"blocks_placed":         s.BlocksPlaced,
			"blocks_digged":         s.BlocksDigged,
			"walked_meters":         s.WalkedMeters,
			"jetpack_meters":        s.JetpackMeters,
			"hv_meters":             s.HvMeters,
			"sv_meters":             s.SvMeters,
			"cv_meters":             s.CvMeters,
			"playtime_hours":        s.Playtime,
			"light_years":           s.LightYears,
			"astronomical_units":    s.AstronomicalUnits,
		}
	}
	sessions := make([]map[string]any, 0, len(res.Sessions))
	for _, sn := range res.Sessions {
		sessions = append(sessions, map[string]any{
			"login_ticks":  sn.LoginTicks,
			"logoff_ticks": sn.LogoffTicks,
			"player_name":  sn.PlayerName,
			"build_nr":     sn.BuildNr,
			"ip":           sn.Ip,
			"os":           sn.Os,
		})
	}
	body["sessions"] = sessions
	inv := make([]map[string]any, 0, len(res.Inventory))
	for _, it := range res.Inventory {
		inv = append(inv, map[string]any{
			"item_id": it.ItemId,
			"count":   it.Count,
		})
	}
	body["inventory"] = inv
	return body, http.StatusOK, nil
}

func (h *httpServer) handleEmpyrionPlayerSummary(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
	}
	sid := strings.TrimSpace(r.URL.Query().Get("player"))
	if sid == "" {
		writeError(w, http.StatusBadRequest, "missing_player", "player query param required")
		return
	}
	body, code, err := h.fetchEmpyrionPlayerSummary(r.Context(), id, agentID, sid)
	if err != nil {
		writeError(w, code, "fetch_failed", err.Error())
		return
	}
	writeJSON(w, code, body)
}

// adminEmpyrionPlayerSummary serves the player-summary data via the
// admin-token surface for external consumers (AMP-Monitor /me page).
func (h *httpServer) adminEmpyrionPlayerSummary(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	instanceID := strings.TrimSpace(q.Get("instance"))
	sid := strings.TrimSpace(q.Get("player"))
	if instanceID == "" || sid == "" {
		writeError(w, http.StatusBadRequest, "missing", "instance and player query params required")
		return
	}
	row, err := h.db.GetInstanceRow(r.Context(), instanceID)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", err.Error())
		return
	}
	if row.ModuleID != "empyrion" {
		writeError(w, http.StatusBadRequest, "wrong_module", "instance is not an empyrion server")
		return
	}
	body, code, err := h.fetchEmpyrionPlayerSummary(r.Context(), instanceID, row.AgentID, sid)
	if err != nil {
		writeError(w, code, "fetch_failed", err.Error())
		return
	}
	writeJSON(w, code, body)
}

// adminEmpyrionDiscoveries serves the same data via the admin-token
// surface so external consumers (AMP-Monitor's player page) can query
// the panel from another host on the LAN without a session cookie.
//
// GET /admin/v1/empyrion/discoveries?instance=<iid>&player=<sid>[&mode=]
func (h *httpServer) adminEmpyrionDiscoveries(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	instanceID := strings.TrimSpace(q.Get("instance"))
	steamID := strings.TrimSpace(q.Get("player"))
	if instanceID == "" || steamID == "" {
		writeError(w, http.StatusBadRequest, "missing", "instance and player query params required")
		return
	}
	row, err := h.db.GetInstanceRow(r.Context(), instanceID)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", err.Error())
		return
	}
	if row.ModuleID != "empyrion" {
		writeError(w, http.StatusBadRequest, "wrong_module", "instance is not an empyrion server")
		return
	}
	body, code, err := h.fetchEmpyrionDiscoveries(r.Context(), instanceID, row.AgentID, steamID, q.Get("mode"))
	if err != nil {
		writeError(w, code, "fetch_failed", err.Error())
		return
	}
	writeJSON(w, code, body)
}
