using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Backend.Common;
using Backend.Configuration;
using Backend.Contracts;
using Backend.Data;
using Backend.Security;
using Backend.Services;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace Backend.Endpoints;

public static partial class AuthEndpoints
{
    private const string TwitchLoginPurpose = "team-login";
    private const string TwitchBindingPurpose = "team-binding";
    private const string TwitchStateCachePrefix = "twitch-oauth-state:";
    private static readonly TimeSpan TwitchStateLifetime = TimeSpan.FromMinutes(10);

    private static async Task<IResult> StartTwitchAuthorization(
        HttpContext context,
        TwitchAuthorizeRequest request,
        IMemoryCache memoryCache,
        IOptions<TwitchAuthOptions> twitchOptions,
        IConfiguration configuration,
        IWebHostEnvironment environment,
        AwardsDbContext db,
        IUserSessionService userSessionService)
    {
        var options = await ResolveEffectiveTwitchOptionsAsync(db, twitchOptions.Value, configuration, context.RequestAborted);
        if (!TwitchAuthConfigured(options))
        {
            return Results.Json(
                new { message = "Twitch OAuth ist noch nicht konfiguriert. Bitte TwitchAuth:ClientId und TwitchAuth:ClientSecret setzen." },
                statusCode: StatusCodes.Status503ServiceUnavailable);
        }

        var purpose = NormalizeTwitchPurpose(request.Purpose);
        if (purpose is null)
        {
            return Results.BadRequest(new { message = "Unbekannter Twitch-Login-Zweck." });
        }

        var frontendOrigin = NormalizeFrontendOrigin(request.FrontendOrigin, configuration, environment);
        if (frontendOrigin is null)
        {
            return Results.BadRequest(new { message = "Frontend-Origin ist fuer Twitch OAuth nicht erlaubt." });
        }

        var sessionToken = string.Empty;
        if (purpose == TwitchBindingPurpose)
        {
            var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted);
            if (session is null)
            {
                return Results.Unauthorized();
            }

            sessionToken = session.SessionToken;
        }

        var state = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant();
        memoryCache.Set(
            $"{TwitchStateCachePrefix}{state}",
            new TwitchOAuthState(purpose, NormalizeReturnUrl(request.ReturnUrl), frontendOrigin, sessionToken),
            TwitchStateLifetime);

        var authorizationParams = new Dictionary<string, string?>
        {
            ["client_id"] = options.ClientId,
            ["redirect_uri"] = ResolveRedirectUri(context, options),
            ["response_type"] = "code",
            ["state"] = state,
        };

        if (!string.IsNullOrWhiteSpace(options.Scope))
        {
            authorizationParams["scope"] = options.Scope.Trim();
        }

        var authorizationUrl = QueryHelpers.AddQueryString(
            "https://id.twitch.tv/oauth2/authorize",
            authorizationParams);

