using DysonNetwork.Pass.Account;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using NodaTime;

namespace DysonNetwork.Pass.Auth.OpenId;

[ApiController]
[Route("/api/auth/login")]
public class OidcController(
    IServiceProvider serviceProvider,
    AppDatabase db,
    AccountService accounts,
    AuthService auth,
    ICacheService cache,
    ILogger<OidcController> logger
)
    : ControllerBase
{
    private const string StateCachePrefix = "oidc-state:";
    private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);

    [HttpGet("{provider}")]
    public async Task<ActionResult> OidcLogin(
        [FromRoute] string provider,
        [FromQuery] string? returnUrl = "/",
        [FromQuery] string? deviceId = null,
        [FromQuery] string? flow = null
    )
    {
        logger.LogInformation("OIDC login request for provider {Provider} with returnUrl {ReturnUrl}, deviceId {DeviceId} and flow {Flow}", provider, returnUrl, deviceId, flow);
        try
        {
            var oidcService = GetOidcService(provider);

            // If the user is already authenticated, treat as an account connection request
            if (flow != "login" && HttpContext.Items["CurrentUser"] is SnAccount currentUser)
            {
                var state = Guid.NewGuid().ToString();
                var nonce = Guid.NewGuid().ToString();

                // Create and store connection state
                var oidcState = OidcState.ForConnection(currentUser.Id, provider, nonce, deviceId);
                await cache.SetAsync($"{StateCachePrefix}{state}", oidcState, StateExpiration);
                logger.LogInformation("OIDC connection flow started for user {UserId} with state {State}", currentUser.Id, state);

                // The state parameter sent to the provider is the GUID key for the cache.
                var authUrl = await oidcService.GetAuthorizationUrlAsync(state, nonce);
                return Redirect(authUrl);
            }
            else // Otherwise, proceed with the login / registration flow
            {
                var nonce = Guid.NewGuid().ToString();
                var state = Guid.NewGuid().ToString();

                // Create login state with return URL and device ID
                var oidcState = OidcState.ForLogin(returnUrl ?? "/", deviceId);
                await cache.SetAsync($"{StateCachePrefix}{state}", oidcState, StateExpiration);
                logger.LogInformation("OIDC login flow started with state {State} and returnUrl {ReturnUrl}", state, oidcState.ReturnUrl);
                var authUrl = await oidcService.GetAuthorizationUrlAsync(state, nonce);
                return Redirect(authUrl);
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error initiating OIDC flow for provider {Provider}", provider);
            return BadRequest($"Error initiating OpenID Connect flow: {ex.Message}");
        }
    }

    /// <summary>
    /// Mobile Apple Sign In endpoint
    /// Handles Apple authentication directly from mobile apps
    /// </summary>
    [HttpPost("apple/mobile")]
    public async Task<ActionResult<AuthController.TokenExchangeResponse>> AppleMobileLogin(
        [FromBody] AppleMobileSignInRequest request
    )
    {
        try
        {
            // Get Apple OIDC service
            if (GetOidcService("apple") is not AppleOidcService appleService)
                return StatusCode(503, "Apple OIDC service not available");

            // Prepare callback data for processing
            var callbackData = new OidcCallbackData
            {
                IdToken = request.IdentityToken,
                Code = request.AuthorizationCode,
            };

            // Process the authentication
            var userInfo = await appleService.ProcessCallbackAsync(callbackData);

            // Find or create user account using existing logic
            var account = await FindOrCreateAccount(userInfo, "apple");

            if (HttpContext.Items["CurrentSession"] is not SnAuthSession parentSession) parentSession = null;
            
            // Create session using the OIDC service
            var session = await appleService.CreateSessionForUserAsync(
                userInfo,
                account,
                HttpContext,
                request.DeviceId,
                request.DeviceName,
                ClientPlatform.Ios,
                parentSession
            );
            
            var token = auth.CreateToken(session);
            return Ok(new AuthController.TokenExchangeResponse { Token = token });
        }
        catch (SecurityTokenValidationException ex)
        {
            return Unauthorized($"Invalid identity token: {ex.Message}");
        }
        catch (Exception ex)
        {
            // Log the error
            return StatusCode(500, $"Authentication failed: {ex.Message}");
        }
    }

    private OidcService GetOidcService(string provider)
    {
        return provider.ToLower() switch
        {
            "apple" => serviceProvider.GetRequiredService<AppleOidcService>(),
            "google" => serviceProvider.GetRequiredService<GoogleOidcService>(),
            "microsoft" => serviceProvider.GetRequiredService<MicrosoftOidcService>(),
            "discord" => serviceProvider.GetRequiredService<DiscordOidcService>(),
            "github" => serviceProvider.GetRequiredService<GitHubOidcService>(),
            "spotify" => serviceProvider.GetRequiredService<SpotifyOidcService>(),
            "steam" => serviceProvider.GetRequiredService<SteamOidcService>(),
            "afdian" => serviceProvider.GetRequiredService<AfdianOidcService>(),
            _ => throw new ArgumentException($"Unsupported provider: {provider}")
        };
    }

    private async Task<SnAccount> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
    {
        if (string.IsNullOrEmpty(userInfo.Email))
            throw new ArgumentException("Email is required for account creation");

        // Check if an account exists by email
        var existingAccount = await accounts.LookupAccount(userInfo.Email);
        if (existingAccount != null)
        {
            // Check if this provider connection already exists
            var existingConnection = await db.AccountConnections
                .FirstOrDefaultAsync(c => c.AccountId == existingAccount.Id &&
                                          c.Provider == provider &&
                                          c.ProvidedIdentifier == userInfo.UserId);

            // If no connection exists, create one
            if (existingConnection != null)
            {
                await db.AccountConnections
                    .Where(c => c.AccountId == existingAccount.Id &&
                                c.Provider == provider &&
                                c.ProvidedIdentifier == userInfo.UserId)
                    .ExecuteUpdateAsync(s => s
                        .SetProperty(c => c.LastUsedAt, SystemClock.Instance.GetCurrentInstant())
                        .SetProperty(c => c.Meta, userInfo.ToMetadata()));

                return existingAccount;
            }

            var connection = new SnAccountConnection
            {
                AccountId = existingAccount.Id,
                Provider = provider,
                ProvidedIdentifier = userInfo.UserId!,
                AccessToken = userInfo.AccessToken,
                RefreshToken = userInfo.RefreshToken,
                LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
                Meta = userInfo.ToMetadata()
            };

            await db.AccountConnections.AddAsync(connection);
            await db.SaveChangesAsync();

            return existingAccount;
        }

        // Create new account using the AccountService
        var newAccount = await accounts.CreateAccount(userInfo);

        // Create the provider connection
        var newConnection = new SnAccountConnection
        {
            AccountId = newAccount.Id,
            Provider = provider,
            ProvidedIdentifier = userInfo.UserId!,
            AccessToken = userInfo.AccessToken,
            RefreshToken = userInfo.RefreshToken,
            LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
            Meta = userInfo.ToMetadata()
        };

        db.AccountConnections.Add(newConnection);
        await db.SaveChangesAsync();

        return newAccount;
    }
}
