package main

// Player-warning log + configurable warning rules.
// Append-only history of admin- or auto-issued warnings, plus CEL-driven
// rules that can auto-escalate to kick/ban.

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"

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

// warningRow mirrors empyrion_warnings.
type warningRow struct {
	ID         int64  `json:"id"`
	PlayerID   int    `json:"playerId"`
	PlayerName string `json:"playerName,omitempty"`
	Reason     string `json:"reason"`
	GivenBy    string `json:"givenBy,omitempty"`
	AutoRule   string `json:"autoRule,omitempty"`
	GivenAt    string `json:"givenAt,omitempty"`
}

// warningRuleRow mirrors empyrion_warning_rules.
type warningRuleRow struct {
	ID              string `json:"id"`
	InstanceID      string `json:"instanceId,omitempty"`
	Description     string `json:"description"`
	CEL             string `json:"cel"`
	WarnMessage     string `json:"warnMessage"`
	Threshold       int    `json:"threshold"`
	ThresholdAction string `json:"thresholdAction"`
	Enabled         bool   `json:"enabled"`
	CreatedAt       string `json:"createdAt,omitempty"`
	UpdatedAt       string `json:"updatedAt,omitempty"`
}

// randomID generates a hex string suitable for use as a warning-rule id.
func randomID() string {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		// Fallback: use a timestamp-based string. Extremely unlikely
		// since crypto/rand only fails on system-level entropy depletion.
		return "rule_fallback"
	}
	return hex.EncodeToString(b)
}

// ---------------------------------------------------------------------------
// GET /api/instances/{id}/empyrion/warnings?player_id=&limit=
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionWarningsList(w http.ResponseWriter, r *http.Request) {
	instanceID := r.PathValue("id")
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	if limit <= 0 || limit > 500 {
		limit = 100
	}

	q := `SELECT id, player_id, COALESCE(player_name,''), reason, COALESCE(given_by,''), COALESCE(auto_rule,''), given_at
	      FROM empyrion_warnings WHERE instance_id = $1`
	args := []any{instanceID}

	if pid := r.URL.Query().Get("player_id"); pid != "" {
		args = append(args, pid)
		q += " AND player_id = $" + strconv.Itoa(len(args))
	}

	args = append(args, limit)
	q += " ORDER BY given_at 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()

	out := make([]warningRow, 0, 64)
	for rows.Next() {
		var w warningRow
		var givenAt any
		if err := rows.Scan(&w.ID, &w.PlayerID, &w.PlayerName, &w.Reason, &w.GivenBy, &w.AutoRule, &givenAt); err != nil {
			continue
		}
		w.GivenAt = formatTS(givenAt)
		out = append(out, w)
	}
	writeJSON(w, http.StatusOK, map[string]any{"warnings": out})
}

