package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
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.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;

/**
 * Generates and executes bounded read-only SQL through the configured schema-owned Select AI profile.
 */
@Service
public class SelectAiService {

  private static final int MAX_PROMPT_LENGTH = 4_000;
  private static final int MAX_RESULT_ROWS = 100;
  private static final int QUERY_TIMEOUT_SECONDS = 30;
  private static final int MAX_FEW_SHOT_EXAMPLES = 3;
  private static final int MAX_FEW_SHOT_SQL_CHARS = 4_000;
  private static final int MAX_ENRICHED_PROMPT_LENGTH = 16_000;
  private static final String POLICY_PREFIX =
      "Answer the original user question using the current approved object list and policy. "
          + "If no game identifier resolves through game-alias metadata, do not select a prefix-specific object "
          + "and do not infer a default game. State that the required game identifier is missing instead.\n\n";
  private static final Pattern UNSAFE_SQL = Pattern.compile(
      "(?is)\\b(?:insert|update|delete|merge|alter|drop|create|truncate|grant|revoke|"
          + "commit|rollback|savepoint|lock|call|exec(?:ute)?|begin|declare|for\\s+update|"
          + "dbms_[a-z0-9_]*|utl_[a-z0-9_]*|sys\\s*\\.)\\b"
  );

  private final BackofficeProperties properties;
  private final BearerTokenService bearerTokenService;
  private final Clock clock;
  private final ObjectMapper objectMapper;
  private final QaVectorService qaVectorService;
  private final GameScopeService gameScopeService;
  private final GameCatalogVectorService gameCatalogVectorService;

  public SelectAiService(
      BackofficeProperties properties,
      BearerTokenService bearerTokenService,
      Clock clock,
      ObjectMapper objectMapper,
      QaVectorService qaVectorService,
      GameScopeService gameScopeService,
      GameCatalogVectorService gameCatalogVectorService
  ) {
    this.properties = properties;
    this.bearerTokenService = bearerTokenService;
    this.clock = clock;
    this.objectMapper = objectMapper;
    this.qaVectorService = qaVectorService;
    this.gameScopeService = gameScopeService;
    this.gameCatalogVectorService = gameCatalogVectorService;
  }

  public JsonNode generateAndExecute(String bearerToken, String prompt) {
    return generateAndExecute(bearerToken, prompt, null);
  }

  /**
   * Executes one DB-resolved game scope. The optional key is revalidated for
   * the original question; it is never a caller-provided table or prefix.
   */
  public JsonNode generateAndExecute(String bearerToken, String prompt, String scopeGameKey) {
    requireActiveToken(bearerToken);
    String normalizedPrompt = requiredPrompt(prompt);
    BackofficeProperties.SelectAi selectAi = requiredSelectAi();
    ResolvedExecutionScope scope = resolveExecutionScope(bearerToken, normalizedPrompt, scopeGameKey);

    GameContext gameContext = resolveGameContext(bearerToken, normalizedPrompt);
    EnrichedPrompt enrichedPrompt = enrichWithFewShot(
        bearerToken, selectAi, gameContext.prompt(scope.prompt()));
    String generatedSql = generate(selectAi, enrichedPrompt.prompt(), "showsql");
    String normalizedSql = validateReadOnlySql(generatedSql);
    QueryExecution execution = executeReadOnly(selectAi, normalizedSql);
    ObjectNode response = objectMapper.createObjectNode();
    response.put("status", "SHOWSQL_AND_EXECUTED");
    response.put("profile", selectAi.profile());
    response.put("originalPrompt", normalizedPrompt);
    response.put("gameScopeType", gameContext.scopeType());
    response.put("gameScopeStatus", gameContext.status());
    response.put("gameCandidateCount", gameContext.candidates().size());
    if (scope.gameKey() != null) {
      response.put("scopeGameKey", scope.gameKey());
      response.put("scopeDisplayName", scope.displayName());
      response.put("scopeStatus", "DB_REVALIDATED");
    }
    response.put("fewShotStatus", enrichedPrompt.status());
    response.put("fewShotExampleCount", enrichedPrompt.exampleCount());
    addFewShotExamples(response, enrichedPrompt.examples());
    response.put("generatedSql", normalizedSql);
    response.put("execution", "READ_ONLY_EXECUTED");
    response.put("rowCount", execution.items().size());
    response.put("truncated", execution.truncated());
    response.set("items", execution.items());
    response.put("nextStep", execution.truncated()
        ? "최초 " + MAX_RESULT_ROWS + "건만 반환했습니다. 생성 SQL로 전체 결과를 확인할 수 있습니다."
        : "생성 SQL을 읽기 전용으로 실행한 결과입니다.");
    return response;
  }

