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

namespace JobTrackerApi.Services;

public interface IMicrosoftGraphOAuthService
{
    string BuildAuthorizationUrl(string ownerUserId, string redirectUri);
    string? ConsumeState(string state);
    Task<MicrosoftGraphOAuthExchangeResult> ExchangeCodeAsync(string ownerUserId, string code, string redirectUri, CancellationToken cancellationToken);
    Task<MicrosoftGraphConnection?> GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken);
    Task DisconnectAsync(string ownerUserId, CancellationToken cancellationToken);
    Task<IReadOnlyList<MicrosoftGraphMessageSummary>> ListMessagesAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken);
    Task<IReadOnlyList<MicrosoftGraphMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string conversationId, CancellationToken cancellationToken);
    Task<MicrosoftGraphMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken);
    Task<MicrosoftGraphSendResult> SendAsync(string ownerUserId, MicrosoftGraphSendRequest request, CancellationToken cancellationToken);
}

public sealed record MicrosoftGraphOAuthExchangeResult(string MailAddress);
public sealed record MicrosoftGraphMessageSummary(string Id, string ConversationId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet);
public sealed record MicrosoftGraphMessageAttachment(string? FileName, string? MimeType, long? SizeBytes, string? GraphAttachmentId, bool Inline);
public sealed record MicrosoftGraphMessageDetail(string Id, string ConversationId, string Subject, string From, string To, DateTimeOffset? Date, string Snippet, string BodyText, string? BodyHtml, IReadOnlyList<string> Labels, IReadOnlyList<MicrosoftGraphMessageAttachment> Attachments);
public sealed record MicrosoftGraphSendRequest(string To, string Subject, string BodyText);
public sealed record MicrosoftGraphSendResult();

internal sealed class MicrosoftGraphTokenResponse
{
    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; }
}

/// <summary>
/// Outlook / Microsoft 365 mail via Microsoft Graph. Mirrors <see cref="GmailOAuthService"/>'s shape
/// (auth-code + offline refresh, encrypted token storage, per-owner connection row) so the two providers
/// stay structurally interchangeable behind <see cref="EmailProviders.IEmailProvider"/>.
/// </summary>
public sealed class MicrosoftGraphOAuthService : IMicrosoftGraphOAuthService
{
    public const string SendScope = "https://graph.microsoft.com/Mail.Send";
    private const string Scope = $"openid email profile offline_access https://graph.microsoft.com/Mail.Read {SendScope}";
    private readonly IConfiguration _cfg;
    private readonly JobTrackerContext _db;
    private readonly IDataProtector _protector;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;