// ---------------------------------------------------------------------------
// POST /api/instances/{id}/empyrion/warnings
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionWarningsIssue(w http.ResponseWriter, r *http.Request) {
	instanceID := r.PathValue("id")

	var req struct {
		PlayerID   int    `json:"playerId"`
		PlayerName string `json:"playerName,omitempty"`
		Reason     string `json:"reason"`
		AutoRule   string `json:"autoRule,omitempty"`
		Escalate   bool   `json:"escalate"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if req.PlayerID <= 0 || strings.TrimSpace(req.Reason) == "" {
		writeError(w, http.StatusBadRequest, "missing", "playerId and reason are required")
		return
	}

	// Determine who gave the warning.
	givenBy := ""
	if u, ok := r.Context().Value(sessionUserKey{}).(*db.UserRow); ok {
		givenBy = u.Email
	}

	// Insert the warning row.
	var warningID int64
	err := h.db.Pool().QueryRow(r.Context(), `
		INSERT INTO empyrion_warnings (instance_id, player_id, player_name, reason, given_by, auto_rule)
		VALUES ($1, $2, $3, $4, $5, $6)
		RETURNING id`,
		instanceID, req.PlayerID, req.PlayerName, req.Reason, givenBy, req.AutoRule,
	).Scan(&warningID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}

	// Count total warnings for this player on this instance.
	var count int
	err = h.db.Pool().QueryRow(r.Context(), `
		SELECT count(*) FROM empyrion_warnings
		WHERE instance_id = $1 AND player_id = $2`,
		instanceID, req.PlayerID,
	).Scan(&count)
	if err != nil {
		count = 0
	}

	escalated := false
	escalationAction := ""

	if req.Escalate && count > 0 {
		// Look up the highest threshold among active rules that apply
		// (instance-specific or global). If count >= threshold, escalate.
		var maxThreshold int
		var action string
		err = h.db.Pool().QueryRow(r.Context(), `
			SELECT COALESCE(MAX(threshold), 0), COALESCE(
				(SELECT threshold_action FROM empyrion_warning_rules
				 WHERE enabled AND (instance_id = $1 OR instance_id IS NULL)
				 ORDER BY threshold DESC LIMIT 1),
				'none')
			FROM empyrion_warning_rules
			WHERE enabled AND (instance_id = $1 OR instance_id IS NULL)`,
			instanceID,
		).Scan(&maxThreshold, &action)
		if err == nil && maxThreshold > 0 && count >= maxThreshold && action != "none" {
			escalated = true
			escalationAction = action

			// Execute escalation via bridge proxy.
			tgt, tgtErr := h.resolveEmpyrionTarget(r.Context(), instanceID)
			if tgtErr == nil {
				switch action {
				case "kick":
					cmd := "kick " + strconv.Itoa(req.PlayerID) + " \"Auto-escalation: warning threshold reached\""
					postBridgeBody(r.Context(), tgt, "/api/v1/console", cmd)
				case "ban":
					cmd := "ban " + strconv.Itoa(req.PlayerID) + " \"Auto-escalation: warning threshold reached\""
					postBridgeBody(r.Context(), tgt, "/api/v1/console", cmd)
				}
			}
		}
	}

	writeJSON(w, http.StatusCreated, map[string]any{
		"warning_id":        warningID,
		"count":             count,
		"escalated":         escalated,
		"escalation_action": escalationAction,
	})
}

// ---------------------------------------------------------------------------
// GET /api/empyrion/warning-rules
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionWarningRulesList(w http.ResponseWriter, r *http.Request) {
	rows, err := h.db.Pool().Query(r.Context(), `
		SELECT id, COALESCE(instance_id,''), description, cel, warn_message,
		       threshold, threshold_action, enabled, created_at, updated_at
		FROM empyrion_warning_rules
		ORDER BY created_at DESC`)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	defer rows.Close()

	out := make([]warningRuleRow, 0, 32)
	for rows.Next() {
		var rule warningRuleRow
		var created, updated any
		if err := rows.Scan(&rule.ID, &rule.InstanceID, &rule.Description, &rule.CEL,
			&rule.WarnMessage, &rule.Threshold, &rule.ThresholdAction, &rule.Enabled,
			&created, &updated); err != nil {
			continue
		}
		rule.CreatedAt = formatTS(created)
		rule.UpdatedAt = formatTS(updated)
		out = append(out, rule)
	}
	writeJSON(w, http.StatusOK, map[string]any{"rules": out})
}

// ---------------------------------------------------------------------------
// POST /api/empyrion/warning-rules
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionWarningRulesCreate(w http.ResponseWriter, r *http.Request) {
	var req struct {
		ID              string `json:"id,omitempty"`
		InstanceID      string `json:"instanceId,omitempty"`
		Description     string `json:"description"`
		CEL             string `json:"cel"`
		WarnMessage     string `json:"warnMessage"`
		Threshold       int    `json:"threshold"`
		ThresholdAction string `json:"thresholdAction"`
		Enabled         bool   `json:"enabled"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if strings.TrimSpace(req.CEL) == "" || strings.TrimSpace(req.WarnMessage) == "" {
		writeError(w, http.StatusBadRequest, "missing", "cel and warnMessage are required")
		return
	}
	if req.Threshold <= 0 {
		req.Threshold = 3
	}
	if req.ThresholdAction == "" {
		req.ThresholdAction = "kick"
	}
	if req.ID == "" {
		req.ID = randomID()
	}

	_, err := h.db.Pool().Exec(r.Context(), `
		INSERT INTO empyrion_warning_rules (id, instance_id, description, cel, warn_message, threshold, threshold_action, enabled)
		VALUES ($1, NULLIF($2,''), $3, $4, $5, $6, $7, $8)
		ON CONFLICT (id) DO UPDATE SET
			description = EXCLUDED.description,
			cel = EXCLUDED.cel,
			warn_message = EXCLUDED.warn_message,
			threshold = EXCLUDED.threshold,
			threshold_action = EXCLUDED.threshold_action,
			enabled = EXCLUDED.enabled,
			updated_at = NOW()`,
		req.ID, req.InstanceID, req.Description, req.CEL, req.WarnMessage,
		req.Threshold, req.ThresholdAction, req.Enabled)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}

	// Re-list to return the full row.
	h.handleEmpyrionWarningRulesList(w, r)
}