  public JsonNode generateAndExecute(
      String bearerToken, String prompt, String scopeGameKey, JsonNode priorToolContext) {
    String context = priorToolContext == null || priorToolContext.isMissingNode()
        || priorToolContext.isNull() ? "" : "\n[PREVIOUS TOOL RESULT]\n" + priorToolContext;
    return generateAndExecute(bearerToken, prompt + context, scopeGameKey);
  }

  private GameContext resolveGameContext(String bearerToken, String question) {
    try {
      List<GameCatalogVectorService.GameCandidate> candidates =
          gameCatalogVectorService.search(bearerToken, question, 5);
      String status = candidates.isEmpty() ? "NO_MATCH" : "RESOLVED";
      String scopeType = candidates.isEmpty() ? "UNKNOWN" : "SINGLE_GAME";
      return new GameContext(scopeType, status, candidates);
    } catch (Exception ignored) {
      return new GameContext("UNKNOWN", "UNAVAILABLE", List.of());
    }
  }

  private ResolvedExecutionScope resolveExecutionScope(
      String bearerToken, String originalPrompt, String scopeGameKey
  ) {
    String requestedKey = scopeGameKey == null ? "" : scopeGameKey.trim();
    if (requestedKey.isEmpty()) {
      return new ResolvedExecutionScope(originalPrompt, null, null);
    }
    if (gameScopeService == null) {
      throw new AppException("게임 범위 검증 서비스를 사용할 수 없습니다.");
    }
    GameScopeService.GameScope scope = gameScopeService.resolve(bearerToken, originalPrompt).scopes().stream()
        .filter(item -> requestedKey.equals(item.gameKey()))
        .findFirst()
        .orElseThrow(() -> new AppException("요청한 게임 범위가 DB 조회 결과에 없습니다."));
    if (!"SUPPORTED".equals(scope.status())) {
      throw new AppException("DB 게임 범위가 조회 실행을 허용하지 않습니다: " + scope.reasonCode());
    }
    String scopedPrompt = "Use only the DB-resolved game scope below. Do not generate SQL for any other "
        + "game mentioned in the original question. The scope was validated through current game alias "
        + "metadata and approved object availability.\n"
        + "Resolved game key: " + scope.gameKey() + "\n"
        + "Resolved display name: " + scope.displayName() + "\n"
        + "Matched alias: " + scope.matchedAlias() + "\n\n"
        + "Original user question:\n" + originalPrompt;
    return new ResolvedExecutionScope(scopedPrompt, scope.gameKey(), scope.displayName());
  }

