using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace JobTrackerApi.Services;

public sealed record MicrosoftTokenPrincipal(
    string Subject,
    string? Email,
    bool EmailVerified,
    string? GivenName,
    string? FamilyName,
    string? Name,
    string? TenantId = null,
    string? ObjectId = null);

public interface IMicrosoftTokenValidator
{
    Task<MicrosoftTokenPrincipal> ValidateAsync(string idToken, CancellationToken cancellationToken = default);
}

public sealed class MicrosoftTenantPolicy
{
    public const string ConsumerTenantId = "9188040d-6c67-4c5b-b112-36a304b66dad";

    private MicrosoftTenantPolicy(string mode, string discoveryTenant)
    {
        Mode = mode;
        DiscoveryTenant = discoveryTenant;
    }

    public string Mode { get; }
    public string DiscoveryTenant { get; }

    public static MicrosoftTenantPolicy Parse(string? configured, bool production)
    {
        var value = configured?.Trim().ToLowerInvariant();
        if (string.IsNullOrWhiteSpace(value))
        {
            if (production)
                throw new InvalidOperationException("Auth:MicrosoftTenant is required in Production when Microsoft sign-in is enabled.");

            value = "common";
        }

        if (value is "common" or "organizations" or "consumers")
            return new MicrosoftTenantPolicy(value, value);

        if (!Guid.TryParse(value, out var tenantId))
            throw new InvalidOperationException("Auth:MicrosoftTenant must be common, organizations, consumers, or a tenant GUID.");

        var normalized = tenantId.ToString("D");
        return new MicrosoftTenantPolicy(normalized, normalized);
    }

    public string Validate(string issuer, string? tenantClaim)
    {
        if (!Guid.TryParse(tenantClaim, out var tenantId))
            throw new SecurityTokenInvalidIssuerException("Microsoft token is missing a GUID-shaped tid claim.");

        var normalizedTenant = tenantId.ToString("D");
        var expectedIssuer = $"https://login.microsoftonline.com/{normalizedTenant}/v2.0";
        if (!string.Equals(issuer, expectedIssuer, StringComparison.OrdinalIgnoreCase))
            throw new SecurityTokenInvalidIssuerException("Microsoft token issuer does not match its tid claim.");

        var allowed = Mode switch
        {
            "common" => true,
            "organizations" => normalizedTenant != ConsumerTenantId,
            "consumers" => normalizedTenant == ConsumerTenantId,
            _ => normalizedTenant == Mode,
        };
        if (!allowed)
            throw new SecurityTokenInvalidIssuerException("Microsoft token tenant is not allowed by Auth:MicrosoftTenant.");

        return normalizedTenant;
    }
}

public sealed class MicrosoftTokenValidator : IMicrosoftTokenValidator
{
    private readonly IConfiguration _cfg;
    private readonly IConfigurationManager<OpenIdConnectConfiguration> _configManager;
    private readonly MicrosoftTenantPolicy _tenantPolicy;

    public MicrosoftTokenValidator(IConfiguration cfg)
    {
        _cfg = cfg;
        _tenantPolicy = MicrosoftTenantPolicy.Parse(cfg["Auth:MicrosoftTenant"], production: false);
        _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            $"https://login.microsoftonline.com/{_tenantPolicy.DiscoveryTenant}/v2.0/.well-known/openid-configuration",
            new OpenIdConnectConfigurationRetriever());
    }

    public MicrosoftTokenValidator(IConfiguration cfg, IConfigurationManager<OpenIdConnectConfiguration> configManager)
    {
        _cfg = cfg;
        _tenantPolicy = MicrosoftTenantPolicy.Parse(cfg["Auth:MicrosoftTenant"], production: false);
        _configManager = configManager;
    }

    public async Task<MicrosoftTokenPrincipal> ValidateAsync(string idToken, CancellationToken cancellationToken = default)
    {
        var audience = (_cfg["Auth:MicrosoftClientId"] ?? "").Trim();
        if (string.IsNullOrWhiteSpace(audience))
            throw new InvalidOperationException("Microsoft sign-in is not configured.");

        var config = await _configManager.GetConfigurationAsync(cancellationToken);
        var handler = new JwtSecurityTokenHandler { MapInboundClaims = false };
        var principal = handler.ValidateToken(idToken, new TokenValidationParameters
        {
            ValidateIssuer = true,
            IssuerValidator = (issuer, token, _) =>
            {
                var tid = (token as JwtSecurityToken)?.Claims.FirstOrDefault(x => x.Type == "tid")?.Value;
                _tenantPolicy.Validate(issuer, tid);
                return issuer;
            },
            ValidateAudience = true,
            ValidAudience = audience,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = config.SigningKeys,
            ClockSkew = TimeSpan.FromMinutes(2),
        }, out var validatedToken);

        var jwt = validatedToken as JwtSecurityToken
            ?? throw new SecurityTokenException("Microsoft token was not a JWT.");
        var tenantId = _tenantPolicy.Validate(jwt.Issuer, principal.FindFirst("tid")?.Value);
        var objectClaim = principal.FindFirst("oid")?.Value?.Trim();
        if (!Guid.TryParse(objectClaim, out var objectId))
            throw new SecurityTokenException("Microsoft token is missing a GUID-shaped oid claim.");
        var normalizedObjectId = objectId.ToString("D");

        var email = principal.FindFirst("email")?.Value?.Trim()
            ?? principal.FindFirst(ClaimTypes.Email)?.Value?.Trim()
            ?? principal.FindFirst("preferred_username")?.Value?.Trim();

        return new MicrosoftTokenPrincipal(
            Subject: normalizedObjectId,
            Email: email,
            EmailVerified: false,
            GivenName: principal.FindFirst("given_name")?.Value?.Trim(),
            FamilyName: principal.FindFirst("family_name")?.Value?.Trim(),
            Name: principal.FindFirst("name")?.Value?.Trim() ?? principal.Identity?.Name?.Trim(),
            TenantId: tenantId,
            ObjectId: normalizedObjectId);
    }
}
