package main

// Empyrion chat-bot: subscribes to each running empyrion instance's bridge
// SSE feed, watches for Event_ChatMessage / Event_ChatMessageEx, and when
// a message starts with "!" looks up the matching rule and POSTs back the
// response via the bridge.
//
// Background goroutine started at controller boot alongside the history
// recorder. One subscription per empyrion instance; reconnects on
// disconnect with exponential backoff.

import (
	"bufio"
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"

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

type chatBotRunner struct {
	log      *slog.Logger
	db       *db.DB
	resolver func(ctx context.Context, instanceID string) (*empyrionTarget, error)
	mu       sync.Mutex
	subs     map[string]context.CancelFunc // instance_id → cancel
	stopped  bool
}

func newChatBotRunner(log *slog.Logger, database *db.DB, resolver func(ctx context.Context, instanceID string) (*empyrionTarget, error)) *chatBotRunner {
	return &chatBotRunner{
		log:      log,
		db:       database,
		resolver: resolver,
		subs:     map[string]context.CancelFunc{},
	}
}

// Run starts the chatbot loop. Periodically reconciles desired-vs-actual
// subscribers (one per empyrion instance), starting / stopping as needed.
func (c *chatBotRunner) Run(ctx context.Context) {
	t := time.NewTicker(30 * time.Second)
	defer t.Stop()
	c.reconcile(ctx)
	for {
		select {
		case <-ctx.Done():
			c.shutdown()
			return
		case <-t.C:
			c.reconcile(ctx)
		}
	}
}

func (c *chatBotRunner) shutdown() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.stopped = true
	for _, cancel := range c.subs {
		cancel()
	}
	c.subs = nil
}

func (c *chatBotRunner) reconcile(ctx context.Context) {
	c.mu.Lock()
	if c.stopped {
		c.mu.Unlock()
		return
	}
	c.mu.Unlock()

	rows, err := c.db.ListInstances(ctx, "")
	if err != nil {
		return
	}
	desired := map[string]bool{}
	for _, row := range rows {
		if row.ModuleID != "empyrion" {
			continue
		}
		desired[row.InstanceID] = true
	}

	c.mu.Lock()
	defer c.mu.Unlock()
	for id := range desired {
		if _, ok := c.subs[id]; ok {
			continue
		}
		subCtx, cancel := context.WithCancel(ctx)
		c.subs[id] = cancel
		go c.runSubscription(subCtx, id)
	}
	for id, cancel := range c.subs {
		if !desired[id] {
			cancel()
			delete(c.subs, id)
		}
	}
}

// runSubscription opens an SSE connection to the bridge and processes
// chat messages until the context is canceled. Reconnects with exponential
// backoff on transient failures.
func (c *chatBotRunner) runSubscription(ctx context.Context, instanceID string) {
	backoff := 5 * time.Second
	for {
		if ctx.Err() != nil {
			return
		}
		err := c.streamOnce(ctx, instanceID)
		if err != nil && !errors.Is(err, context.Canceled) {
			if c.log != nil {
				c.log.Info("empyrion chatbot subscription closed", "instance_id", instanceID, "err", err)
			}
		}
		select {
		case <-ctx.Done():
			return
		case <-time.After(backoff):
		}
		if backoff < 60*time.Second {
			backoff *= 2
		}
	}
}

// chatMessage is the decoded payload from Event_ChatMessage / Event_ChatMessageEx.
type chatMessage struct {
	SenderEntityId int    `json:"senderEntityId"`
	Msg            string `json:"msg"`
	Type           int    `json:"type"` // 0=global, 1=faction, 2=allies, 3=whisper, 4=server
	PlayerName     string `json:"playerName,omitempty"`
	Playfield      string `json:"playfield,omitempty"`
	FactionId      int    `json:"factionId,omitempty"`
}

// chatbotRule is a row from empyrion_chatbot_rules.
type chatbotRule struct {
	ID          string `json:"id"`
	InstanceID  string `json:"instance_id,omitempty"`
	Command     string `json:"command"`
	Description string `json:"description"`
	Response    string `json:"response"`
	Channel     string `json:"channel"`
	Enabled     bool   `json:"enabled"`
}

func (c *chatBotRunner) streamOnce(ctx context.Context, instanceID string) error {
	tgt, err := c.resolver(ctx, instanceID)
	if err != nil {
		return err
	}
	url := fmt.Sprintf("%s/api/v1/events?host=%s&port=%d", strings.TrimRight(tgt.bridgeURL, "/"), tgt.host, tgt.port)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	if tgt.bridgeToken != "" {
		req.Header.Set("Authorization", "Bearer "+tgt.bridgeToken)
	}
	req.Header.Set("Accept", "text/event-stream")
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return fmt.Errorf("bridge SSE returned %d", resp.StatusCode)
	}
	rd := bufio.NewReaderSize(resp.Body, 64*1024)
	var dataBuf bytes.Buffer
	for {
		line, err := rd.ReadBytes('\n')
		if err != nil {
			return err
		}
		ln := strings.TrimRight(string(line), "\r\n")
		if ln == "" {
			if dataBuf.Len() > 0 {
				c.handleEvent(ctx, instanceID, tgt, dataBuf.Bytes())
				dataBuf.Reset()
			}
			continue
		}
		if strings.HasPrefix(ln, "data:") {
			dataBuf.WriteString(strings.TrimPrefix(strings.TrimPrefix(ln, "data:"), " "))
		}
	}
}

