package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.config.GameScopeProperties;
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;

/**
 * Resolves only DB-declared game scopes before an agent invokes a SQL worker.
 *
 * <p>The service never infers a game, table, prefix, or support status. It reads
 * the configured customer view and returns only aliases physically present in the
 * question. This makes unavailable games observable when they are retained in
 * the customer's game master, while keeping product-specific data out of code.</p>
 */
@Service
public class GameScopeService {

  private static final int MAX_QUESTION_LENGTH = 4_000;
  private static final Pattern VIEW_NAME = Pattern.compile(
      "^[A-Za-z][A-Za-z0-9_$#]*(?:\\.[A-Za-z][A-Za-z0-9_$#]*)?$");

  private final BackofficeProperties properties;
  private final GameScopeProperties gameScopeProperties;
  private final BearerTokenService bearerTokenService;
  private final Clock clock;

  public GameScopeService(
      BackofficeProperties properties,
      GameScopeProperties gameScopeProperties,
      BearerTokenService bearerTokenService,
      Clock clock
  ) {
    this.properties = properties;
    this.gameScopeProperties = gameScopeProperties;
    this.bearerTokenService = bearerTokenService;
    this.clock = clock;
  }

  public GameScopeResult resolve(String bearerToken, String question) {
    requireActiveToken(bearerToken);
    String normalizedQuestion = requiredQuestion(question);
    BackofficeProperties.SelectAi selectAi = requiredSelectAi();
    String viewName = requiredViewName();
    String comparableQuestion = comparable(normalizedQuestion);

    List<GameScope> matches = new ArrayList<>();
    String sql = "SELECT game_key, display_name, game_alias, query_allowed_yn, reason_code, "
        + "alias_priority, scope_version FROM " + viewName
        + " WHERE profile_name = ?"
        + " AND INSTR(?, REGEXP_REPLACE(UPPER(game_alias), '[[:space:][:punct:]]', '')) > 0"
        + " ORDER BY alias_priority DESC, LENGTH(game_alias) DESC, game_key";
    try (Connection connection = DriverManager.getConnection(
        selectAi.dbUrl(), selectAi.dbUsername(), selectAi.dbPassword());
         PreparedStatement statement = connection.prepareStatement(sql)) {
      statement.setString(1, selectAi.profile());
      statement.setString(2, comparableQuestion);
      try (ResultSet rows = statement.executeQuery()) {
        while (rows.next()) {
          matches.add(new GameScope(
              rows.getString("GAME_KEY"),
              rows.getString("DISPLAY_NAME"),
              rows.getString("GAME_ALIAS"),
              "Y".equalsIgnoreCase(rows.getString("QUERY_ALLOWED_YN")) ? "SUPPORTED" : "UNSUPPORTED",
              rows.getString("REASON_CODE"),
              rows.getInt("ALIAS_PRIORITY"),
              rows.getString("SCOPE_VERSION")
          ));
        }
      }
    } catch (Exception exception) {
      throw new AppException("게임 범위 DB 조회 실패: " + exception.getMessage());
    }

    Map<String, GameScope> uniqueGames = new LinkedHashMap<>();
    for (GameScope match : matches) {
      uniqueGames.putIfAbsent(match.gameKey(), match);
    }
    List<GameScope> scopes = uniqueGames.values().stream()
        .sorted(Comparator.comparing(GameScope::aliasPriority).reversed()
            .thenComparing(GameScope::gameKey))
        .limit(gameScopeProperties.resolvedMaxScopes())
        .toList();
    String status = scopes.isEmpty() ? "NO_MATCH" : "RESOLVED";
    return new GameScopeResult(normalizedQuestion, status, scopes);
  }

  public boolean configured() {
    return gameScopeProperties != null && gameScopeProperties.configured();
  }

  /**
   * Returns true when a generated SQL references a DB-declared game-prefix
   * object. Prefixes are read from the configured scope view; no game or table
   * name is embedded in application code.
   */
  public boolean referencesGameScopedObject(String bearerToken, String sql) {
    requireActiveToken(bearerToken);
    if (!gameScopeProperties.configured() || sql == null || sql.isBlank()) {
      return false;
    }
    BackofficeProperties.SelectAi selectAi = requiredSelectAi();
    String viewName = requiredViewName();
    String prefixSql = "SELECT DISTINCT game_prefix FROM " + viewName
        + " WHERE profile_name = ? AND active_yn = 'Y'"
        + " AND query_allowed_yn = 'Y' AND game_prefix IS NOT NULL";
    try (Connection connection = DriverManager.getConnection(
        selectAi.dbUrl(), selectAi.dbUsername(), selectAi.dbPassword());
         PreparedStatement statement = connection.prepareStatement(prefixSql)) {
      statement.setString(1, selectAi.profile());
      try (ResultSet rows = statement.executeQuery()) {
        String normalizedSql = sql.toUpperCase(Locale.ROOT);
        while (rows.next()) {
          String prefix = rows.getString("GAME_PREFIX");
          if (prefix != null && Pattern.compile(
              "(?<![A-Z0-9_$#])" + Pattern.quote(prefix.toUpperCase(Locale.ROOT))
                  + "_[A-Z0-9_$#]+(?![A-Z0-9_$#])").matcher(normalizedSql).find()) {
            return true;
          }
        }
      }
    } catch (Exception exception) {
      throw new AppException("게임 범위 prefix 검증 실패: " + exception.getMessage());
    }
    return false;
  }

  private String requiredViewName() {
    String value = gameScopeProperties == null ? "" : gameScopeProperties.viewName();
    if (gameScopeProperties == null || !gameScopeProperties.configured() || !VIEW_NAME.matcher(value.trim()).matches()) {
      throw new AppException("게임 범위 view 설정이 필요합니다. BACKOFFICE_GAME_SCOPE_VIEW를 확인하세요.");
    }
    return value.trim();
  }

  private BackofficeProperties.SelectAi requiredSelectAi() {
    BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
    if (selectAi == null || !selectAi.configured()) {
      throw new AppException("게임 범위 DB 연결 설정이 필요합니다.");
    }
    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 requiredQuestion(String question) {
    String value = question == null ? "" : question.trim();
    if (value.isEmpty()) {
      throw new AppException("question은 필수입니다.");
    }
    if (value.length() > MAX_QUESTION_LENGTH) {
      throw new AppException("question은 " + MAX_QUESTION_LENGTH + "자 이하여야 합니다.");
    }
    return value;
  }

  private String comparable(String value) {
    return value.toUpperCase(Locale.ROOT).replaceAll("[\\s\\p{Punct}]", "");
  }

  public record GameScope(
      String gameKey,
      String displayName,
      String matchedAlias,
      String status,
      String reasonCode,
      int aliasPriority,
      String scopeVersion
  ) {}

  public record GameScopeResult(String question, String status, List<GameScope> scopes) {}
}
