using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Security.Cryptography;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using JobTrackerApi.Services.EmailProviders;
using MimeKit;

namespace JobTrackerApi.Services;

public interface IGmailOAuthService
{
    string BuildAuthorizationUrl(string ownerUserId, string redirectUri);
    string? ConsumeState(string state);
    Task<GmailOAuthExchangeResult> ExchangeCodeAsync(string ownerUserId, string code, string redirectUri, CancellationToken cancellationToken);
    Task<GmailConnection?> GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken);
    Task DisconnectAsync(string ownerUserId, CancellationToken cancellationToken);
    Task<IReadOnlyList<GmailMessageSummary>> ListMessagesAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken);
    Task<IReadOnlyList<GmailMessageSummary>> ListMessagesForQueriesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken);
    Task<IReadOnlyList<GmailQueryMatchedMessage>> ListJobCandidateMessagesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken);
    Task<IReadOnlyList<GmailMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string threadId, CancellationToken cancellationToken);
    Task<GmailMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken);
    Task<GmailSendResult> SendAsync(string ownerUserId, GmailSendRequest request, CancellationToken cancellationToken);
}

public sealed record GmailOAuthExchangeResult(string GmailAddress);
public sealed record GmailMessageSummary(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet);
public sealed record GmailQueryMatchedMessage(GmailMessageSummary Message, IReadOnlyList<string> MatchedQueries);
public sealed record GmailMessageAttachment(string? FileName, string? MimeType, long? SizeBytes, string? GmailAttachmentId, bool Inline);
public sealed record GmailMessageDetail(string Id, string ThreadId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet, string BodyText, string? BodyHtml, IReadOnlyList<string> Labels, IReadOnlyList<GmailMessageAttachment> Attachments);
public sealed record GmailSendRequest(string To, string Subject, string BodyText, string? ThreadId);
public sealed record GmailSendResult(string? MessageId, string? ThreadId);

internal sealed class GmailTokenResponse
{
    public string? access_token { get; set; }
    public int expires_in { get; set; }
    public string? refresh_token { get; set; }
    public string? scope { get; set; }
    public string? token_type { get; set; }
}

public sealed class GmailOAuthService : IGmailOAuthService
{
    public const string SendScope = "https://www.googleapis.com/auth/gmail.send";
    private const string Scope = $"openid email profile https://www.googleapis.com/auth/gmail.readonly {SendScope}";
    private readonly IConfiguration _cfg;
    private readonly JobTrackerContext _db;
    private readonly IDataProtector _protector;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;

    public GmailOAuthService(
        IConfiguration cfg,
        JobTrackerContext db,
        IDataProtectionProvider protectionProvider,
        IHttpClientFactory httpClientFactory,
        IMemoryCache cache)
    {
        _cfg = cfg;
        _db = db;
        _protector = protectionProvider.CreateProtector("gmail-oauth-tokens-v1");
        _httpClientFactory = httpClientFactory;
        _cache = cache;
    }

