package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingTemplate;
import com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * Reconciles the backoffice masking metadata with the managed Oracle
 * Data Redaction policies. Metadata is the source of truth: a table with no
 * active linked columns has its managed policy disabled, rather than silently
 * retaining redaction after its UI configuration was removed.
 */
@Service
public class MaskingPolicySynchronizer {

  private static final Pattern COLUMN_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");

  private final JdbcTemplate jdbcTemplate;
  private final MaskingRuleMapper mapper;
  private final DataCatalog dataCatalog;
  private final MaskingPolicyCatalog policyCatalog;

  public MaskingPolicySynchronizer(
      JdbcTemplate jdbcTemplate,
      MaskingRuleMapper mapper,
      DataCatalog dataCatalog,
      MaskingPolicyCatalog policyCatalog
  ) {
    this.jdbcTemplate = jdbcTemplate;
    this.mapper = mapper;
    this.dataCatalog = dataCatalog;
    this.policyCatalog = policyCatalog;
  }

  public Set<String> managedObjectNames() {
    return policyCatalog.objectNames();
  }

  public boolean isManagedObject(String objectName) {
    return policyCatalog.containsObject(objectName);
  }

  /**
   * Applies the current active column-rule metadata to managed DBMS_REDACT policies.
   *
   * <p>The backoffice metadata is the source of truth. In particular, when an object has no
   * active linked column rule, its policy is disabled. This avoids an old redaction policy
   * continuing to mask data after an operator removed every rule from the UI.</p>
   */
  public MaskingPolicySyncResult synchronize() {
    Map<String, List<ColumnMaskingRule>> desiredByObject = new LinkedHashMap<>();
    for (ColumnMaskingRule rule : mapper.findColumnRules()) {
      if (dataCatalog.owner().equalsIgnoreCase(rule.owner())
          && rule.ruleEnabled()
          && policyCatalog.containsObject(rule.objectName())) {
        desiredByObject.computeIfAbsent(rule.objectName(), ignored -> new ArrayList<>()).add(rule);
      }
    }

    int disabledPolicies = 0;
    int enabledPolicies = 0;
    int addedColumns = 0;
    int modifiedColumns = 0;
    int droppedColumns = 0;
    for (MaskingPolicyTarget policy : policyCatalog.targets()) {
      String objectName = policy.objectName();
      String policyName = policy.policyName();
      List<ColumnMaskingRule> desired = desiredByObject.getOrDefault(objectName, List.of());
      String enableStatus = policyEnableStatus(objectName, policyName);
      if (desired.isEmpty()) {
        if ("YES".equals(enableStatus)) {
          disablePolicy(objectName, policyName);
          disabledPolicies++;
        }
        continue;
      }

      boolean exists = enableStatus != null;
      if (exists) {
        if (!"YES".equals(enableStatus)) {
          enablePolicy(objectName, policyName);
          enabledPolicies++;
        }
      }
      Set<String> desiredColumns = desired.stream()
          .map(ColumnMaskingRule::columnName)
          .map(this::requiredColumnName)
          .collect(LinkedHashSet::new, Set::add, Set::addAll);
      Set<String> actualColumns = exists
          ? new LinkedHashSet<>(redactionColumns(objectName))
          : new LinkedHashSet<>();

      for (String actualColumn : actualColumns) {
        if (!desiredColumns.contains(actualColumn)) {
          dropColumn(objectName, policyName, actualColumn);
          droppedColumns++;
        }
      }

      boolean firstColumn = !exists;
      for (ColumnMaskingRule desiredColumn : desired) {
        String columnName = requiredColumnName(desiredColumn.columnName());
        if (firstColumn) {
          addPolicy(objectName, policyName, columnName, desiredColumn.template());
          firstColumn = false;
          addedColumns++;
        } else if (actualColumns.contains(columnName)) {
          modifyColumn(objectName, policyName, columnName, desiredColumn.template());
          modifiedColumns++;
        } else {
          addColumn(objectName, policyName, columnName, desiredColumn.template());
          addedColumns++;
        }
        upsertColumnExpression(objectName, columnName, desiredColumn.columnId());
      }
    }
    return new MaskingPolicySyncResult(
        disabledPolicies, enabledPolicies, addedColumns, modifiedColumns, droppedColumns
    );
  }

