package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.config.McpProperties;
import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import org.springframework.stereotype.Service;

/** MCP boundary exposing Select AI query and read-only prompt diagnostic tools. */
@Service
public class McpSseService {

  private static final String SELECT_AI_TOOL_PATH = "/mcp (tools/call)";
  private static final String GAME_CATALOG_TOOL = "oracle.select_ai.game_catalog_resolve";
  private static final String GAME_QUERY_PLAN_TOOL = "oracle.select_ai.game_query_plan";

  private final SelectAiService selectAiService;
  private final ObjectMapper objectMapper;
  private final BackofficeProperties properties;
  private final McpProperties mcpProperties;
  private final QaVectorService qaVectorService;
  private final GameScopeService gameScopeService;
  private final GameCatalogVectorService gameCatalogVectorService;

  public McpSseService(
      SelectAiService selectAiService,
      ObjectMapper objectMapper,
      BackofficeProperties properties,
      McpProperties mcpProperties,
      QaVectorService qaVectorService,
      GameScopeService gameScopeService,
      GameCatalogVectorService gameCatalogVectorService
  ) {
    this.selectAiService = selectAiService;
    this.objectMapper = objectMapper;
    this.properties = properties;
    this.mcpProperties = mcpProperties;
    this.qaVectorService = qaVectorService;
    this.gameScopeService = gameScopeService;
    this.gameCatalogVectorService = gameCatalogVectorService;
  }

  public ObjectNode handle(String contextPath, JsonNode request) {
    return handle(contextPath, request, "");
  }

  /**
   * The HTTP bearer token is the business-user subject token; no separate MCP token is used.
   */
  public ObjectNode handle(String contextPath, JsonNode request, String vpdBearerToken) {
    ObjectNode response = objectMapper.createObjectNode();
    response.put("jsonrpc", "2.0");
    if (request != null && request.has("id")) {
      response.set("id", request.get("id"));
    }

    String method = request == null || !request.hasNonNull("method") ? "" : request.get("method").asText();
    JsonNode parameters = request == null ? objectMapper.createObjectNode() : request.path("params");
    try {
      response.set("result", switch (method) {
        case "initialize" -> initializeResult(contextPath);
        case "notifications/initialized" -> objectMapper.createObjectNode();
        case "tools/list" -> toolsListResult();
        case "tools/call" -> toolsCallResult(parameters, vpdBearerToken);
        default -> throw new AppException("지원하지 않는 MCP method입니다: " + method);
      });
    } catch (Exception e) {
      response.remove("result");
      ObjectNode error = objectMapper.createObjectNode();
      error.put("code", -32000);
      error.put("message", e.getMessage());
      response.set("error", error);
    }
    return response;
  }

  /** Tools registered by this MCP server. */
  public List<McpToolView> registeredTools() {
    return List.of(
        selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(),
        qaVectorStoreView(), fewShotNl2SqlView(), gameScopeView(), gameCatalogView(), gameQueryPlanView());
  }

  private ObjectNode initializeResult(String contextPath) {
    ObjectNode result = objectMapper.createObjectNode();
    result.put("protocolVersion", "2024-11-05");
    ObjectNode serverInfo = objectMapper.createObjectNode();
    serverInfo.put("name", "vpd-ords-backoffice-" + contextPath);
    serverInfo.put("version", "0.1.0");
    result.set("serverInfo", serverInfo);
    ObjectNode capabilities = objectMapper.createObjectNode();
    capabilities.set("tools", objectMapper.createObjectNode());
    result.set("capabilities", capabilities);
    return result;
  }

