package main

import (
	"bytes"
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"fn-registry/functions/infra"
)

// runMCPServer is the entry point for the `kanban mcp` subcommand. It runs
// stdio JSON-RPC and forwards each tool call to the kanban backend's
// /api/tool/{name} endpoint, authenticated with a shared internal token.
//
// Required env vars (set by the parent kanban process when generating mcp.json):
//   KANBAN_BACKEND_URL    — e.g. http://127.0.0.1:8095
//   KANBAN_INTERNAL_TOKEN — token to send in X-Internal-Token header
func runMCPServer(args []string) error {
	fs := flag.NewFlagSet("kanban mcp", flag.ContinueOnError)
	urlFlag := fs.String("url", os.Getenv("KANBAN_BACKEND_URL"), "kanban backend URL")
	tokenFlag := fs.String("token", os.Getenv("KANBAN_INTERNAL_TOKEN"), "internal token")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *urlFlag == "" {
		return fmt.Errorf("--url or KANBAN_BACKEND_URL required")
	}
	if *tokenFlag == "" {
		return fmt.Errorf("--token or KANBAN_INTERNAL_TOKEN required")
	}

	httpClient := &http.Client{Timeout: 30 * time.Second}

	tools := mcpToolDefs()
	handler := func(ctx context.Context, name string, input json.RawMessage) (any, bool, error) {
		body := []byte(input)
		if len(body) == 0 {
			body = []byte("{}")
		}
		req, err := http.NewRequestWithContext(ctx, "POST", *urlFlag+"/api/tool/"+name, bytes.NewReader(body))
		if err != nil {
			return nil, false, err
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set(internalTokenHeader, *tokenFlag)
		resp, err := httpClient.Do(req)
		if err != nil {
			return nil, false, err
		}
		defer resp.Body.Close()
		buf, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, false, err
		}
		if resp.StatusCode >= 500 {
			return nil, false, fmt.Errorf("backend %d: %s", resp.StatusCode, string(buf))
		}
		// 4xx and 2xx both serialize as ToolResult JSON. Decode and map.
		var tr ToolResult
		if err := json.Unmarshal(buf, &tr); err != nil {
			// Non-ToolResult body (e.g. unauthorized error envelope from infra).
			return string(buf), resp.StatusCode >= 400, nil
		}
		if !tr.OK {
			return tr.Error, true, nil
		}
		return tr.Result, false, nil
	}

	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	return infra.ServeMCP(ctx, infra.MCPServerOpts{
		Name:    "kanban",
		Version: "1.0.0",
		Tools:   tools,
		Handler: handler,
		In:      os.Stdin,
		Out:     os.Stdout,
		Logger:  os.Stderr,
	})
}