// ---------------------------------------------------------------------------
// PATCH /api/empyrion/warning-rules/{id}
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionWarningRulesUpdate(w http.ResponseWriter, r *http.Request) {
	ruleID := r.PathValue("id")
	if ruleID == "" {
		writeError(w, http.StatusBadRequest, "missing_id", "rule id is required")
		return
	}

	var req struct {
		InstanceID      *string `json:"instanceId,omitempty"`
		Description     *string `json:"description,omitempty"`
		CEL             *string `json:"cel,omitempty"`
		WarnMessage     *string `json:"warnMessage,omitempty"`
		Threshold       *int    `json:"threshold,omitempty"`
		ThresholdAction *string `json:"thresholdAction,omitempty"`
		Enabled         *bool   `json:"enabled,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}

	// Build dynamic SET clause.
	sets := []string{}
	args := []any{}
	argIdx := 1

	if req.InstanceID != nil {
		sets = append(sets, "instance_id = $"+strconv.Itoa(argIdx))
		args = append(args, *req.InstanceID)
		argIdx++
	}
	if req.Description != nil {
		sets = append(sets, "description = $"+strconv.Itoa(argIdx))
		args = append(args, *req.Description)
		argIdx++
	}
	if req.CEL != nil {
		sets = append(sets, "cel = $"+strconv.Itoa(argIdx))
		args = append(args, *req.CEL)
		argIdx++
	}
	if req.WarnMessage != nil {
		sets = append(sets, "warn_message = $"+strconv.Itoa(argIdx))
		args = append(args, *req.WarnMessage)
		argIdx++
	}
	if req.Threshold != nil {
		sets = append(sets, "threshold = $"+strconv.Itoa(argIdx))
		args = append(args, *req.Threshold)
		argIdx++
	}
	if req.ThresholdAction != nil {
		sets = append(sets, "threshold_action = $"+strconv.Itoa(argIdx))
		args = append(args, *req.ThresholdAction)
		argIdx++
	}
	if req.Enabled != nil {
		sets = append(sets, "enabled = $"+strconv.Itoa(argIdx))
		args = append(args, *req.Enabled)
		argIdx++
	}

	if len(sets) == 0 {
		writeError(w, http.StatusBadRequest, "no_fields", "no fields to update")
		return
	}

	sets = append(sets, "updated_at = NOW()")
	args = append(args, ruleID)
	q := "UPDATE empyrion_warning_rules SET " + strings.Join(sets, ", ") + " WHERE id = $" + strconv.Itoa(argIdx)

	tag, err := h.db.Pool().Exec(r.Context(), q, args...)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	if tag.RowsAffected() == 0 {
		writeError(w, http.StatusNotFound, "not_found", "warning rule not found")
		return
	}

	h.handleEmpyrionWarningRulesList(w, r)
}

// ---------------------------------------------------------------------------
// DELETE /api/empyrion/warning-rules/{id}
// ---------------------------------------------------------------------------

func (h *httpServer) handleEmpyrionWarningRulesDelete(w http.ResponseWriter, r *http.Request) {
	ruleID := r.PathValue("id")
	if ruleID == "" {
		writeError(w, http.StatusBadRequest, "missing_id", "rule id is required")
		return
	}

	tag, err := h.db.Pool().Exec(r.Context(), `DELETE FROM empyrion_warning_rules WHERE id = $1`, ruleID)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	if tag.RowsAffected() == 0 {
		writeError(w, http.StatusNotFound, "not_found", "warning rule not found")
		return
	}
	w.WriteHeader(http.StatusNoContent)
}
