package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.domain.schema.SchemaActionResult;
import com.cloudhandson.vpdbackoffice.domain.schema.SchemaPreflightCheck;
import com.cloudhandson.vpdbackoffice.domain.schema.SchemaPreflightView;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BackofficeSchemaService {

  private static final String ORDS_SCHEMA = "CB_ORDS";

  private static final List<SequenceDefinition> SEQUENCES = List.of(
      new SequenceDefinition("CB_AGENT_BEARER_KEY_SEQ", 1000),
      new SequenceDefinition("CB_PERMISSION_SEQ", 1000),
      new SequenceDefinition("CB_PERMISSION_RULE_SEQ", 10000),
      new SequenceDefinition("CB_ORDS_PROBE_AUDIT_SEQ", 1)
  );

  private static final List<TableDefinition> TABLES = List.of(
      new TableDefinition("CB_APP_USER", """
          CREATE TABLE cb_app_user (
            user_id            NUMBER PRIMARY KEY,
            user_name          VARCHAR2(100) NOT NULL UNIQUE,
            employee_no        VARCHAR2(50),
            dept_code          VARCHAR2(50),
            stakeholder_user_id VARCHAR2(30),
            can_read_contents  CHAR(1) DEFAULT 'N' CHECK (can_read_contents IN ('Y','N')) NOT NULL,
            active             CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL
          )
          """),
      new TableDefinition("CB_APP_ROLE", """
          CREATE TABLE cb_app_role (
            role_id               NUMBER PRIMARY KEY,
            role_name             VARCHAR2(100) NOT NULL UNIQUE,
            max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL
          )
          """),
      new TableDefinition("CB_APP_GROUP", """
          CREATE TABLE cb_app_group (
            group_id     NUMBER PRIMARY KEY,
            group_code   VARCHAR2(100) NOT NULL UNIQUE,
            group_name   VARCHAR2(100) NOT NULL,
            description  VARCHAR2(200),
            active_yn    CHAR(1) DEFAULT 'Y' CHECK (active_yn IN ('Y','N')) NOT NULL
          )
          """),
      new TableDefinition("CB_USER_ROLE", """
          CREATE TABLE cb_user_role (
            user_id  NUMBER NOT NULL,
            role_id  NUMBER NOT NULL,
            CONSTRAINT cb_user_role_pk PRIMARY KEY (user_id, role_id)
          )
          """),
      new TableDefinition("CB_USER_GROUP", """
          CREATE TABLE cb_user_group (
            group_id  NUMBER NOT NULL,
            user_id   NUMBER NOT NULL,
            CONSTRAINT cb_user_group_pk PRIMARY KEY (group_id, user_id)
          )
          """),
      new TableDefinition("CB_GROUP_ROLE", """
          CREATE TABLE cb_group_role (
            group_id  NUMBER NOT NULL,
            role_id   NUMBER NOT NULL,
            CONSTRAINT cb_group_role_pk PRIMARY KEY (group_id, role_id)
          )
          """),
      new TableDefinition("CB_PERMISSION", """
          CREATE TABLE cb_permission (
            perm_id           NUMBER PRIMARY KEY,
            role_id           NUMBER NOT NULL,
            target_name       VARCHAR2(128) NOT NULL,
            action_name       VARCHAR2(30) DEFAULT 'SELECT' NOT NULL,
            permission_effect VARCHAR2(10) DEFAULT 'ALLOW' NOT NULL
          )
          """),
      new TableDefinition("CB_PERMISSION_RULE", """
          CREATE TABLE cb_permission_rule (
            rule_id      NUMBER PRIMARY KEY,
            perm_id      NUMBER NOT NULL,
            rule_column  VARCHAR2(128),
            rule_type    VARCHAR2(30) NOT NULL,
            rule_value   VARCHAR2(4000)
          )
          """),
      new TableDefinition("CB_PERMISSION_COLUMN", """
          CREATE TABLE cb_permission_column (
            permission_id  NUMBER NOT NULL,
            column_name    VARCHAR2(128) NOT NULL,
            CONSTRAINT cb_permission_column_pk PRIMARY KEY (permission_id, column_name)
          )
          """),
      new TableDefinition("CB_AGENT_BEARER_KEY", """
          CREATE TABLE cb_agent_bearer_key (
            key_id       NUMBER PRIMARY KEY,
            user_id      NUMBER NOT NULL,
            stakeholder_user_id VARCHAR2(30),
            key_prefix   VARCHAR2(30) NOT NULL,
            key_hash     VARCHAR2(128) NOT NULL UNIQUE,
            expires_at   TIMESTAMP NOT NULL,
            revoked_at   TIMESTAMP,
            description  VARCHAR2(200)
          )
          """),
      new TableDefinition("CB_PROTECTED_OBJECT", """
          CREATE TABLE cb_protected_object (
            object_id    NUMBER PRIMARY KEY,
            owner        VARCHAR2(128) NOT NULL,
            object_name  VARCHAR2(128) NOT NULL UNIQUE,
            ords_path    VARCHAR2(300) NOT NULL,
            enabled_yn   CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL,
            description  VARCHAR2(200)
          )
          """),
      new TableDefinition("CB_PROTECTED_COLUMN", """
          CREATE TABLE cb_protected_column (
            column_id         NUMBER PRIMARY KEY,
            object_id         NUMBER NOT NULL,
            column_name       VARCHAR2(128) NOT NULL,
            sensitive_yn      CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL,
            visible_role_id   NUMBER,
            sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL,
            redaction_method  VARCHAR2(20) DEFAULT 'NONE' NOT NULL,
            CONSTRAINT cb_protected_column_uk UNIQUE (object_id, column_name)
          )
          """),
      new TableDefinition("CB_MASKING_RULE", """
          CREATE TABLE cb_masking_rule (
            rule_id       NUMBER PRIMARY KEY,
            rule_code     VARCHAR2(64) NOT NULL UNIQUE,
            rule_name     VARCHAR2(100) NOT NULL,
            template_code VARCHAR2(30) NOT NULL,
            description   VARCHAR2(400),
            enabled_yn    CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL
          )
          """),
      new TableDefinition("CB_COLUMN_MASKING_RULE", """
          CREATE TABLE cb_column_masking_rule (
            column_id  NUMBER PRIMARY KEY,
            rule_id    NUMBER NOT NULL,
            updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
          )
          """),
      new TableDefinition("CB_USER_MASKING_RULE", """
          CREATE TABLE cb_user_masking_rule (
            user_id    NUMBER NOT NULL,
            column_id  NUMBER NOT NULL,
            decision   VARCHAR2(10) NOT NULL CHECK (decision IN ('MASK','UNMASK')),
            active_yn  CHAR(1) DEFAULT 'Y' CHECK (active_yn IN ('Y','N')) NOT NULL,
            updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
            CONSTRAINT cb_user_masking_rule_pk PRIMARY KEY (user_id, column_id)
          )
          """),
      new TableDefinition("CB_ORDS_PROBE_AUDIT", """
          CREATE TABLE cb_ords_probe_audit (
            audit_id    NUMBER PRIMARY KEY,
            event_type  VARCHAR2(50) NOT NULL,
            key_id      NUMBER,
            object_id   NUMBER,
            status      VARCHAR2(50),
            row_count   NUMBER,
            error_code  VARCHAR2(100),
            message     VARCHAR2(500),
            created_at  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
          )
          """),
      new TableDefinition("CB_BACKOFFICE_SETTING", """
          CREATE TABLE cb_backoffice_setting (
            setting_key    VARCHAR2(100) PRIMARY KEY,
            setting_value  VARCHAR2(1000),
            updated_at     TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
          )
          """),
      new TableDefinition("CB_VPD_POLICY_NOTE", """
          CREATE TABLE cb_vpd_policy_note (
            object_owner VARCHAR2(128) NOT NULL,
            object_name  VARCHAR2(128) NOT NULL,
            policy_name  VARCHAR2(128) NOT NULL,
            description  VARCHAR2(500) NOT NULL,
            updated_at   TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
            CONSTRAINT cb_vpd_policy_note_pk PRIMARY KEY (object_owner, object_name, policy_name)
          )
          """),
      new TableDefinition("CB_VPD_FILTER_NOTE", """
          CREATE TABLE cb_vpd_filter_note (
            function_owner VARCHAR2(128) NOT NULL,
            function_name  VARCHAR2(128) NOT NULL,
            description    VARCHAR2(500) NOT NULL,
            updated_at     TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
            CONSTRAINT cb_vpd_filter_note_pk PRIMARY KEY (function_owner, function_name)
          )
          """)
  );

  private static final List<ColumnDefinition> COLUMNS = List.of(
      new ColumnDefinition("CB_APP_ROLE", "MAX_SENSITIVITY_LEVEL",
          "ALTER TABLE cb_app_role ADD (max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL)"),
      new ColumnDefinition("CB_PERMISSION", "PERMISSION_EFFECT",
          "ALTER TABLE cb_permission ADD (permission_effect VARCHAR2(10) DEFAULT 'ALLOW' NOT NULL)"),
      new ColumnDefinition("CB_PERMISSION_RULE", "RULE_COLUMN",
          "ALTER TABLE cb_permission_rule ADD (rule_column VARCHAR2(128))"),
      new ColumnDefinition("CB_APP_USER", "STAKEHOLDER_USER_ID",
          "ALTER TABLE cb_app_user ADD (stakeholder_user_id VARCHAR2(30))"),
      new ColumnDefinition("CB_AGENT_BEARER_KEY", "STAKEHOLDER_USER_ID",
          "ALTER TABLE cb_agent_bearer_key ADD (stakeholder_user_id VARCHAR2(30))"),
      new ColumnDefinition("CB_AGENT_BEARER_KEY", "DESCRIPTION",
          "ALTER TABLE cb_agent_bearer_key ADD (description VARCHAR2(200))"),
      new ColumnDefinition("CB_PROTECTED_OBJECT", "DESCRIPTION",
          "ALTER TABLE cb_protected_object ADD (description VARCHAR2(200))"),
      new ColumnDefinition("CB_PROTECTED_COLUMN", "SENSITIVITY_LEVEL",
          "ALTER TABLE cb_protected_column ADD (sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL)"),
      new ColumnDefinition("CB_PROTECTED_COLUMN", "REDACTION_METHOD",
          "ALTER TABLE cb_protected_column ADD (redaction_method VARCHAR2(20) DEFAULT 'NONE' NOT NULL)")
  );

  private static final List<ConstraintDefinition> CONSTRAINTS = List.of(
      new ConstraintDefinition("CB_USER_ROLE_PK", "CB_USER_ROLE", "PK/INDEX"),
      new ConstraintDefinition("CB_USER_GROUP_PK", "CB_USER_GROUP", "PK/INDEX"),
      new ConstraintDefinition("CB_GROUP_ROLE_PK", "CB_GROUP_ROLE", "PK/INDEX"),
      new ConstraintDefinition("CB_PERMISSION_COLUMN_PK", "CB_PERMISSION_COLUMN", "PK/INDEX"),
      new ConstraintDefinition("CB_PROTECTED_COLUMN_UK", "CB_PROTECTED_COLUMN", "UK/INDEX")
  );

  private static final String PROTECTED_COLUMN_MIGRATION_SQL = """
      UPDATE cb_protected_column
      SET sensitivity_level = CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END,
          redaction_method = CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END
      WHERE sensitivity_level = 'PUBLIC'
        AND redaction_method = 'NONE'
        AND sensitive_yn = 'Y'
      """;

  private static final String SETTINGS_MERGE_SQL = """
      MERGE INTO cb_backoffice_setting dst
      USING (
        SELECT 'ORDS_BASE_URL' setting_key, ? setting_value
        FROM dual
      ) src
      ON (dst.setting_key = src.setting_key)
      WHEN MATCHED THEN UPDATE SET dst.setting_value = src.setting_value, dst.updated_at = SYSTIMESTAMP
        WHERE dst.setting_value IS NULL OR TRIM(dst.setting_value) IS NULL
      WHEN NOT MATCHED THEN INSERT (setting_key, setting_value, updated_at)
        VALUES (src.setting_key, src.setting_value, SYSTIMESTAMP)
      """;

  private static final List<MaskingRuleSeed> DEFAULT_MASKING_RULES = List.of(
      new MaskingRuleSeed("MASK_NULLIFY", "값 숨김 (NULL)", "NULLIFY",
          "값을 NULL로 반환하는 기본 마스킹 방식"),
      new MaskingRuleSeed("MASK_FULL", "전체 마스킹", "FULL",
          "문자형은 공백, 숫자형은 0으로 반환하는 전체 마스킹 방식"),
      new MaskingRuleSeed("MASK_TEXT_PARTIAL", "문자열 일부 마스킹", "TEXT_PARTIAL",
          "첫 글자만 보이고 나머지는 가리는 문자열 마스킹 방식"),
      new MaskingRuleSeed("MASK_IDENTIFIER_PARTIAL", "식별번호 부분 마스킹", "RRN_PARTIAL",
          "앞 6자리만 보이고 나머지는 가리는 게임 사용자 식별번호 마스킹 방식")
  );

  private static final String MASKING_RULE_SEED_SQL = """
      MERGE INTO cb_masking_rule dst
      USING (SELECT ? rule_id, ? rule_code, ? rule_name, ? template_code, ? description FROM dual) src
      ON (dst.rule_code = src.rule_code)
      WHEN NOT MATCHED THEN INSERT (rule_id, rule_code, rule_name, template_code, description, enabled_yn)
        VALUES (src.rule_id, src.rule_code, src.rule_name, src.template_code, src.description, 'Y')
      """;

  private final JdbcTemplate jdbcTemplate;
  private final BackofficeProperties properties;
  private final DataCatalog dataCatalog;

  public BackofficeSchemaService(
      JdbcTemplate jdbcTemplate,
      BackofficeProperties properties,
      DataCatalog dataCatalog
  ) {
    this.jdbcTemplate = jdbcTemplate;
    this.properties = properties;
    this.dataCatalog = dataCatalog;
  }

  public SchemaPreflightView preflight() {
    String currentUser = currentUser();
    List<SchemaPreflightCheck> checks = new ArrayList<>();

    for (SequenceDefinition sequence : SEQUENCES) {
      addCountCheck(
          checks,
          "Backoffice Metadata",
          sequence.name(),
          "SEQUENCE",
          "SELECT COUNT(*) FROM user_sequences WHERE sequence_name = ?",
          "지원 테이블 생성/확인을 실행하면 생성됩니다.",
          false,
          sequence.name()
      );
    }
    for (TableDefinition table : TABLES) {
      addCountCheck(
          checks,
          "Backoffice Metadata",
          table.name(),
          "TABLE",
          "SELECT COUNT(*) FROM user_tables WHERE table_name = ?",
          "지원 테이블 생성/확인을 실행하면 생성됩니다.",
          false,
          table.name()
      );
    }
    for (ColumnDefinition column : COLUMNS) {
      addCountCheck(
          checks,
          "Backoffice Metadata",
          column.table() + "." + column.column(),
          "COLUMN",
          "SELECT COUNT(*) FROM user_tab_columns WHERE table_name = ? AND column_name = ?",
          "지원 테이블 생성/확인을 실행하면 보강됩니다.",
          false,
          column.table(),
          column.column()
      );
    }
    for (ConstraintDefinition constraint : CONSTRAINTS) {
      addCountCheck(
          checks,
          "Constraint/Index",
          constraint.name(),
          constraint.objectType(),
          "SELECT COUNT(*) FROM user_constraints WHERE constraint_name = ? AND table_name = ?",
          "테이블을 재생성하거나 constraint 생성 SQL을 sqlcl로 확인하세요.",
          false,
          constraint.name(),
          constraint.table()
      );
    }

    addCountCheck(checks, "VPD Runtime", "CB_AGENT_CTX", "CONTEXT",
        "SELECT COUNT(*) FROM all_context WHERE namespace = 'CB_AGENT_CTX'",
        "sqlcl에서 17_agent_ords_security_local_vpd_setup.sql 또는 CREATE CONTEXT를 실행하세요.",
        true);
    addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG", "PACKAGE",
        "sqlcl에서 cb_agent_ctx_pkg package를 생성/컴파일하세요.");
    addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG BODY", "PACKAGE BODY",
        "sqlcl에서 cb_agent_ctx_pkg package body를 생성/컴파일하세요.", "CB_AGENT_CTX_PKG");
    addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_DOC_VPD_FILTER", "FUNCTION",
        "sqlcl에서 26_agent_ords_security_dynamic_vpd_filter.sql을 실행하세요.");
    addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CAN_READ_COLUMN", "FUNCTION",
        "sqlcl에서 backoffice support redaction function을 생성/컴파일하세요.");
    addPolicyCheck(checks);

    addCountCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "USER",
        "SELECT COUNT(*) FROM all_users WHERE username = ?",
        "sqlcl 또는 DBA 계정으로 CB_ORDS 사용자를 생성하세요.",
        true,
        ORDS_SCHEMA);
    addCountCheck(checks, "ORDS Runtime", "ORDS.ENABLE_SCHEMA(" + ORDS_SCHEMA + ")", "ORDS SCHEMA",
        "SELECT COUNT(*) FROM dba_ords_schemas WHERE parsing_schema = ?",
        "sqlcl에서 21_agent_ords_security_ords_enable_schema.sql을 실행하세요.",
        true,
        ORDS_SCHEMA);
    addAllObjectCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "CB_ORDS_HANDLER_PKG", "PACKAGE",
        "CB_ORDS로 접속해 22_agent_ords_security_ords_handler_setup.sql을 실행하세요.");

    addGrantCheck(checks, currentUser, "CB_AGENT_CTX_PKG", "EXECUTE");
    addGrantCheck(checks, currentUser, "CB_AGENT_CAN_READ_COLUMN", "EXECUTE");
    addProtectedObjectGrantCheck(checks);

    return new SchemaPreflightView(currentUser, ORDS_SCHEMA, checks, appDdlPreview(), sqlclScript(currentUser));
  }

  @Transactional
  public List<SchemaActionResult> initializeSchema() {
    List<SchemaActionResult> results = new ArrayList<>();
    try {
      currentUser();
    } catch (DataAccessException exception) {
      results.add(new SchemaActionResult("DB_CONNECTION", "PREFLIGHT", "FAILED",
          "DB 연결을 확인할 수 없습니다. " + safeMessage(exception), "SELECT USER FROM dual"));
      return results;
    }
    for (SequenceDefinition sequence : SEQUENCES) {
      createSequence(results, sequence);
    }
    for (TableDefinition table : TABLES) {
      createTable(results, table);
    }
    for (ColumnDefinition column : COLUMNS) {
      addColumn(results, column);
    }
    runDml(results, "CB_PROTECTED_COLUMN", "DATA", PROTECTED_COLUMN_MIGRATION_SQL,
        "민감 컬럼 legacy 값을 보강했습니다.", "UPDATED");
    seedDefaultMaskingRules(results);
    seedDefaultSettings(results);
    return results;
  }

  private void createSequence(List<SchemaActionResult> results, SequenceDefinition sequence) {
    runIgnoringAlreadyExists(results, sequence.name(), "SEQUENCE", sequenceDdl(sequence));
  }

  private void createTable(List<SchemaActionResult> results, TableDefinition table) {
    runIgnoringAlreadyExists(results, table.name(), "TABLE", table.ddl());
  }

  private void addColumn(List<SchemaActionResult> results, ColumnDefinition column) {
    try {
      jdbcTemplate.execute(column.ddl());
      results.add(new SchemaActionResult(column.displayName(), "COLUMN", "CREATED", "컬럼을 추가했습니다.", column.ddl()));
    } catch (RuntimeException exception) {
      if (oracleErrorCode(exception) == 1430) {
        results.add(new SchemaActionResult(column.displayName(), "COLUMN", "EXISTS", "이미 존재합니다.", column.ddl()));
        return;
      }
      results.add(new SchemaActionResult(column.displayName(), "COLUMN", "FAILED", safeMessage(exception), column.ddl()));
    }
  }

  private void runIgnoringAlreadyExists(
      List<SchemaActionResult> results,
      String name,
      String action,
      String ddl
  ) {
    try {
      jdbcTemplate.execute(ddl);
      results.add(new SchemaActionResult(name, action, "CREATED", "생성했습니다.", ddl));
    } catch (RuntimeException exception) {
      if (oracleErrorCode(exception) == 955) {
        results.add(new SchemaActionResult(name, action, "EXISTS", "이미 존재합니다.", ddl));
        return;
      }
      results.add(new SchemaActionResult(name, action, "FAILED", safeMessage(exception), ddl));
    }
  }

  private void runDml(
      List<SchemaActionResult> results,
      String name,
      String action,
      String sql,
      String message,
      String status
  ) {
    try {
      int updated = jdbcTemplate.update(sql);
      results.add(new SchemaActionResult(name, action, status, message + " (" + updated + " rows)", sql));
    } catch (RuntimeException exception) {
      results.add(new SchemaActionResult(name, action, "FAILED", safeMessage(exception), sql));
    }
  }

  private void seedDefaultSettings(List<SchemaActionResult> results) {
    try {
      jdbcTemplate.update(SETTINGS_MERGE_SQL, properties.ords().baseUrl());
      results.add(new SchemaActionResult("ORDS_BASE_URL", "SETTING", "MERGED",
          "기본 ORDS URL 설정을 보강했습니다.", SETTINGS_MERGE_SQL));
    } catch (RuntimeException exception) {
      results.add(new SchemaActionResult("ORDS_BASE_URL", "SETTING", "FAILED",
          safeMessage(exception), SETTINGS_MERGE_SQL));
    }
  }

  private void seedDefaultMaskingRules(List<SchemaActionResult> results) {
    long nextRuleId;
    try {
      Long currentMax = jdbcTemplate.queryForObject("SELECT NVL(MAX(rule_id), 0) FROM cb_masking_rule", Long.class);
      nextRuleId = currentMax == null ? 1L : currentMax + 1L;
    } catch (RuntimeException exception) {
      results.add(new SchemaActionResult("CB_MASKING_RULE", "MASKING_RULE", "FAILED",
          safeMessage(exception), MASKING_RULE_SEED_SQL));
      return;
    }
    for (MaskingRuleSeed seed : DEFAULT_MASKING_RULES) {
      try {
        jdbcTemplate.update(MASKING_RULE_SEED_SQL, nextRuleId++, seed.code(), seed.name(), seed.templateCode(), seed.description());
        results.add(new SchemaActionResult(seed.code(), "MASKING_RULE", "MERGED",
            "기본 마스킹 규칙을 확인했습니다.", MASKING_RULE_SEED_SQL));
      } catch (RuntimeException exception) {
        results.add(new SchemaActionResult(seed.code(), "MASKING_RULE", "FAILED",
            safeMessage(exception), MASKING_RULE_SEED_SQL));
      }
    }
  }

  private String currentUser() {
    return jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
  }

  private void addCountCheck(
      List<SchemaPreflightCheck> checks,
      String layer,
      String itemName,
      String objectType,
      String sql,
      String missingAction,
      boolean sqlclRequired,
      Object... args
  ) {
    try {
      int count = count(sql, args);
      checks.add(new SchemaPreflightCheck(
          layer,
          itemName,
          objectType,
          count > 0 ? "OK" : "MISSING",
          count > 0 ? "존재합니다." : "찾을 수 없습니다.",
          count > 0 ? "조치 없음" : missingAction,
          sqlclRequired
      ));
    } catch (DataAccessException exception) {
      checks.add(new SchemaPreflightCheck(layer, itemName, objectType, "ERROR",
          safeMessage(exception), missingAction, sqlclRequired));
    }
  }

  private void addUserObjectCheck(
      List<SchemaPreflightCheck> checks,
      String layer,
      String itemName,
      String objectType,
      String nextAction
  ) {
    addUserObjectCheck(checks, layer, itemName, objectType, nextAction, itemName);
  }

  private void addUserObjectCheck(
      List<SchemaPreflightCheck> checks,
      String layer,
      String itemName,
      String objectType,
      String nextAction,
      String objectName
  ) {
    try {
      List<String> statuses = jdbcTemplate.queryForList("""
          SELECT status
          FROM user_objects
          WHERE object_name = ?
            AND object_type = ?
          """, String.class, objectName, objectType);
      if (statuses.isEmpty()) {
        checks.add(new SchemaPreflightCheck(layer, itemName, objectType, "MISSING",
            "찾을 수 없습니다.", nextAction, true));
        return;
      }
      boolean valid = statuses.stream().allMatch(status -> "VALID".equalsIgnoreCase(status));
      checks.add(new SchemaPreflightCheck(layer, itemName, objectType, valid ? "OK" : "WARN",
          valid ? "VALID" : "INVALID 또는 컴파일 확인 필요",
          valid ? "조치 없음" : nextAction,
          true));
    } catch (DataAccessException exception) {
      checks.add(new SchemaPreflightCheck(layer, itemName, objectType, "ERROR",
          safeMessage(exception), nextAction, true));
    }
  }

  private void addAllObjectCheck(
      List<SchemaPreflightCheck> checks,
      String layer,
      String owner,
      String objectName,
      String objectType,
      String nextAction
  ) {
    try {
      List<String> statuses = jdbcTemplate.queryForList("""
          SELECT status
          FROM all_objects
          WHERE owner = ?
            AND object_name = ?
            AND object_type = ?
          """, String.class, owner, objectName, objectType);
      if (statuses.isEmpty()) {
        checks.add(new SchemaPreflightCheck(layer, owner + "." + objectName, objectType, "MISSING",
            "찾을 수 없습니다.", nextAction, true));
        return;
      }
      boolean valid = statuses.stream().allMatch(status -> "VALID".equalsIgnoreCase(status));
      checks.add(new SchemaPreflightCheck(layer, owner + "." + objectName, objectType, valid ? "OK" : "WARN",
          valid ? "VALID" : "INVALID 또는 컴파일 확인 필요",
          valid ? "조치 없음" : nextAction,
          true));
    } catch (DataAccessException exception) {
      checks.add(new SchemaPreflightCheck(layer, owner + "." + objectName, objectType, "ERROR",
          safeMessage(exception), nextAction, true));
    }
  }

  private void addPolicyCheck(List<SchemaPreflightCheck> checks) {
    try {
      int count = count("""
          SELECT COUNT(*)
          FROM all_policies
          WHERE pf_owner = USER
            AND function = 'CB_AGENT_DOC_VPD_FILTER'
          """);
      checks.add(new SchemaPreflightCheck(
          "VPD Runtime",
          "CB_AGENT_DOC_VPD_FILTER 적용 policy",
          "DBMS_RLS POLICY",
          count > 0 ? "OK" : "WARN",
          count > 0 ? count + "개 policy가 기본 filter를 사용합니다." : "기본 filter가 적용된 TABLE/VIEW가 없습니다.",
          count > 0 ? "조치 없음" : "운영 > VPD 설정에서 개별 또는 벌크 적용을 실행하세요.",
          false
      ));
    } catch (DataAccessException exception) {
      checks.add(new SchemaPreflightCheck("VPD Runtime", "VPD policy catalog", "DBMS_RLS POLICY", "ERROR",
          safeMessage(exception), "ALL_POLICIES 조회 권한 또는 VPD 설정을 확인하세요.", true));
    }
  }

  private void addGrantCheck(List<SchemaPreflightCheck> checks, String owner, String objectName, String privilege) {
    try {
      int count = count("""
          SELECT COUNT(*)
          FROM all_tab_privs
          WHERE table_schema = ?
            AND table_name = ?
            AND grantee = ?
            AND privilege = ?
          """, owner, objectName, ORDS_SCHEMA, privilege);
      checks.add(new SchemaPreflightCheck(
          "Grants",
          "GRANT " + privilege + " ON " + owner + "." + objectName + " TO " + ORDS_SCHEMA,
          "GRANT",
          count > 0 ? "OK" : "MISSING",
          count > 0 ? "권한이 부여되어 있습니다." : "권한을 찾을 수 없습니다.",
          count > 0 ? "조치 없음" : "sqlcl에서 GRANT " + privilege + " ON " + objectName + " TO " + ORDS_SCHEMA + "를 실행하세요.",
          true
      ));
    } catch (DataAccessException exception) {
      checks.add(new SchemaPreflightCheck("Grants", objectName + " " + privilege, "GRANT", "ERROR",
          safeMessage(exception), "ALL_TAB_PRIVS 조회 권한을 확인하세요.", true));
    }
  }

  private void addProtectedObjectGrantCheck(List<SchemaPreflightCheck> checks) {
    if (!exists("SELECT COUNT(*) FROM user_tables WHERE table_name = 'CB_PROTECTED_OBJECT'")) {
      checks.add(new SchemaPreflightCheck("Grants", "보호 객체 SELECT grants", "GRANT", "WARN",
          "CB_PROTECTED_OBJECT 테이블이 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
          "지원 테이블 생성/확인 후 다시 점검하세요.", false));
      return;
    }
    try {
      int protectedObjects = count("SELECT COUNT(*) FROM cb_protected_object WHERE enabled_yn = 'Y'");
      int grants = count("""
          SELECT COUNT(*)
          FROM cb_protected_object po
          JOIN all_tab_privs tp
            ON tp.table_schema = po.owner
           AND tp.table_name = po.object_name
           AND tp.grantee = ?
           AND tp.privilege = 'SELECT'
          WHERE po.enabled_yn = 'Y'
          """, ORDS_SCHEMA);
      String status = protectedObjects == 0 ? "WARN" : (grants >= protectedObjects ? "OK" : "MISSING");
      String detail = protectedObjects == 0
          ? "등록된 활성 보호 객체가 없습니다."
          : grants + "/" + protectedObjects + "개 보호 객체에 SELECT grant가 있습니다.";
      String nextAction = "ORDS에서 서빙할 TABLE/VIEW마다 GRANT SELECT ON owner.object TO " + ORDS_SCHEMA + "를 실행하세요.";
      checks.add(new SchemaPreflightCheck("Grants", "보호 객체 SELECT grants", "GRANT", status, detail,
          "OK".equals(status) ? "조치 없음" : nextAction, true));
    } catch (DataAccessException exception) {
      checks.add(new SchemaPreflightCheck("Grants", "보호 객체 SELECT grants", "GRANT", "ERROR",
          safeMessage(exception), "등록 보호 객체와 ALL_TAB_PRIVS 조회 권한을 확인하세요.", true));
    }
  }

  private int count(String sql, Object... args) {
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class, args);
    return count == null ? 0 : count;
  }

  private boolean exists(String sql, Object... args) {
    try {
      return count(sql, args) > 0;
    } catch (DataAccessException exception) {
      return false;
    }
  }

  private String appDdlPreview() {
    StringBuilder builder = new StringBuilder();
    builder.append("-- /settings/schema/initialize 버튼이 현재 연결 사용자로 실행하는 SQL preview\n");
    builder.append("-- 이 섹션은 백오피스 메타데이터 테이블/시퀀스 보강만 수행합니다.\n\n");
    for (SequenceDefinition sequence : SEQUENCES) {
      appendSql(builder, sequenceDdl(sequence));
    }
    for (TableDefinition table : TABLES) {
      appendSql(builder, table.ddl());
    }
    for (ColumnDefinition column : COLUMNS) {
      appendSql(builder, column.ddl());
    }
    appendSql(builder, PROTECTED_COLUMN_MIGRATION_SQL);
    appendSql(builder, MASKING_RULE_SEED_SQL.replace("?", "'<MASKING_RULE_VALUE>'"));
    appendSql(builder, SETTINGS_MERGE_SQL.replace("?", "'<BACKOFFICE_ORDS_BASE_URL>'"));
    return builder.toString();
  }

  private String sqlclScript(String currentUser) {
    String owner = currentUser == null || currentUser.isBlank() ? "ADMIN" : currentUser;
    return """
        -- sqlcl에서 실행할 VPD/ORDS runtime 준비 순서
        -- 1. ADMIN 또는 보호 객체 owner로 접속
        @sql/adb/17_agent_ords_security_local_vpd_setup.sql
        @sql/adb/25_agent_ords_security_backoffice_support.sql
        @sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
        -- 4. 배포 환경에서 선택한 사용자·권한 초기화 SQL을 별도로 실행
        @sql/adb/21_agent_ords_security_ords_enable_schema.sql

        -- 2. ORDS parsing schema로 접속
        CONNECT cb_ords/<password>@<tns_alias>
        @sql/adb/22_agent_ords_security_ords_handler_setup.sql

        -- 3. 대표 권한 부여 SQL
        CONNECT %s/<password>@<tns_alias>
        GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
        GRANT SELECT ON <owner>.<table_or_view> TO cb_ords;

        -- 5. 마스킹 규칙을 UI에서 등록된 업무 데이터 컬럼에 연결
        --    DBMS_REDACT 정책은 백오피스가 %s 대상에 자동 동기화합니다.
        """.formatted(owner.toLowerCase(), dataCatalog.owner());
  }

  private void appendSql(StringBuilder builder, String sql) {
    builder.append(sql.strip()).append(";\n\n");
  }

  private String sequenceDdl(SequenceDefinition sequence) {
    return "CREATE SEQUENCE " + sequence.name().toLowerCase()
        + " START WITH " + sequence.startWith() + " INCREMENT BY 1 NOCACHE";
  }

  private int oracleErrorCode(RuntimeException exception) {
    Throwable current = exception;
    while (current != null) {
      if (current instanceof SQLException sqlException) {
        return Math.abs(sqlException.getErrorCode());
      }
      current = current.getCause();
    }
    return 0;
  }

  private String safeMessage(Throwable throwable) {
    String message = throwable.getMessage();
    if (message == null || message.isBlank()) {
      return throwable.getClass().getSimpleName();
    }
    String normalized = message.replaceAll("\\s+", " ").trim();
    return normalized.length() <= 300 ? normalized : normalized.substring(0, 300) + "...";
  }

  private record SequenceDefinition(String name, int startWith) {
  }

  private record TableDefinition(String name, String ddl) {
  }

  private record ColumnDefinition(String table, String column, String ddl) {

    private String displayName() {
      return table + "." + column;
    }
  }

  private record ConstraintDefinition(String name, String table, String objectType) {
  }

  private record MaskingRuleSeed(String code, String name, String templateCode, String description) {
  }
}
