using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using FlatRender.IdentitySvc.Domain.Entities;
using FlatRender.IdentitySvc.Domain.Enums;
using FlatRender.IdentitySvc.Infrastructure.Data;
using FlatRender.IdentitySvc.Models.Responses;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;

namespace FlatRender.IdentitySvc.Application.Services;

/// <summary>External OAuth (Google) — admin config + the authorization-code login flow.</summary>
public class OAuthService(
    IdentityDbContext db,
    AuthService authService,
    IHttpClientFactory httpFactory,
    IConfiguration config)
{
    private const string GoogleAuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
    private const string GoogleTokenEndpoint = "https://oauth2.googleapis.com/token";
    private const string GoogleUserInfo = "https://www.googleapis.com/oauth2/v3/userinfo";

    private string DefaultTenantSlug => config["DefaultTenantSlug"] ?? "flatrender";

    // ── Config ───────────────────────────────────────────────────────────────

    public async Task<OAuthConfig?> GetConfigAsync(string provider) =>
        await db.OAuthConfigs.FindAsync(provider);

    public async Task<OAuthConfig> UpsertConfigAsync(string provider, string? clientId, string? clientSecret, string? redirectUri, bool enabled)
    {
        var cfg = await db.OAuthConfigs.FindAsync(provider);
        if (cfg == null)
        {
            cfg = new OAuthConfig { Provider = provider };
            db.OAuthConfigs.Add(cfg);
        }
        if (clientId != null) cfg.ClientId = clientId;
        // Keep the existing secret if the client sent it blank/masked.
        if (!string.IsNullOrEmpty(clientSecret) && !clientSecret.Contains('•')) cfg.ClientSecret = clientSecret;
        if (redirectUri != null) cfg.RedirectUri = redirectUri;
        cfg.Enabled = enabled;
        cfg.UpdatedAt = DateTime.UtcNow;
        await db.SaveChangesAsync();
        return cfg;
    }

    // ── Google authorization-code flow ─────────────────────────────────────────

    public async Task<string> BuildGoogleAuthUrlAsync(string returnUrl)
    {
        var cfg = await GetConfigAsync("google");
        if (cfg is not { Enabled: true } || string.IsNullOrEmpty(cfg.ClientId) || string.IsNullOrEmpty(cfg.RedirectUri))
            throw new InvalidOperationException("Google login is not configured");

        var state = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(returnUrl));
        var query = new Dictionary<string, string?>
        {
            ["client_id"] = cfg.ClientId,
            ["redirect_uri"] = cfg.RedirectUri,
            ["response_type"] = "code",
            ["scope"] = "openid email profile",
            ["state"] = state,
            ["access_type"] = "online",
            ["prompt"] = "select_account",
        };
        return QueryHelpers.AddQueryString(GoogleAuthEndpoint, query);
    }

    public record GoogleResult(AuthTokensResponse Tokens, string ReturnUrl);

    public async Task<GoogleResult> HandleGoogleCallbackAsync(string code, string state, string? ipAddress)
    {
        var returnUrl = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(state));
        var cfg = await GetConfigAsync("google")
            ?? throw new InvalidOperationException("Google login is not configured");

        var http = httpFactory.CreateClient();

        // 1) Exchange the authorization code for tokens.
        var tokenResp = await http.PostAsync(GoogleTokenEndpoint, new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["code"] = code,
            ["client_id"] = cfg.ClientId ?? "",
            ["client_secret"] = cfg.ClientSecret ?? "",
            ["redirect_uri"] = cfg.RedirectUri ?? "",
            ["grant_type"] = "authorization_code",
        }));
        var tokenBody = await tokenResp.Content.ReadAsStringAsync();
        if (!tokenResp.IsSuccessStatusCode)
            throw new InvalidOperationException($"Google token exchange failed: {tokenBody}");
        using var tokenDoc = JsonDocument.Parse(tokenBody);
        var accessToken = tokenDoc.RootElement.GetProperty("access_token").GetString();

        // 2) Fetch the user's profile.
        var infoReq = new HttpRequestMessage(HttpMethod.Get, GoogleUserInfo);
        infoReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var infoResp = await http.SendAsync(infoReq);
        var infoBody = await infoResp.Content.ReadAsStringAsync();
        if (!infoResp.IsSuccessStatusCode)
            throw new InvalidOperationException($"Google userinfo failed: {infoBody}");
        using var infoDoc = JsonDocument.Parse(infoBody);
        var root = infoDoc.RootElement;
        var email = root.TryGetProperty("email", out var e) ? e.GetString() : null;
        var name = root.TryGetProperty("name", out var n) ? n.GetString() : null;
        var picture = root.TryGetProperty("picture", out var p) ? p.GetString() : null;
        if (string.IsNullOrEmpty(email))
            throw new InvalidOperationException("Google account has no email");
        email = email.ToLowerInvariant();

        // 3) Find or create the user in the default tenant.
        var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Slug == DefaultTenantSlug && t.DeletedAt == null)
            ?? await db.Tenants.FirstOrDefaultAsync(t => t.DeletedAt == null)
            ?? throw new InvalidOperationException("No tenant configured");

        var user = await db.Users.FirstOrDefaultAsync(u => u.TenantId == tenant.Id && u.Email == email && u.DeletedAt == null);
        if (user == null)
        {
            user = new User
            {
                TenantId = tenant.Id,
                Email = email,
                FullName = name,
                AvatarUrl = picture,
                EmailVerified = true,
                RegisterMode = RegisterMode.Google,
                RegisterDate = DateTime.UtcNow,
            };
            db.Users.Add(user);
            await db.SaveChangesAsync();
        }
        else if (!user.EmailVerified)
        {
            user.EmailVerified = true;
            await db.SaveChangesAsync();
        }

        var tokens = await authService.IssueOAuthSessionAsync(user, tenant, "Google", ipAddress);
        return new GoogleResult(tokens, returnUrl);
    }
}