  private void addFewShotExamples(ObjectNode response, List<QaVectorService.VectorExample> examples) {
    ArrayNode items = response.putArray("fewShotExamples");
    for (QaVectorService.VectorExample example : examples) {
      ObjectNode item = items.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());
    }
  }

  /** Returns the prompt Select AI assembled for SQL generation without executing generated SQL. */
  public JsonNode generatePrompt(String bearerToken, String prompt) {
    requireActiveToken(bearerToken);
    String normalizedPrompt = requiredPrompt(prompt);
    BackofficeProperties.SelectAi selectAi = requiredSelectAi();
    EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, normalizedPrompt);
    String selectAiPrompt = generate(selectAi, enrichedPrompt.prompt(), "showprompt");

    ObjectNode response = objectMapper.createObjectNode();
    response.put("status", "SHOWPROMPT");
    response.put("profile", selectAi.profile());
    response.put("originalPrompt", normalizedPrompt);
    response.put("fewShotStatus", enrichedPrompt.status());
    response.put("fewShotExampleCount", enrichedPrompt.exampleCount());
    response.put("selectAiPrompt", selectAiPrompt);
    return response;
  }

  private BackofficeProperties.SelectAi requiredSelectAi() {
    BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
    if (selectAi == null || !selectAi.configured()) {
      throw new AppException("Select AI 연결 설정이 필요합니다. "
          + "BACKOFFICE_SELECT_AI_DB_URL, BACKOFFICE_SELECT_AI_DB_USERNAME, "
          + "BACKOFFICE_SELECT_AI_DB_PASSWORD를 확인하세요.");
    }
    return selectAi;
  }

  private void requireActiveToken(String bearerToken) {
    if (bearerToken == null || bearerToken.isBlank()) {
      throw new VpdTokenAccessDeniedException();
    }
    BearerTokenRecord token = bearerTokenService.findByPlainToken(bearerToken.trim());
    LocalDateTime now = LocalDateTime.now(clock.withZone(ZoneId.systemDefault()));
    if (token == null || !token.active(now)) {
      throw new VpdTokenAccessDeniedException();
    }
  }

  private String requiredPrompt(String prompt) {
    String normalized = prompt == null ? "" : prompt.trim();
    if (normalized.isEmpty()) {
      throw new AppException("prompt는 필수입니다.");
    }
    if (normalized.length() > MAX_PROMPT_LENGTH) {
      throw new AppException("prompt는 " + MAX_PROMPT_LENGTH + "자 이하여야 합니다.");
    }
    return normalized;
  }

  private String generate(
      BackofficeProperties.SelectAi selectAi,
      String prompt,
      String action
  ) {
    String sql = "SELECT DBMS_CLOUD_AI.GENERATE(?, ?, ?) FROM dual";
    try (Connection connection = DriverManager.getConnection(
        selectAi.dbUrl(), selectAi.dbUsername(), selectAi.dbPassword());
         PreparedStatement statement = connection.prepareStatement(sql)) {
      statement.setString(1, prompt);
      statement.setString(2, selectAi.profile());
      statement.setString(3, action);
      try (ResultSet resultSet = statement.executeQuery()) {
        if (!resultSet.next() || resultSet.getString(1) == null) {
          throw new AppException("Select AI가 " + action.toUpperCase() + " 결과를 반환하지 않았습니다.");
        }
        return resultSet.getString(1);
      }
    } catch (AppException exception) {
      throw exception;
    } catch (Exception exception) {
      throw new AppException("Select AI " + action.toUpperCase() + " 생성 실패: "
          + exception.getMessage());
    }
  }

  private EnrichedPrompt enrichWithFewShot(
      String bearerToken,
      BackofficeProperties.SelectAi selectAi,
      String prompt
  ) {
    if (!fewShotEnabled(selectAi) || qaVectorService == null) {
      return new EnrichedPrompt(prompt, "DISABLED", 0, List.of());
    }
    try {
      List<QaVectorService.VectorExample> examples = qaVectorService
          .search(bearerToken, prompt, fewShotTopK(selectAi))
          .examples();
      if (examples.isEmpty()) {
        return new EnrichedPrompt(composePolicyPrompt(prompt), "NO_MATCH", 0, List.of());
      }
      return new EnrichedPrompt(
          composeFewShotPrompt(prompt, examples), "APPLIED", Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES),
          examples.subList(0, Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES)));
    } catch (Exception ignored) {
      // Vector retrieval is an optional prompt aid; preserve the normal Text2SQL path on failure.
      return new EnrichedPrompt(composePolicyPrompt(prompt), "UNAVAILABLE", 0, List.of());
    }
  }

  static String composePolicyPrompt(String prompt) {
    return POLICY_PREFIX
        + "Resolve business terms and game names from the approved game-alias metadata before selecting a "
        + "game-scoped object. A generic term such as common user means no particular game. If no game alias "
        + "is resolved, do not substitute an arbitrary game-scoped object. Return a read-only no-match result "
        + "and preserve the resolver status for the answer layer; do not invent a user-facing explanation.\n"
        + "Original user question:\n" + prompt;
  }

  static String composeFewShotPrompt(String prompt, List<QaVectorService.VectorExample> examples) {
    StringBuilder enriched = new StringBuilder(POLICY_PREFIX
        + "The verified examples below are guidance only: use only relevant SQL patterns, do not invent "
            + "identifiers, and do not override current metadata or game-alias resolution policy. "
            + "When examples use a game-specific object, reuse that pattern only after the current question "
            + "resolves the same game alias; otherwise keep the query unscoped or request clarification.\n\n"
            + "Verified few-shot examples:\n");
    int included = 0;
    for (QaVectorService.VectorExample example : examples) {
      if (included >= MAX_FEW_SHOT_EXAMPLES) {
        break;
      }
      String answerSql = truncate(example.answerSql(), MAX_FEW_SHOT_SQL_CHARS);
      if (answerSql.isBlank()) {
        continue;
      }
      String candidate = "Example " + (included + 1) + " question:\n" + example.question()
          + "\nExample " + (included + 1) + " verified SQL:\n" + answerSql + "\n\n";
      if (enriched.length() + candidate.length() + prompt.length() > MAX_ENRICHED_PROMPT_LENGTH) {
        break;
      }
      enriched.append(candidate);
      included++;
    }
    if (included == 0) {
      return composePolicyPrompt(prompt);
    }
    return enriched.append("Original user question:\n").append(prompt).toString();
  }

  private static String truncate(String value, int maxLength) {
    String normalized = value == null ? "" : value.trim();
    return normalized.length() <= maxLength ? normalized : normalized.substring(0, maxLength);
  }

  private boolean fewShotEnabled(BackofficeProperties.SelectAi selectAi) {
    return selectAi.fewShotEnabled() == null || selectAi.fewShotEnabled();
  }

  private int fewShotTopK(BackofficeProperties.SelectAi selectAi) {
    Integer configured = selectAi.fewShotTopK();
    if (configured == null) {
      return MAX_FEW_SHOT_EXAMPLES;
    }
    return Math.max(1, Math.min(configured, MAX_FEW_SHOT_EXAMPLES));
  }

  private QueryExecution executeReadOnly(BackofficeProperties.SelectAi selectAi, String generatedSql) {
    ArrayNode items = objectMapper.createArrayNode();
    boolean truncated = false;
    try (Connection connection = DriverManager.getConnection(
        selectAi.dbUrl(), selectAi.dbUsername(), selectAi.dbPassword());
         Statement transaction = connection.createStatement()) {
      connection.setAutoCommit(false);
      connection.setReadOnly(true);
      transaction.execute("SET TRANSACTION READ ONLY");
      try (PreparedStatement statement = connection.prepareStatement(generatedSql)) {
        statement.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
        statement.setFetchSize(MAX_RESULT_ROWS + 1);
        statement.setMaxRows(MAX_RESULT_ROWS + 1);
        try (ResultSet resultSet = statement.executeQuery()) {
          ResultSetMetaData metadata = resultSet.getMetaData();
          while (resultSet.next()) {
            if (items.size() >= MAX_RESULT_ROWS) {
              truncated = true;
              break;
            }
            ObjectNode row = items.addObject();
            for (int columnIndex = 1; columnIndex <= metadata.getColumnCount(); columnIndex++) {
              String column = metadata.getColumnLabel(columnIndex);
              if (column == null || column.isBlank()) {
                column = metadata.getColumnName(columnIndex);
              }
              putResultValue(row, column, resultSet.getObject(columnIndex));
            }
          }
        }
      } finally {
        connection.rollback();
      }
    } catch (Exception exception) {
      throw new AppException("Select AI 생성 SQL 실행 실패: " + exception.getMessage());
    }
    return new QueryExecution(items, truncated);
  }

  private void putResultValue(ObjectNode row, String column, Object value) {
    if (value == null) {
      row.putNull(column);
    } else if (value instanceof BigDecimal number) {
      row.put(column, number);
    } else if (value instanceof BigInteger number) {
      row.put(column, number);
    } else if (value instanceof Integer number) {
      row.put(column, number);
    } else if (value instanceof Long number) {
      row.put(column, number);
    } else if (value instanceof Short number) {
      row.put(column, number);
    } else if (value instanceof Float number) {
      row.put(column, number);
    } else if (value instanceof Double number) {
      row.put(column, number);
    } else if (value instanceof Boolean bool) {
      row.put(column, bool);
    } else {
      row.put(column, String.valueOf(value));
    }
  }

  private String validateReadOnlySql(String generatedSql) {
    String normalized = generatedSql == null ? "" : generatedSql.trim();
    if (normalized.startsWith("```")) {
      int firstLineEnd = normalized.indexOf('\n');
      int closingFence = normalized.lastIndexOf("```");
      if (firstLineEnd >= 0 && closingFence > firstLineEnd) {
        normalized = normalized.substring(firstLineEnd + 1, closingFence).trim();
      }
    }
    normalized = normalized.replaceFirst(";\\s*$", "").trim();
    if (!normalized.matches("(?is)^(select|with)\\b.*")) {
      throw new AppException("Select AI가 읽기 전용 SELECT/WITH SQL을 반환하지 않았습니다.");
    }
    if (normalized.contains(";")) {
      throw new AppException("Select AI 결과에 여러 SQL 문장이 포함되어 있어 반환하지 않습니다.");
    }
    if (normalized.contains("--") || normalized.contains("/*") || normalized.contains("*/")
        || UNSAFE_SQL.matcher(normalized).find()) {
      throw new AppException("Select AI 결과에 실행이 허용되지 않는 SQL 구문이 포함되어 있습니다.");
    }
    return normalized;
  }

  private record QueryExecution(ArrayNode items, boolean truncated) {}

  private record EnrichedPrompt(
      String prompt, String status, int exampleCount, List<QaVectorService.VectorExample> examples) {}

  private record ResolvedExecutionScope(String prompt, String gameKey, String displayName) {}

  private record GameContext(
      String scopeType, String status, List<GameCatalogVectorService.GameCandidate> candidates) {
    String prompt(String original) {
      StringBuilder context = new StringBuilder();
      context.append("[GAME CATALOG MATCH]\n")
          .append("scope_type: ").append(scopeType).append('\n')
          .append("status: ").append(status).append('\n');
      for (GameCatalogVectorService.GameCandidate candidate : candidates) {
        context.append("game_key: ").append(candidate.gameKey()).append('\n')
            .append("game_id: ").append(candidate.gameId()).append('\n')
            .append("game_prefix: ").append(candidate.gamePrefix()).append('\n')
            .append("game_name: ").append(candidate.gameName()).append('\n')
            .append("matched_aliases: ").append(candidate.aliases()).append('\n')
            .append("cosine_distance: ").append(candidate.similarity()).append('\n');
      }
      context.append("[GAME SCOPE METADATA]\n")
          .append("The following resolver facts are authoritative metadata. ")
          .append("Apply the table and column annotations associated with these facts; ")
          .append("do not invent identifiers or scope rules.\n\n")
          .append(original);
      return context.toString();
    }
  }
}
