package com.cameleer.server.app.security;

import com.cameleer.server.app.dto.AuthTokenResponse;
import com.cameleer.server.app.dto.ErrorResponse;
import com.cameleer.server.app.dto.OidcPublicConfigResponse;
import com.cameleer.server.app.license.LicenseEnforcer;
import com.cameleer.server.core.admin.AuditCategory;
import com.cameleer.server.core.admin.AuditResult;
import com.cameleer.server.core.admin.AuditService;
import com.cameleer.server.core.rbac.ClaimMappingRepository;
import com.cameleer.server.core.rbac.ClaimMappingRule;
import com.cameleer.server.core.rbac.ClaimMappingService;
import com.cameleer.server.core.rbac.GroupRepository;
import com.cameleer.server.core.rbac.RbacService;
import com.cameleer.server.core.rbac.SystemRole;
import com.cameleer.server.core.security.JwtService;
import com.cameleer.server.core.security.OidcConfig;
import com.cameleer.server.core.security.OidcConfigRepository;
import com.cameleer.server.core.security.UserInfo;
import com.cameleer.server.core.security.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

/**
 * OIDC authentication endpoints for the UI.
 * <p>
 * Always registered -- returns 404 when OIDC is not configured or disabled.
 * Configuration is read from the database (managed via admin UI).
 */
@RestController
@RequestMapping("/api/v1/auth/oidc")
@Tag(name = "Authentication", description = "Login and token refresh endpoints")
public class OidcAuthController {

    private static final Logger log = LoggerFactory.getLogger(OidcAuthController.class);

    private final OidcTokenExchanger tokenExchanger;
    private final OidcConfigRepository configRepository;
    private final JwtService jwtService;
    private final UserRepository userRepository;
    private final AuditService auditService;
    private final RbacService rbacService;
    private final ClaimMappingService claimMappingService;
    private final ClaimMappingRepository claimMappingRepository;
    private final GroupRepository groupRepository;
    private final LicenseEnforcer licenseEnforcer;

    public OidcAuthController(OidcTokenExchanger tokenExchanger,
                               OidcConfigRepository configRepository,
                               JwtService jwtService,
                               UserRepository userRepository,
                               AuditService auditService,
                               RbacService rbacService,
                               ClaimMappingService claimMappingService,
                               ClaimMappingRepository claimMappingRepository,
                               GroupRepository groupRepository,
                               LicenseEnforcer licenseEnforcer) {
        this.tokenExchanger = tokenExchanger;
        this.configRepository = configRepository;
        this.jwtService = jwtService;
        this.userRepository = userRepository;
        this.auditService = auditService;
        this.rbacService = rbacService;
        this.claimMappingService = claimMappingService;
        this.claimMappingRepository = claimMappingRepository;
        this.groupRepository = groupRepository;
        this.licenseEnforcer = licenseEnforcer;
    }

    /**
     * Returns OIDC configuration for the SPA to initiate the authorization code flow.
     * Returns 404 if OIDC is not configured or disabled.
     */
    @GetMapping("/config")
    @Operation(summary = "Get OIDC config for SPA login flow")
    @ApiResponse(responseCode = "200", description = "OIDC configuration")
    @ApiResponse(responseCode = "404", description = "OIDC not configured or disabled")
    @ApiResponse(responseCode = "500", description = "Failed to retrieve OIDC provider metadata",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    public ResponseEntity<OidcPublicConfigResponse> getConfig() {
        Optional<OidcConfig> config = configRepository.find();
        if (config.isEmpty() || !config.get().enabled()) {
            return ResponseEntity.notFound().build();
        }

        try {
            OidcConfig oidc = config.get();
            String endSessionEndpoint = tokenExchanger.getEndSessionEndpoint();
            return ResponseEntity.ok(new OidcPublicConfigResponse(
                    oidc.issuerUri(),
                    oidc.clientId(),
                    tokenExchanger.getAuthorizationEndpoint(),
                    endSessionEndpoint,
                    oidc.audience() != null && !oidc.audience().isBlank() ? oidc.audience() : null,
                    oidc.additionalScopes() != null && !oidc.additionalScopes().isEmpty() ? oidc.additionalScopes() : null
            ));
        } catch (Exception e) {
            log.error("Failed to retrieve OIDC provider metadata: {}", e.getMessage());
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
                    "Failed to retrieve OIDC provider metadata");
        }
    }

