package com.cloudhandson.vpdbackoffice.service;

import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRuleCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingTemplate;
import com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper;
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MaskingRuleService {

  private static final Pattern RULE_CODE = Pattern.compile("[A-Z][A-Z0-9_]{2,63}");
  private static final Set<String> DECISIONS = Set.of("MASK", "UNMASK");

  private final MaskingRuleMapper mapper;
  private final UserMapper userMapper;
  private final ProtectedObjectService protectedObjectService;
  private final AuditService auditService;
  private final MaskingPolicySynchronizer maskingPolicySynchronizer;
  private final DataCatalog dataCatalog;
  private final MaskingPolicyCatalog maskingPolicyCatalog;

  public MaskingRuleService(
      MaskingRuleMapper mapper,
      UserMapper userMapper,
      ProtectedObjectService protectedObjectService,
      AuditService auditService,
      MaskingPolicySynchronizer maskingPolicySynchronizer,
      DataCatalog dataCatalog,
      MaskingPolicyCatalog maskingPolicyCatalog
  ) {
    this.mapper = mapper;
    this.userMapper = userMapper;
    this.protectedObjectService = protectedObjectService;
    this.auditService = auditService;
    this.maskingPolicySynchronizer = maskingPolicySynchronizer;
    this.dataCatalog = dataCatalog;
    this.maskingPolicyCatalog = maskingPolicyCatalog;
  }

  public List<MaskingRule> findAllRules() {
    return mapper.findAllRules();
  }

  public List<MaskingRule> findEnabledRules() {
    return mapper.findEnabledRules();
  }

  public List<ColumnMaskingRule> findColumnRules() {
    return mapper.findColumnRules();
  }

  /** Reads the actual Oracle Data Redaction state for configured managed objects. */
  public List<MaskingPolicyStatus> findPolicyStatuses() {
    if (maskingPolicyCatalog.targets().isEmpty()) {
      return List.of();
    }
    return mapper.findPolicyStatuses(dataCatalog.owner(), maskingPolicyCatalog.targets());
  }

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

  public List<UserMaskingRule> findUserRules() {
    return mapper.findUserRules();
  }

  @Transactional
  public void createRule(MaskingRuleCreateCommand command) {
    String code = normalizeCode(command.ruleCode());
    String name = normalizeRequired(command.ruleName(), 100, "규칙명");
    String templateCode = normalizeTemplate(command.templateCode());
    String description = normalizeOptional(command.description(), 400, "설명");
    if (mapper.findRuleByCode(code) != null) {
      throw new AppException("이미 등록된 컬럼 마스킹 규칙 코드입니다: " + code);
    }
    long ruleId = mapper.nextRuleId();
    mapper.insertRule(ruleId, new MaskingRuleCreateCommand(code, name, templateCode, description));
    auditService.record(new AuditEvent("MASKING_RULE_CREATED", null, null, "SUCCESS", null, null,
        "ruleId=" + ruleId + ",code=" + code + ",template=" + templateCode));
  }

  @Transactional
  public MaskingPolicySynchronizer.MaskingPolicySyncResult setRuleActive(long ruleId, boolean active) {
    if (mapper.updateRuleActive(ruleId, active ? "Y" : "N") == 0) {
      throw new AppException("컬럼 마스킹 규칙을 찾을 수 없습니다.");
    }
    auditService.record(new AuditEvent("MASKING_RULE_ACTIVE_CHANGED", null, null, "SUCCESS", null, null,
        "ruleId=" + ruleId + ",active=" + active));
    return synchronizeDatabasePolicies();
  }

  @Transactional
  public MaskingPolicySynchronizer.MaskingPolicySyncResult assignRuleToColumn(long columnId, long ruleId) {
    var column = protectedObjectService.findColumn(columnId);
    if (column == null) {
      throw new AppException("보호 컬럼을 찾을 수 없습니다.");
    }
    if (!column.sensitive()) {
      throw new AppException("민감 표시 보호 컬럼에만 컬럼 마스킹 규칙을 연결할 수 있습니다.");
    }
    MaskingRule rule = requireEnabledRule(ruleId);
    mapper.upsertColumnRule(columnId, ruleId);
    auditService.record(new AuditEvent("COLUMN_MASKING_RULE_ASSIGNED", null, column.objectId(), "SUCCESS", null, null,
        "columnId=" + columnId + ",rule=" + rule.ruleCode()));
    return synchronizeDatabasePolicies();
  }

  @Transactional
  public MaskingPolicySynchronizer.MaskingPolicySyncResult addTargetColumn(long objectId, String columnName) {
    ProtectedObject object = protectedObjectService.assertEnabled(objectId);
    if (!maskingPolicySynchronizer.isManagedObject(object.objectName())) {
      throw new AppException("ASO 마스킹 정책 관리 대상 객체가 아닙니다: " + object.objectName());
    }
    var column = protectedObjectService.addSensitiveColumnTarget(objectId, columnName);
    auditService.record(new AuditEvent("MASKING_TARGET_COLUMN_REGISTERED", null, objectId, "SUCCESS", null, null,
        object.objectName() + "." + column.columnName()));
    return synchronizeDatabasePolicies();
  }

  @Transactional
  public MaskingPolicySynchronizer.MaskingPolicySyncResult removeRuleFromColumn(long columnId) {
    mapper.deleteUserRulesForColumn(columnId);
    if (mapper.deleteColumnRule(columnId) == 0) {
      throw new AppException("해제할 컬럼 마스킹 규칙을 찾을 수 없습니다.");
    }
    auditService.record(new AuditEvent("COLUMN_MASKING_RULE_REMOVED", null, null, "SUCCESS", null, null,
        "columnId=" + columnId));
    return synchronizeDatabasePolicies();
  }

  /**
   * Reconciles the current metadata with Oracle Data Redaction. This is exposed for the one-time
   * repair of settings saved before automatic synchronization was introduced.
   */
  @Transactional
  public MaskingPolicySynchronizer.MaskingPolicySyncResult synchronizeDatabasePolicies() {
    MaskingPolicySynchronizer.MaskingPolicySyncResult result = maskingPolicySynchronizer.synchronize();
    auditService.record(new AuditEvent("MASKING_POLICY_SYNCHRONIZED", null, null, "SUCCESS", null, null,
        "disabled=" + result.disabledPolicies() + ",enabled=" + result.enabledPolicies()
            + ",added=" + result.addedColumns() + ",modified=" + result.modifiedColumns()
            + ",dropped=" + result.droppedColumns()));
    return result;
  }

  @Transactional
  public void assignUserRule(long userId, long columnId, String decision) {
    if (userMapper.findById(userId) == null) {
      throw new AppException("사용자를 찾을 수 없습니다.");
    }
    ColumnMaskingRule columnRule = mapper.findColumnRule(columnId);
    if (columnRule == null || !columnRule.ruleEnabled()) {
      throw new AppException("먼저 활성 컬럼 마스킹 규칙을 민감 컬럼에 연결하세요.");
    }
    String normalizedDecision = normalizeDecision(decision);
    mapper.upsertUserRule(userId, columnId, normalizedDecision);
    auditService.record(new AuditEvent("USER_MASKING_RULE_ASSIGNED", userId, columnRule.objectId(), "SUCCESS", null, null,
        "columnId=" + columnId + ",decision=" + normalizedDecision));
  }

  @Transactional
  public void removeUserRule(long userId, long columnId) {
    if (mapper.deleteUserRule(userId, columnId) == 0) {
      throw new AppException("해제할 사용자별 컬럼 마스킹 규칙을 찾을 수 없습니다.");
    }
    auditService.record(new AuditEvent("USER_MASKING_RULE_REMOVED", userId, null, "SUCCESS", null, null,
        "columnId=" + columnId));
  }

  private MaskingRule requireEnabledRule(long ruleId) {
    MaskingRule rule = mapper.findRuleById(ruleId);
    if (rule == null || !rule.enabled()) {
      throw new AppException("활성 컬럼 마스킹 규칙을 선택하세요.");
    }
    return rule;
  }

  private String normalizeCode(String value) {
    String normalized = normalizeRequired(value, 64, "규칙 코드").toUpperCase(Locale.ROOT);
    if (!RULE_CODE.matcher(normalized).matches()) {
      throw new AppException("규칙 코드는 영문 대문자·숫자·밑줄로 3~64자여야 합니다.");
    }
    return normalized;
  }

  private String normalizeTemplate(String value) {
    try {
      return MaskingTemplate.from(normalizeRequired(value, 30, "마스킹 템플릿")).code();
    } catch (IllegalArgumentException exception) {
      throw new AppException(exception.getMessage());
    }
  }

  private String normalizeDecision(String value) {
    String normalized = normalizeRequired(value, 10, "사용자별 적용 방식").toUpperCase(Locale.ROOT);
    if (!DECISIONS.contains(normalized)) {
      throw new AppException("사용자별 적용 방식은 MASK 또는 UNMASK만 가능합니다.");
    }
    return normalized;
  }

  private String normalizeRequired(String value, int maxLength, String label) {
    String normalized = value == null ? "" : value.trim();
    if (normalized.isEmpty()) {
      throw new AppException(label + "은(는) 필수입니다.");
    }
    if (normalized.length() > maxLength) {
      throw new AppException(label + "은(는) " + maxLength + "자 이내여야 합니다.");
    }
    return normalized;
  }

  private String normalizeOptional(String value, int maxLength, String label) {
    String normalized = value == null ? "" : value.trim();
    if (normalized.length() > maxLength) {
      throw new AppException(label + "은(는) " + maxLength + "자 이내여야 합니다.");
    }
    return normalized.isEmpty() ? null : normalized;
  }
}
