// Package mcp_service собирает MCP-сервер (Model Context Protocol) для
// игры «Вечерний детектив»: инструменты MCP вызывают игровые сервисы
// напрямую (через интерфейс GamePlayer), без HTTP-прослойки. MCP-сервер
// монтируется в основной процесс как HTTP-эндпоинт /mcp (streamable HTTP)
// на REST-gateway.
package mcp_service

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	"evening_detective_server/internal/modules/storytelling"
	"evening_detective_server/internal/services/game_service"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// defaultCallTimeout — таймаут на выполнение одного инструмента:
// защита от зависших запросов к БД (10s — как у прежнего HTTP-прокси).
const defaultCallTimeout = 10 * time.Second

// GamePlayer — граница доступа к игровым сервисам, реализуемая
// *game_service.GameService; интерфейс позволяет тестировать сервис
// без БД.
type GamePlayer interface {
	GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_service.Game, error)
	AddTeamAction(ctx context.Context, teamId int, password, actionCode string) error
}

// MCPService собирает MCP-сервер с игровыми инструментами. Сервис не
// хранит состояние между вызовами: каждый вызов получает id команды и
// пароль явно — либо из ссылки на игру через инструмент connect, либо
// напрямую.
type MCPService struct {
	game GamePlayer
}

// NewMCPService создаёт сервис поверх игровых сервисов.
func NewMCPService(game GamePlayer) *MCPService {
	return &MCPService{game: game}
}

// Server собирает и возвращает MCP-сервер с зарегистрированными
// инструментами.
func (s *MCPService) Server() *server.MCPServer {
	srv := server.NewMCPServer(
		"evening-detective-mcp",
		"0.1.0",
		server.WithInstructions("Инструменты для игры «Вечерний детектив»: подключитесь к игре по ссылке (connect) — организатор выдаёт ссылку вида /team-story/{id}?password=..., — затем смотрите историю команды (get_team_story) и делайте ходы (make_move) по кодам точек сценария."),
	)

	srv.AddTool(
		mcp.NewTool(
			"connect",
			mcp.WithDescription("Подключиться к игре по ссылке вида /team-story/{id}?password=... (принимаются относительные и абсолютные ссылки, в т.ч. с любым хостом — запросы по ним не выполняются, из ссылки берутся только id команды и пароль). Извлекает id команды и пароль и сразу возвращает текущую историю команды вместе с этими параметрами для последующих вызовов get_team_story/make_move."),
			mcp.WithString("url", mcp.Required(), mcp.Description("Ссылка на игру, например /team-story/10?password=team-pass-1")),
		),
		s.handleConnect,
	)

	srv.AddTool(
		mcp.NewTool(
			"get_team_story",
			mcp.WithDescription("Получить текущую историю команды: введение сценария (текст и аудио), видимые точки сценария (текст, двери, улики) и информацию об игре. Команда идентифицируется паролем."),
			mcp.WithNumber("team_id", mcp.Required(), mcp.Description("ID команды")),
			mcp.WithString("password", mcp.Required(), mcp.Description("Пароль команды")),
		),
		s.handleGetTeamStory,
	)

	srv.AddTool(
		mcp.NewTool(
			"make_move",
			mcp.WithDescription("Сделать ход команды — перейти в точку сценария по её коду. После успешного хода возвращает обновлённую историю команды. Команда идентифицируется паролем."),
			mcp.WithNumber("team_id", mcp.Required(), mcp.Description("ID команды")),
			mcp.WithString("password", mcp.Required(), mcp.Description("Пароль команды")),
			mcp.WithString("code", mcp.Required(), mcp.Description("Код точки сценария, в которую идёт команда")),
		),
		s.handleMakeMove,
	)

	return srv
}

// connectArgs — результат разбора ссылки на игру.
type connectArgs struct {
	teamID   int64
	password string
}