  private ObjectNode toolsListResult() {
    ObjectNode result = objectMapper.createObjectNode();
    ArrayNode tools = objectMapper.createArrayNode();
    tools.add(toolDefinition(selectAiQueryView()));
    tools.add(toolDefinition(selectAiShowpromptView()));
    tools.add(toolDefinition(qaVectorSearchView()));
    tools.add(toolDefinition(qaVectorStoreView()));
    tools.add(toolDefinition(fewShotNl2SqlView()));
    tools.add(toolDefinition(gameScopeView()));
    tools.add(toolDefinition(gameCatalogView()));
    tools.add(toolDefinition(gameQueryPlanView()));
    result.set("tools", tools);
    return result;
  }

  private ObjectNode toolDefinition(McpToolView toolView) {
    ObjectNode item = objectMapper.createObjectNode();
    item.put("name", toolView.name());
    item.put("description", toolView.description());

    ObjectNode schema = objectMapper.createObjectNode();
    schema.put("type", "object");
    ObjectNode properties = objectMapper.createObjectNode();

    ArrayNode required = objectMapper.createArrayNode();
    if (qaVectorSearchToolName().equals(toolView.name()) || gameScopeToolName().equals(toolView.name())
        || GAME_CATALOG_TOOL.equals(toolView.name()) || GAME_QUERY_PLAN_TOOL.equals(toolView.name())) {
      addStringProperty(properties, "question", "few-shot 예제 SQL을 찾을 현재 질문입니다.", 4000);
      ObjectNode topK = properties.putObject("topK");
      topK.put("type", "integer");
      topK.put("description", "반환할 유사 예제 수입니다. 기본값은 3입니다.");
      topK.put("minimum", 1);
      topK.put("maximum", 20);
      topK.put("default", 3);
      required.add("question");
    } else if (qaVectorStoreToolName().equals(toolView.name())) {
      addStringProperty(properties, "question", "검토된 Select AI 예제가 답한 업무 질문입니다.", 4000);
      addStringProperty(properties, "answerSql", "검토된 단일 읽기 전용 SELECT/WITH SQL입니다.", 20000);
      addStringProperty(properties, "answer", "선택 사항인 답변 또는 검토 메모입니다.", 20000);
      required.add("question");
      required.add("answerSql");
    } else {
      addStringProperty(properties, "prompt", promptDescription(), 4000);
      if (fewShotNl2SqlToolName().equals(toolView.name())) {
        addStringProperty(properties, "scopeGameKey",
            "선택 사항입니다. game_scope_resolve가 반환한 SUPPORTED gameKey만 전달하세요.", 128);
        ObjectNode plan = properties.putObject("queryPlan");
        plan.put("type", "object");
        plan.put("description", "앞 단계 game_query_plan의 구조화된 결과입니다.");
        plan.putObject("additionalProperties").put("type", "object");
      }
      required.add("prompt");
    }
    schema.set("properties", properties);
    schema.set("required", required);
    schema.put("additionalProperties", false);
    item.set("inputSchema", schema);
    return item;
  }

  private void addStringProperty(ObjectNode properties, String name, String description, int maxLength) {
    ObjectNode property = properties.putObject(name);
    property.put("type", "string");
    property.put("description", description);
    property.put("maxLength", maxLength);
  }

  private ObjectNode toolsCallResult(JsonNode params, String vpdBearerToken) {
    String calledToolName = params.path("name").asText("");
    boolean queryTool = toolName().equals(calledToolName);
    boolean showpromptTool = showpromptToolName().equals(calledToolName);
    boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
    boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
    boolean fewShotNl2SqlTool = fewShotNl2SqlToolName().equals(calledToolName);
    boolean gameScopeTool = gameScopeToolName().equals(calledToolName);
    boolean gameCatalogTool = GAME_CATALOG_TOOL.equals(calledToolName);
    boolean gameQueryPlanTool = GAME_QUERY_PLAN_TOOL.equals(calledToolName);
    if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool && !fewShotNl2SqlTool
        && !gameScopeTool && !gameCatalogTool && !gameQueryPlanTool) {
      throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
    }

