package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"fn-registry/functions/core"
	"fn-registry/functions/infra"
	"nhooyr.io/websocket"
)

const chatSystemPrompt = `Asistente del tablero kanban. Modifica el tablero llamando a tools MCP cuando el usuario pida cambios. Responde texto en markdown cuando solo informe.

Tools (MCP server "kanban"):
- Lectura: list_board, find_cards, card_history, list_users
- Columnas: create_column, update_column, delete_column, reorder_columns
- Tarjetas: create_card, update_card, delete_card, move_card, assign_card

El estado actual del tablero viene en <board_state> al final del mensaje. Usa esos IDs directamente — NO llames list_board si ya tienes lo que necesitas. NUNCA inventes IDs.

Cuando termines, responde texto natural sin mas llamadas — eso cierra la conversacion.`

const claudeTimeout = 300 * time.Second

func claudeBinary() string {
	if b := os.Getenv("KANBAN_CLAUDE_BIN"); b != "" {
		return b
	}
	return "claude"
}

func claudeModel() string {
	if m := os.Getenv("KANBAN_CLAUDE_MODEL"); m != "" {
		return m
	}
	return "claude-haiku-4-5-20251001"
}

type chatMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type chatRequest struct {
	Messages []chatMessage `json:"messages"`
}

// wsEvent is the envelope sent to the browser. Type discriminates the payload.
type wsEvent struct {
	Type    string          `json:"type"`
	Text    string          `json:"text,omitempty"`
	ToolID  string          `json:"tool_id,omitempty"`
	Tool    string          `json:"tool,omitempty"`
	Input   json.RawMessage `json:"input,omitempty"`
	Result  string          `json:"result,omitempty"`
	IsError bool            `json:"is_error,omitempty"`
	BoardChanged bool       `json:"board_changed,omitempty"`
	Error   string          `json:"error,omitempty"`
}

// handleChatWS upgrades the request to WebSocket and streams claude events.
//
// Wire protocol:
//   client → server (one message): { "messages": [{role, content}, ...] }
//   server → client (many): wsEvent ndjson-style messages
//     types: "delta" (assistant text), "tool_use", "tool_result", "result", "error"
//   server closes connection at end.
func handleChatWS(db *DB, workdir string, logger *ChatLogger, internalToken string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		conn, err := infra.WSUpgrader(w, r, []string{"*"})
		if err != nil {
			return
		}
		defer conn.Close(websocket.StatusInternalError, "internal")

		ctx, cancel := context.WithTimeout(r.Context(), claudeTimeout)
		defer cancel()

		// Read the initial chat request.
		_, raw, err := conn.Read(ctx)
		if err != nil {
			return
		}
		var req chatRequest
		if err := json.Unmarshal(raw, &req); err != nil {
			sendWS(ctx, conn, wsEvent{Type: "error", Error: "invalid chat request: " + err.Error()})
			return
		}
		if len(req.Messages) == 0 {
			sendWS(ctx, conn, wsEvent{Type: "error", Error: "messages required"})
			return
		}

		boardChanged, err := streamChat(ctx, conn, db, workdir, internalToken, req.Messages, logger)
		if err != nil {
			sendWS(ctx, conn, wsEvent{Type: "error", Error: err.Error()})
			return
		}
		sendWS(ctx, conn, wsEvent{Type: "done", BoardChanged: boardChanged})
		conn.Close(websocket.StatusNormalClosure, "")
	}
}

