package com.cloudhandson.ddsbackoffice.service;

import com.cloudhandson.ddsbackoffice.config.DdsProperties;
import com.cloudhandson.ddsbackoffice.domain.DdsGrantPlan;
import com.cloudhandson.ddsbackoffice.domain.DdsProvisioningPlan;
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.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * Compiles the application's effective permission model into DDS grants.
 *
 * This service is the explicit publish boundary for the direct END USER
 * comparison path: application users/groups/roles are expanded, ALLOW/DENY
 * rules are compiled into one predicate per user/object, and the resulting
 * DATA GRANT is created or replaced. A publish with no ALLOW rule drops the
 * reserved grant, preserving default deny.
 *
 * The token-driven vector path is intentionally separate. Its object-level
 * DATA GRANT calls a definer-rights predicate function that evaluates the
 * common CB_* tables at query time after CB_AGENT_CTX has been initialized by
 * the bearer token.
 */
@Service
public class DdsGrantPublisher implements DdsProvisionPlanProvider {

  private static final Pattern IDENTIFIER = Pattern.compile("[A-Z][A-Z0-9_$#]*");
  private static final Pattern OBJECT = Pattern.compile(
      "[A-Z][A-Z0-9_$#]*(\\.[A-Z][A-Z0-9_$#]*)*"
  );

  private final JdbcTemplate jdbcTemplate;
  private final DdsProperties properties;

  public DdsGrantPublisher(JdbcTemplate jdbcTemplate, DdsProperties properties) {
    this.jdbcTemplate = jdbcTemplate;
    this.properties = properties;
  }

  public DdsProvisioningPlan preview() {
    List<DdsGrantPlan> grants = new ArrayList<>();
    List<String> warnings = new ArrayList<>();
    int mappedUsers = 0;

    for (Map.Entry<String, DdsProperties.User> entry : properties.users().entrySet()) {
      String userKey = entry.getKey();
      DdsProperties.User ddsUser = entry.getValue();
      if (!ddsUser.mapped()) {
        // The demo's "none" identity is deliberately a DDS-only default-deny
        // subject. It has no application user mapping by design, so do not
        // turn that expected state into a noisy warning.
        if (ddsUser.applicationUserId() == 0L && "none".equals(userKey)) {
          continue;
        }
        warnings.add(userKey + ": 애플리케이션 사용자 ID 또는 DDS DATA ROLE 매핑이 없습니다.");
        continue;
      }
      AppIdentity identity = findIdentity(ddsUser.applicationUserId());
      if (identity == null) {
        warnings.add(userKey + ": CB_APP_USER " + ddsUser.applicationUserId() + "를 찾을 수 없습니다.");
        continue;
      }
      if (!identity.active()) {
        warnings.add(userKey + ": 애플리케이션 사용자가 비활성 상태입니다.");
      }
      mappedUsers++;

      Map<Long, PermissionEntry> permissions = findEffectivePermissions(identity.userId());
      // Include every approved mapping, not only objects that still have a
      // permission row. This lets an explicit publish revoke a previously
      // generated grant when an application permission is removed; the DDS
      // side otherwise has no way to observe that deletion.
      Set<String> targets = new LinkedHashSet<>(properties.objectMappings().keySet());
      permissions.values().forEach(permission -> targets.add(permission.targetName()));
      for (String target : targets) {
        String normalizedTarget = target.toUpperCase(Locale.ROOT);
        String ddsObject = properties.objectMappings().get(normalizedTarget);
        String grantName = grantName(userKey, normalizedTarget);
        if (ddsObject == null || ddsObject.isBlank()) {
          warnings.add(userKey + ": " + normalizedTarget + "에 대한 DDS 보호 객체 매핑이 없습니다.");
          grants.add(unpublishable(userKey, ddsUser, normalizedTarget, grantName,
              "DDS object mapping 필요"));
          continue;
        }
        if (!OBJECT.matcher(ddsObject).matches()) {
          warnings.add(userKey + ": 안전하지 않은 DDS 객체 매핑을 건너뛰었습니다: " + ddsObject);
          grants.add(unpublishable(userKey, ddsUser, normalizedTarget, grantName,
              "DDS object 이름 검증 실패"));
          continue;
        }
        ObjectInfo object = findProtectedObject(normalizedTarget);
        if (object == null) {
          warnings.add(userKey + ": 보호 객체 메타데이터가 없습니다: " + normalizedTarget);
          grants.add(unpublishable(userKey, ddsUser, normalizedTarget, grantName,
              "CB_PROTECTED_OBJECT 등록 필요"));
          continue;
        }
        if (!ddsObjectExists(ddsObject)) {
          warnings.add(userKey + ": DDS 보호 객체가 DB에 없습니다: " + ddsObject);
          grants.add(unpublishable(userKey, ddsUser, normalizedTarget, grantName,
              "DDS VIEW/TABLE 생성 필요"));
          continue;
        }

        List<PermissionEntry> targetPermissions = permissions.values().stream()
            .filter(permission -> normalizedTarget.equals(permission.targetName()))
            .toList();
        PredicateBuild predicate = buildPredicate(targetPermissions, identity, object);
        String excludedColumns = excludedColumns(normalizedTarget, targetPermissions, object.objectId());
        if (predicate.allow().isBlank()) {
          grants.add(new DdsGrantPlan(
              userKey, valueOrDefault(ddsUser.label(), userKey), identity.userId(),
              ddsUser.dataRole(), normalizedTarget, ddsObject, grantName,
              "1 = 0", excludedColumns, "", false,
              "ALLOW 규칙이 없어 기존 DDS Grant를 회수합니다.")
          );
          continue;
        }

        String where = predicate.expression();
        String sql = createGrantSql(grantName, ddsObject, where, excludedColumns, ddsUser.dataRole());
        grants.add(new DdsGrantPlan(
            userKey, valueOrDefault(ddsUser.label(), userKey), identity.userId(),
            ddsUser.dataRole(), normalizedTarget, ddsObject, grantName,
            where, excludedColumns, sql, true,
            "애플리케이션 유효 권한을 DDS DATA GRANT로 게시할 수 있습니다.")
        );
        warnings.addAll(predicate.warnings());
      }
    }
    return new DdsProvisioningPlan(grants, warnings, mappedUsers);
  }