        return Results.Ok(new TwitchAuthorizeResponse(authorizationUrl));
    }

    private static async Task<IResult> CompleteTwitchAuthorization(
        HttpContext context,
        string? code,
        string? state,
        string? error,
        string? error_description,
        IMemoryCache memoryCache,
        IOptions<TwitchAuthOptions> twitchOptions,
        IConfiguration configuration,
        IHttpClientFactory httpClientFactory,
        AwardsDbContext db,
        IUserSessionService userSessionService)
    {
        if (string.IsNullOrWhiteSpace(state)
            || !memoryCache.TryGetValue<TwitchOAuthState>($"{TwitchStateCachePrefix}{state}", out var oauthState)
            || oauthState is null)
        {
            return RedirectToTwitchCallbackError("/", "Der Twitch-Login ist abgelaufen. Bitte starte ihn erneut.");
        }

        memoryCache.Remove($"{TwitchStateCachePrefix}{state}");

        if (!string.IsNullOrWhiteSpace(error))
        {
            return RedirectToTwitchCallbackError(
                oauthState,
                string.IsNullOrWhiteSpace(error_description)
                    ? "Twitch hat den Login abgebrochen."
                    : error_description);
        }

        if (string.IsNullOrWhiteSpace(code))
        {
            return RedirectToTwitchCallbackError(oauthState, "Twitch hat keinen Login-Code zurueckgegeben.");
        }

        var options = await ResolveEffectiveTwitchOptionsAsync(db, twitchOptions.Value, configuration, context.RequestAborted);
        if (!TwitchAuthConfigured(options))
        {
            return RedirectToTwitchCallbackError(oauthState, "Twitch OAuth ist auf dem Server nicht vollstaendig konfiguriert.");
        }

        var httpClient = httpClientFactory.CreateClient();
        var token = await ExchangeTwitchCodeAsync(httpClient, context, options, code, context.RequestAborted);
        if (token is null)
        {
            return RedirectToTwitchCallbackError(oauthState, "Twitch konnte den Login-Code nicht bestaetigen.");
        }

        var twitchUser = await LoadTwitchUserAsync(httpClient, options.ClientId, token.AccessToken, context.RequestAborted);
        if (twitchUser is null)
        {
            return RedirectToTwitchCallbackError(oauthState, "Twitch konnte dein Profil nicht laden.");
        }

        var twitchLogin = NormalizeTwitchUserId(twitchUser.Login);
        if (string.IsNullOrWhiteSpace(twitchLogin))
        {
            return RedirectToTwitchCallbackError(oauthState, "Twitch hat keinen gueltigen Login-Namen geliefert.");
        }

        if (oauthState.Purpose == TwitchBindingPurpose)
        {
            return await CompleteTwitchBindingAsync(context, oauthState, twitchLogin, twitchUser.DisplayName, db);
        }

        return await CompleteTwitchTeamLoginAsync(context, oauthState, twitchLogin, db, userSessionService);
    }

    private static async Task<IResult> CompleteTwitchBindingAsync(
        HttpContext context,
        TwitchOAuthState oauthState,
        string twitchLogin,
        string twitchDisplayName,
        AwardsDbContext db)
    {
        var session = await db.UserSessions.FirstOrDefaultAsync(
            item => item.SessionToken == oauthState.SessionToken && item.IsActive,
            context.RequestAborted);
        if (session is null)
        {
            return RedirectToTwitchCallbackError(oauthState, "Deine Team-Session ist abgelaufen. Bitte melde dich erneut an.");
        }

        var member = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
        if (member is null || !member.IsActive)
        {
            return RedirectToTwitchCallbackError(oauthState, "Dieser Account ist kein aktiver Team-Login.");
        }

        if (member.MustChangePassword)
        {
            return RedirectToTwitchCallbackError(oauthState, "Bitte aendere zuerst dein temporaeres Passwort.");
        }

        var alreadyBound = await db.TeamMembers.AnyAsync(
            item => item.Id != member.Id && item.BoundTwitchUserId == twitchLogin,
            context.RequestAborted);
        if (alreadyBound)
        {
            return RedirectToTwitchCallbackError(oauthState, "Dieser Twitch-Account ist bereits mit einem Team-Account verbunden.");
        }

        var previousTwitchUserId = member.BoundTwitchUserId;
        if (!string.IsNullOrWhiteSpace(previousTwitchUserId)
            && !string.Equals(previousTwitchUserId, twitchLogin, StringComparison.OrdinalIgnoreCase))
        {
            foreach (var oldSession in db.UserSessions.Where(item => item.TwitchUserId == previousTwitchUserId))
            {
                oldSession.IsActive = false;
            }
        }

        member.BoundTwitchUserId = twitchLogin;
        member.BoundTwitchDisplayName = string.IsNullOrWhiteSpace(twitchDisplayName) ? twitchLogin : twitchDisplayName;
        member.TwitchBoundAt = DateTimeOffset.UtcNow;
        member.UpdatedAt = DateTimeOffset.UtcNow;
        member.UpdatedByTwitchId = session.TwitchUserId;

        await db.SaveChangesAsync(context.RequestAborted);
        return RedirectToTwitchCallback(oauthState, "connected");
    }

    private static async Task<IResult> DisconnectTwitchBinding(
        HttpContext context,
        AwardsDbContext db,
        IUserSessionService userSessionService)
    {
        var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted);
        if (session is null)
        {
            return Results.Unauthorized();
        }

        var member = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
        if (member is null || !member.IsActive)
        {
            return Results.BadRequest(new { message = "Dieser Account nutzt keinen aktiven Team-Login." });
        }

        var boundTwitchUserId = NormalizeTwitchUserId(member.BoundTwitchUserId);
        if (string.IsNullOrWhiteSpace(boundTwitchUserId))
        {
            return Results.Ok(new TwitchBindingDisconnectResponse(
                false,
                false,
                await ToAuthSessionDtoAsync(db, userSessionService, session, cancellationToken: context.RequestAborted)));
        }

        var currentTeamLogin = ReadTeamLoginFromSession(session.TwitchUserId);
        var currentSessionUsesBoundTwitch = string.IsNullOrWhiteSpace(currentTeamLogin)
            && string.Equals(NormalizeTwitchUserId(session.TwitchUserId), boundTwitchUserId, StringComparison.OrdinalIgnoreCase);

        foreach (var linkedSession in db.UserSessions.Where(item => item.TwitchUserId == boundTwitchUserId))
        {
            linkedSession.IsActive = false;
        }

        member.BoundTwitchUserId = null;
        member.BoundTwitchDisplayName = null;
        member.TwitchBoundAt = null;
        member.UpdatedAt = DateTimeOffset.UtcNow;
        member.UpdatedByTwitchId = session.TwitchUserId;

        if (currentSessionUsesBoundTwitch)
        {
            session.IsActive = false;
        }

        await db.SaveChangesAsync(context.RequestAborted);

        return Results.Ok(new TwitchBindingDisconnectResponse(
            true,
            currentSessionUsesBoundTwitch,
            currentSessionUsesBoundTwitch
                ? null
                : await ToAuthSessionDtoAsync(db, userSessionService, session, cancellationToken: context.RequestAborted)));
    }

    private static async Task<IResult> CompleteTwitchTeamLoginAsync(
        HttpContext context,
        TwitchOAuthState oauthState,
        string twitchLogin,
        AwardsDbContext db,
        IUserSessionService userSessionService)
    {
        var member = await db.TeamMembers.FirstOrDefaultAsync(
            item => item.BoundTwitchUserId == twitchLogin,
            context.RequestAborted);
        if (member is null || !member.IsActive)
        {
            return RedirectToTwitchCallbackError(oauthState, "Fuer diesen Twitch-Account ist kein aktiver Team-Account gebunden.");
        }

        member.LastLoginAt = DateTimeOffset.UtcNow;
        var session = await userSessionService.CreateSessionAsync(
            twitchLogin,
            member.DisplayName,
            member.Role,
            RequestMetadataReader.Read(context),
            context.RequestAborted);

        return RedirectToTwitchCallback(oauthState, "authenticated", session.SessionToken);
    }

    private static async Task<TwitchTokenResponse?> ExchangeTwitchCodeAsync(
        HttpClient httpClient,
        HttpContext context,
        TwitchAuthOptions options,
        string code,
        CancellationToken cancellationToken)
    {
        using var response = await httpClient.PostAsync(
            "https://id.twitch.tv/oauth2/token",
            new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["client_id"] = options.ClientId,
                ["client_secret"] = options.ClientSecret,
                ["code"] = code,
                ["grant_type"] = "authorization_code",
                ["redirect_uri"] = ResolveRedirectUri(context, options),
            }),
            cancellationToken);

        if (!response.IsSuccessStatusCode)
        {
            return null;
        }

        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
        return await JsonSerializer.DeserializeAsync<TwitchTokenResponse>(stream, cancellationToken: cancellationToken);
    }

    private static async Task<TwitchUserResponseItem?> LoadTwitchUserAsync(
        HttpClient httpClient,
        string clientId,
        string accessToken,
        CancellationToken cancellationToken)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.twitch.tv/helix/users");
        request.Headers.Add("Client-Id", clientId);
        request.Headers.Authorization = new("Bearer", accessToken);

        using var response = await httpClient.SendAsync(request, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }

        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
        var payload = await JsonSerializer.DeserializeAsync<TwitchUsersResponse>(stream, cancellationToken: cancellationToken);
        return payload?.Data.FirstOrDefault();
    }

    private static IResult RedirectToTwitchCallback(TwitchOAuthState state, string status, string? sessionToken = null, string? message = null)
    {
        var values = new Dictionary<string, string>
        {
            ["status"] = status,
            ["returnUrl"] = state.ReturnUrl,
        };

        if (!string.IsNullOrWhiteSpace(sessionToken))
        {
            values["sessionToken"] = sessionToken;
        }

        if (!string.IsNullOrWhiteSpace(message))
        {
            values["message"] = message;
        }

        var fragment = string.Join('&', values.Select(item => $"{item.Key}={Uri.EscapeDataString(item.Value)}"));
        return Results.Redirect($"{state.FrontendOrigin}/auth/twitch/callback#{fragment}");
    }

    private static IResult RedirectToTwitchCallbackError(TwitchOAuthState state, string message) =>
        RedirectToTwitchCallback(state, "error", message: message);

    private static IResult RedirectToTwitchCallbackError(string returnUrl, string message) =>
        RedirectToTwitchCallback(new TwitchOAuthState(TwitchLoginPurpose, NormalizeReturnUrl(returnUrl), ApplicationDefaults.FrontendOrigins[0], string.Empty), "error", message: message);

    private static bool TwitchAuthConfigured(TwitchAuthOptions options) =>
        !string.IsNullOrWhiteSpace(options.ClientId)
        && !string.IsNullOrWhiteSpace(options.ClientSecret);

    private static async Task<TwitchAuthOptions> ResolveEffectiveTwitchOptionsAsync(
        AwardsDbContext db,
        TwitchAuthOptions configuredOptions,
        IConfiguration configuration,
        CancellationToken cancellationToken)
    {
        var settings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1, cancellationToken);
        return new TwitchAuthOptions
        {
            ClientId = FirstConfigured(settings?.TwitchClientId, ReadTwitchSetting(configuration, "ClientId", "VTSA_TWITCH_CLIENT_ID"), configuredOptions.ClientId),
            ClientSecret = FirstConfigured(settings?.TwitchClientSecret, ReadTwitchSetting(configuration, "ClientSecret", "VTSA_TWITCH_CLIENT_SECRET"), configuredOptions.ClientSecret),
            RedirectUri = FirstConfigured(settings?.TwitchRedirectUri, ReadTwitchSetting(configuration, "RedirectUri", "VTSA_TWITCH_REDIRECT_URI"), configuredOptions.RedirectUri),
            Scope = FirstConfigured(settings?.TwitchScope, ReadTwitchSetting(configuration, "Scope", "VTSA_TWITCH_SCOPE"), configuredOptions.Scope),
        };
    }

    private static string FirstConfigured(params string?[] values) =>
        values.Select(value => value?.Trim() ?? string.Empty).FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;

    private static string ReadTwitchSetting(IConfiguration configuration, string key, string environmentKey) =>
        configuration[environmentKey] ?? configuration[$"TwitchAuth:{key}"] ?? string.Empty;

    private static string ResolveRedirectUri(HttpContext context, TwitchAuthOptions options)
    {
        if (!string.IsNullOrWhiteSpace(options.RedirectUri))
        {
            return options.RedirectUri.Trim();
        }

        return $"{context.Request.Scheme}://{context.Request.Host}/api/auth/twitch/callback";
    }

    private static string? NormalizeTwitchPurpose(string? purpose)
    {
        var normalized = (purpose ?? string.Empty).Trim().ToLowerInvariant();
        return normalized is TwitchLoginPurpose or TwitchBindingPurpose ? normalized : null;
    }

    private static string NormalizeReturnUrl(string? returnUrl)
    {
        var normalized = (returnUrl ?? "/admin").Trim();
        return normalized.StartsWith("/", StringComparison.Ordinal)
            && !normalized.StartsWith("//", StringComparison.Ordinal)
            && !normalized.StartsWith("/auth/twitch/callback", StringComparison.OrdinalIgnoreCase)
            ? normalized
            : "/admin";
    }

    private static string? NormalizeFrontendOrigin(
        string? frontendOrigin,
        IConfiguration configuration,
        IWebHostEnvironment environment)
    {
        if (!Uri.TryCreate(frontendOrigin?.Trim(), UriKind.Absolute, out var uri)
            || uri.Scheme is not ("http" or "https")
            || string.IsNullOrWhiteSpace(uri.Host))
        {
            return null;
        }

        var origin = uri.GetLeftPart(UriPartial.Authority);
        var configuredOrigins = configuration
            .GetSection(FrontendOptions.SectionName)
            .Get<FrontendOptions>()?
            .AllowedOrigins ?? [];
        var allowedOrigins = configuredOrigins.Length > 0 || !environment.IsDevelopment()
            ? configuredOrigins
            : ApplicationDefaults.FrontendOrigins;

        return allowedOrigins.Any(item => string.Equals(item.TrimEnd('/'), origin, StringComparison.OrdinalIgnoreCase))
            ? origin
            : null;
    }

    private sealed record TwitchOAuthState(
        string Purpose,
        string ReturnUrl,
        string FrontendOrigin,
        string SessionToken);

    private sealed record TwitchTokenResponse(
        [property: JsonPropertyName("access_token")] string AccessToken);

    private sealed record TwitchUsersResponse(
        [property: JsonPropertyName("data")] TwitchUserResponseItem[] Data);

    private sealed record TwitchUserResponseItem(
        [property: JsonPropertyName("id")] string Id,
        [property: JsonPropertyName("login")] string Login,
        [property: JsonPropertyName("display_name")] string DisplayName);
}