    public MicrosoftGraphOAuthService(
        IConfiguration cfg,
        JobTrackerContext db,
        IDataProtectionProvider protectionProvider,
        IHttpClientFactory httpClientFactory,
        IMemoryCache cache)
    {
        _cfg = cfg;
        _db = db;
        _protector = protectionProvider.CreateProtector("microsoft-graph-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",
            ["response_mode"] = "query",
            ["scope"] = Scope,
            ["state"] = state,
        };

        var encoded = string.Join("&", query.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value ?? "")}"));
        return $"https://login.microsoftonline.com/{GetTenant()}/oauth2/v2.0/authorize?{encoded}";
    }

    public async Task<MicrosoftGraphOAuthExchangeResult> 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("Microsoft did not return an access token.");
        if (string.IsNullOrWhiteSpace(refreshToken))
            throw new InvalidOperationException("Microsoft did not return a refresh token. Reconnect Outlook and ensure offline_access consent is granted.");

        var mailAddress = await GetProfileAsync(accessToken, cancellationToken);
        if (string.IsNullOrWhiteSpace(mailAddress))
            throw new InvalidOperationException("Microsoft did not return a mail address.");

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

        existing.MailAddress = mailAddress.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 MicrosoftGraphOAuthExchangeResult(existing.MailAddress);
    }

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

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

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

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

    public async Task<IReadOnlyList<MicrosoftGraphMessageSummary>> 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 select = "id,conversationId,subject,from,toRecipients,receivedDateTime,bodyPreview";
            var url = $"https://graph.microsoft.com/v1.0/me/messages?$top={maxResults}&$select={select}&$orderby=receivedDateTime desc";
            if (!string.IsNullOrWhiteSpace(query))
            {
                client.DefaultRequestHeaders.Add("ConsistencyLevel", "eventual");
                url += $"&$search={Uri.EscapeDataString(EscapeSearchQuery(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);
            var results = ReadMessageSummaries(doc.RootElement);

            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<MicrosoftGraphMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string conversationId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(conversationId))
        {
            return Array.Empty<MicrosoftGraphMessageSummary>();
        }

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

            var select = "id,conversationId,subject,from,toRecipients,receivedDateTime,bodyPreview";
            var filter = $"conversationId eq '{conversationId.Trim().Replace("'", "''")}'";
            var url = $"https://graph.microsoft.com/v1.0/me/messages?$filter={Uri.EscapeDataString(filter)}&$select={select}&$orderby=receivedDateTime asc";
            using var response = await client.GetAsync(url, cancellationToken);
            response.EnsureSuccessStatusCode();

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

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

    public async Task<MicrosoftGraphMessageDetail> 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 select = "id,conversationId,subject,from,toRecipients,receivedDateTime,bodyPreview,body,categories,hasAttachments";
            var url = $"https://graph.microsoft.com/v1.0/me/messages/{Uri.EscapeDataString(messageId)}?$select={select}";
            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 conversationId = root.TryGetProperty("conversationId", out var convEl) ? convEl.GetString() ?? "" : "";
            var snippet = root.TryGetProperty("bodyPreview", out var previewEl) ? previewEl.GetString() ?? "" : "";
            var categories = root.TryGetProperty("categories", out var categoriesEl) && categoriesEl.ValueKind == JsonValueKind.Array
                ? categoriesEl.EnumerateArray().Where(x => x.ValueKind == JsonValueKind.String).Select(x => x.GetString()).Where(x => !string.IsNullOrWhiteSpace(x)).Cast<string>().ToList()
                : new List<string>();

            string bodyText = "";
            string? bodyHtml = null;
            if (root.TryGetProperty("body", out var bodyEl) && bodyEl.ValueKind == JsonValueKind.Object)
            {
                var contentType = bodyEl.TryGetProperty("contentType", out var ctEl) ? ctEl.GetString() ?? "" : "";
                var content = bodyEl.TryGetProperty("content", out var contentEl) ? contentEl.GetString() ?? "" : "";
                if (string.Equals(contentType, "html", StringComparison.OrdinalIgnoreCase))
                {
                    bodyHtml = content;
                    bodyText = StripHtml(content);
                }
                else
                {
                    bodyText = content;
                }
            }

            var attachments = root.TryGetProperty("hasAttachments", out var hasAttEl) && hasAttEl.ValueKind == JsonValueKind.True
                ? await ListAttachmentsAsync(client, messageId, cancellationToken)
                : Array.Empty<MicrosoftGraphMessageAttachment>();

            await TouchSyncStateAsync(ownerUserId, "message-detail", "graph-message", true, null, cancellationToken);
            return new MicrosoftGraphMessageDetail(
                messageId,
                conversationId,
                ReadSubject(root),
                ReadRecipientAddress(root, "from"),
                ReadRecipientListAddresses(root, "toRecipients"),
                ReadReceivedDate(root),
                snippet,
                bodyText.Trim(),
                bodyHtml,
                categories,
                attachments);
        }
        catch (Exception ex)
        {
            await TouchSyncStateAsync(ownerUserId, "message-detail", "graph-message", false, ex.Message, cancellationToken);
            throw;
        }
    }

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

        var payload = JsonSerializer.Serialize(new
        {
            message = new
            {
                subject = request.Subject.Trim(),
                body = new { contentType = "Text", content = request.BodyText },
                toRecipients = new[] { new { emailAddress = new { address = request.To.Trim() } } },
            },
            saveToSentItems = true,
        });

        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://graph.microsoft.com/v1.0/me/sendMail",
                new StringContent(payload, System.Text.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, "Microsoft Graph rejected the send request.");
            }

            return new MicrosoftGraphSendResult();
        }
        catch (EmailProviderDeliveryException)
        {
            throw;
        }
        catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException)
        {
            throw new EmailProviderDeliveryException("transport_interrupted", true, "Outlook delivery status is uncertain.", ex);
        }
    }

    public static bool HasSendScope(string? scope)
    {
        if (string.IsNullOrWhiteSpace(scope)) return false;
        return scope.Split(' ', StringSplitOptions.RemoveEmptyEntries).Any(value =>
            string.Equals(value, SendScope, StringComparison.OrdinalIgnoreCase) ||
            string.Equals(value, "Mail.Send", StringComparison.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 static async Task<IReadOnlyList<MicrosoftGraphMessageAttachment>> ListAttachmentsAsync(HttpClient client, string messageId, CancellationToken cancellationToken)
    {
        var url = $"https://graph.microsoft.com/v1.0/me/messages/{Uri.EscapeDataString(messageId)}/attachments?$select=id,name,contentType,size,isInline";
        using var response = await client.GetAsync(url, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            return Array.Empty<MicrosoftGraphMessageAttachment>();
        }

        using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
        if (!doc.RootElement.TryGetProperty("value", out var valueEl) || valueEl.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<MicrosoftGraphMessageAttachment>();
        }

        return valueEl.EnumerateArray().Select(item => new MicrosoftGraphMessageAttachment(
            item.TryGetProperty("name", out var nameEl) ? nameEl.GetString() : null,
            item.TryGetProperty("contentType", out var typeEl) ? typeEl.GetString() : null,
            item.TryGetProperty("size", out var sizeEl) && sizeEl.TryGetInt64(out var size) ? size : null,
            item.TryGetProperty("id", out var idEl) ? idEl.GetString() : null,
            item.TryGetProperty("isInline", out var inlineEl) && inlineEl.ValueKind == JsonValueKind.True
        )).ToList();
    }

    private static List<MicrosoftGraphMessageSummary> ReadMessageSummaries(JsonElement root)
    {
        if (!root.TryGetProperty("value", out var valueEl) || valueEl.ValueKind != JsonValueKind.Array)
        {
            return new List<MicrosoftGraphMessageSummary>();
        }

        return valueEl.EnumerateArray().Select(item => new MicrosoftGraphMessageSummary(
            item.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? "" : "",
            item.TryGetProperty("conversationId", out var convEl) ? convEl.GetString() ?? "" : "",
            ReadSubject(item),
            ReadRecipientAddress(item, "from"),
            ReadRecipientListAddresses(item, "toRecipients"),
            ReadReceivedDate(item),
            item.TryGetProperty("bodyPreview", out var previewEl) ? previewEl.GetString() ?? "" : ""
        )).Where(m => !string.IsNullOrWhiteSpace(m.Id)).ToList();
    }

    private static string ReadSubject(JsonElement item)
        => item.TryGetProperty("subject", out var subjectEl) ? subjectEl.GetString() ?? "" : "";

    private static DateTimeOffset? ReadReceivedDate(JsonElement item)
        => item.TryGetProperty("receivedDateTime", out var dateEl) && dateEl.ValueKind == JsonValueKind.String
            && DateTimeOffset.TryParse(dateEl.GetString(), out var parsed) ? parsed : null;

    private static string ReadRecipientAddress(JsonElement item, string property)
    {
        if (!item.TryGetProperty(property, out var recipientEl) || recipientEl.ValueKind != JsonValueKind.Object) return "";
        if (!recipientEl.TryGetProperty("emailAddress", out var addressEl) || addressEl.ValueKind != JsonValueKind.Object) return "";
        return addressEl.TryGetProperty("address", out var addrEl) ? addrEl.GetString() ?? "" : "";
    }

    private static string ReadRecipientListAddresses(JsonElement item, string property)
    {
        if (!item.TryGetProperty(property, out var listEl) || listEl.ValueKind != JsonValueKind.Array) return "";
        var addresses = listEl.EnumerateArray()
            .Where(r => r.TryGetProperty("emailAddress", out _))
            .Select(r => r.GetProperty("emailAddress").TryGetProperty("address", out var a) ? a.GetString() : null)
            .Where(a => !string.IsNullOrWhiteSpace(a));
        return string.Join(", ", addresses);
    }

    private static string EscapeSearchQuery(string query) => "\"" + query.Replace("\"", "'") + "\"";

    private static string StripHtml(string html)
        => System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", " ").Trim();

    private async Task<string> GetValidAccessTokenAsync(string ownerUserId, CancellationToken cancellationToken)
    {
        var connection = await _db.MicrosoftGraphConnections.FirstOrDefaultAsync(x => x.OwnerUserId == ownerUserId, cancellationToken);
        if (connection is null)
            throw new InvalidOperationException("Outlook 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 Outlook connection can no longer be decrypted after a server key change. Disconnect Outlook 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 Outlook 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();
        }
        if (!string.IsNullOrWhiteSpace(refreshed.refresh_token))
        {
            connection.EncryptedRefreshToken = _protector.Protect(refreshed.refresh_token.Trim());
        }

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

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

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

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

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

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

        return JsonSerializer.Deserialize<MicrosoftGraphTokenResponse>(payload)
            ?? throw new InvalidOperationException("Unable to parse Microsoft 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://graph.microsoft.com/v1.0/me?$select=mail,userPrincipalName", cancellationToken);
        var payload = await response.Content.ReadAsStringAsync(cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new InvalidOperationException($"Failed to read Microsoft profile: {payload}");
        }

        using var doc = JsonDocument.Parse(payload);
        var mail = doc.RootElement.TryGetProperty("mail", out var mailEl) ? mailEl.GetString() : null;
        if (!string.IsNullOrWhiteSpace(mail)) return mail;
        return doc.RootElement.TryGetProperty("userPrincipalName", out var upnEl) ? upnEl.GetString() ?? "" : "";
    }

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

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

        await _db.SaveChangesAsync(cancellationToken);
    }

    private string GetTenant() => (_cfg["Microsoft:TenantId"] ?? "common").Trim();

    private string GetRequiredClientId()
    {
        var value = (_cfg["Microsoft:ClientId"] ?? "").Trim();
        return value.Length > 0 ? value : throw new InvalidOperationException("Microsoft:ClientId is not configured.");
    }

    private string GetRequiredClientSecret()
    {
        var value = (_cfg["Microsoft:ClientSecret"] ?? "").Trim();
        return value.Length > 0 ? value : throw new InvalidOperationException("Microsoft:ClientSecret is not configured.");
    }

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