func streamChat(ctx context.Context, conn *websocket.Conn, db *DB, workdir, token string, msgs []chatMessage, logger *ChatLogger) (bool, error) {
	binPath, err := os.Executable()
	if err != nil {
		return false, fmt.Errorf("locate kanban binary: %w", err)
	}

	// Backend URL: trust X-Forwarded or fall back to localhost (kanban listens
	// on its main port). The MCP subprocess hits the loopback interface.
	backendURL := os.Getenv("KANBAN_PUBLIC_URL")
	if backendURL == "" {
		port := os.Getenv("KANBAN_LISTEN_PORT")
		if port == "" {
			port = "8095"
		}
		backendURL = "http://127.0.0.1:" + port
	}

	mcpPath, err := writeMCPConfig(binPath, backendURL, token)
	if err != nil {
		return false, fmt.Errorf("write mcp config: %w", err)
	}
	defer os.Remove(mcpPath)

	prompt := flattenMessages(msgs)
	if board, err := boardSnapshot(db); err == nil && board != "" {
		prompt += "\n\n<board_state>\n" + board + "\n</board_state>\n"
	}

	stdin := strings.NewReader(prompt)
	events, err := core.StreamClaude(ctx, core.ClaudeStreamOpts{
		Bin: claudeBinary(),
		Args: []string{
			"--model", claudeModel(),
			"--no-session-persistence",
			"--mcp-config", mcpPath,
			"--strict-mcp-config",
			"--system-prompt", chatSystemPrompt,
			"--allowedTools",
			"mcp__kanban__list_board,mcp__kanban__create_column,mcp__kanban__update_column,mcp__kanban__rename_column,mcp__kanban__delete_column,mcp__kanban__reorder_columns,mcp__kanban__create_card,mcp__kanban__update_card,mcp__kanban__delete_card,mcp__kanban__move_card,mcp__kanban__card_history,mcp__kanban__find_cards,mcp__kanban__list_users,mcp__kanban__assign_card",
		},
		Stdin:   stdin,
		Workdir: workdir,
	})
	if err != nil {
		return false, fmt.Errorf("spawn claude: %w", err)
	}

	boardChanged := false
	for ev := range events {
		switch ev.Type {
		case core.ClaudeEventTextDelta:
			sendWS(ctx, conn, wsEvent{Type: "delta", Text: ev.Text})
		case core.ClaudeEventToolUse:
			toolName := stripMCPPrefix(ev.ToolName)
			sendWS(ctx, conn, wsEvent{
				Type:   "tool_use",
				ToolID: ev.ToolUseID,
				Tool:   toolName,
				Input:  ev.ToolInput,
			})
			if toolMutates(toolName) {
				boardChanged = true
			}
		case core.ClaudeEventToolResult:
			sendWS(ctx, conn, wsEvent{
				Type:    "tool_result",
				ToolID:  ev.ToolResultID,
				Result:  ev.ToolResultContent,
				IsError: ev.ToolResultIsError,
			})
		case core.ClaudeEventResult:
			sendWS(ctx, conn, wsEvent{
				Type:    "result",
				Text:    ev.Result,
				IsError: ev.IsError,
			})
		case core.ClaudeEventError:
			sendWS(ctx, conn, wsEvent{Type: "error", Error: ev.Error})
		}
	}
	return boardChanged, nil
}

// stripMCPPrefix removes the "mcp__<server>__" prefix added by claude when
// tools come from an MCP server, leaving the bare tool name.
func stripMCPPrefix(name string) string {
	const pre = "mcp__kanban__"
	if strings.HasPrefix(name, pre) {
		return name[len(pre):]
	}
	return name
}

func sendWS(ctx context.Context, conn *websocket.Conn, ev wsEvent) {
	b, err := json.Marshal(ev)
	if err != nil {
		return
	}
	wctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	_ = conn.Write(wctx, websocket.MessageText, b)
}

// flattenMessages converts chat history into a single prompt for `claude -p`.
func flattenMessages(msgs []chatMessage) string {
	var b strings.Builder
	for _, m := range msgs {
		role := "Usuario"
		if m.Role == "assistant" {
			role = "Asistente"
		}
		b.WriteString(role)
		b.WriteString(": ")
		b.WriteString(m.Content)
		b.WriteString("\n\n")
	}
	return b.String()
}

// boardSnapshot returns a JSON dump of columns + cards to inject in the
// initial prompt, saving a list_board round-trip.
func boardSnapshot(db *DB) (string, error) {
	cols, err := db.ListColumns()
	if err != nil {
		return "", err
	}
	cards, err := db.ListCardsWithTime()
	if err != nil {
		return "", err
	}
	b, err := json.Marshal(map[string]any{"columns": cols, "cards": cards})
	if err != nil {
		return "", err
	}
	return string(b), nil
}

// chatWorkdir resolves an absolute working directory for `claude -p`.
func chatWorkdir(dbPath string) string {
	abs, err := filepath.Abs(dbPath)
	if err != nil {
		return "."
	}
	return filepath.Dir(abs)
}

// --- Legacy handleChat retained as a thin shim that returns 410 Gone. -------
// Kept so existing clients see a clear error instead of a 404 while they
// migrate to the WebSocket endpoint.

func handleChat(_ *DB, _ string, _ *ChatLogger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		infra.HTTPErrorResponse(w, infra.HTTPError{
			Status:  http.StatusGone,
			Code:    "deprecated",
			Message: "POST /api/chat removed; use WebSocket at /api/chat/ws",
		})
	}
}