    public string BuildAuthorizationUrl(string ownerUserId, string redirectUri)
    {
        var clientId = GetRequiredClientId();
        var state = Convert.ToBase64String(Guid.NewGuid().ToByteArray())
            .Replace("+", "-")
            .Replace("/", "_")
            .TrimEnd('=');
        _cache.Set(GetStateCacheKey(state), ownerUserId, TimeSpan.FromMinutes(15));

        var query = new Dictionary<string, string?>
        {
            ["client_id"] = clientId,
            ["redirect_uri"] = redirectUri,
            ["response_type"] = "code",
            ["access_type"] = "offline",
            ["prompt"] = "consent",
            ["include_granted_scopes"] = "true",
            ["scope"] = Scope,
            ["state"] = state,
        };

        var encoded = string.Join("&", query.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value ?? "")}"));
        return $"https://accounts.google.com/o/oauth2/v2/auth?{encoded}";
    }

    public async Task<GmailOAuthExchangeResult> ExchangeCodeAsync(string ownerUserId, string code, string redirectUri, CancellationToken cancellationToken)
    {
        var tokens = await ExchangeCodeForTokensAsync(code, redirectUri, cancellationToken);
        var accessToken = tokens.access_token?.Trim();
        var refreshToken = tokens.refresh_token?.Trim();
        if (string.IsNullOrWhiteSpace(accessToken))
            throw new InvalidOperationException("Google did not return an access token.");
        if (string.IsNullOrWhiteSpace(refreshToken))
            throw new InvalidOperationException("Google did not return a refresh token. Reconnect Gmail and ensure consent is granted.");

        var gmailAddress = await GetProfileAsync(accessToken, cancellationToken);
        if (string.IsNullOrWhiteSpace(gmailAddress))
            throw new InvalidOperationException("Google did not return a Gmail address.");

        var existing = await _db.GmailConnections.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
        if (existing is null)
        {
            existing = new GmailConnection
            {
                OwnerUserId = ownerUserId,
            };
            _db.GmailConnections.Add(existing);
        }

        existing.GmailAddress = gmailAddress.Trim();
        existing.EncryptedRefreshToken = _protector.Protect(refreshToken);
        existing.EncryptedAccessToken = _protector.Protect(accessToken);
        existing.AccessTokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(tokens.expires_in - 60, 60));
        existing.Scope = tokens.scope?.Trim() ?? Scope;
        existing.ConnectedAt = DateTimeOffset.UtcNow;
        existing.LastSyncStatus = "connected";
        existing.LastSyncSource = "oauth-callback";
        existing.LastSyncMode = "connect";
        existing.LastSyncError = null;
        existing.LastSyncAttemptedAt = DateTimeOffset.UtcNow;
        existing.LastSyncSucceededAt = existing.LastSyncAttemptedAt;

        await _db.SaveChangesAsync(cancellationToken);
        return new GmailOAuthExchangeResult(existing.GmailAddress);
    }

    public string? ConsumeState(string state)
    {
        if (!_cache.TryGetValue<string>(GetStateCacheKey(state), out var ownerUserId))
        {
            return null;
        }

        _cache.Remove(GetStateCacheKey(state));
        return ownerUserId;
    }

    public Task<GmailConnection?> GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken)
    {
        return _db.GmailConnections.AsNoTracking().FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
    }

    public async Task DisconnectAsync(string ownerUserId, CancellationToken cancellationToken)
    {
        var existing = await _db.GmailConnections.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
        if (existing is null) return;
        _db.GmailConnections.Remove(existing);
        await _db.SaveChangesAsync(cancellationToken);
    }

    public async Task<IReadOnlyList<GmailMessageSummary>> ListMessagesAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken)
    {
        maxResults = Math.Clamp(maxResults, 1, 25);
        try
        {
            var accessToken = await GetValidAccessTokenAsync(ownerUserId, cancellationToken);
            var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var url = $"https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults={maxResults}";
            if (!string.IsNullOrWhiteSpace(query))
            {
                url += $"&q={Uri.EscapeDataString(query.Trim())}";
            }

            using var response = await client.GetAsync(url, cancellationToken);
            response.EnsureSuccessStatusCode();

            using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
            if (!doc.RootElement.TryGetProperty("messages", out var messagesElement) || messagesElement.ValueKind != JsonValueKind.Array)
            {
                await TouchSyncStateAsync(ownerUserId, "list-messages", string.IsNullOrWhiteSpace(query) ? "default-query" : "custom-query", true, null, cancellationToken);
                return Array.Empty<GmailMessageSummary>();
            }

            var ids = messagesElement.EnumerateArray()
                .Select(x => x.TryGetProperty("id", out var id) ? id.GetString() : null)
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .Cast<string>()
                .ToList();

            var results = new List<GmailMessageSummary>(ids.Count);
            foreach (var id in ids)
            {
                var detail = await GetMessageAsync(ownerUserId, id, cancellationToken);
                results.Add(new GmailMessageSummary(detail.Id, detail.ThreadId, detail.Subject, detail.From, detail.To, detail.Date, detail.Snippet));
            }

            await TouchSyncStateAsync(ownerUserId, "list-messages", string.IsNullOrWhiteSpace(query) ? "default-query" : "custom-query", true, null, cancellationToken);
            return results;
        }
        catch (Exception ex)
        {
            await TouchSyncStateAsync(ownerUserId, "list-messages", string.IsNullOrWhiteSpace(query) ? "default-query" : "custom-query", false, ex.Message, cancellationToken);
            throw;
        }
    }

    public async Task<IReadOnlyList<GmailMessageSummary>> ListMessagesForQueriesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken)
    {
        var matchedMessages = await ListJobCandidateMessagesAsync(ownerUserId, queries, maxResultsPerQuery, cancellationToken);
        return matchedMessages.Select(static item => item.Message).ToList();
    }

    public async Task<IReadOnlyList<GmailQueryMatchedMessage>> ListJobCandidateMessagesAsync(string ownerUserId, IEnumerable<string> queries, int maxResultsPerQuery, CancellationToken cancellationToken)
    {
        maxResultsPerQuery = Math.Clamp(maxResultsPerQuery, 1, 25);
        var normalizedQueries = queries
            .Where(static query => !string.IsNullOrWhiteSpace(query))
            .Select(static query => query.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();

        if (normalizedQueries.Count == 0)
        {
            return Array.Empty<GmailQueryMatchedMessage>();
        }

        var combined = new Dictionary<string, (GmailMessageSummary Message, HashSet<string> Queries)>(StringComparer.Ordinal);
        foreach (var query in normalizedQueries)
        {
            var items = await ListMessagesAsync(ownerUserId, query, maxResultsPerQuery, cancellationToken);
            foreach (var item in items)
            {
                if (!combined.TryGetValue(item.Id, out var existing))
                {
                    combined[item.Id] = (item, new HashSet<string>(StringComparer.OrdinalIgnoreCase) { query });
                    continue;
                }

                existing.Queries.Add(query);
                combined[item.Id] = existing;
            }
        }

        return combined.Values
            .Select(static entry => new GmailQueryMatchedMessage(entry.Message, entry.Queries.OrderBy(static query => query, StringComparer.OrdinalIgnoreCase).ToList()))
            .ToList();
    }

    public async Task<IReadOnlyList<GmailMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string threadId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(threadId))
        {
            return Array.Empty<GmailMessageSummary>();
        }

        try
        {
            var accessToken = await GetValidAccessTokenAsync(ownerUserId, cancellationToken);
            var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var url = $"https://gmail.googleapis.com/gmail/v1/users/me/threads/{Uri.EscapeDataString(threadId.Trim())}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date";
            using var response = await client.GetAsync(url, cancellationToken);
            response.EnsureSuccessStatusCode();

            using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
            if (!doc.RootElement.TryGetProperty("messages", out var messagesElement) || messagesElement.ValueKind != JsonValueKind.Array)
            {
                await TouchSyncStateAsync(ownerUserId, "thread-refresh", "thread-metadata", true, null, cancellationToken);
                return Array.Empty<GmailMessageSummary>();
            }

            var results = new List<GmailMessageSummary>();
            foreach (var messageElement in messagesElement.EnumerateArray())
            {
                var id = messageElement.TryGetProperty("id", out var idEl) ? idEl.GetString() : null;
                if (string.IsNullOrWhiteSpace(id)) continue;

                var messageThreadId = messageElement.TryGetProperty("threadId", out var messageThreadIdEl)
                    ? messageThreadIdEl.GetString() ?? threadId.Trim()
                    : threadId.Trim();
                var snippet = messageElement.TryGetProperty("snippet", out var snippetEl) ? snippetEl.GetString() ?? string.Empty : string.Empty;
                var payload = messageElement.TryGetProperty("payload", out var payloadEl) ? payloadEl : default;
                var headers = payload.ValueKind == JsonValueKind.Object ? ReadHeaders(payload) : new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

                DateTimeOffset? date = null;
                if (headers.TryGetValue("date", out var dateHeader) && DateTimeOffset.TryParse(dateHeader, out var parsedDate))
                {
                    date = parsedDate;
                }

                results.Add(new GmailMessageSummary(
                    id.Trim(),
                    messageThreadId,
                    headers.TryGetValue("subject", out var subject) ? subject : string.Empty,
                    headers.TryGetValue("from", out var from) ? from : string.Empty,
                    headers.TryGetValue("to", out var to) ? to : string.Empty,
                    date,
                    snippet));
            }

            await TouchSyncStateAsync(ownerUserId, "thread-refresh", "thread-metadata", true, null, cancellationToken);
            return results;
        }
        catch (Exception ex)
        {
            await TouchSyncStateAsync(ownerUserId, "thread-refresh", "thread-metadata", false, ex.Message, cancellationToken);
            throw;
        }
    }

    public async Task<GmailMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken)
    {
        try
        {
            var accessToken = await GetValidAccessTokenAsync(ownerUserId, cancellationToken);
            var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var url = $"https://gmail.googleapis.com/gmail/v1/users/me/messages/{Uri.EscapeDataString(messageId)}?format=full";
            using var response = await client.GetAsync(url, cancellationToken);
            response.EnsureSuccessStatusCode();

            using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
            var root = doc.RootElement;

            var threadId = root.TryGetProperty("threadId", out var threadEl) ? threadEl.GetString() ?? "" : "";
            var snippet = root.TryGetProperty("snippet", out var snippetEl) ? snippetEl.GetString() ?? "" : "";
            var labels = root.TryGetProperty("labelIds", out var labelIdsEl) && labelIdsEl.ValueKind == JsonValueKind.Array
                ? labelIdsEl.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.String).Select(x => x.GetString()).Where(x => !string.IsNullOrWhiteSpace(x)).Cast<string>().ToList()
                : new List<string>();

            var payload = root.GetProperty("payload");
            var headers = ReadHeaders(payload);
            var attachments = ReadAttachments(payload);
            var bodyText = ExtractBody(payload, "text/plain");
            var bodyHtml = ExtractBody(payload, "text/html");
            if (string.IsNullOrWhiteSpace(bodyText) && !string.IsNullOrWhiteSpace(bodyHtml))
            {
                bodyText = StripHtml(bodyHtml);
            }
            else if (LooksLikeHtml(bodyText))
            {
                bodyText = StripHtml(bodyText);
            }

            await TouchSyncStateAsync(ownerUserId, "message-detail", "gmail-message", true, null, cancellationToken);
            return new GmailMessageDetail(
                messageId,
                threadId,
                headers.TryGetValue("subject", out var subject) ? subject : "",
                headers.TryGetValue("from", out var from) ? from : "",
                headers.TryGetValue("to", out var to) ? to : "",
                headers.TryGetValue("date", out var dateRaw) && DateTimeOffset.TryParse(dateRaw, out var parsedDate) ? parsedDate : null,
                snippet,
                bodyText.Trim(),
                string.IsNullOrWhiteSpace(bodyHtml) ? null : bodyHtml,
                labels,
                attachments
            );
        }
        catch (Exception ex)
        {
            await TouchSyncStateAsync(ownerUserId, "message-detail", "gmail-message", false, ex.Message, cancellationToken);
            throw;
        }
    }

    public async Task<GmailSendResult> SendAsync(string ownerUserId, GmailSendRequest request, CancellationToken cancellationToken)
    {
        var connection = await _db.GmailConnections.AsNoTracking().FirstOrDefaultAsync(item => item.OwnerUserId == ownerUserId, cancellationToken);
        if (connection is null || !HasSendScope(connection.Scope))
            throw new EmailProviderDeliveryException("reauthorization_required", false, "Reconnect Gmail and approve send access before sending.");
        ValidateSendRequest(request.To, request.Subject, request.BodyText);

        var message = new MimeMessage();
        message.To.Add(MailboxAddress.Parse(request.To.Trim()));
        message.Subject = request.Subject.Trim();
        message.Body = new TextPart("plain") { Text = request.BodyText };
        await using var stream = new MemoryStream();
        await message.WriteToAsync(stream, cancellationToken);
        var raw = Convert.ToBase64String(stream.ToArray()).TrimEnd('=').Replace('+', '-').Replace('/', '_');
        var payload = JsonSerializer.Serialize(new { raw, threadId = string.IsNullOrWhiteSpace(request.ThreadId) ? null : request.ThreadId.Trim() });

        try
        {
            var accessToken = await GetValidAccessTokenAsync(ownerUserId, cancellationToken);
            var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            using var response = await client.PostAsync(
                "https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
                new StringContent(payload, Encoding.UTF8, "application/json"),
                cancellationToken);
            if (!response.IsSuccessStatusCode)
            {
                var category = response.StatusCode is System.Net.HttpStatusCode.Unauthorized or System.Net.HttpStatusCode.Forbidden
                    ? "reauthorization_required"
                    : "provider_rejected";
                throw new EmailProviderDeliveryException(category, false, "Gmail rejected the send request.");
            }

            using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
            return new GmailSendResult(
                document.RootElement.TryGetProperty("id", out var id) ? id.GetString() : null,
                document.RootElement.TryGetProperty("threadId", out var threadId) ? threadId.GetString() : request.ThreadId);
        }
        catch (EmailProviderDeliveryException)
        {
            throw;
        }
        catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException)
        {
            throw new EmailProviderDeliveryException("transport_interrupted", true, "Gmail delivery status is uncertain.", ex);
        }
    }

    public static bool HasSendScope(string? scope) =>
        !string.IsNullOrWhiteSpace(scope) && scope.Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains(SendScope, StringComparer.OrdinalIgnoreCase);

    private static void ValidateSendRequest(string to, string subject, string bodyText)
    {
        if (string.IsNullOrWhiteSpace(to)) throw new ArgumentException("Recipient is required.", nameof(to));
        if (string.IsNullOrWhiteSpace(subject)) throw new ArgumentException("Subject is required.", nameof(subject));
        if (string.IsNullOrWhiteSpace(bodyText)) throw new ArgumentException("Body is required.", nameof(bodyText));
        if (to.Length > 320 || subject.Length > 998 || bodyText.Length > 200_000) throw new ArgumentException("Email content exceeds the supported limit.");
    }

    private async Task<string> GetValidAccessTokenAsync(string ownerUserId, CancellationToken cancellationToken)
    {
        var connection = await _db.GmailConnections.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
        if (connection is null)
            throw new InvalidOperationException("Gmail is not connected for this account.");

        if (!string.IsNullOrWhiteSpace(connection.EncryptedAccessToken) &&
            connection.AccessTokenExpiresAt is { } expiresAt &&
            expiresAt > DateTimeOffset.UtcNow.AddMinutes(1))
        {
            try
            {
                return _protector.Unprotect(connection.EncryptedAccessToken);
            }
            catch (CryptographicException)
            {
                connection.EncryptedAccessToken = null;
                connection.AccessTokenExpiresAt = null;
            }
        }

        string refreshToken;
        try
        {
            refreshToken = _protector.Unprotect(connection.EncryptedRefreshToken);
        }
        catch (CryptographicException)
        {
            throw new InvalidOperationException("Your stored Gmail connection can no longer be decrypted after a server key change. Disconnect Gmail and connect it again.");
        }
        var refreshed = await RefreshAccessTokenAsync(refreshToken, cancellationToken);
        var accessToken = refreshed.access_token?.Trim();
        if (string.IsNullOrWhiteSpace(accessToken))
            throw new InvalidOperationException("Failed to refresh Gmail access token.");

        connection.EncryptedAccessToken = _protector.Protect(accessToken);
        connection.AccessTokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(refreshed.expires_in - 60, 60));
        if (!string.IsNullOrWhiteSpace(refreshed.scope))
        {
            connection.Scope = refreshed.scope.Trim();
        }

        await _db.SaveChangesAsync(cancellationToken);
        return accessToken;
    }

    private async Task<GmailTokenResponse> ExchangeCodeForTokensAsync(string code, string redirectUri, CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient();
        using var response = await client.PostAsync(
            "https://oauth2.googleapis.com/token",
            new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["code"] = code,
                ["client_id"] = GetRequiredClientId(),
                ["client_secret"] = GetRequiredClientSecret(),
                ["redirect_uri"] = redirectUri,
                ["grant_type"] = "authorization_code",
            }),
            cancellationToken);

        var payload = await response.Content.ReadAsStringAsync(cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new InvalidOperationException($"Google token exchange failed: {payload}");
        }

        return JsonSerializer.Deserialize<GmailTokenResponse>(payload)
            ?? throw new InvalidOperationException("Unable to parse Google token response.");
    }

    private async Task<GmailTokenResponse> RefreshAccessTokenAsync(string refreshToken, CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient();
        using var response = await client.PostAsync(
            "https://oauth2.googleapis.com/token",
            new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["refresh_token"] = refreshToken,
                ["client_id"] = GetRequiredClientId(),
                ["client_secret"] = GetRequiredClientSecret(),
                ["grant_type"] = "refresh_token",
            }),
            cancellationToken);

        var payload = await response.Content.ReadAsStringAsync(cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new InvalidOperationException($"Google token refresh failed: {payload}");
        }

        return JsonSerializer.Deserialize<GmailTokenResponse>(payload)
            ?? throw new InvalidOperationException("Unable to parse Google refresh response.");
    }

    private async Task<string> GetProfileAsync(string accessToken, CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        using var response = await client.GetAsync("https://gmail.googleapis.com/gmail/v1/users/me/profile", cancellationToken);
        var payload = await response.Content.ReadAsStringAsync(cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new InvalidOperationException($"Failed to read Gmail profile: {payload}");
        }

        using var doc = JsonDocument.Parse(payload);
        return doc.RootElement.TryGetProperty("emailAddress", out var emailEl)
            ? emailEl.GetString() ?? ""
            : "";
    }

    private async Task TouchSyncTimeAsync(string ownerUserId, CancellationToken cancellationToken)
    {
        await TouchSyncStateAsync(ownerUserId, "sync", "gmail", true, null, cancellationToken);
    }

    private async Task TouchSyncStateAsync(string ownerUserId, string mode, string source, bool succeeded, string? error, CancellationToken cancellationToken)
    {
        var connection = await _db.GmailConnections.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
        if (connection is null) return;

        var now = DateTimeOffset.UtcNow;
        connection.LastSyncAttemptedAt = now;
        connection.LastSyncMode = mode;
        connection.LastSyncSource = source;
        connection.LastSyncStatus = succeeded ? "success" : "error";
        connection.LastSyncError = succeeded ? null : TrimError(error);
        if (succeeded)
        {
            connection.LastSyncedAt = now;
            connection.LastSyncSucceededAt = now;
        }

        await _db.SaveChangesAsync(cancellationToken);
    }

    private static string? TrimError(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return null;
        var trimmed = value.Trim();
        return trimmed.Length <= 300 ? trimmed : trimmed[..300];
    }

    private string GetRequiredClientId()
    {
        return (_cfg["Google:ClientId"] ?? _cfg["Auth:GoogleClientId"] ?? "").Trim() switch
        {
            "" => throw new InvalidOperationException("Google Gmail client ID is not configured."),
            var value => value,
        };
    }

    private string GetRequiredClientSecret()
    {
        return (_cfg["Google:GmailClientSecret"] ?? _cfg["Google:ClientSecret"] ?? "").Trim() switch
        {
            "" => throw new InvalidOperationException("Google Gmail client secret is not configured."),
            var value => value,
        };
    }

    private static string GetStateCacheKey(string state) => $"gmail-oauth-state:{state}";

    private static Dictionary<string, string> ReadHeaders(JsonElement payload)
    {
        var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        if (!payload.TryGetProperty("headers", out var headers) || headers.ValueKind != JsonValueKind.Array)
        {
            return result;
        }

        foreach (var item in headers.EnumerateArray())
        {
            var name = item.TryGetProperty("name", out var nameEl) ? nameEl.GetString() : null;
            var value = item.TryGetProperty("value", out var valueEl) ? valueEl.GetString() : null;
            if (string.IsNullOrWhiteSpace(name) || value is null) continue;
            result[name.Trim()] = value;
        }

        return result;
    }

    private static List<GmailMessageAttachment> ReadAttachments(JsonElement payload)
    {
        var results = new List<GmailMessageAttachment>();
        ReadAttachmentsRecursive(payload, results);
        return results;
    }

    private static void ReadAttachmentsRecursive(JsonElement payload, List<GmailMessageAttachment> results)
    {
        var body = payload.TryGetProperty("body", out var bodyEl) && bodyEl.ValueKind == JsonValueKind.Object
            ? bodyEl
            : default;
        var gmailAttachmentId = body.ValueKind == JsonValueKind.Object && body.TryGetProperty("attachmentId", out var attachmentIdEl) && attachmentIdEl.ValueKind == JsonValueKind.String
            ? attachmentIdEl.GetString()
            : null;
        var filename = payload.TryGetProperty("filename", out var filenameEl) ? filenameEl.GetString() : null;
        var mimeType = payload.TryGetProperty("mimeType", out var mimeTypeEl) ? mimeTypeEl.GetString() : null;
        var sizeBytes = body.ValueKind == JsonValueKind.Object && body.TryGetProperty("size", out var sizeEl) && sizeEl.ValueKind == JsonValueKind.Number
            ? sizeEl.GetInt64()
            : (long?)null;
        var disposition = payload.TryGetProperty("headers", out var headersEl) && headersEl.ValueKind == JsonValueKind.Array
            ? headersEl.EnumerateArray()
                .Where(h => h.TryGetProperty("name", out var n) && string.Equals(n.GetString(), "Content-Disposition", StringComparison.OrdinalIgnoreCase))
                .Select(h => h.TryGetProperty("value", out var v) ? v.GetString() : null)
                .FirstOrDefault()
            : null;
        var isInline = !string.IsNullOrWhiteSpace(disposition) && disposition.Contains("inline", StringComparison.OrdinalIgnoreCase);

        if (!string.IsNullOrWhiteSpace(gmailAttachmentId) || !string.IsNullOrWhiteSpace(filename))
        {
            results.Add(new GmailMessageAttachment(filename, mimeType, sizeBytes, gmailAttachmentId, isInline));
        }

        if (payload.TryGetProperty("parts", out var partsEl) && partsEl.ValueKind == JsonValueKind.Array)
        {
            foreach (var part in partsEl.EnumerateArray())
            {
                ReadAttachmentsRecursive(part, results);
            }
        }
    }

    private static string ExtractBody(JsonElement payload, string mimeType)
    {
        if (payload.TryGetProperty("mimeType", out var mimeTypeEl) &&
            string.Equals(mimeTypeEl.GetString(), mimeType, StringComparison.OrdinalIgnoreCase) &&
            payload.TryGetProperty("body", out var bodyEl) &&
            bodyEl.TryGetProperty("data", out var dataEl))
        {
            return DecodeBody(dataEl.GetString());
        }

        if (payload.TryGetProperty("parts", out var partsEl) && partsEl.ValueKind == JsonValueKind.Array)
        {
            foreach (var part in partsEl.EnumerateArray())
            {
                var nested = ExtractBody(part, mimeType);
                if (!string.IsNullOrWhiteSpace(nested))
                {
                    return nested;
                }
            }
        }

        return "";
    }

    private static string DecodeBody(string? raw)
    {
        if (string.IsNullOrWhiteSpace(raw)) return "";
        var normalized = raw.Replace('-', '+').Replace('_', '/');
        var padding = normalized.Length % 4;
        if (padding > 0)
        {
            normalized = normalized.PadRight(normalized.Length + (4 - padding), '=');
        }

        try
        {
            var bytes = Convert.FromBase64String(normalized);
            return Encoding.UTF8.GetString(bytes);
        }
        catch
        {
            return "";
        }
    }

    private static bool LooksLikeHtml(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return false;
        return value.Contains("<html", StringComparison.OrdinalIgnoreCase)
            || value.Contains("<body", StringComparison.OrdinalIgnoreCase)
            || value.Contains("<div", StringComparison.OrdinalIgnoreCase)
            || value.Contains("<p", StringComparison.OrdinalIgnoreCase)
            || value.Contains("<br", StringComparison.OrdinalIgnoreCase)
            || value.Contains("<span", StringComparison.OrdinalIgnoreCase);
    }

    private static string StripHtml(string html)
    {
        if (string.IsNullOrWhiteSpace(html)) return "";
        return System.Text.RegularExpressions.Regex.Replace(html, "<.*?>", string.Empty)
            .Replace("&nbsp;", " ")
            .Replace("&amp;", "&")
            .Trim();
    }
}
