package io.cameleer.server.app.security;

import io.cameleer.server.app.license.LicenseEnforcer;
import io.cameleer.server.core.security.OidcConfig;
import io.cameleer.server.core.security.OidcConfigRepository;
import io.cameleer.server.core.security.UserInfo;
import io.cameleer.server.core.security.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.Optional;

/**
 * Provisions OIDC users into the {@code users} table on first contact through the
 * resource-server (Logto/external IdP) flow. Mirrors the upsert path in
 * {@link OidcAuthController} but runs from {@link JwtAuthenticationFilter} where only
 * a parsed {@link Jwt} is available -- there is no token-exchange round-trip and no
 * {@code OidcUserInfo} record.
 *
 * <p>Returns {@link Optional#empty()} (rather than throwing) when the token represents
 * a user who must not be auto-provisioned, so the filter can fall through to anonymous
 * and Spring returns 401 on protected endpoints rather than a 5xx FK violation later.
 *
 * <p>OIDC config row may be absent when OIDC is configured purely via env var
 * ({@code CAMELEER_SERVER_SECURITY_OIDCISSUERURI}). In that case autoSignup defaults to
 * true, since accepting external tokens at boot already implies admin opt-in.
 */
@Component
public class OidcAccountSyncService {

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

    private final UserRepository userRepository;
    private final OidcConfigRepository oidcConfigRepository;
    private final LicenseEnforcer licenseEnforcer;

    public OidcAccountSyncService(UserRepository userRepository,
                                  OidcConfigRepository oidcConfigRepository,
                                  LicenseEnforcer licenseEnforcer) {
        this.userRepository = userRepository;
        this.oidcConfigRepository = oidcConfigRepository;
        this.licenseEnforcer = licenseEnforcer;
    }

    /**
     * Ensures a {@code users} row exists for the OIDC subject in {@code jwt}.
     * Returns the bare {@code users.user_id} (e.g. {@code "oidc:r6uzeamz0wuy"}) on
     * success, or empty if provisioning was refused.
     */
    public Optional<String> ensureProvisioned(Jwt jwt) {
        String userId = "oidc:" + jwt.getSubject();
        if (userRepository.findById(userId).isPresent()) {
            return Optional.of(userId);
        }

        boolean autoSignup = oidcConfigRepository.find()
                .map(OidcConfig::autoSignup)
                .orElse(true);
        if (!autoSignup) {
            log.warn("OIDC user {} not provisioned and autoSignup is disabled; rejecting token.", userId);
            return Optional.empty();
        }

        try {
            licenseEnforcer.assertWithinCap("max_users", userRepository.count(), 1);
        } catch (RuntimeException e) {
            log.warn("OIDC user {} not provisioned: {}.", userId, e.getMessage());
            return Optional.empty();
        }

        String issuerHost = jwt.getIssuer() != null ? jwt.getIssuer().getHost() : "unknown";
        String email = jwt.getClaimAsString("email");
        String name = firstNonBlank(
                jwt.getClaimAsString("name"),
                jwt.getClaimAsString("preferred_username"),
                email);
        userRepository.upsert(new UserInfo(
                userId,
                "oidc:" + issuerHost,
                email != null ? email : "",
                name != null ? name : "",
                Instant.now()));
        log.info("Provisioned OIDC user via resource-server flow: userId={}, issuer={}", userId, issuerHost);
        return Optional.of(userId);
    }

    private static String firstNonBlank(String... values) {
        for (String v : values) {
            if (v != null && !v.isBlank()) return v;
        }
        return "";
    }
}