// parseConnectURL разбирает ссылку на игру вида /team-story/{id}?password=...
// и возвращает id команды и пароль.
//
// Ссылка используется ТОЛЬКО как носитель id и пароля: MCP-сервер не
// выполняет HTTP-запросов по ней, поэтому схема и хост не проверяются
// (протокол-относительная ссылка //host/team-story/{id}?password=...
// принимается, «mailto:...» отклоняется на проверке пути).
func parseConnectURL(rawURL string) (connectArgs, error) {
	if strings.TrimSpace(rawURL) == "" {
		return connectArgs{}, fmt.Errorf("требуется url")
	}

	u, err := url.Parse(rawURL)
	if err != nil {
		return connectArgs{}, fmt.Errorf("невалидная ссылка: %w", err)
	}

	const prefix = "/team-story/"
	if !strings.HasPrefix(u.Path, prefix) {
		return connectArgs{}, fmt.Errorf("невалидная ссылка: путь должен быть %s{id}", prefix)
	}
	idPart := strings.TrimPrefix(u.Path, prefix)
	if idPart == "" {
		return connectArgs{}, fmt.Errorf("невалидная ссылка: отсутствует id команды в пути %q", u.Path)
	}
	teamID, err := strconv.ParseInt(idPart, 10, 64)
	if err != nil || teamID < 0 {
		return connectArgs{}, fmt.Errorf("невалидная ссылка: id команды %q — не целое неотрицательное число", idPart)
	}

	password := u.Query().Get("password")
	if password == "" {
		return connectArgs{}, fmt.Errorf("невалидная ссылка: отсутствует query-параметр password")
	}

	return connectArgs{teamID: teamID, password: password}, nil
}

// gameResponse — компактное описание игры для ответа инструментов
// (game_service.Game JSON-тегов не имеет).
type gameResponse struct {
	ID     int    `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
}

// storyResponse — JSON-ответ инструментов: история команды + инфо об игре.
type storyResponse struct {
	Story *storytelling.Story `json:"story"`
	Game  *gameResponse       `json:"game"`
}

// storyJSON сериализует историю команды и игру в JSON.
func storyJSON(story *storytelling.Story, game *game_service.Game) (string, error) {
	resp := storyResponse{
		Story: story,
		Game: &gameResponse{
			ID:     game.ID,
			Name:   game.Name,
			Status: game.Status,
		},
	}
	encoded, err := json.Marshal(resp)
	if err != nil {
		return "", err
	}
	return string(encoded), nil
}

func (s *MCPService) handleConnect(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	rawURL, _ := req.GetArguments()["url"].(string)

	args, err := parseConnectURL(rawURL)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("connect: %v", err)), nil
	}

	ctx, cancel := context.WithTimeout(ctx, defaultCallTimeout)
	defer cancel()

	story, game, err := s.game.GetTeamActions(ctx, int(args.teamID), args.password)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("connect: %v", err)), nil
	}

	jsonBody, err := storyJSON(story, game)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("connect: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf(
		"Подключено к команде %d. Пароль: %s. История команды:\n%s",
		args.teamID,
		args.password,
		jsonBody,
	)), nil
}

func (s *MCPService) handleGetTeamStory(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	teamID, ok := argInt64(req.GetArguments()["team_id"])
	if !ok {
		return mcp.NewToolResultError("get_team_story: требуется team_id (число)"), nil
	}
	password, _ := req.GetArguments()["password"].(string)
	if password == "" {
		return mcp.NewToolResultError("get_team_story: требуется password"), nil
	}

	ctx, cancel := context.WithTimeout(ctx, defaultCallTimeout)
	defer cancel()

	story, game, err := s.game.GetTeamActions(ctx, int(teamID), password)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("get_team_story: %v", err)), nil
	}

	jsonBody, err := storyJSON(story, game)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("get_team_story: %v", err)), nil
	}
	return mcp.NewToolResultText(jsonBody), nil
}

func (s *MCPService) handleMakeMove(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := req.GetArguments()
	teamID, ok := argInt64(args["team_id"])
	if !ok {
		return mcp.NewToolResultError("make_move: требуется team_id (число)"), nil
	}
	password, _ := args["password"].(string)
	code, _ := args["code"].(string)
	if password == "" || code == "" {
		return mcp.NewToolResultError("make_move: требуется password и code"), nil
	}

	ctx, cancel := context.WithTimeout(ctx, defaultCallTimeout)
	defer cancel()

	if err := s.game.AddTeamAction(ctx, int(teamID), password, code); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("make_move: %v", err)), nil
	}

	story, game, err := s.game.GetTeamActions(ctx, int(teamID), password)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("make_move: %v", err)), nil
	}

	jsonBody, err := storyJSON(story, game)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("make_move: %v", err)), nil
	}
	return mcp.NewToolResultText(jsonBody), nil
}

// argInt64 достаёт int64 из аргумента инструмента: числа приходят как
// float64 (JSON), строки — как string.
func argInt64(v any) (int64, bool) {
	switch n := v.(type) {
	case float64:
		return int64(n), true
	case string:
		id, err := strconv.ParseInt(n, 10, 64)
		return id, err == nil
	default:
		return 0, false
	}
}