    JsonNode arguments = params.path("arguments");
    String token = vpdBearerToken == null ? "" : vpdBearerToken.trim();
    if (token.isBlank()) {
      return tokenAccessDeniedResult();
    }
    JsonNode response;
    try {
      if (qaVectorSearchTool) {
        response = qaVectorSearchResponse(
            qaVectorService.search(token, arguments.path("question").asText(""), arguments.path("topK").asInt(3)));
      } else if (qaVectorStoreTool) {
        response = qaVectorStoreResponse(qaVectorService.store(
            token,
            arguments.path("question").asText(""),
            arguments.path("answerSql").asText(""),
            arguments.path("answer").isMissingNode() ? null : arguments.path("answer").asText(null)
        ));
      } else if (gameScopeTool) {
        response = gameScopeResponse(gameScopeService.resolve(
            token, arguments.path("question").asText("")));
      } else if (gameCatalogTool) {
        response = gameCatalogVectorResponse(gameCatalogVectorService.resolve(
            token, arguments.path("question").asText(""), arguments.path("topK").asInt(5)));
      } else if (gameQueryPlanTool) {
        response = gameQueryPlanResponse(gameCatalogVectorService.resolve(
            token, arguments.path("question").asText(""), arguments.path("topK").asInt(5)));
      } else {
        String prompt = arguments.path("prompt").asText("");
        response = queryTool
            ? selectAiService.generateAndExecute(token, prompt)
            : fewShotNl2SqlTool
            ? selectAiService.generateAndExecute(token, prompt,
                    arguments.path("scopeGameKey").asText(""),
                    arguments.path("queryPlan"))
            : selectAiService.generatePrompt(token, prompt);
      }
    } catch (VpdTokenAccessDeniedException ignored) {
      return tokenAccessDeniedResult();
    }

    ObjectNode payload = objectMapper.createObjectNode();
    payload.put("toolName", calledToolName);
    payload.put("profile", selectAiProfile());
    payload.put("ordsPath", SELECT_AI_TOOL_PATH);
    payload.set("response", response);

