package main

// Blueprint library — upload, list, delete, and spawn .epb blueprints
// from a central DB-backed store. Blueprints are stored on the controller's
// local filesystem under ./data/empyrion-bp-library/ and written to the
// target instance's /game-saves/Saves/Blueprints/0/ via the agent's FsWrite
// RPC at spawn time.

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/dbledeez/panel/controller/internal/db"

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

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

// blueprintRow mirrors the empyrion_blueprints table.
type blueprintRow struct {
	ID         int64     `json:"id"`
	Name       string    `json:"name"`
	SizeBytes  int64     `json:"size_bytes"`
	UploadedAt time.Time `json:"uploaded_at"`
	Uploader   string    `json:"uploader,omitempty"`
	Notes      string    `json:"notes,omitempty"`
}

// bpSanitizeRE matches characters we keep in blueprint filenames.
var bpSanitizeRE = regexp.MustCompile(`[^A-Za-z0-9._-]`)

// bpLibraryDir is the local directory where uploaded .epb files are stored.
const bpLibraryDir = "./data/empyrion-bp-library"

// bpSanitizeName strips path separators and replaces anything outside
// [A-Za-z0-9._-] with underscore.
func bpSanitizeName(name string) string {
	// Strip any leading directory components.
	name = filepath.Base(name)
	// Replace unsafe characters.
	return bpSanitizeRE.ReplaceAllString(name, "_")
}

// bpDiskPath returns the local filesystem path for a blueprint row.
func bpDiskPath(id int64, sanitizedName string) string {
	return filepath.Join(bpLibraryDir, fmt.Sprintf("%d_%s", id, sanitizedName))
}

// bpEnsureDir creates the blueprint library directory if it doesn't exist.
func bpEnsureDir() error {
	return os.MkdirAll(bpLibraryDir, 0755)
}

// ---------------------------------------------------------------------------
// GET /api/empyrion/blueprint-library
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionBpLibList(w http.ResponseWriter, r *http.Request) {
	rows, err := h.db.Pool().Query(r.Context(), `
		SELECT id, name, size_bytes, uploaded_at, uploader, notes
		FROM empyrion_blueprints
		ORDER BY uploaded_at DESC
		LIMIT 500`)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	defer rows.Close()

	out := make([]blueprintRow, 0, 128)
	for rows.Next() {
		var bp blueprintRow
		var uploader, notes *string
		if err := rows.Scan(&bp.ID, &bp.Name, &bp.SizeBytes, &bp.UploadedAt, &uploader, &notes); err != nil {
			continue
		}
		if uploader != nil {
			bp.Uploader = *uploader
		}
		if notes != nil {
			bp.Notes = *notes
		}
		out = append(out, bp)
	}
	writeJSON(w, http.StatusOK, map[string]any{"blueprints": out})
}

// ---------------------------------------------------------------------------
// POST /api/empyrion/blueprint-library  (multipart/form-data)
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionBpLibUpload(w http.ResponseWriter, r *http.Request) {
	// Limit to 50 MB.
	if err := r.ParseMultipartForm(50 << 20); err != nil {
		writeError(w, http.StatusBadRequest, "bad_form", err.Error())
		return
	}

	file, header, err := r.FormFile("blueprint")
	if err != nil {
		writeError(w, http.StatusBadRequest, "missing_file", "blueprint file field is required")
		return
	}
	defer file.Close()

	// Validate .epb extension (case-insensitive).
	fname := header.Filename
	if !strings.HasSuffix(strings.ToLower(fname), ".epb") {
		writeError(w, http.StatusBadRequest, "bad_extension", "blueprint file must have .epb extension")
		return
	}

	// Read the file bytes.
	body, err := io.ReadAll(io.LimitReader(file, 50<<20))
	if err != nil {
		writeError(w, http.StatusBadRequest, "read_failed", err.Error())
		return
	}
	if len(body) == 0 {
		writeError(w, http.StatusBadRequest, "empty_file", "blueprint file is empty")
		return
	}

	// Optional notes.
	notes := strings.TrimSpace(r.FormValue("notes"))

	// Uploader from auth context if available.
	uploader := ""
	if u, ok := r.Context().Value(sessionUserKey{}).(*db.UserRow); ok {
		uploader = u.Email
	}

	// Sanitize the name.
	sanitized := bpSanitizeName(fname)
	if sanitized == "" {
		writeError(w, http.StatusBadRequest, "bad_name", "blueprint name is empty after sanitization")
		return
	}

	// Insert into DB.
	var bpID int64
	err = h.db.Pool().QueryRow(r.Context(), `
		INSERT INTO empyrion_blueprints (name, size_bytes, uploader, notes)
		VALUES ($1, $2, $3, $4)
		RETURNING id`,
		sanitized, int64(len(body)), uploader, notes).Scan(&bpID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}

	// Ensure local directory exists.
	if err := bpEnsureDir(); err != nil {
		// Best-effort: if we can't save the file, delete the DB row.
		_, _ = h.db.Pool().Exec(r.Context(), `DELETE FROM empyrion_blueprints WHERE id=$1`, bpID)
		writeError(w, http.StatusInternalServerError, "mkdir", err.Error())
		return
	}

	// Write the file to disk.
	diskPath := bpDiskPath(bpID, sanitized)
	if err := os.WriteFile(diskPath, body, 0644); err != nil {
		// Best-effort cleanup.
		_, _ = h.db.Pool().Exec(r.Context(), `DELETE FROM empyrion_blueprints WHERE id=$1`, bpID)
		writeError(w, http.StatusInternalServerError, "write_file", err.Error())
		return
	}

	writeJSON(w, http.StatusCreated, blueprintRow{
		ID:         bpID,
		Name:       sanitized,
		SizeBytes:  int64(len(body)),
		UploadedAt: time.Now(),
		Uploader:   uploader,
		Notes:      notes,
	})
}

