using System.Security.Cryptography;
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
using DysonNetwork.Pass.Auth.OidcProvider.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text.Json.Serialization;
using System.Web;
using DysonNetwork.Pass.Account;
using DysonNetwork.Pass.Auth.OidcProvider.Options;
using DysonNetwork.Shared.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using NodaTime;

namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers;

[Route("/api/auth/open")]
[ApiController]
public class OidcProviderController(
    AppDatabase db,
    OidcProviderService oidcService,
    IConfiguration configuration,
    IOptions<OidcProviderOptions> options,
    ILogger<OidcProviderController> logger
) : ControllerBase
{
    [HttpGet("authorize")]
    [Produces("application/json")]
    public async Task<IActionResult> Authorize(
        [FromQuery(Name = "client_id")] string clientId,
        [FromQuery(Name = "response_type")] string responseType,
        [FromQuery(Name = "redirect_uri")] string? redirectUri = null,
        [FromQuery] string? scope = null,
        [FromQuery] string? state = null,
        [FromQuery(Name = "response_mode")] string? responseMode = null,
        [FromQuery] string? nonce = null,
        [FromQuery] string? display = null,
        [FromQuery] string? prompt = null,
        [FromQuery(Name = "code_challenge")] string? codeChallenge = null,
        [FromQuery(Name = "code_challenge_method")]
        string? codeChallengeMethod = null)
    {
        if (string.IsNullOrEmpty(clientId))
        {
            return BadRequest(new ErrorResponse
            {
                Error = "invalid_request",
                ErrorDescription = "client_id is required"
            });
        }

        var client = await oidcService.FindClientBySlugAsync(clientId);
        if (client == null)
        {
            return BadRequest(new ErrorResponse
            {
                Error = "unauthorized_client",
                ErrorDescription = "Client not found"
            });
        }

        // Validate response_type
        if (string.IsNullOrEmpty(responseType))
        {
            return BadRequest(new ErrorResponse
            {
                Error = "invalid_request",
                ErrorDescription = "response_type is required"
            });
        }

        // Check if the client is allowed to use the requested response type
        var allowedResponseTypes = new[] { "code", "token", "id_token" };
        var requestedResponseTypes = responseType.Split(' ', StringSplitOptions.RemoveEmptyEntries);

        if (requestedResponseTypes.Any(rt => !allowedResponseTypes.Contains(rt)))
        {
            return BadRequest(new ErrorResponse
            {
                Error = "unsupported_response_type",
                ErrorDescription = "The requested response type is not supported"
            });
        }

        // Validate redirect_uri if provided
        if (!string.IsNullOrEmpty(redirectUri) &&
            !await oidcService.ValidateRedirectUriAsync(Guid.Parse(client.Id), redirectUri))
        {
            return BadRequest(new ErrorResponse
            {
                Error = "invalid_request",
                ErrorDescription = "Invalid redirect_uri"
            });
        }

        // Return client information
        var clientInfo = new ClientInfoResponse
        {
            ClientId = Guid.Parse(client.Id),
            Picture = client.Picture is not null ? CloudFileReferenceObject.FromProtoValue(client.Picture) : null,
            Background = client.Background is not null
                ? CloudFileReferenceObject.FromProtoValue(client.Background)
                : null,
            ClientName = client.Name,
            HomeUri = client.Links.HomePage,
            PolicyUri = client.Links.PrivacyPolicy,
            TermsOfServiceUri = client.Links.TermsOfService,
            ResponseTypes = responseType,
            Scopes = scope?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? [],
            State = state,
            Nonce = nonce,
            CodeChallenge = codeChallenge,
            CodeChallengeMethod = codeChallengeMethod
        };

        return Ok(clientInfo);
    }

    [HttpPost("authorize")]
    [Consumes("application/x-www-form-urlencoded")]
    [Authorize]
    public async Task<IActionResult> HandleAuthorizationResponse(
        [FromForm(Name = "authorize")] string? authorize,
        [FromForm(Name = "client_id")] string clientId,
        [FromForm(Name = "redirect_uri")] string? redirectUri = null,
        [FromForm] string? scope = null,
        [FromForm] string? state = null,
        [FromForm(Name = "response_type")] string? responseType = null,
        [FromForm] string? nonce = null,
        [FromForm(Name = "code_challenge")] string? codeChallenge = null,
        [FromForm(Name = "code_challenge_method")]
        string? codeChallengeMethod = null)
    {
        if (HttpContext.Items["CurrentUser"] is not Account.Account account)
            return Unauthorized();

        // Find the client
        var client = await oidcService.FindClientBySlugAsync(clientId);
        if (client == null)
        {
            return BadRequest(new ErrorResponse
            {
                Error = "unauthorized_client",
                ErrorDescription = "Client not found"
            });
        }

        // If user denied the request
        if (string.IsNullOrEmpty(authorize) || !bool.TryParse(authorize, out var isAuthorized) || !isAuthorized)
        {
            var errorUri = new UriBuilder(redirectUri ?? client.Links?.HomePage ?? "https://example.com");
            var queryParams = HttpUtility.ParseQueryString(errorUri.Query);
            queryParams["error"] = "access_denied";
            queryParams["error_description"] = "The user denied the authorization request";
            if (!string.IsNullOrEmpty(state)) queryParams["state"] = state;

            errorUri.Query = queryParams.ToString();
            return Redirect(errorUri.Uri.ToString());
        }

        // Validate redirect_uri if provided
        if (!string.IsNullOrEmpty(redirectUri) &&
            !await oidcService.ValidateRedirectUriAsync(Guid.Parse(client!.Id), redirectUri))
        {
            return BadRequest(new ErrorResponse
            {
                Error = "invalid_request",
                ErrorDescription = "Invalid redirect_uri"
            });
        }

        // Default to client's first redirect URI if not provided
        redirectUri ??= client.OauthConfig?.RedirectUris?.FirstOrDefault();
        if (string.IsNullOrEmpty(redirectUri))
        {
            return BadRequest(new ErrorResponse
            {
                Error = "invalid_request",
                ErrorDescription = "No valid redirect_uri available"
            });
        }

        try
        {
            // Generate authorization code and create session
            var authorizationCode = await oidcService.GenerateAuthorizationCodeAsync(
                Guid.Parse(client.Id),
                account.Id,
                redirectUri,
                scope?.Split(' ') ?? [],
                codeChallenge,
                codeChallengeMethod,
                nonce);

            // Build the redirect URI with the authorization code
            var redirectBuilder = new UriBuilder(redirectUri);
            var queryParams = HttpUtility.ParseQueryString(redirectBuilder.Query);
            queryParams["code"] = authorizationCode;
            if (!string.IsNullOrEmpty(state)) queryParams["state"] = state;

            redirectBuilder.Query = queryParams.ToString();

            return Redirect(redirectBuilder.Uri.ToString());
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error processing authorization request");
            return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse
            {
                Error = "server_error",
                ErrorDescription = "An error occurred while processing your request"
            });
        }
    }

    [HttpPost("token")]
    [Consumes("application/x-www-form-urlencoded")]
    public async Task<IActionResult> Token([FromForm] TokenRequest request)
    {
        switch (request.GrantType)
        {
            // Validate client credentials
            case "authorization_code" when request.ClientId == null || string.IsNullOrEmpty(request.ClientSecret):
                return BadRequest("Client credentials are required");
            case "authorization_code" when request.Code == null:
                return BadRequest("Authorization code is required");
            case "authorization_code":
            {
                var client = await oidcService.FindClientBySlugAsync(request.ClientId);
                if (client == null ||
                    !await oidcService.ValidateClientCredentialsAsync(Guid.Parse(client.Id), request.ClientSecret))
                    return BadRequest(new ErrorResponse
                        { Error = "invalid_client", ErrorDescription = "Invalid client credentials" });

                // Generate tokens
                var tokenResponse = await oidcService.GenerateTokenResponseAsync(
                    clientId: Guid.Parse(client.Id),
                    authorizationCode: request.Code!,
                    redirectUri: request.RedirectUri,
                    codeVerifier: request.CodeVerifier
                );

                return Ok(tokenResponse);
            }
            case "refresh_token" when string.IsNullOrEmpty(request.RefreshToken):
                return BadRequest(new ErrorResponse
                    { Error = "invalid_request", ErrorDescription = "Refresh token is required" });
            case "refresh_token":
            {
                try
                {
                    // Decode the base64 refresh token to get the session ID
                    var sessionIdBytes = Convert.FromBase64String(request.RefreshToken);
                    var sessionId = new Guid(sessionIdBytes);

                    // Find the session and related data
                    var session = await oidcService.FindSessionByIdAsync(sessionId);
                    var now = SystemClock.Instance.GetCurrentInstant();
                    if (session?.AppId is null || session.ExpiredAt < now)
                    {
                        return BadRequest(new ErrorResponse
                        {
                            Error = "invalid_grant",
                            ErrorDescription = "Invalid or expired refresh token"
                        });
                    }

                    // Get the client
                    var client = await oidcService.FindClientByIdAsync(session.AppId.Value);
                    if (client == null)
                    {
                        return BadRequest(new ErrorResponse
                        {
                            Error = "invalid_client",
                            ErrorDescription = "Client not found"
                        });
                    }

                    // Generate new tokens
                    var tokenResponse = await oidcService.GenerateTokenResponseAsync(
                        clientId: session.AppId!.Value,
                        sessionId: session.Id
                    );

                    return Ok(tokenResponse);
                }
                catch (FormatException)
                {
                    return BadRequest(new ErrorResponse
                    {
                        Error = "invalid_grant",
                        ErrorDescription = "Invalid refresh token format"
                    });
                }
            }
            default:
                return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" });
        }
    }

    [HttpGet("userinfo")]
    [Authorize]
    public async Task<IActionResult> GetUserInfo()
    {
        if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser ||
            HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();

        // Get requested scopes from the token
        var scopes = currentSession.Challenge.Scopes;

        var userInfo = new Dictionary<string, object>
        {
            ["sub"] = currentUser.Id
        };

        // Include standard claims based on scopes
        if (scopes.Contains("profile") || scopes.Contains("name"))
        {
            userInfo["name"] = currentUser.Name;
            userInfo["preferred_username"] = currentUser.Nick;
        }

        var userEmail = await db.AccountContacts
            .Where(c => c.Type == AccountContactType.Email && c.AccountId == currentUser.Id)
            .FirstOrDefaultAsync();
        if (scopes.Contains("email") && userEmail is not null)
        {
            userInfo["email"] = userEmail.Content;
            userInfo["email_verified"] = userEmail.VerifiedAt is not null;
        }

        return Ok(userInfo);
    }

    [HttpGet("/.well-known/openid-configuration")]
    public IActionResult GetConfiguration()
    {
        var baseUrl = configuration["BaseUrl"];
        var issuer = options.Value.IssuerUri.TrimEnd('/');

        return Ok(new
        {
            issuer,
            authorization_endpoint = $"{baseUrl}/auth/authorize",
            token_endpoint = $"{baseUrl}/api/auth/open/token",
            userinfo_endpoint = $"{baseUrl}/api/auth/open/userinfo",
            jwks_uri = $"{baseUrl}/.well-known/jwks",
            scopes_supported = new[] { "openid", "profile", "email" },
            response_types_supported = new[]
                { "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token" },
            grant_types_supported = new[] { "authorization_code", "refresh_token" },
            token_endpoint_auth_methods_supported = new[] { "client_secret_basic", "client_secret_post" },
            id_token_signing_alg_values_supported = new[] { "HS256" },
            subject_types_supported = new[] { "public" },
            claims_supported = new[] { "sub", "name", "email", "email_verified" },
            code_challenge_methods_supported = new[] { "S256" },
            response_modes_supported = new[] { "query", "fragment", "form_post" },
            request_parameter_supported = true,
            request_uri_parameter_supported = true,
            require_request_uri_registration = false
        });
    }

    [HttpGet("/.well-known/jwks")]
    public IActionResult GetJwks()
    {
        using var rsa = options.Value.GetRsaPublicKey();
        if (rsa == null)
        {
            return BadRequest("Public key is not configured");
        }

        var parameters = rsa.ExportParameters(false);
        var keyId = Convert.ToBase64String(SHA256.HashData(parameters.Modulus!)[..8])
            .Replace("+", "-")
            .Replace("/", "_")
            .Replace("=", "");

        return Ok(new
        {
            keys = new[]
            {
                new
                {
                    kty = "RSA",
                    use = "sig",
                    kid = keyId,
                    n = Base64UrlEncoder.Encode(parameters.Modulus!),
                    e = Base64UrlEncoder.Encode(parameters.Exponent!),
                    alg = "RS256"
                }
            }
        });
    }
}

public class TokenRequest
{
    [JsonPropertyName("grant_type")]
    [FromForm(Name = "grant_type")]
    public string? GrantType { get; set; }

    [JsonPropertyName("code")]
    [FromForm(Name = "code")]
    public string? Code { get; set; }

    [JsonPropertyName("redirect_uri")]
    [FromForm(Name = "redirect_uri")]
    public string? RedirectUri { get; set; }

    [JsonPropertyName("client_id")]
    [FromForm(Name = "client_id")]
    public string? ClientId { get; set; }

    [JsonPropertyName("client_secret")]
    [FromForm(Name = "client_secret")]
    public string? ClientSecret { get; set; }

    [JsonPropertyName("refresh_token")]
    [FromForm(Name = "refresh_token")]
    public string? RefreshToken { get; set; }

    [JsonPropertyName("scope")]
    [FromForm(Name = "scope")]
    public string? Scope { get; set; }

    [JsonPropertyName("code_verifier")]
    [FromForm(Name = "code_verifier")]
    public string? CodeVerifier { get; set; }
}