  public DdsProvisioningPlan publish() {
    DdsProvisioningPlan plan = preview();
    for (DdsGrantPlan grant : plan.grants()) {
      // Never touch a grant for an application object that has no approved
      // DDS object mapping. An operator may still manage that DDS object by a
      // separate process; the preview warning must not become an accidental
      // revoke. Known mappings are safe to replace or revoke.
      if (grant.ddsObject() == null || grant.ddsObject().isBlank()) {
        continue;
      }
      dropGrant(grant.grantName());
      if (grant.publishable()) {
        jdbcTemplate.execute(grant.sql());
      }
    }
    return plan;
  }

  private Map<Long, PermissionEntry> findEffectivePermissions(long userId) {
    String sql = """
        WITH effective_role AS (
          SELECT ur.role_id
          FROM cb_user_role ur
          WHERE ur.user_id = ?
          UNION
          SELECT gr.role_id
          FROM cb_user_group ug
          JOIN cb_app_group g ON g.group_id = ug.group_id AND g.active_yn = 'Y'
          JOIN cb_group_role gr ON gr.group_id = ug.group_id
          WHERE ug.user_id = ?
        )
        SELECT p.perm_id, p.target_name, p.permission_effect,
               r.rule_id, r.rule_column, r.rule_type, r.rule_value
        FROM effective_role er
        JOIN cb_permission p ON p.role_id = er.role_id
        LEFT JOIN cb_permission_rule r ON r.perm_id = p.perm_id
        WHERE p.action_name = 'SELECT'
        ORDER BY p.perm_id, r.rule_id
        """;
    Map<Long, PermissionEntry> result = new LinkedHashMap<>();
    List<RawPermissionRow> rows = jdbcTemplate.query(sql, (row, ignored) -> new RawPermissionRow(
        row.getLong("perm_id"),
        row.getString("target_name"),
        row.getString("permission_effect"),
        row.getString("rule_column"),
        row.getString("rule_type"),
        row.getString("rule_value")
    ), userId, userId);
    for (RawPermissionRow row : rows) {
      long permissionId = row.permissionId();
      PermissionEntry permission = result.computeIfAbsent(permissionId, ignored ->
          new PermissionEntry(
              permissionId,
              upper(row.targetName()),
              normalizeEffect(row.permissionEffect()),
              new ArrayList<>(),
              findVisibleColumns(permissionId)
          ));
      if (row.ruleType() != null && !row.ruleType().isBlank()) {
        permission.rules().add(new Rule(
            upperNullable(row.ruleColumn()),
            upper(row.ruleType()),
            row.ruleValue()
        ));
      }
    }
    return result;
  }

  private List<String> findVisibleColumns(long permissionId) {
    return jdbcTemplate.query(
        "SELECT column_name FROM cb_permission_column WHERE permission_id = ? ORDER BY column_name",
        (row, ignored) -> upper(row.getString("column_name")), permissionId);
  }