    ObjectNode result = objectMapper.createObjectNode();
    ArrayNode content = objectMapper.createArrayNode();
    ObjectNode text = objectMapper.createObjectNode();
    text.put("type", "text");
    text.put("text", pretty(payload));
    content.add(text);
    result.set("content", content);
    result.put("isError", false);
    return result;
  }

  private ObjectNode qaVectorSearchResponse(QaVectorService.VectorSearchResult result) {
    ObjectNode response = objectMapper.createObjectNode();
    response.put("status", "QA_VECTOR_SEARCH");
    response.put("instruction", "Select AI SQL 생성 전에 few-shot 예제 SQL 후보를 확인합니다.");
    response.put("question", result.question());
    response.put("topK", result.topK());
    ArrayNode examples = response.putArray("examples");
    for (QaVectorService.VectorExample example : result.examples()) {
      ObjectNode item = examples.addObject();
      item.put("exampleId", example.exampleId());
      item.put("question", example.question());
      item.put("answerSql", example.answerSql());
      if (example.answer() != null) {
        item.put("answer", example.answer());
      }
      item.put("embeddingModel", example.embeddingModel());
      item.put("cosineDistance", example.cosineDistance());
    }
    return response;
  }

  private ObjectNode gameCatalogVectorResponse(GameCatalogVectorService.Resolution resolution) {
    ObjectNode response = objectMapper.createObjectNode();
    response.put("scopeType", resolution.scopeType());
    boolean hasUnmatched = resolution.mentions().stream()
        .anyMatch(mention -> "UNMATCHED".equals(mention.status()));
    response.put("status", resolution.candidates().isEmpty()
        ? "NO_MATCH" : hasUnmatched ? "PARTIAL" : "CANDIDATES");
    ArrayNode items = response.putArray("matchedGames");
    for (GameCatalogVectorService.GameCandidate c : resolution.candidates()) {
      addGameCandidate(items.addObject(), c, true);
    }
    ArrayNode mentionResults = response.putArray("mentionResults");
    for (GameCatalogVectorService.MentionResolution mention
        : resolution.mentions()) {
      ObjectNode item = mentionResults.addObject();
      item.put("mention", mention.mention());
      item.put("status", mention.status());
      if (mention.matchedGame() != null) {
        item.put("matchedGameKey", mention.matchedGame().gameKey());
      }
      ArrayNode candidates = item.putArray("candidateGames");
      for (GameCatalogVectorService.GameCandidate candidate
          : mention.candidates()) {
        addGameCandidate(candidates.addObject(), candidate, false);
      }
    }
    return response;
  }

  private void addGameCandidate(
      ObjectNode item,
      GameCatalogVectorService.GameCandidate candidate,
      boolean includeAliases
  ) {
    item.put("gameKey", candidate.gameKey());
    item.put("gameId", candidate.gameId());
    item.put("gamePrefix", candidate.gamePrefix());
    item.put("gameName", candidate.gameName());
    if (includeAliases) {
      item.put("aliases", candidate.aliases());
    }
    item.put("cosineDistance", candidate.similarity());
  }

  private ObjectNode qaVectorStoreResponse(QaVectorService.VectorStoreResult stored) {
    ObjectNode response = objectMapper.createObjectNode();
    response.put("status", "QA_VECTOR_STORED");
    response.put("instruction", "검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장했습니다.");
    response.put("exampleId", stored.exampleId());
    response.put("question", stored.question());
    response.put("embeddingModel", stored.embeddingModel());
    return response;
  }

  /** Converts DB-derived scope facts to a tool response; this tool never executes query SQL. */
  private ObjectNode gameScopeResponse(GameScopeService.GameScopeResult result) {
    ObjectNode response = objectMapper.createObjectNode();
    response.put("status", result.status());
    response.put("instruction", "반환된 SUPPORTED scope에만 Few-shot NL2SQL을 호출하세요.");
    response.put("question", result.question());
    ArrayNode scopes = response.putArray("scopes");
    for (GameScopeService.GameScope scope : result.scopes()) {
      ObjectNode item = scopes.addObject();
      item.put("gameKey", scope.gameKey());
      item.put("displayName", scope.displayName());
      item.put("matchedAlias", scope.matchedAlias());
      item.put("status", scope.status());
      item.put("reasonCode", scope.reasonCode());
      item.put("aliasPriority", scope.aliasPriority());
      item.put("scopeVersion", scope.scopeVersion());
      item.put("nextAction", "SUPPORTED".equals(scope.status())
          ? "CALL_FEW_SHOT_NL2SQL" : "REPORT_UNSUPPORTED");
    }
    return response;
  }

  private ObjectNode tokenAccessDeniedResult() {
    ObjectNode payload = objectMapper.createObjectNode();
    payload.put("status", "VPD_TOKEN_DENIED");
    payload.put("message", "토큰이 없거나 유효하지 않아 이 요청을 수행할 권한이 없습니다.");

    ObjectNode result = objectMapper.createObjectNode();
    ArrayNode content = objectMapper.createArrayNode();
    ObjectNode text = objectMapper.createObjectNode();
    text.put("type", "text");
    text.put("text", pretty(payload));
    content.add(text);
    result.set("content", content);
    result.put("isError", true);
    return result;
  }

  private McpToolView selectAiQueryView() {
    String profile = selectAiProfile();
    return new McpToolView(
        toolName(),
        profile + " 프로파일로 " + toolDescription(),
        -1L,
        toolLabel(),
        SELECT_AI_TOOL_PATH
    );
  }

  private McpToolView selectAiShowpromptView() {
    String profile = selectAiProfile();
    return new McpToolView(
        showpromptToolName(),
        profile + " 프로파일로 " + showpromptToolDescription(),
        -1L,
        showpromptToolLabel(),
        SELECT_AI_TOOL_PATH
    );
  }

  private McpToolView qaVectorSearchView() {
    return new McpToolView(
        qaVectorSearchToolName(), qaVectorSearchToolDescription(), -1L,
        qaVectorSearchToolLabel(), SELECT_AI_TOOL_PATH);
  }

  private McpToolView qaVectorStoreView() {
    return new McpToolView(
        qaVectorStoreToolName(), qaVectorStoreToolDescription(), -1L,
        qaVectorStoreToolLabel(), SELECT_AI_TOOL_PATH);
  }

  private McpToolView fewShotNl2SqlView() {
    return new McpToolView(
        fewShotNl2SqlToolName(), fewShotNl2SqlToolDescription(), -1L,
        fewShotNl2SqlToolLabel(), SELECT_AI_TOOL_PATH);
  }

  private McpToolView gameScopeView() {
    return new McpToolView(
        gameScopeToolName(), gameScopeToolDescription(), -1L,
        gameScopeToolLabel(), SELECT_AI_TOOL_PATH);
  }

  private McpToolView gameCatalogView() {
    return new McpToolView(
        GAME_CATALOG_TOOL,
        "게임 카탈로그에서 질문의 게임 범위를 확인합니다. SQL은 실행하지 않습니다.",
        -1L, "게임 카탈로그 범위 확인", SELECT_AI_TOOL_PATH);
  }

  private McpToolView gameQueryPlanView() {
    return new McpToolView(GAME_QUERY_PLAN_TOOL,
        "질문에서 게임 범위와 실행 모드를 판정합니다. SQL은 실행하지 않습니다.",
        -1L, "게임 질의 계획", SELECT_AI_TOOL_PATH);
  }

  private ObjectNode gameQueryPlanResponse(GameCatalogVectorService.Resolution resolution) {
    ObjectNode response = gameCatalogVectorResponse(resolution);
    List<GameCatalogVectorService.MentionResolution> unmatchedMentions =
        resolution.mentions().stream()
            .filter(mention -> "UNMATCHED".equals(mention.status()))
            .toList();
    response.put("status", resolution.candidates().isEmpty()
        ? "NO_MATCH" : unmatchedMentions.isEmpty() ? "SUPPORTED" : "PARTIAL");
    ArrayNode supported = response.putArray("supportedGames");
    for (GameCatalogVectorService.GameCandidate c : resolution.candidates()) {
      ObjectNode item = supported.addObject();
      item.put("gameKey", c.gameKey());
      item.put("gameId", c.gameId());
      item.put("gameName", c.gameName());
      item.put("gamePrefix", c.gamePrefix());
      item.put("cosineDistance", c.similarity());
    }
    ArrayNode unmatched = response.putArray("unmatchedGames");
    for (GameCatalogVectorService.MentionResolution mention
        : unmatchedMentions) {
      ObjectNode item = unmatched.addObject();
      item.put("mention", mention.mention());
      item.put("reasonCode", "NO_CATALOG_IDENTITY_MATCH");
    }
    response.put("nextAction", resolution.candidates().isEmpty()
        ? "STOP_NO_MATCH"
        : ("MULTI_GAME".equals(resolution.scopeType())
            ? "CALL_FEWSHOT_PER_GAME" : "CALL_FEWSHOT"));
    String mode = switch (resolution.scopeType()) {
      case "SINGLE_GAME" -> "SINGLE";
      case "MULTI_GAME" -> "FAN_OUT";
      case "ALL_GAMES" -> "GROUPED";
      default -> "BLOCKED";
    };
    response.put("executionMode", mode);
    return response;
  }

  private String selectAiProfile() {
    BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
    if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
      return "";
    }
    return selectAi.profile().trim();
  }

  private String toolName() {
    return mcpProperties == null ? "oracle.select_ai.data_text2sql" : mcpProperties.resolvedToolName();
  }

  private String toolLabel() {
    return mcpProperties == null ? "업무 데이터 Text2SQL" : mcpProperties.resolvedToolLabel();
  }

  private String toolDescription() {
    return mcpProperties == null
        ? "승인된 업무 데이터용 읽기 전용 SELECT/WITH SQL을 생성하고 검증 후 실행합니다."
        : mcpProperties.resolvedToolDescription();
  }

  private String promptDescription() {
    return mcpProperties == null ? "업무 데이터에서 조회할 내용을 자연어로 입력합니다." : mcpProperties.resolvedPromptDescription();
  }

  private String showpromptToolName() {
    return mcpProperties == null
        ? "oracle.select_ai.data_showprompt"
        : mcpProperties.resolvedShowpromptToolName();
  }

  private String showpromptToolLabel() {
    return mcpProperties == null
        ? "업무 데이터 SHOWPROMPT"
        : mcpProperties.resolvedShowpromptToolLabel();
  }

  private String showpromptToolDescription() {
    return mcpProperties == null
        ? "Select AI가 SQL 생성에 사용한 prompt를 조회하는 읽기 전용 진단 도구입니다."
        : mcpProperties.resolvedShowpromptToolDescription();
  }

  private String qaVectorSearchToolName() {
    return mcpProperties == null ? "oracle.select_ai.qa_vector_search"
        : mcpProperties.resolvedQaVectorSearchToolName();
  }

  private String qaVectorSearchToolLabel() {
    return mcpProperties == null ? "Select AI 예제 SQL 조회"
        : mcpProperties.resolvedQaVectorSearchToolLabel();
  }

  private String qaVectorSearchToolDescription() {
    return mcpProperties == null
        ? "현재 질문에 사용할 유사 예제 SQL을 Select AI 실행 전에 조회합니다."
        : mcpProperties.resolvedQaVectorSearchToolDescription();
  }

  private String qaVectorStoreToolName() {
    return mcpProperties == null ? "oracle.select_ai.qa_vector_store"
        : mcpProperties.resolvedQaVectorStoreToolName();
  }

  private String qaVectorStoreToolLabel() {
    return mcpProperties == null ? "Select AI 예제 SQL 저장"
        : mcpProperties.resolvedQaVectorStoreToolLabel();
  }

  private String qaVectorStoreToolDescription() {
    return mcpProperties == null
        ? "검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다."
        : mcpProperties.resolvedQaVectorStoreToolDescription();
  }

  private String fewShotNl2SqlToolName() {
    return mcpProperties == null ? "oracle.select_ai.smilegate_fewshot_nl2sql"
        : mcpProperties.resolvedFewShotNl2SqlToolName();
  }

  private String fewShotNl2SqlToolLabel() {
    return mcpProperties == null ? "Few-shot NL2SQL 실행"
        : mcpProperties.resolvedFewShotNl2SqlToolLabel();
  }

  private String fewShotNl2SqlToolDescription() {
    return mcpProperties == null
        ? "벡터 Few-shot 예제를 찾아 prompt에 반영하고 SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다."
        : mcpProperties.resolvedFewShotNl2SqlToolDescription();
  }

  private String gameScopeToolName() {
    return mcpProperties == null ? "oracle.select_ai.game_scope_resolve"
        : mcpProperties.resolvedGameScopeToolName();
  }

  private String gameScopeToolLabel() {
    return mcpProperties == null ? "게임 조회 범위 확인" : mcpProperties.resolvedGameScopeToolLabel();
  }

  private String gameScopeToolDescription() {
    return mcpProperties == null
        ? "질문에서 언급된 게임 별칭을 DB 게임 범위 view로 확인합니다."
        : mcpProperties.resolvedGameScopeToolDescription();
  }

  private String pretty(Object value) {
    try {
      return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
    } catch (Exception e) {
      return String.valueOf(value);
    }
  }
}