// mcpToolDefs returns the JSON-Schema definitions for every kanban tool.
// Names match the executeTool dispatch table in tools.go.
func mcpToolDefs() []infra.MCPToolDef {
	return []infra.MCPToolDef{
		{
			Name:        "list_board",
			Description: "Lista columnas y tarjetas del tablero. Sin argumentos. Devuelve {columns, cards}.",
			InputSchema: rawSchema(map[string]any{"type": "object", "properties": map[string]any{}}),
		},
		{
			Name:        "create_column",
			Description: "Crea una columna nueva. Devuelve la columna creada con su id.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"name": map[string]any{"type": "string", "description": "Nombre de la columna"},
				},
				"required": []string{"name"},
			}),
		},
		{
			Name:        "update_column",
			Description: "Modifica una columna existente. Pasa al menos uno: name, location ('board'|'sidebar'), width (200..800 px), wip_limit (0=sin limite), is_done (terminal: cards cuentan como completadas).",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id":        map[string]any{"type": "string"},
					"name":      map[string]any{"type": "string"},
					"location":  map[string]any{"type": "string", "enum": []string{"board", "sidebar"}},
					"width":     map[string]any{"type": "integer"},
					"wip_limit": map[string]any{"type": "integer"},
					"is_done":   map[string]any{"type": "boolean"},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name:        "rename_column",
			Description: "Alias de update_column con solo {id, name}.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id":   map[string]any{"type": "string"},
					"name": map[string]any{"type": "string"},
				},
				"required": []string{"id", "name"},
			}),
		},
		{
			Name:        "delete_column",
			Description: "Elimina una columna y todas sus tarjetas (las envia a la papelera).",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id": map[string]any{"type": "string"},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name:        "reorder_columns",
			Description: "Reordena columnas. ids es el array completo de columnas en el nuevo orden.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"ids": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
				},
				"required": []string{"ids"},
			}),
		},
		{
			Name:        "create_card",
			Description: "Crea una tarjeta en una columna. column_id y title obligatorios.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"column_id":   map[string]any{"type": "string"},
					"requester":   map[string]any{"type": "string"},
					"title":       map[string]any{"type": "string"},
					"description": map[string]any{"type": "string"},
				},
				"required": []string{"column_id", "title"},
			}),
		},
		{
			Name:        "update_card",
			Description: "Edita campos de una tarjeta. Color: blue|teal|violet|pink|orange|green|yellow|red|''. locked bloquea movimiento. assignee_id null para desasignar.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id":          map[string]any{"type": "string"},
					"requester":   map[string]any{"type": "string"},
					"title":       map[string]any{"type": "string"},
					"description": map[string]any{"type": "string"},
					"color":       map[string]any{"type": "string"},
					"locked":      map[string]any{"type": "boolean"},
					"assignee_id": map[string]any{"type": []string{"string", "null"}},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name:        "delete_card",
			Description: "Envia una tarjeta a la papelera.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id": map[string]any{"type": "string"},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name:        "move_card",
			Description: "Mueve una tarjeta a otra columna. Si omites ordered_ids, se anade al final.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id":          map[string]any{"type": "string"},
					"column_id":   map[string]any{"type": "string"},
					"ordered_ids": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
				},
				"required": []string{"id", "column_id"},
			}),
		},
		{
			Name:        "card_history",
			Description: "Devuelve el historial de cambios de una tarjeta.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id": map[string]any{"type": "string"},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name:        "find_cards",
			Description: "Busca tarjetas. query (texto en title/description/requester), column_id (filtra por columna), requester (filtra por solicitante).",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"query":     map[string]any{"type": "string"},
					"column_id": map[string]any{"type": "string"},
					"requester": map[string]any{"type": "string"},
				},
			}),
		},
		{
			Name:        "list_users",
			Description: "Lista usuarios disponibles para asignar tarjetas.",
			InputSchema: rawSchema(map[string]any{"type": "object", "properties": map[string]any{}}),
		},
		{
			Name:        "assign_card",
			Description: "Asigna o desasigna una tarjeta. assignee_id null para desasignar.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id":          map[string]any{"type": "string"},
					"assignee_id": map[string]any{"type": []string{"string", "null"}},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name:        "add_comment",
			Description: "Anade un comentario (card_message) a una tarjeta. Requiere card_id, body y autor (author_id o author_username). Devuelve el CardMessage creado.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"card_id":         map[string]any{"type": "string"},
					"body":            map[string]any{"type": "string"},
					"author_id":       map[string]any{"type": "string"},
					"author_username": map[string]any{"type": "string"},
				},
				"required": []string{"card_id", "body"},
			}),
		},
		{
			Name:        "list_comments",
			Description: "Lista los comentarios (card_messages) de una tarjeta en orden cronologico.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"card_id": map[string]any{"type": "string"},
				},
				"required": []string{"card_id"},
			}),
		},
		{
			Name: "delete_comment",
			Description: "Borra un comentario propio. Solo el autor original puede borrar (validado en server). " +
				"Requiere autenticacion via MCP HTTP — el actor se infiere del bearer token. " +
				"Output: {ok:true}.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id": map[string]any{"type": "string", "description": "ID del card_message a borrar (no de la card)."},
				},
				"required": []string{"id"},
			}),
		},
		{
			Name: "get_card",
			Description: "Devuelve una tarjeta activa (no archivada) por id o por seq_num. Read-only. " +
				"Pasa exactamente UNO de los dos: id (hash interno) o seq_num (entero visible, ej. la '115' de 'card 00115'). " +
				"Output: Card completa con time_in_column_ms, total_locked_ms, tags, stickers, deadline.",
			InputSchema: rawSchema(map[string]any{
				"type": "object",
				"properties": map[string]any{
					"id":      map[string]any{"type": "string", "description": "ID hash de la tarjeta (16 hex)."},
					"seq_num": map[string]any{"type": "integer", "description": "Numero secuencial visible al usuario."},
				},
			}),
		},
	}
}

func rawSchema(s map[string]any) json.RawMessage {
	b, err := json.Marshal(s)
	if err != nil {
		panic(err)
	}
	return b
}

// writeMCPConfig writes a temporary mcp.json that points to this binary's
// `mcp` subcommand with the given URL and token. Returns the absolute path of
// the file created. Caller is responsible for removing it.
func writeMCPConfig(binPath, backendURL, token string) (string, error) {
	cfg := map[string]any{
		"mcpServers": map[string]any{
			"kanban": map[string]any{
				"command": binPath,
				"args":    []string{"mcp"},
				"env": map[string]string{
					"KANBAN_BACKEND_URL":    backendURL,
					"KANBAN_INTERNAL_TOKEN": token,
				},
			},
		},
	}
	b, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return "", err
	}
	f, err := os.CreateTemp("", "kanban-mcp-*.json")
	if err != nil {
		return "", err
	}
	if _, err := f.Write(b); err != nil {
		f.Close()
		os.Remove(f.Name())
		return "", err
	}
	if err := f.Close(); err != nil {
		os.Remove(f.Name())
		return "", err
	}
	return f.Name(), nil
}