  private AppIdentity findIdentity(long userId) {
    List<AppIdentity> rows = jdbcTemplate.query(
        "SELECT user_id, employee_no, dept_code, active FROM cb_app_user WHERE user_id = ?",
        (row, ignored) -> new AppIdentity(
            row.getLong("user_id"),
            row.getString("employee_no"),
            row.getString("dept_code"),
            "Y".equalsIgnoreCase(row.getString("active"))
        ), userId);
    return rows.isEmpty() ? null : rows.get(0);
  }

  private ObjectInfo findProtectedObject(String targetName) {
    List<ObjectInfo> rows = jdbcTemplate.query(
        "SELECT object_id, owner, object_name FROM cb_protected_object WHERE object_name = UPPER(?)",
        (row, ignored) -> new ObjectInfo(
            row.getLong("object_id"), upper(row.getString("owner")), upper(row.getString("object_name"))),
        targetName);
    return rows.isEmpty() ? null : rows.get(0);
  }

  private List<String> sensitiveColumns(long objectId) {
    return jdbcTemplate.query(
        "SELECT column_name FROM cb_protected_column WHERE object_id = ? AND sensitive_yn = 'Y' ORDER BY column_id",
        (row, ignored) -> upper(row.getString("column_name")), objectId);
  }

  private boolean ddsObjectExists(String objectName) {
    String[] parts = objectName.split("\\.", 2);
    if (parts.length != 2) {
      return false;
    }
    Integer count = jdbcTemplate.queryForObject(
        "SELECT COUNT(*) FROM all_objects WHERE owner = ? AND object_name = ? AND object_type IN ('TABLE','VIEW')",
        Integer.class, parts[0], parts[1]);
    return count != null && count > 0;
  }

  private PredicateBuild buildPredicate(
      List<PermissionEntry> permissions,
      AppIdentity identity,
      ObjectInfo object
  ) {
    List<String> allow = new ArrayList<>();
    List<String> deny = new ArrayList<>();
    List<String> warnings = new ArrayList<>();
    for (PermissionEntry permission : permissions) {
      List<String> clauses = new ArrayList<>();
      for (Rule rule : permission.rules()) {
        String clause = compileRule(rule, identity, object);
        if (clause == null || clause.isBlank()) {
          warnings.add(permission.targetName() + ": 지원하지 않거나 컬럼을 확인할 수 없는 규칙을 건너뛰었습니다: "
              + rule.ruleType());
          continue;
        }
        clauses.add(clause);
      }
      if (clauses.isEmpty()) {
        continue;
      }
      String expression = clauses.size() == 1 ? clauses.get(0) : "(" + String.join(" OR ", clauses) + ")";
      if ("DENY".equals(permission.effect())) {
        deny.add(expression);
      } else {
        allow.add(expression);
      }
    }
    if (allow.isEmpty()) {
      return new PredicateBuild("", "", warnings);
    }
    String allowExpression = allow.size() == 1 ? allow.get(0) : "(" + String.join(" OR ", allow) + ")";
    if (deny.isEmpty()) {
      return new PredicateBuild(allowExpression, allowExpression, warnings);
    }
    String denyExpression = deny.size() == 1 ? deny.get(0) : "(" + String.join(" OR ", deny) + ")";
    return new PredicateBuild(allowExpression, "(" + allowExpression + ") AND NOT (" + denyExpression + ")", warnings);
  }

  private String compileRule(Rule rule, AppIdentity identity, ObjectInfo object) {
    String type = rule.ruleType();
    if ("ALL".equals(type)) {
      return "1 = 1";
    }
    String column = rule.ruleColumn();
    if (column == null || column.isBlank()) {
      column = switch (type) {
        case "MY_DEPT", "DEPT" -> "DEPT_CODE";
        case "SELF", "EMP_NO" -> "OWNER_EMP_NO";
        case "TAG" -> "TECH_TAG";
        default -> null;
      };
    }
    if (column == null || !IDENTIFIER.matcher(column).matches() || !hasColumn(object, column)) {
      return null;
    }
    return switch (type) {
      case "MY_DEPT" -> identity.deptCode() == null ? null : column + " = " + quote(identity.deptCode());
      case "SELF" -> identity.employeeNo() == null ? null : column + " = " + quote(identity.employeeNo());
      case "DEPT", "EMP_NO", "=" -> rule.ruleValue() == null ? null
          : "TO_CHAR(" + column + ") = " + quote(rule.ruleValue());
      case "!=", "<>" -> rule.ruleValue() == null ? null
          : "TO_CHAR(" + column + ") <> " + quote(rule.ruleValue());
      case "TAG" -> rule.ruleValue() == null || !rule.ruleValue().trim().matches("[A-Za-z0-9_-]+") ? null
          : "REGEXP_LIKE(UPPER(" + column + "), "
              + quote("(^|,)" + rule.ruleValue().trim().toUpperCase(Locale.ROOT) + "(,|$)") + ")";
      default -> null;
    };
  }