func (c *chatBotRunner) handleEvent(ctx context.Context, instanceID string, tgt *empyrionTarget, data []byte) {
	var ev struct {
		Cmd  string          `json:"cmd"`
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(data, &ev); err != nil {
		return
	}
	if ev.Cmd != "Event_ChatMessage" && ev.Cmd != "Event_ChatMessageEx" {
		return
	}
	var msg chatMessage
	if err := json.Unmarshal(ev.Data, &msg); err != nil {
		return
	}
	text := strings.TrimSpace(msg.Msg)
	if text == "" || !strings.HasPrefix(text, "!") {
		return
	}

	// Tokenize: first word is the command (minus "!"), rest are args.
	parts := strings.Fields(text[1:])
	if len(parts) == 0 {
		return
	}
	cmd := strings.ToLower(parts[0])

	// Look up matching rule.
	rule, err := c.lookupRule(ctx, instanceID, cmd)
	if err != nil || rule == nil {
		return
	}

	// Build response with placeholders.
	response := rule.Response
	response = strings.ReplaceAll(response, "{playerName}", msg.PlayerName)
	response = strings.ReplaceAll(response, "{playerId}", fmt.Sprintf("%d", msg.SenderEntityId))
	response = strings.ReplaceAll(response, "{factionId}", fmt.Sprintf("%d", msg.FactionId))
	response = strings.ReplaceAll(response, "{playfield}", msg.Playfield)
	response = strings.ReplaceAll(response, "{now}", time.Now().Format("2006-01-02 15:04:05"))
	// {x}, {y}, {z} are left as-is since the bridge doesn't provide them
	// in chat events — the coords rule is a template for future expansion.

	// POST response to the appropriate bridge endpoint.
	switch rule.Channel {
	case "whisper":
		path := fmt.Sprintf("/api/v1/chat/whisper/%d", msg.SenderEntityId)
		postBridgeJSON(ctx, tgt, path, []byte(response))
	case "global":
		postBridgeJSON(ctx, tgt, "/api/v1/chat/broadcast", []byte(response))
	case "faction":
		if msg.FactionId > 0 {
			path := fmt.Sprintf("/api/v1/chat/faction/%d", msg.FactionId)
			postBridgeJSON(ctx, tgt, path, []byte(response))
		}
	}
}

func (c *chatBotRunner) lookupRule(ctx context.Context, instanceID, command string) (*chatbotRule, error) {
	row := c.db.Pool().QueryRow(ctx, `
		SELECT id, instance_id, command, description, response, channel, enabled
		FROM empyrion_chatbot_rules
		WHERE (instance_id = $1 OR instance_id IS NULL)
		  AND command = $2
		  AND enabled = true
		ORDER BY instance_id NULLS LAST
		LIMIT 1`, instanceID, command)
	var r chatbotRule
	err := row.Scan(&r.ID, &r.InstanceID, &r.Command, &r.Description, &r.Response, &r.Channel, &r.Enabled)
	if err != nil {
		if errors.Is(err, errNoRows) {
			return nil, nil
		}
		return nil, err
	}
	return &r, nil
}

// errNoRows is a sentinel for pgx no rows.
var errNoRows = errors.New("no rows in result set")

// ---- HTTP handlers ----

// GET /api/empyrion/chatbot-rules
func (h *httpServer) handleEmpyrionChatBotRulesList(w http.ResponseWriter, r *http.Request) {
	rows, err := h.db.Pool().Query(r.Context(), `
		SELECT id, instance_id, command, description, response, channel, enabled, created_at, updated_at
		FROM empyrion_chatbot_rules ORDER BY command, instance_id NULLS FIRST`)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	defer rows.Close()
	type ruleDTO struct {
		ID          string `json:"id"`
		InstanceID  string `json:"instance_id,omitempty"`
		Command     string `json:"command"`
		Description string `json:"description"`
		Response    string `json:"response"`
		Channel     string `json:"channel"`
		Enabled     bool   `json:"enabled"`
		CreatedAt   string `json:"created_at"`
		UpdatedAt   string `json:"updated_at"`
	}
	out := []ruleDTO{}
	for rows.Next() {
		var r ruleDTO
		var instID *string // instance_id is nullable for global-scope rules
		var created, updated any
		if err := rows.Scan(&r.ID, &instID, &r.Command, &r.Description, &r.Response, &r.Channel, &r.Enabled, &created, &updated); err != nil {
			continue
		}
		if instID != nil {
			r.InstanceID = *instID
		}
		r.CreatedAt = formatTS(created)
		r.UpdatedAt = formatTS(updated)
		out = append(out, r)
	}
	writeJSON(w, http.StatusOK, map[string]any{"rules": out})
}

// POST /api/empyrion/chatbot-rules
func (h *httpServer) handleEmpyrionChatBotRulesCreate(w http.ResponseWriter, r *http.Request) {
	var req struct {
		ID          string `json:"id,omitempty"`
		InstanceID  string `json:"instance_id,omitempty"`
		Command     string `json:"command"`
		Description string `json:"description"`
		Response    string `json:"response"`
		Channel     string `json:"channel"`
		Enabled     *bool  `json:"enabled,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if strings.TrimSpace(req.Command) == "" || strings.TrimSpace(req.Response) == "" {
		writeError(w, http.StatusBadRequest, "missing_fields", "command and response are required")
		return
	}
	if req.Channel == "" {
		req.Channel = "whisper"
	}
	if req.Channel != "whisper" && req.Channel != "global" && req.Channel != "faction" {
		writeError(w, http.StatusBadRequest, "bad_channel", "channel must be whisper, global, or faction")
		return
	}
	id := req.ID
	if id == "" {
		id = "cb_" + newShortID()
	}
	enabled := true
	if req.Enabled != nil {
		enabled = *req.Enabled
	}
	_, err := h.db.Pool().Exec(r.Context(), `
		INSERT INTO empyrion_chatbot_rules (id, instance_id, command, description, response, channel, enabled)
		VALUES ($1, NULLIF($2, ''), $3, $4, $5, $6, $7)
		ON CONFLICT (instance_id, command) DO UPDATE SET
		  response = EXCLUDED.response,
		  description = EXCLUDED.description,
		  channel = EXCLUDED.channel,
		  enabled = EXCLUDED.enabled,
		  updated_at = NOW()`,
		id, req.InstanceID, req.Command, req.Description, req.Response, req.Channel, enabled)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"id": id})
}

// PATCH /api/empyrion/chatbot-rules/{id}
func (h *httpServer) handleEmpyrionChatBotRulesUpdate(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	var req struct {
		InstanceID  *string `json:"instance_id,omitempty"`
		Command     *string `json:"command,omitempty"`
		Description *string `json:"description,omitempty"`
		Response    *string `json:"response,omitempty"`
		Channel     *string `json:"channel,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 SET clause dynamically.
	set := []string{}
	args := []any{}
	argIdx := 1
	if req.Command != nil {
		set = append(set, fmt.Sprintf("command = $%d", argIdx))
		args = append(args, *req.Command)
		argIdx++
	}
	if req.Description != nil {
		set = append(set, fmt.Sprintf("description = $%d", argIdx))
		args = append(args, *req.Description)
		argIdx++
	}
	if req.Response != nil {
		set = append(set, fmt.Sprintf("response = $%d", argIdx))
		args = append(args, *req.Response)
		argIdx++
	}
	if req.Channel != nil {
		set = append(set, fmt.Sprintf("channel = $%d", argIdx))
		args = append(args, *req.Channel)
		argIdx++
	}
	if req.Enabled != nil {
		set = append(set, fmt.Sprintf("enabled = $%d", argIdx))
		args = append(args, *req.Enabled)
		argIdx++
	}
	if req.InstanceID != nil {
		set = append(set, fmt.Sprintf("instance_id = $%d", argIdx))
		args = append(args, *req.InstanceID)
		argIdx++
	}
	if len(set) == 0 {
		writeError(w, http.StatusBadRequest, "no_fields", "no fields to update")
		return
	}
	set = append(set, "updated_at = NOW()")
	args = append(args, id)
	q := fmt.Sprintf("UPDATE empyrion_chatbot_rules SET %s WHERE id = $%d", strings.Join(set, ", "), 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", "rule not found")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"id": id, "updated": true})
}

// DELETE /api/empyrion/chatbot-rules/{id}
func (h *httpServer) handleEmpyrionChatBotRulesDelete(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	tag, err := h.db.Pool().Exec(r.Context(), `DELETE FROM empyrion_chatbot_rules WHERE id = $1`, id)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	if tag.RowsAffected() == 0 {
		writeError(w, http.StatusNotFound, "not_found", "rule not found")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"id": id, "deleted": true})
}

// newShortID generates a short random hex id for chatbot rules.
func newShortID() string {
	b := make([]byte, 4)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}

// randRead is a variable so tests can override it.
var randRead = rand.Read