// ---------------------------------------------------------------------------
// DELETE /api/empyrion/blueprint-library/{id}
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionBpLibDelete(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
	if err != nil || id <= 0 {
		writeError(w, http.StatusBadRequest, "bad_id", "id must be a positive integer")
		return
	}

	// Look up the name so we can compute the disk path.
	var name string
	err = h.db.Pool().QueryRow(r.Context(), `SELECT name FROM empyrion_blueprints WHERE id=$1`, id).Scan(&name)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", "blueprint not found")
		return
	}

	// Delete from DB.
	_, err = h.db.Pool().Exec(r.Context(), `DELETE FROM empyrion_blueprints WHERE id=$1`, id)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}

	// Best-effort remove the file from disk.
	_ = os.Remove(bpDiskPath(id, name))

	w.WriteHeader(http.StatusNoContent)
}

// ---------------------------------------------------------------------------
// POST /api/instances/{instId}/empyrion/blueprint-library/{bpId}/spawn
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionBpLibSpawn(w http.ResponseWriter, r *http.Request) {
	instID := r.PathValue("instId")
	bpID, err := strconv.ParseInt(r.PathValue("bpId"), 10, 64)
	if err != nil || bpID <= 0 {
		writeError(w, http.StatusBadRequest, "bad_bp_id", "bpId must be a positive integer")
		return
	}

	var body struct {
		PlayerID  int  `json:"playerId"`
		FactionID *int `json:"factionId,omitempty"`
	}
	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 is required")
		return
	}

	// Look up the blueprint row.
	var bp blueprintRow
	var uploader, notes *string
	err = h.db.Pool().QueryRow(r.Context(), `
		SELECT id, name, size_bytes, uploaded_at, uploader, notes
		FROM empyrion_blueprints WHERE id=$1`, bpID).Scan(
		&bp.ID, &bp.Name, &bp.SizeBytes, &bp.UploadedAt, &uploader, &notes)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", "blueprint not found")
		return
	}
	if uploader != nil {
		bp.Uploader = *uploader
	}
	if notes != nil {
		bp.Notes = *notes
	}

	// Load the .epb bytes from disk.
	diskPath := bpDiskPath(bp.ID, bp.Name)
	epbData, err := os.ReadFile(diskPath)
	if err != nil {
		writeError(w, http.StatusNotFound, "file_missing", "blueprint file not found on disk")
		return
	}

	// Look up the instance + agent.
	agentID, err := h.lookupInstance(r, instID)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", err.Error())
		return
	}
	conn := h.registry.get(agentID)
	if conn == nil {
		writeError(w, http.StatusServiceUnavailable, "agent_offline", "agent not connected")
		return
	}

	// Derive the in-game name (strip .epb extension).
	bpNameNoExt := strings.TrimSuffix(bp.Name, ".epb")
	if bpNameNoExt == "" {
		bpNameNoExt = bp.Name
	}

	// Write the .epb to the instance's Blueprints/0/ directory via FsWrite.
	destPath := fmt.Sprintf("/game-saves/Saves/Blueprints/0/%s.epb", bpNameNoExt)
	corrID := newCorrelationID("bp_lib_spawn_write")
	if err := conn.Send(&panelv1.ControllerEnvelope{
		CorrelationId: corrID,
		SentAt:        timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_FsWrite{
			FsWrite: &panelv1.FsWriteRequest{
				InstanceId: instID,
				Path:       destPath,
				Content:    epbData,
			},
		},
	}); err != nil {
		writeError(w, http.StatusBadGateway, "forward", err.Error())
		return
	}
	env, err := h.pending.Await(r.Context(), corrID, 30*time.Second)
	if err != nil {
		writeError(w, http.StatusGatewayTimeout, "timeout", err.Error())
		return
	}
	res := env.GetFsWriteResult()
	if res == nil {
		writeError(w, http.StatusInternalServerError, "bad_reply", "agent reply missing fs_write_result")
		return
	}
	if res.Error != "" {
		writeError(w, http.StatusBadRequest, "fs_write", res.Error)
		return
	}

	// Resolve bridge target for console commands.
	tgt, err := h.resolveEmpyrionTarget(r.Context(), instID)
	if err != nil {
		writeError(w, http.StatusServiceUnavailable, "no_bridge", err.Error())
		return
	}

	// Build the spawn command.
	bpClean := strings.ReplaceAll(bpNameNoExt, "'", "")
	spawnCmd := fmt.Sprintf("spawn '%s' %d", strings.TrimSpace(bpClean), body.PlayerID)
	sent := []string{spawnCmd}

	ok, msg := postBridgeBody(r.Context(), tgt, "/api/v1/console", spawnCmd)
	if !ok {
		writeError(w, http.StatusBadGateway, "console_failed", msg)
		return
	}

	// Best-effort faction assignment.
	if body.FactionID != nil && *body.FactionID > 0 {
		factionCmd := fmt.Sprintf("faction lastspawned %d", *body.FactionID)
		ok2, _ := postBridgeBody(r.Context(), tgt, "/api/v1/console", factionCmd)
		if ok2 {
			sent = append(sent, factionCmd)
		}
	}

	writeJSON(w, http.StatusOK, map[string]any{"sent": sent})
}
