package cloud.nstart.terravault.ordinis.auth;

import cloud.nstart.terravault.ordinis.domain.DataScope;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;

/**
 * Резолвер scope для текущего запроса.
 *
 * <p>Источник scope (приоритет сверху-вниз):
 * <ol>
 *   <li>JWT roles claim (для Keycloak — {@code realm_access.roles}, путь
 *       настраивается через {@link OrdinisAuthProperties#rolesClaim()}).
 *       Роли мапятся в {@link DataScope} по имени:
 *       <ul>
 *         <li>{@code ordinis-public} / {@code public} → {@link DataScope#PUBLIC}</li>
 *         <li>{@code ordinis-internal} / {@code internal} → {@link DataScope#INTERNAL}</li>
 *         <li>{@code ordinis-restricted} / {@code restricted} → {@link DataScope#RESTRICTED}</li>
 *       </ul>
 *       Любая роль с суффиксом {@code -PUBLIC|INTERNAL|RESTRICTED} тоже работает.</li>
 *   <li>Legacy query param {@code as_scope} (если {@code allowQueryScope=true}).</li>
 *   <li>Anonymous → {@link DataScope#PUBLIC}.</li>
 * </ol>
 *
 * <p>JWT всегда побеждает query param — защита от scope escalation через URL.
 */
@Component
public class ScopeContext {

  private final OrdinisAuthProperties props;

  public ScopeContext(OrdinisAuthProperties props) {
    this.props = props;
  }

  public Set<DataScope> currentScopes() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth instanceof JwtAuthenticationToken jwtAuth) {
      return scopesFromJwt(jwtAuth.getToken());
    }
    return EnumSet.of(DataScope.PUBLIC);
  }

  /**
   * True если пользовательский scope видит данные {@code dataScope}.
   * Использует {@link DataScope#visibleTo(DataScope)}: PUBLIC видит PUBLIC,
   * INTERNAL видит PUBLIC+INTERNAL, RESTRICTED видит всё.
   */
  public boolean canAccess(DataScope dataScope) {
    if (dataScope == null) return true;
    for (DataScope userScope : currentScopes()) {
      if (dataScope.visibleTo(userScope)) return true;
    }
    return false;
  }

  public Set<DataScope> resolveForRead(String queryAsScope) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth instanceof JwtAuthenticationToken jwtAuth) {
      return scopesFromJwt(jwtAuth.getToken());
    }
    if (props.allowQueryScope() && queryAsScope != null && !queryAsScope.isBlank()) {
      return fromQueryParam(queryAsScope);
    }
    return EnumSet.of(DataScope.PUBLIC);
  }

  public static Set<DataScope> fromQueryParam(String csv) {
    if (csv == null || csv.isBlank()) return EnumSet.of(DataScope.PUBLIC);
    EnumSet<DataScope> result = EnumSet.noneOf(DataScope.class);
    for (String part : csv.split(",")) {
      addScope(result, part);
    }
    return result.isEmpty() ? EnumSet.of(DataScope.PUBLIC) : result;
  }

  private Set<DataScope> scopesFromJwt(Jwt token) {
    Object raw = readClaimByPath(token, props.rolesClaim());
    if (raw == null) return EnumSet.of(DataScope.PUBLIC);
    EnumSet<DataScope> result = EnumSet.noneOf(DataScope.class);
    if (raw instanceof Collection<?> col) {
      for (Object o : col) addScope(result, normalizeRoleToScope(String.valueOf(o)));
    } else {
      for (String part : String.valueOf(raw).split("[,\\s]+")) {
        addScope(result, normalizeRoleToScope(part));
      }
    }
    return result.isEmpty() ? EnumSet.of(DataScope.PUBLIC) : result;
  }

  /** Поддерживает nested путь "realm_access.roles" или просто "scopes". */
  private static Object readClaimByPath(Jwt token, String path) {
    String[] parts = path.split("\\.");
    Object current = token.getClaim(parts[0]);
    for (int i = 1; i < parts.length && current != null; i++) {
      if (current instanceof Map<?, ?> m) {
        current = m.get(parts[i]);
      } else {
        return null;
      }
    }
    return current;
  }

  /**
   * Маппинг имени роли в scope. Поддерживает:
   *   "ordinis-public" / "public" → PUBLIC
   *   "ORDINIS_INTERNAL" → INTERNAL
   *   "ordinis:client:public" → PUBLIC (Альтум-style colon-separated)
   *   суффикс после "-", "_" или ":": "x:RESTRICTED" → RESTRICTED
   *   просто "PUBLIC"|"INTERNAL"|"RESTRICTED"
   */
  private static String normalizeRoleToScope(String role) {
    if (role == null) return null;
    String s = role.trim().toUpperCase()
        .replace("ORDINIS-", "")
        .replace("ORDINIS_", "")
        .replace("ORDINIS:", "");
    int sep = Math.max(Math.max(s.lastIndexOf('-'), s.lastIndexOf('_')), s.lastIndexOf(':'));
    if (sep >= 0) s = s.substring(sep + 1);
    return s;
  }

  private static void addScope(EnumSet<DataScope> target, String s) {
    if (s == null || s.isBlank()) return;
    try {
      target.add(DataScope.valueOf(s.trim().toUpperCase()));
    } catch (IllegalArgumentException ignored) {
    }
  }
}
