package com.kamco.cd.kamcoback.auth;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.enums.RoleType;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;

/** redis에 등록된 메뉴별 권한 확인 */
@Component
@RequiredArgsConstructor
public class RedisAuthorizationManager
    implements AuthorizationManager<RequestAuthorizationContext> {

  private static final Logger log = LogManager.getLogger(RedisAuthorizationManager.class);
  private static final String REDIS_KEY = "auth:api:role";

  private final StringRedisTemplate redisTemplate;
  private final ObjectMapper objectMapper;

  @Override
  public AuthorizationDecision check(
      Supplier<Authentication> authenticationSupplier, RequestAuthorizationContext context) {

    HttpServletRequest request = context.getRequest();
    String requestPath = normalizePath(request.getRequestURI());

    Authentication authentication = authenticationSupplier.get();
    if (authentication == null || !authentication.isAuthenticated()) {
      return new AuthorizationDecision(false);
    }

    // ADMIN 은 무조건 허용
    if (hasAdmin(authentication)) {
      return new AuthorizationDecision(true);
    }

    // 사용자 role 목록
    List<String> userRoles = extractRoles(authentication);
    if (userRoles.isEmpty()) {
      return new AuthorizationDecision(false);
    }

    // Redis 조회 (1회)
    String json = redisTemplate.opsForValue().get(REDIS_KEY);
    if (json == null || json.isBlank()) {
      return new AuthorizationDecision(false);
    }

    // JSON 파싱 (1회)
    List<MenuDto.MenuWithRolesDto> menus = parseMenus(json);
    if (menus.isEmpty()) {
      return new AuthorizationDecision(false);
    }

    // role + prefix URI 매칭
    for (String role : userRoles) {
      if (isAllowed(menus, role, requestPath)) {
        return new AuthorizationDecision(true);
      }
    }

    return new AuthorizationDecision(false);
  }

  /* =========================
   * 핵심 권한 판별 로직
   * ========================= */
  private boolean isAllowed(List<MenuDto.MenuWithRolesDto> menus, String role, String requestPath) {

    for (MenuDto.MenuWithRolesDto menu : menus) {
      if (menu == null) {
        continue;
      }

      String baseUri = menu.getMenuApiUri();
      if (baseUri == null || baseUri.isBlank()) {
        continue;
      }

      // role 포함 여부
      if (!hasRole(menu.getRoles(), role)) {
        continue;
      }

      // prefix URI 허용
      if (matchUri(baseUri, requestPath)) {
        return true;
      }
    }
    return false;
  }

  /* =========================
   * URI prefix 매칭
   * ========================= */
  private boolean matchUri(String baseUri, String requestPath) {
    String base = normalizePath(baseUri);
    String req = normalizePath(requestPath);

    // /api/log/audit  →  /api/log/audit/**
    return req.equals(base) || req.startsWith(base + "/");
  }

  /* =========================
   * role 문자열 처리
   * ========================= */
  private boolean hasRole(String rolesCsv, String targetRole) {
    if (rolesCsv == null || rolesCsv.isBlank()) {
      return false;
    }
    if (targetRole == null || targetRole.isBlank()) {
      return false;
    }

    for (String r : rolesCsv.split(",")) {
      if (targetRole.equalsIgnoreCase(r.trim())) {
        return true;
      }
    }
    return false;
  }

  /* =========================
   * Redis JSON 파싱
   * ========================= */
  private List<MenuDto.MenuWithRolesDto> parseMenus(String json) {
    try {
      return objectMapper.readValue(json, new TypeReference<List<MenuDto.MenuWithRolesDto>>() {});
    } catch (Exception e) {
      log.warn("Failed to parse redis menu json. key={}", REDIS_KEY, e);
      return Collections.emptyList();
    }
  }

  /* =========================
   * ADMIN 판별
   * ========================= */
  private boolean hasAdmin(Authentication authentication) {
    for (GrantedAuthority ga : authentication.getAuthorities()) {
      if (ga == null || ga.getAuthority() == null) {
        continue;
      }

      String auth = ga.getAuthority();
      String admin = RoleType.ADMIN.getId();

      if (auth.equals(admin) || auth.equals("ROLE_" + admin)) {
        return true;
      }
    }
    return false;
  }

  /* =========================
   * ROLE 목록 추출
   * ========================= */
  private List<String> extractRoles(Authentication authentication) {
    return authentication.getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .filter(a -> a != null && !a.isBlank())
        .map(a -> a.startsWith("ROLE_") ? a.substring(5) : a)
        .toList();
  }

  /* =========================
   * URI 정규화
   * ========================= */
  private String normalizePath(String path) {
    if (path == null || path.isBlank()) {
      return "/";
    }
    return path.replaceAll("//+", "/");
  }
}
