package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerUpdateCommand;
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView;
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsObjectHandlerResult;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.annotation.PreDestroy;
import java.util.List;
import java.util.Locale;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrdsMetadataService {

  private static final String VECTOR_SEARCH_OBJECT = "CB_VECTOR_SEARCH_DOCUMENTS";
  private static final Logger log = LoggerFactory.getLogger(OrdsMetadataService.class);

  private final OrdsMetadataMapper mapper;
  private final JdbcTemplate jdbcTemplate;
  private final JdbcTemplate ordsMetadataJdbcTemplate;
  private final HikariDataSource ordsMetadataDataSource;
  private final ProtectedObjectService protectedObjectService;

  public OrdsMetadataService(
      OrdsMetadataMapper mapper,
      JdbcTemplate jdbcTemplate,
      ProtectedObjectService protectedObjectService,
      DataSource dataSource,
      @Value("${backoffice.ords.metadata-db.url:}") String ordsMetadataUrl,
      @Value("${backoffice.ords.metadata-db.username:}") String ordsMetadataUsername,
      @Value("${backoffice.ords.metadata-db.password:}") String ordsMetadataPassword
  ) {
    this.mapper = mapper;
    this.jdbcTemplate = jdbcTemplate;
    this.protectedObjectService = protectedObjectService;
    HikariDataSource metadataDataSource = null;
    JdbcTemplate metadataJdbcTemplate = new JdbcTemplate(dataSource);
    if (hasText(ordsMetadataUrl) && hasText(ordsMetadataUsername)) {
      HikariConfig config = new HikariConfig();
      config.setPoolName("vpd-ords-metadata-pool");
      config.setJdbcUrl(ordsMetadataUrl.trim());
      config.setUsername(ordsMetadataUsername.trim());
      config.setPassword(ordsMetadataPassword == null ? "" : ordsMetadataPassword);
      config.setDriverClassName("oracle.jdbc.OracleDriver");
      config.setMaximumPoolSize(2);
      config.setMinimumIdle(0);
      config.setConnectionTimeout(10000);
      // ORDS metadata is an optional administrative connection. Do not fail
      // the whole backoffice startup when the remote TNS host is temporarily
      // unavailable; Handler create/update will report the connection error
      // at the point of use instead.
      config.setInitializationFailTimeout(-1);
      try {
        metadataDataSource = new HikariDataSource(config);
        metadataJdbcTemplate = new JdbcTemplate(metadataDataSource);
      } catch (RuntimeException exception) {
        log.warn("ORDS metadata DB is unavailable; using the main DB for read-only fallback: {}",
            exception.getMessage());
        if (metadataDataSource != null) {
          metadataDataSource.close();
        }
      }
    }
    this.ordsMetadataDataSource = metadataDataSource;
    this.ordsMetadataJdbcTemplate = metadataJdbcTemplate;
  }

  public List<OrdsHandlerView> findHandlers() {
    String packageSource = mapper.findHandlerPackageSource();
    return mapper.findHandlers().stream()
        .map(handler -> new OrdsHandlerView(
            handler.handlerId(),
            handler.schemaName(),
            handler.parsingSchema(),
            handler.moduleName(),
            handler.basePath(),
            handler.template(),
            handler.method(),
            handler.sourceType(),
            handler.source(),
            handler.parameters(),
            packageSource
        ))
        .toList();
  }

  public String objectQueryHandlerSource(long objectId) {
    ProtectedObject object = protectedObjectService.assertEnabled(objectId);
    rejectGenericVectorHandler(object);
    List<String> columns = protectedObjectService.findColumns(objectId).stream()
        .map(ProtectedColumn::columnName)
        .toList();
    if (columns.isEmpty()) {
      throw new AppException("보호 객체 컬럼이 없어 ORDS 조회 Handler Source를 만들 수 없습니다.");
    }
    requireIdentifier(object.owner(), "owner");
    requireIdentifier(object.objectName(), "objectName");
    columns.forEach(column -> requireIdentifier(column, "column"));
    return objectQuerySource(object, columns);
  }

  @Transactional
  public void updateHandlerSource(OrdsHandlerUpdateCommand command) {
    if (command.source() == null || command.source().isBlank()) {
      throw new AppException("ORDS Handler Source는 필수입니다.");
    }
    OrdsHandlerView handler = mapper.findHandler(command.handlerId());
    if (handler == null) {
      throw new AppException("수정할 ORDS Handler를 찾을 수 없습니다.");
    }
    String sourceType = handler.sourceType() == null ? "" : handler.sourceType().toLowerCase(Locale.ROOT);
    if (!sourceType.contains("plsql") && !sourceType.contains("query")) {
      throw new AppException("현재는 PL/SQL/SQL Query ORDS Handler만 수정할 수 있습니다: " + handler.sourceType());
    }
    String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
    if (currentUser == null || !currentUser.equalsIgnoreCase(handler.parsingSchema())) {
      throw new AppException("ORDS Handler 수정은 parsing schema로 DB에 연결했을 때만 가능합니다. 현재 연결 사용자: "
          + currentUser + ", Handler parsing schema: " + handler.parsingSchema());
    }
    jdbcTemplate.update("""
        DECLARE
          v_source_type VARCHAR2(30);
        BEGIN
          IF ? = 'QUERY' THEN
            v_source_type := ORDS.source_type_query;
          ELSE
            v_source_type := ORDS.source_type_plsql;
          END IF;

          ORDS.DEFINE_HANDLER(
            p_module_name    => ?,
            p_pattern        => ?,
            p_method         => ?,
            p_source_type    => v_source_type,
            p_source         => ?,
            p_items_per_page => 0
          );
          COMMIT;
        END;
        """,
        handlerSourceTypeCode(handler.sourceType()),
        handler.moduleName(),
        handler.template(),
        handler.method(),
        command.source());
  }

  @Transactional
  public OrdsObjectHandlerResult createObjectQueryHandler(long objectId) {
    ProtectedObject object = protectedObjectService.assertEnabled(objectId);
    rejectGenericVectorHandler(object);
    List<String> columns = protectedObjectService.findColumns(objectId).stream()
        .map(ProtectedColumn::columnName)
        .toList();
    if (columns.isEmpty()) {
      throw new AppException("보호 객체 컬럼이 없어 ORDS 조회 Handler를 만들 수 없습니다.");
    }
    requireIdentifier(object.owner(), "owner");
    requireIdentifier(object.objectName(), "objectName");
    columns.forEach(column -> requireIdentifier(column, "column"));

    String currentUser = ordsMetadataJdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
    if (currentUser == null || !currentUser.equalsIgnoreCase("CB_ORDS")) {
      throw new AppException("ORDS 조회 Handler 생성은 ORDS parsing schema(CB_ORDS)로 DB에 연결했을 때만 가능합니다. 현재 연결 사용자: "
          + currentUser + ". 현재 연결에서는 소스 보기를 눌러 기본 PL/SQL을 확인한 뒤 ORDS Handler에 적용하세요.");
    }

    String moduleName = "cb.object.query";
    String basePath = "cb-object-query/";
    String template = object.owner().toLowerCase(Locale.ROOT) + "/" + object.objectName().toLowerCase(Locale.ROOT);
    String ordsPath = "cb-ords/" + basePath + template;
    String source = objectQueryHandlerSource(objectId);

    ordsMetadataJdbcTemplate.update("""
        DECLARE
          v_module_count NUMBER;
        BEGIN
          SELECT COUNT(*)
          INTO   v_module_count
          FROM   user_ords_modules
          WHERE  name = ?;

          IF v_module_count = 0 THEN
            ORDS.DEFINE_MODULE(
              p_module_name    => ?,
              p_base_path      => ?,
              p_items_per_page => 25,
              p_status         => 'PUBLISHED'
            );
          END IF;

          ORDS.DEFINE_TEMPLATE(
            p_module_name => ?,
            p_pattern     => ?
          );
          ORDS.DEFINE_HANDLER(
            p_module_name    => ?,
            p_pattern        => ?,
            p_method         => 'POST',
            p_source_type    => ORDS.source_type_plsql,
            p_source         => ?,
            p_items_per_page => 0
          );
          ORDS.DEFINE_PARAMETER(
            p_module_name        => ?,
            p_pattern            => ?,
            p_method             => 'POST',
            p_name               => 'Authorization',
            p_bind_variable_name => 'auth_header',
            p_source_type        => 'HEADER',
            p_param_type         => 'STRING',
            p_access_method      => 'IN'
          );
          ORDS.DEFINE_PARAMETER(
            p_module_name        => ?,
            p_pattern            => ?,
            p_method             => 'POST',
            p_name               => 'X-VPD-Probe-Id',
            p_bind_variable_name => 'probe_id',
            p_source_type        => 'HEADER',
            p_param_type         => 'STRING',
            p_access_method      => 'IN'
          );
          ORDS.DEFINE_PARAMETER(
            p_module_name        => ?,
            p_pattern            => ?,
            p_method             => 'POST',
            p_name               => 'limit',
            p_bind_variable_name => 'row_limit',
            p_source_type        => 'URI',
            p_param_type         => 'INT',
            p_access_method      => 'IN'
          );
          COMMIT;
        END;
        """,
        moduleName,
        moduleName, basePath,
        moduleName, template,
        moduleName, template, source,
        moduleName, template,
        moduleName, template,
        moduleName, template);
    protectedObjectService.updateOrdsPath(object.objectId(), ordsPath);
    return new OrdsObjectHandlerResult(object.objectId(), ordsPath, moduleName, template);
  }

  private String objectQuerySource(ProtectedObject object, List<String> columns) {
    String selectColumns = columns.stream()
        .map(column -> "o." + column)
        .reduce((left, right) -> left + ",\n       " + right)
        .orElseThrow();
    String traceSelectColumns = columns.stream()
        .map(column -> "o." + column)
        .reduce((left, right) -> left + ", " + right)
        .orElseThrow();
    return """
        DECLARE
          v_rows SYS_REFCURSOR;
          v_vpd_predicate VARCHAR2(32767);
          v_effective_sql VARCHAR2(32767);
        BEGIN
          cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);

          -- This is the same predicate function invoked by DBMS_RLS for the
          -- SELECT below. It is returned only as diagnostic metadata.
          BEGIN
            v_vpd_predicate := admin.cb_agent_doc_vpd_filter('%s', '%s');
            v_effective_sql := 'SELECT %s FROM %s.%s o WHERE (' || v_vpd_predicate
                || ') AND ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500)';
          EXCEPTION
            WHEN OTHERS THEN
              v_vpd_predicate := NULL;
              v_effective_sql := NULL;
          END;

          OPEN v_rows FOR
            SELECT %s
            FROM   %s.%s o
            WHERE  ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500);

          :status_code := 200;
          OWA_UTIL.MIME_HEADER('application/json', FALSE);
          HTP.P('Cache-Control: no-store');
          OWA_UTIL.HTTP_HEADER_CLOSE;

          APEX_JSON.OPEN_OBJECT;
          IF v_effective_sql IS NOT NULL THEN
            APEX_JSON.WRITE('vpd_predicate', v_vpd_predicate);
            APEX_JSON.WRITE('effective_sql', v_effective_sql);
          END IF;
          APEX_JSON.WRITE('items', v_rows);
          APEX_JSON.CLOSE_OBJECT;

          cb_ords_handler_pkg.clear_vpd_context;
        EXCEPTION
          WHEN OTHERS THEN
            cb_ords_handler_pkg.clear_vpd_context;
            :status_code := 403;
            OWA_UTIL.MIME_HEADER('application/json', FALSE);
            HTP.P('Cache-Control: no-store');
            OWA_UTIL.HTTP_HEADER_CLOSE;
            APEX_JSON.OPEN_OBJECT;
            APEX_JSON.WRITE('error', SQLERRM);
          APEX_JSON.CLOSE_OBJECT;
        END;
        """.formatted(
            object.owner(),
            object.objectName(),
            traceSelectColumns,
            object.owner(),
            object.objectName(),
            selectColumns,
            object.owner(),
            object.objectName());
  }

  private void rejectGenericVectorHandler(ProtectedObject object) {
    if (VECTOR_SEARCH_OBJECT.equalsIgnoreCase(object.objectName())) {
      throw new AppException(
          "벡터 검색 객체는 일반 Handler로 등록할 수 없습니다. "
              + "29_agent_ords_vector_search_ords.sql의 전용 Handler를 사용하세요(EMBEDDING 미노출).");
    }
  }

  private String handlerSourceTypeCode(String sourceType) {
    String normalized = sourceType == null ? "" : sourceType.toLowerCase(Locale.ROOT);
    if (normalized.contains("query")) {
      return "QUERY";
    }
    return "PLSQL";
  }

  private void requireIdentifier(String value, String label) {
    if (value == null || !value.matches("[A-Z][A-Z0-9_$#]*")) {
      throw new AppException("ORDS Handler 생성에 사용할 수 없는 " + label + " 식별자입니다: " + value);
    }
  }

  private boolean hasText(String value) {
    return value != null && !value.isBlank();
  }

  @PreDestroy
  public void closeOrdsMetadataDataSource() {
    if (ordsMetadataDataSource != null) {
      ordsMetadataDataSource.close();
    }
  }
}
