using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Shared;

namespace Ikar.Server.Infrastructure.Push;

public sealed class FirebasePushNotificationSender(
    HttpClient httpClient,
    IOptions<PushOptions> options,
    IHostEnvironment hostEnvironment,
    IkarDbContext dbContext,
    ILogger<FirebasePushNotificationSender> logger) : IPushNotificationSender
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
    private readonly PushOptions _options = options.Value;
    private readonly SemaphoreSlim _tokenLock = new(1, 1);

    private OAuthTokenCache? _cachedToken;
    private ServiceAccountCredentials? _cachedCredentials;

    public async Task SendMessageAsync(
        Chat chat,
        Message message,
        IReadOnlyCollection<PushDevice> devices,
        CancellationToken cancellationToken = default)
    {
        var androidDevices = GetAndroidDevices(devices);

        if (androidDevices.Count == 0)
        {
            return;
        }

        var credentials = LoadCredentials();
        if (credentials is null)
        {
            logger.LogDebug("Firebase push is not configured. Skipping {DeviceCount} Android device(s).", androidDevices.Count);
            return;
        }

        var projectId = string.IsNullOrWhiteSpace(_options.FirebaseProjectId)
            ? credentials.ProjectId
            : _options.FirebaseProjectId.Trim();

        if (string.IsNullOrWhiteSpace(projectId))
        {
            logger.LogWarning("Push:FirebaseProjectId is empty and the service account JSON does not contain project_id.");
            return;
        }

        var accessToken = await GetAccessTokenAsync(credentials, cancellationToken);
        var title = chat.BuildPushTitle(message);
        var body = message.BuildPushBody();

        foreach (var device in androidDevices)
        {
            await SendToAndroidAsync(
                projectId,
                accessToken,
                device,
                new Dictionary<string, string>
                {
                    [PushDataKeys.Kind] = PushDataKinds.ChatMessage,
                    [PushDataKeys.ChatId] = message.ChatId.ToString(),
                    [PushDataKeys.ChatType] = chat.Type.ToString(),
                    [PushDataKeys.MessageId] = message.Id.ToString(),
                    [PushDataKeys.Title] = title,
                    [PushDataKeys.Body] = body,
                    [PushDataKeys.SenderId] = message.SenderId.ToString(),
                    [PushDataKeys.SenderName] = message.Sender.DisplayName,
                    [PushDataKeys.SenderPhoneNumber] = message.Sender.PhoneNumber ?? string.Empty
                },
                cancellationToken);
        }
    }

    public async Task SendMessageReactionAsync(
        Chat chat,
        Message message,
        User reactor,
        string emoji,
        IReadOnlyCollection<PushDevice> devices,
        CancellationToken cancellationToken = default)
    {
        var androidDevices = GetAndroidDevices(devices);
        if (androidDevices.Count == 0)
        {
            return;
        }

        var credentials = LoadCredentials();
        if (credentials is null)
        {
            logger.LogDebug("Firebase push is not configured. Skipping reaction push for {DeviceCount} Android device(s).", androidDevices.Count);
            return;
        }

        var projectId = ResolveProjectId(credentials);
        if (string.IsNullOrWhiteSpace(projectId))
        {
            logger.LogWarning("Push:FirebaseProjectId is empty and the service account JSON does not contain project_id.");
            return;
        }

        var accessToken = await GetAccessTokenAsync(credentials, cancellationToken);
        var title = BuildReactionPushTitle(chat, reactor);
        var body = BuildReactionPushBody(emoji);

        foreach (var device in androidDevices)
        {
            await SendToAndroidAsync(
                projectId,
                accessToken,
                device,
                new Dictionary<string, string>
                {
                    [PushDataKeys.Kind] = PushDataKinds.MessageReaction,
                    [PushDataKeys.ChatId] = message.ChatId.ToString(),
                    [PushDataKeys.ChatType] = chat.Type.ToString(),
                    [PushDataKeys.MessageId] = message.Id.ToString(),
                    [PushDataKeys.Title] = title,
                    [PushDataKeys.Body] = body,
                    [PushDataKeys.SenderId] = reactor.Id.ToString(),
                    [PushDataKeys.SenderName] = ResolveDisplayName(reactor),
                    [PushDataKeys.SenderPhoneNumber] = reactor.PhoneNumber ?? string.Empty,
                    [PushDataKeys.ReactionEmoji] = emoji
                },
                cancellationToken);
        }
    }

    public async Task SendIncomingCallAsync(
        IncomingCallDto call,
        IReadOnlyCollection<PushDevice> devices,
        CancellationToken cancellationToken = default)
    {
        var androidDevices = GetAndroidDevices(devices);
        if (androidDevices.Count == 0)
        {
            return;
        }

        var credentials = LoadCredentials();
        if (credentials is null)
        {
            logger.LogDebug("Firebase push is not configured. Skipping incoming call push for {CallId}.", call.CallId);
            return;
        }

        var projectId = ResolveProjectId(credentials);
        if (string.IsNullOrWhiteSpace(projectId))
        {
            logger.LogWarning("Push:FirebaseProjectId is empty and the service account JSON does not contain project_id.");
            return;
        }

        var accessToken = await GetAccessTokenAsync(credentials, cancellationToken);
        var isVideoCall = call.MediaType == CallMediaType.Video;
        var data = new Dictionary<string, string>
        {
            [PushDataKeys.Kind] = isVideoCall ? PushDataKinds.VideoCallInvite : PushDataKinds.AudioCallInvite,
            [PushDataKeys.CallId] = call.CallId.ToString(),
            [PushDataKeys.ChatId] = call.ChatId.ToString(),
            [PushDataKeys.SenderId] = call.Caller.Id.ToString(),
            [PushDataKeys.SenderName] = call.Caller.DisplayName,
            [PushDataKeys.SenderPhoneNumber] = call.Caller.PhoneNumber ?? string.Empty,
            [PushDataKeys.CallMediaType] = call.MediaType.ToString(),
            [PushDataKeys.Title] = call.Caller.DisplayName,
            [PushDataKeys.Body] = isVideoCall ? "Входящий видеозвонок" : "Входящий аудиозвонок"
        };

        foreach (var device in androidDevices)
        {
            await SendToAndroidAsync(projectId, accessToken, device, data, cancellationToken);
        }
    }

    public async Task SendCallEndedAsync(
        CallEndedDto call,
        IReadOnlyCollection<PushDevice> devices,
        CancellationToken cancellationToken = default)
    {
        var androidDevices = GetAndroidDevices(devices);
        if (androidDevices.Count == 0)
        {
            return;
        }

        var credentials = LoadCredentials();
        if (credentials is null)
        {
            logger.LogDebug("Firebase push is not configured. Skipping call ended push for {CallId}.", call.CallId);
            return;
        }

        var projectId = ResolveProjectId(credentials);
        if (string.IsNullOrWhiteSpace(projectId))
        {
            logger.LogWarning("Push:FirebaseProjectId is empty and the service account JSON does not contain project_id.");
            return;
        }

        var accessToken = await GetAccessTokenAsync(credentials, cancellationToken);
        var isVideoCall = call.MediaType == CallMediaType.Video;
        var data = new Dictionary<string, string>
        {
            [PushDataKeys.Kind] = isVideoCall ? PushDataKinds.VideoCallEnded : PushDataKinds.AudioCallEnded,
            [PushDataKeys.CallId] = call.CallId.ToString(),
            [PushDataKeys.ChatId] = call.ChatId.ToString(),
            [PushDataKeys.CallMediaType] = call.MediaType.ToString(),
            [PushDataKeys.Body] = call.Reason
        };

        foreach (var device in androidDevices)
        {
            await SendToAndroidAsync(projectId, accessToken, device, data, cancellationToken);
        }
    }

    public async Task SendAppUpdateAvailableAsync(
        AndroidAppReleaseDto release,
        IReadOnlyCollection<PushDevice> devices,
        CancellationToken cancellationToken = default)
    {
        var androidDevices = GetAndroidDevices(devices);
        if (androidDevices.Count == 0)
        {
            return;
        }

        var credentials = LoadCredentials();
        if (credentials is null)
        {
            logger.LogDebug("Firebase push is not configured. Skipping app update push for version {VersionCode}.", release.VersionCode);
            return;
        }

        var projectId = ResolveProjectId(credentials);
        if (string.IsNullOrWhiteSpace(projectId))
        {
            logger.LogWarning("Push:FirebaseProjectId is empty and the service account JSON does not contain project_id.");
            return;
        }

        var accessToken = await GetAccessTokenAsync(credentials, cancellationToken);
        var data = new Dictionary<string, string>
        {
            [PushDataKeys.Kind] = PushDataKinds.AppUpdateAvailable,
            [PushDataKeys.Title] = "Доступно обновление Ikar",
            [PushDataKeys.Body] = $"Доступна версия {release.VersionName}",
            [PushDataKeys.VersionCode] = release.VersionCode.ToString(),
            [PushDataKeys.VersionName] = release.VersionName
        };

        foreach (var device in androidDevices)
        {
            await SendToAndroidAsync(projectId, accessToken, device, data, cancellationToken);
        }
    }

    private ServiceAccountCredentials? LoadCredentials()
    {
        if (_cachedCredentials is not null)
        {
            return _cachedCredentials;
        }

        var json = _options.ServiceAccountJson;
        if (string.IsNullOrWhiteSpace(json) && !string.IsNullOrWhiteSpace(_options.ServiceAccountJsonPath))
        {
            var path = _options.ServiceAccountJsonPath;
            if (!Path.IsPathRooted(path))
            {
                path = Path.Combine(hostEnvironment.ContentRootPath, path);
            }

            if (!File.Exists(path))
            {
                logger.LogWarning("Push service account file was not found: {Path}", path);
                return null;
            }

            json = File.ReadAllText(path);
        }

        if (string.IsNullOrWhiteSpace(json))
        {
            return null;
        }

        try
        {
            var credentials = JsonSerializer.Deserialize<ServiceAccountCredentials>(json, JsonOptions);
            if (credentials is null ||
                string.IsNullOrWhiteSpace(credentials.ClientEmail) ||
                string.IsNullOrWhiteSpace(credentials.PrivateKey))
            {
                logger.LogWarning("Push service account JSON is missing required fields.");
                return null;
            }

            _cachedCredentials = credentials;
            return credentials;
        }
        catch (JsonException ex)
        {
            logger.LogWarning(ex, "Push service account JSON could not be parsed.");
            return null;
        }
    }

    private async Task<string> GetAccessTokenAsync(ServiceAccountCredentials credentials, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        if (_cachedToken is not null && _cachedToken.ExpiresAt > now.AddMinutes(5))
        {
            return _cachedToken.AccessToken;
        }

        await _tokenLock.WaitAsync(cancellationToken);
        try
        {
            now = DateTimeOffset.UtcNow;
            if (_cachedToken is not null && _cachedToken.ExpiresAt > now.AddMinutes(5))
            {
                return _cachedToken.AccessToken;
            }

            var assertion = CreateJwtAssertion(credentials, now);
            using var request = new HttpRequestMessage(HttpMethod.Post, credentials.TokenUri ?? "https://oauth2.googleapis.com/token")
            {
                Content = new FormUrlEncodedContent(
                [
                    new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
                    new KeyValuePair<string, string>("assertion", assertion)
                ])
            };

            using var response = await httpClient.SendAsync(request, cancellationToken);
            var payload = await response.Content.ReadAsStringAsync(cancellationToken);
            if (!response.IsSuccessStatusCode)
            {
                throw new InvalidOperationException(
                    string.IsNullOrWhiteSpace(payload)
                        ? $"Failed to acquire Firebase access token: {(int)response.StatusCode}."
                        : $"Failed to acquire Firebase access token: {payload}");
            }

            var tokenResponse = JsonSerializer.Deserialize<OAuthTokenResponse>(payload, JsonOptions)
                ?? throw new InvalidOperationException("Firebase access token response was empty.");

            _cachedToken = new OAuthTokenCache(
                tokenResponse.AccessToken,
                now.AddSeconds(tokenResponse.ExpiresIn));

            return _cachedToken.AccessToken;
        }
        finally
        {
            _tokenLock.Release();
        }
    }

    private async Task SendToAndroidAsync(
        string projectId,
        string accessToken,
        PushDevice device,
        IReadOnlyDictionary<string, string> data,
        CancellationToken cancellationToken)
    {
        var payload = new
        {
            message = new
            {
                token = device.DeviceToken,
                data,
                android = new
                {
                    priority = "high",
                    ttl = "45s"
                }
            }
        };

        using var request = new HttpRequestMessage(
            HttpMethod.Post,
            $"https://fcm.googleapis.com/v1/projects/{projectId}/messages:send");
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
        request.Content = new StringContent(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");

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

        var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
        logger.LogWarning(
            "Push send failed for installation {InstallationId} with status {StatusCode}. Response: {ResponseBody}",
            device.InstallationId,
            (int)response.StatusCode,
            responseBody);

        if (!ShouldDeleteDevice(responseBody))
        {
            return;
        }

        dbContext.PushDevices.Remove(device);
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private static bool ShouldDeleteDevice(string? responseBody) =>
        !string.IsNullOrWhiteSpace(responseBody) &&
        (responseBody.Contains("UNREGISTERED", StringComparison.OrdinalIgnoreCase) ||
         responseBody.Contains("registration-token-not-registered", StringComparison.OrdinalIgnoreCase));

    private static List<PushDevice> GetAndroidDevices(IReadOnlyCollection<PushDevice> devices) =>
        devices
            .Where(x => x.Platform == PushPlatform.Android && !string.IsNullOrWhiteSpace(x.DeviceToken))
            .ToList();

    private string ResolveProjectId(ServiceAccountCredentials credentials) =>
        string.IsNullOrWhiteSpace(_options.FirebaseProjectId)
            ? credentials.ProjectId
            : _options.FirebaseProjectId.Trim();

    private static string BuildReactionPushTitle(Chat chat, User reactor)
    {
        var reactorName = ResolveDisplayName(reactor);
        return chat.Type switch
        {
            ChatType.Direct => reactorName,
            _ => string.IsNullOrWhiteSpace(chat.Title) ? reactorName : chat.Title!
        };
    }

    private static string BuildReactionPushBody(string emoji) =>
        string.IsNullOrWhiteSpace(emoji)
            ? "\u041d\u043e\u0432\u0430\u044f \u0440\u0435\u0430\u043a\u0446\u0438\u044f \u043d\u0430 \u0432\u0430\u0448\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435"
            : $"\u0420\u0435\u0430\u043a\u0446\u0438\u044f {emoji} \u043d\u0430 \u0432\u0430\u0448\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435";

    private static string ResolveDisplayName(User user) =>
        string.IsNullOrWhiteSpace(user.DisplayName)
            ? user.Username
            : user.DisplayName;

    private static string CreateJwtAssertion(ServiceAccountCredentials credentials, DateTimeOffset now)
    {
        var header = Base64UrlEncode("""{"alg":"RS256","typ":"JWT"}""");
        var payload = Base64UrlEncode(JsonSerializer.Serialize(new Dictionary<string, object>
        {
            ["iss"] = credentials.ClientEmail,
            ["scope"] = "https://www.googleapis.com/auth/firebase.messaging",
            ["aud"] = credentials.TokenUri ?? "https://oauth2.googleapis.com/token",
            ["iat"] = now.ToUnixTimeSeconds(),
            ["exp"] = now.AddHours(1).ToUnixTimeSeconds()
        }));

        var unsignedToken = $"{header}.{payload}";
        using var rsa = RSA.Create();
        rsa.ImportFromPem(credentials.PrivateKey);
        var signatureBytes = rsa.SignData(Encoding.UTF8.GetBytes(unsignedToken), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return $"{unsignedToken}.{Base64UrlEncode(signatureBytes)}";
    }

    private static string Base64UrlEncode(string value) =>
        Base64UrlEncode(Encoding.UTF8.GetBytes(value));

    private static string Base64UrlEncode(byte[] value) =>
        Convert.ToBase64String(value)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');

    private sealed record OAuthTokenCache(string AccessToken, DateTimeOffset ExpiresAt);

    private sealed record OAuthTokenResponse(
        [property: JsonPropertyName("access_token")] string AccessToken,
        [property: JsonPropertyName("expires_in")] int ExpiresIn);

    private sealed record ServiceAccountCredentials(
        [property: JsonPropertyName("project_id")] string ProjectId,
        [property: JsonPropertyName("client_email")] string ClientEmail,
        [property: JsonPropertyName("private_key")] string PrivateKey,
        [property: JsonPropertyName("token_uri")] string? TokenUri);
}