    /**
     * Exchanges an OIDC authorization code for internal Cameleer JWTs.
     */
    @PostMapping("/callback")
    @Operation(summary = "Exchange OIDC authorization code for JWTs")
    @ApiResponse(responseCode = "200", description = "Authentication successful")
    @ApiResponse(responseCode = "401", description = "OIDC authentication failed",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    @ApiResponse(responseCode = "403", description = "Account not provisioned",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    @ApiResponse(responseCode = "404", description = "OIDC not configured or disabled")
    public ResponseEntity<AuthTokenResponse> callback(@RequestBody CallbackRequest request,
                                                          HttpServletRequest httpRequest) {
        Optional<OidcConfig> config = configRepository.find();
        if (config.isEmpty() || !config.get().enabled()) {
            return ResponseEntity.notFound().build();
        }

        try {
            OidcTokenExchanger.OidcUserInfo oidcUser =
                    tokenExchanger.exchange(request.code(), request.redirectUri());

            // DB key is unprefixed (matches alert_rules.created_by FK shape used by the
            // env-scoped read-path controllers). JWT subject keeps the "user:" namespace
            // so JwtAuthenticationFilter can still distinguish user vs agent tokens.
            String userId  = "oidc:" + oidcUser.subject();
            String subject = "user:" + userId;
            String issuerHost = URI.create(config.get().issuerUri()).getHost();
            String provider = "oidc:" + issuerHost;

            Optional<UserInfo> existingUser = userRepository.findById(userId);
            if (!config.get().autoSignup() && existingUser.isEmpty()) {
                throw new ResponseStatusException(HttpStatus.FORBIDDEN,
                        "Account not provisioned. Contact your administrator.");
            }

            // Auto-signup branch: when the user does not yet exist and the IdP is allowed to
            // provision new accounts, enforce the max_users license cap before persisting.
            // The global LicenseExceptionAdvice maps this to a structured 403 envelope.
            if (existingUser.isEmpty() && config.get().autoSignup()) {
                licenseEnforcer.assertWithinCap("max_users", userRepository.count(), 1);
            }

            userRepository.upsert(new UserInfo(
                    userId, provider, oidcUser.email(), oidcUser.name(), Instant.now()));

            applyClaimMappings(userId, oidcUser.allClaims(), oidcUser.roles(), config.get());

            List<String> roles = rbacService.getSystemRoleNames(userId);

            String accessToken = jwtService.createAccessToken(subject, "user", roles);
            String refreshToken = jwtService.createRefreshToken(subject, "user", roles);

            String displayName = oidcUser.name() != null && !oidcUser.name().isBlank()
                    ? oidcUser.name() : oidcUser.email();
            auditService.log(userId, "login_oidc", AuditCategory.AUTH, null,
                    Map.of("provider", config.get().issuerUri()), AuditResult.SUCCESS, httpRequest);
            return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, oidcUser.idToken()));
        } catch (ResponseStatusException e) {
            throw e;
        } catch (Exception e) {
            log.error("OIDC callback failed: {}", e.getMessage(), e);
            auditService.log("unknown", "login_oidc", AuditCategory.AUTH, null,
                    Map.of("reason", e.getMessage() != null ? e.getMessage() : "unknown"),
                    AuditResult.FAILURE, httpRequest);
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
                    "OIDC authentication failed: " + e.getMessage());
        }
    }

    private void applyClaimMappings(String userId, Map<String, Object> claims,
                                    List<String> oidcExtractedRoles, OidcConfig oidcConfig) {
        List<ClaimMappingRule> rules = claimMappingRepository.findAll();

        rbacService.clearManagedAssignments(userId);

        List<ClaimMappingService.MappingResult> results = List.of();
        if (!rules.isEmpty()) {
            results = claimMappingService.evaluate(rules, claims);
            for (var result : results) {
                ClaimMappingRule rule = result.rule();
                switch (rule.action()) {
                    case "assignRole" -> {
                        UUID roleId = SystemRole.BY_NAME.get(SystemRole.normalizeScope(rule.target()));
                        if (roleId == null) {
                            log.warn("Claim mapping target role '{}' not found, skipping", rule.target());
                            continue;
                        }
                        rbacService.assignManagedRole(userId, roleId, rule.id());
                        log.debug("Managed role {} assigned to {} via mapping {}", rule.target(), userId, rule.id());
                    }
                    case "addToGroup" -> {
                        var groups = groupRepository.findAll();
                        var group = groups.stream().filter(g -> g.name().equalsIgnoreCase(rule.target())).findFirst();
                        if (group.isEmpty()) {
                            log.warn("Claim mapping target group '{}' not found, skipping", rule.target());
                            continue;
                        }
                        rbacService.addUserToManagedGroup(userId, group.get().id(), rule.id());
                        log.debug("Managed group {} assigned to {} via mapping {}", rule.target(), userId, rule.id());
                    }
                }
            }
        }

        // Fallback priority: claim mapping rules > OIDC token roles > defaultRoles
        if (results.isEmpty()) {
            // Use roles extracted directly from the OIDC token (e.g. Custom JWT 'roles' claim)
            if (oidcExtractedRoles != null && !oidcExtractedRoles.isEmpty()) {
                for (String roleName : oidcExtractedRoles) {
                    UUID roleId = SystemRole.BY_NAME.get(SystemRole.normalizeScope(roleName));
                    if (roleId != null) {
                        rbacService.assignManagedRole(userId, roleId, null);
                        log.info("OIDC role {} assigned to {} (from token claim)", roleName, userId);
                    }
                }
            } else {
                List<String> defaultRoles = oidcConfig.defaultRoles();
                if (defaultRoles != null && !defaultRoles.isEmpty()) {
                    for (String roleName : defaultRoles) {
                        UUID roleId = SystemRole.BY_NAME.get(SystemRole.normalizeScope(roleName));
                        if (roleId != null) {
                            rbacService.assignManagedRole(userId, roleId, null);
                            log.debug("Default role {} assigned to {} (no claim mapping or OIDC roles)", roleName, userId);
                        }
                    }
                }
            }
        }
    }

    public record CallbackRequest(String code, String redirectUri) {}
}