  private boolean hasColumn(ObjectInfo object, String column) {
    Integer count = jdbcTemplate.queryForObject(
        "SELECT COUNT(*) FROM all_tab_columns WHERE owner = ? AND table_name = ? AND column_name = ?",
        Integer.class, object.owner(), object.objectName(), column);
    return count != null && count > 0;
  }

  private String excludedColumns(
      String applicationObject,
      List<PermissionEntry> permissions,
      long objectId
  ) {
    Set<String> visible = new LinkedHashSet<>();
    permissions.stream()
        .filter(permission -> "ALLOW".equals(permission.effect()))
        .forEach(permission -> visible.addAll(permission.visibleColumns()));
    return sensitiveColumns(objectId).stream()
        // VECTOR_DISTANCE needs the VECTOR column while DDS evaluates the
        // search. The service deliberately omits EMBEDDING from its response,
        // but excluding a VECTOR column from DATA GRANT makes the predicate
        // invalid at runtime (ORA-52561). Keep it available to the engine and
        // enforce response shaping in DdsVectorKnowledgeService instead.
        .filter(column -> !("CB_VECTOR_SEARCH_DOCUMENTS".equals(applicationObject)
            && "EMBEDDING".equals(column)))
        .filter(column -> !visible.contains(column))
        .reduce((left, right) -> left + ", " + right)
        .orElse("");
  }

  private String createGrantSql(
      String grantName,
      String ddsObject,
      String predicate,
      String excludedColumns,
      String dataRole
  ) {
    String select = excludedColumns == null || excludedColumns.isBlank()
        ? "AS SELECT"
        : "AS SELECT (ALL COLUMNS EXCEPT " + excludedColumns + ")";
    return "CREATE OR REPLACE DATA GRANT ADMIN." + grantName
        + " " + select
        + " ON " + ddsObject
        + " WHERE " + predicate
        + " TO " + safeRole(dataRole);
  }

  private void dropGrant(String grantName) {
    try {
      jdbcTemplate.execute("DROP DATA GRANT ADMIN." + grantName);
    } catch (DataAccessException ignored) {
      // The first publish has no generated grant yet. DDS returns an object
      // not found error, which is intentionally harmless here.
    }
  }

  private DdsGrantPlan unpublishable(
      String userKey,
      DdsProperties.User user,
      String target,
      String grantName,
      String note
  ) {
    return new DdsGrantPlan(
        userKey, valueOrDefault(user.label(), userKey), user.applicationUserId(), user.dataRole(),
        target, properties.objectMappings().getOrDefault(target, ""), grantName,
        "", "", "", false, note);
  }

  private String grantName(String userKey, String target) {
    if ("CB_VECTOR_SEARCH_DOCUMENTS".equals(target)) {
      return "DDS_DEMO_" + userKey.toUpperCase(Locale.ROOT) + "_VECTOR_GRANT";
    }
    String candidate = "DDS_" + userKey.toUpperCase(Locale.ROOT) + "_" + target + "_GRANT";
    return candidate.length() <= 120 ? candidate : candidate.substring(0, 120);
  }

  private String safeRole(String value) {
    String normalized = upper(value);
    if (normalized == null || !OBJECT.matcher(normalized).matches()) {
      throw new IllegalArgumentException("안전하지 않은 DDS DATA ROLE 이름입니다: " + value);
    }
    return normalized;
  }

  private String quote(String value) {
    return "'" + value.replace("'", "''") + "'";
  }

  private String normalizeEffect(String value) {
    return "DENY".equalsIgnoreCase(value) ? "DENY" : "ALLOW";
  }

  private String upper(String value) {
    return value == null ? null : value.trim().toUpperCase(Locale.ROOT);
  }

  private String upperNullable(String value) {
    return value == null || value.isBlank() ? null : upper(value);
  }

  private String valueOrDefault(String value, String fallback) {
    return value == null || value.isBlank() ? fallback : value;
  }

  private record AppIdentity(long userId, String employeeNo, String deptCode, boolean active) {
  }

  private record ObjectInfo(long objectId, String owner, String objectName) {
  }

  private record Rule(String ruleColumn, String ruleType, String ruleValue) {
  }

  private record RawPermissionRow(
      long permissionId,
      String targetName,
      String permissionEffect,
      String ruleColumn,
      String ruleType,
      String ruleValue
  ) {
  }

  private record PermissionEntry(
      long permissionId,
      String targetName,
      String effect,
      List<Rule> rules,
      List<String> visibleColumns
  ) {
  }

  private record PredicateBuild(String allow, String expression, List<String> warnings) {
  }
}