  private String policyEnableStatus(String objectName, String policyName) {
    List<String> statuses = jdbcTemplate.queryForList("""
        SELECT enable
        FROM redaction_policies
        WHERE object_owner = ? AND object_name = ? AND policy_name = ?
        """, String.class, dataCatalog.owner(), objectName, policyName);
    return statuses.isEmpty() ? null : statuses.getFirst();
  }

  private List<String> redactionColumns(String objectName) {
    return jdbcTemplate.queryForList("""
        SELECT column_name
        FROM redaction_columns
        WHERE object_owner = ? AND object_name = ?
        """, String.class, dataCatalog.owner(), objectName).stream()
        .map(this::requiredColumnName)
        .toList();
  }

  private void disablePolicy(String objectName, String policyName) {
    jdbcTemplate.update("""
        BEGIN
          DBMS_REDACT.DISABLE_POLICY(object_schema => ?, object_name => ?, policy_name => ?);
        END;
        """, dataCatalog.owner(), objectName, policyName);
  }

  private void enablePolicy(String objectName, String policyName) {
    jdbcTemplate.update("""
        BEGIN
          DBMS_REDACT.ENABLE_POLICY(object_schema => ?, object_name => ?, policy_name => ?);
        END;
        """, dataCatalog.owner(), objectName, policyName);
  }

  private void dropColumn(String objectName, String policyName, String columnName) {
    jdbcTemplate.update("""
        BEGIN
          DBMS_REDACT.ALTER_POLICY(
            object_schema => ?, object_name => ?, policy_name => ?,
            action => DBMS_REDACT.DROP_COLUMN, column_name => ?
          );
        END;
        """, dataCatalog.owner(), objectName, policyName, columnName);
  }

  private void addPolicy(
      String objectName, String policyName, String columnName, MaskingTemplate template
  ) {
    callTemplate("ADD_POLICY", objectName, policyName, columnName, template);
  }

  private void addColumn(
      String objectName, String policyName, String columnName, MaskingTemplate template
  ) {
    callTemplate("ADD_COLUMN", objectName, policyName, columnName, template);
  }

  private void modifyColumn(
      String objectName, String policyName, String columnName, MaskingTemplate template
  ) {
    callTemplate("MODIFY_COLUMN", objectName, policyName, columnName, template);
  }

  /**
   * Template choice is an enum, so DBMS_REDACT constants are rendered only from trusted source
   * code. They cannot be supplied from a request parameter or backoffice table value.
   */
  private void callTemplate(
      String operation, String objectName, String policyName, String columnName, MaskingTemplate template
  ) {
    String functionConstant = switch (template) {
      case NULLIFY -> "DBMS_REDACT.NULLIFY";
      case FULL -> "DBMS_REDACT.FULL";
      case TEXT_PARTIAL, RRN_PARTIAL -> "DBMS_REDACT.REGEXP";
    };
    String actionConstant = switch (operation) {
      case "ADD_POLICY" -> null;
      case "ADD_COLUMN" -> "DBMS_REDACT.ADD_COLUMN";
      case "MODIFY_COLUMN" -> "DBMS_REDACT.MODIFY_COLUMN";
      default -> throw new IllegalArgumentException("Unsupported redaction operation");
    };
    String regexPattern = switch (template) {
      case TEXT_PARTIAL -> "(^.).*$";
      case RRN_PARTIAL -> "(^[0-9]{6})-?[0-9]{7}$";
      default -> null;
    };
    String regexReplacement = switch (template) {
      case TEXT_PARTIAL -> "\\1***";
      case RRN_PARTIAL -> "\\1-*******";
      default -> null;
    };

    if ("ADD_POLICY".equals(operation)) {
      String sql = template == MaskingTemplate.TEXT_PARTIAL || template == MaskingTemplate.RRN_PARTIAL
          ? """
              BEGIN
                DBMS_REDACT.ADD_POLICY(
                  object_schema => ?, object_name => ?, policy_name => ?,
                  policy_description => 'Managed by VPD masking backoffice',
                  column_name => ?, function_type => %s, expression => '1=1',
                  regexp_pattern => ?, regexp_replace_string => ?, enable => TRUE
                );
              END;
              """.formatted(functionConstant)
          : """
              BEGIN
                DBMS_REDACT.ADD_POLICY(
                  object_schema => ?, object_name => ?, policy_name => ?,
                  policy_description => 'Managed by VPD masking backoffice',
                  column_name => ?, function_type => %s, expression => '1=1', enable => TRUE
                );
              END;
              """.formatted(functionConstant);
      if (regexPattern == null) {
        jdbcTemplate.update(sql, dataCatalog.owner(), objectName, policyName, columnName);
      } else {
        jdbcTemplate.update(sql, dataCatalog.owner(), objectName, policyName, columnName, regexPattern, regexReplacement);
      }
      return;
    }

    String sql = template == MaskingTemplate.TEXT_PARTIAL || template == MaskingTemplate.RRN_PARTIAL
        ? """
            BEGIN
              DBMS_REDACT.ALTER_POLICY(
                object_schema => ?, object_name => ?, policy_name => ?, action => %s,
                column_name => ?, function_type => %s, regexp_pattern => ?, regexp_replace_string => ?
              );
            END;
            """.formatted(actionConstant, functionConstant)
        : """
            BEGIN
              DBMS_REDACT.ALTER_POLICY(
                object_schema => ?, object_name => ?, policy_name => ?, action => %s,
                column_name => ?, function_type => %s
              );
            END;
            """.formatted(actionConstant, functionConstant);
    if (regexPattern == null) {
      jdbcTemplate.update(sql, dataCatalog.owner(), objectName, policyName, columnName);
    } else {
      jdbcTemplate.update(sql, dataCatalog.owner(), objectName, policyName, columnName, regexPattern, regexReplacement);
    }
  }

  private void upsertColumnExpression(String objectName, String columnName, long columnId) {
    String expressionName = "CBMR_" + columnId;
    String expression = maskingExpression(columnId);
    Integer count = jdbcTemplate.queryForObject("""
        SELECT COUNT(*) FROM redaction_expressions WHERE policy_expression_name = ?
        """, Integer.class, expressionName);
    if (count != null && count > 0) {
      jdbcTemplate.update("""
          BEGIN
            DBMS_REDACT.UPDATE_POLICY_EXPRESSION(
              policy_expression_name => ?, expression => ?,
              policy_expression_description => 'Mask unless trusted context allows original value'
            );
          END;
          """, expressionName, expression);
    } else {
      jdbcTemplate.update("""
          BEGIN
            DBMS_REDACT.CREATE_POLICY_EXPRESSION(
              policy_expression_name => ?, expression => ?,
              policy_expression_description => 'Mask unless trusted context allows original value'
            );
          END;
          """, expressionName, expression);
    }
    if (!expressionAppliedToColumn(expressionName, objectName, columnName)) {
      jdbcTemplate.update("""
          BEGIN
            DBMS_REDACT.APPLY_POLICY_EXPR_TO_COL(
              object_schema => ?, object_name => ?, column_name => ?, policy_expression_name => ?
            );
          END;
        """, dataCatalog.owner(), objectName, columnName, expressionName);
    }
  }

  static String maskingExpression(long columnId) {
    return "SYS_CONTEXT('CB_AGENT_CTX', 'MR_" + columnId
        + "') IS NULL OR SYS_CONTEXT('CB_AGENT_CTX', 'MR_" + columnId + "') <> 'Y'";
  }

  private boolean expressionAppliedToColumn(String expressionName, String objectName, String columnName) {
    Integer count = jdbcTemplate.queryForObject("""
        SELECT COUNT(*)
        FROM redaction_expressions
        WHERE policy_expression_name = ?
          AND object_name = ?
          AND column_name = ?
        """, Integer.class, expressionName, objectName, columnName);
    return count != null && count > 0;
  }

  private String requiredColumnName(String value) {
    String normalized = value == null ? "" : value.trim().toUpperCase();
    if (!COLUMN_NAME.matcher(normalized).matches()) {
      throw new AppException("동기화할 보호 컬럼명이 유효하지 않습니다.");
    }
    return normalized;
  }

  public record MaskingPolicySyncResult(
      int disabledPolicies,
      int enabledPolicies,
      int addedColumns,
      int modifiedColumns,
      int droppedColumns
  ) {

    public String summary() {
      return "DB ASO 정책 동기화 완료: 비활성 " + disabledPolicies + "건, 활성 " + enabledPolicies
          + "건, 컬럼 추가 " + addedColumns + "건, 변경 " + modifiedColumns + "건, 해제 "
          + droppedColumns + "건";
    }
  }
}
