using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.IdentityModel.Tokens;

namespace Books.Api.Banking;

public class EnableBankingClient : IEnableBankingClient, IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly EnableBankingOptions _options;
    private readonly ILogger<EnableBankingClient> _logger;
    private readonly JsonSerializerOptions _jsonOptions;
    private readonly RSA _rsaKey;
    private readonly SigningCredentials _signingCredentials;

    public EnableBankingClient(
        HttpClient httpClient,
        EnableBankingOptions options,
        ILogger<EnableBankingClient> logger)
    {
        _httpClient = httpClient;
        _options = options;
        _logger = logger;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };

        // Parse RSA key once at startup and cache it
        _rsaKey = RSA.Create();
        if (!string.IsNullOrEmpty(options.PrivateKey))
        {
            _rsaKey.ImportFromPem(options.PrivateKey);
            _signingCredentials = new SigningCredentials(
                new RsaSecurityKey(_rsaKey) { KeyId = options.KeyId },
                SecurityAlgorithms.RsaSha256)
            {
                CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
            };
        }
        else
        {
            _signingCredentials = null!;
        }
    }

    public void Dispose()
    {
        _rsaKey?.Dispose();
    }

    public async Task<IReadOnlyList<Aspsp>> GetAspspsAsync(string country = "DK", CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"/aspsps?country={country}");
        AddAuthHeader(request);

        var response = await _httpClient.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadFromJsonAsync<AspspsApiResponse>(_jsonOptions, ct);
        return result?.Aspsps?.Select(MapAspsp).ToList() ?? [];
    }

    public async Task<AuthorizationResponse> StartAuthorizationAsync(
        string aspspName,
        string redirectUrl,
        string state,
        string psuType = "personal",
        string? psuIpAddress = null,
        string? psuUserAgent = null,
        CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "/auth");
        AddAuthHeader(request);
        AddPsuHeaders(request, psuIpAddress, psuUserAgent);

        var body = new
        {
            access = new
            {
                valid_until = DateTimeOffset.UtcNow.AddDays(90).ToString("o")
            },
            aspsp = new { name = aspspName, country = "DK" },
            state,
            redirect_url = redirectUrl,
            psu_type = psuType
        };

        request.Content = JsonContent.Create(body, options: _jsonOptions);

        _logger.LogDebug(
            "Starting authorization for {Bank} with PSU IP: {PsuIp}, PSU UA: {PsuUa}",
            aspspName, psuIpAddress ?? "(not provided)", psuUserAgent ?? "(not provided)");

        var response = await _httpClient.SendAsync(request, ct);

        if (!response.IsSuccessStatusCode)
        {
            var errorContent = await response.Content.ReadAsStringAsync(ct);
            throw new HttpRequestException($"Enable Banking auth error {response.StatusCode}: {errorContent}");
        }

        var result = await response.Content.ReadFromJsonAsync<AuthApiResponse>(_jsonOptions, ct);
        return new AuthorizationResponse(result!.AuthorizationId, result.Url);
    }

    public async Task<SessionResponse> CreateSessionAsync(
        string authorizationCode,
        string? psuIpAddress = null,
        string? psuUserAgent = null,
        CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "/sessions");
        AddAuthHeader(request);
        AddPsuHeaders(request, psuIpAddress, psuUserAgent);

        _logger.LogDebug(
            "Creating session with PSU IP: {PsuIp}, PSU UA: {PsuUa}",
            psuIpAddress ?? "(not provided)", psuUserAgent ?? "(not provided)");

        request.Content = JsonContent.Create(new { code = authorizationCode }, options: _jsonOptions);

        var response = await _httpClient.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        // Log raw response for debugging
        var rawJson = await response.Content.ReadAsStringAsync(ct);
        _logger.LogInformation("Raw session response: {RawJson}", rawJson);

        var result = System.Text.Json.JsonSerializer.Deserialize<SessionApiResponse>(rawJson, _jsonOptions);
        var sessionId = result!.SessionId;
        var aspspName = result.Aspsp?.Name ?? "";

        _logger.LogDebug(
            "Session created: {SessionId}, Bank: {Bank}, Accounts in response: {AccountCount}",
            sessionId, aspspName, result.Accounts?.Count ?? 0);

        // Map accounts from session response
        // Use Uid as AccountId - this is required for fetching transactions/balances
        // Filter out accounts without uid as they cannot be used for API calls
        var accounts = result.Accounts?
            .Where(a => !string.IsNullOrEmpty(a.Uid))
            .Select(a => new SessionAccount(
                a.Uid!,
                a.AccountId?.Iban ?? "",
                a.Currency ?? "DKK",
                a.Name)).ToList() ?? [];

        _logger.LogDebug(
            "Mapped {MappedCount} accounts with valid uid from {TotalCount} accounts in response",
            accounts.Count, result.Accounts?.Count ?? 0);

        // Note: There is NO separate /accounts endpoint in Enable Banking API
        // The session response is the ONLY source for account information
        // If accounts array is empty, it may be because:
        // 1. Wrong psuType (personal vs business)
        // 2. Bank doesn't provide account list for this connection type
        if (accounts.Count == 0)
        {
            _logger.LogWarning(
                "No accounts with valid uid in session response for {Bank}. " +
                "This may indicate wrong psuType or bank limitation. Session: {SessionId}",
                aspspName, sessionId);
        }

        // valid_until is nested inside the access object in the API response
        var validUntil = result.Access?.ValidUntil ?? DateTimeOffset.UtcNow.AddDays(90);

        return new SessionResponse(sessionId, aspspName, accounts, validUntil);
    }

    public async Task<AccountDetails> GetAccountDetailsAsync(string sessionId, string accountId, CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"/accounts/{accountId}/details");
        AddAuthHeader(request, sessionId);

        var response = await _httpClient.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadFromJsonAsync<AccountDetailsApiResponse>(_jsonOptions, ct);
        return new AccountDetails(
            accountId,
            result?.Iban ?? "",
            result?.Currency ?? "DKK",
            result?.Name,
            result?.OwnerName,
            result?.Product);
    }

    public async Task<IReadOnlyList<Balance>> GetBalancesAsync(string sessionId, string accountId, CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"/accounts/{accountId}/balances");
        AddAuthHeader(request, sessionId);

        var response = await _httpClient.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadFromJsonAsync<BalancesApiResponse>(_jsonOptions, ct);
        return result?.Balances?.Select(b => new Balance(
            b.BalanceType ?? "",
            b.BalanceAmount?.Amount ?? 0,
            b.BalanceAmount?.Currency ?? "DKK",
            b.ReferenceDate)).ToList() ?? [];
    }

    public async Task<TransactionsResponse> GetTransactionsAsync(
        string sessionId,
        string accountId,
        DateOnly? dateFrom = null,
        DateOnly? dateTo = null,
        CancellationToken ct = default)
    {
        var from = dateFrom ?? DateOnly.FromDateTime(DateTime.Today.AddMonths(-3));
        var to = dateTo ?? DateOnly.FromDateTime(DateTime.Today);

        var url = $"/accounts/{accountId}/transactions?date_from={from:yyyy-MM-dd}&date_to={to:yyyy-MM-dd}";
        var request = new HttpRequestMessage(HttpMethod.Get, url);
        AddAuthHeader(request, sessionId);

        var response = await _httpClient.SendAsync(request, ct);

        var rawJson = await response.Content.ReadAsStringAsync(ct);
        _logger.LogInformation("Transactions API response ({Status}): {RawJson}", response.StatusCode, rawJson);

        response.EnsureSuccessStatusCode();

        var result = JsonSerializer.Deserialize<TransactionsApiResponse>(rawJson, _jsonOptions);
        return new TransactionsResponse(
            result?.Transactions?.Select(t => new Transaction(
                t.TransactionId ?? t.EntryReference ?? GenerateDeterministicId(t),
                t.IsDebit ? -(t.TransactionAmount?.Amount ?? 0) : (t.TransactionAmount?.Amount ?? 0),
                t.TransactionAmount?.Currency ?? "DKK",
                t.BookingDate ?? DateOnly.FromDateTime(DateTime.Today),
                t.ValueDate,
                t.CreditorName,
                t.DebtorName,
                t.RemittanceInformationUnstructured,
                t.EndToEndId,
                t.IsDebit)).ToList() ?? [],
            result?.ContinuationKey);
    }

    private static string GenerateDeterministicId(TransactionApiModel t)
    {
        // Create a deterministic ID based on transaction content
        // Used when bank doesn't provide a unique ID
        var content = string.Join("|",
            t.TransactionAmount?.Amount.ToString(System.Globalization.CultureInfo.InvariantCulture),
            t.TransactionAmount?.Currency,
            t.BookingDate?.ToString("yyyy-MM-dd"),
            t.RemittanceInformationUnstructured?.Trim(),
            t.CreditorName?.Trim(),
            t.DebtorName?.Trim());
            
        using var sha256 = System.Security.Cryptography.SHA256.Create();
        var bytes = System.Text.Encoding.UTF8.GetBytes(content);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private void AddAuthHeader(HttpRequestMessage request, string? sessionId = null)
    {
        var jwt = GenerateJwt(sessionId);
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", jwt);
    }

    private void AddPsuHeaders(HttpRequestMessage request, string? psuIpAddress, string? psuUserAgent)
    {
        if (!string.IsNullOrEmpty(psuIpAddress))
            request.Headers.Add("psu-ip-address", psuIpAddress);
        if (!string.IsNullOrEmpty(psuUserAgent))
            request.Headers.Add("psu-user-agent", psuUserAgent);
    }

    private string GenerateJwt(string? sessionId = null)
    {
        var now = DateTime.UtcNow;

        // Build claims list - only add access claim if session is provided
        List<Claim>? claims = null;
        if (!string.IsNullOrEmpty(sessionId))
        {
            claims = [new Claim("access", JsonSerializer.Serialize(new { session_id = sessionId }))];
        }

        // Create header with kid explicitly set
        var header = new JwtHeader(_signingCredentials)
        {
            ["kid"] = _options.ApplicationId
        };

        // Create payload matching Enable Banking's expected format
        var payload = new JwtPayload(
            issuer: "enablebanking.com",
            audience: "api.enablebanking.com",
            claims: claims,
            notBefore: null,
            expires: now.AddHours(1),
            issuedAt: now);

        var token = new JwtSecurityToken(header, payload);
        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    private static Aspsp MapAspsp(AspspApiModel a) => new(
        a.Name ?? "",
        a.Country ?? "",
        a.Logo ?? "",
        a.PsuTypes ?? [],
        a.PsuTypes?.Contains("business") ?? false,
        a.PsuTypes?.Contains("personal") ?? false);

    // API Response models
    private record AspspsApiResponse(List<AspspApiModel>? Aspsps);
    private record AspspApiModel(string? Name, string? Country, string? Logo, List<string>? PsuTypes);
    private record AuthApiResponse(string AuthorizationId, string Url);
    private record AccessApiModel(DateTimeOffset? ValidUntil);
    private record SessionApiResponse(
        string SessionId,
        AspspApiModel? Aspsp,
        List<AccountApiModel>? Accounts,
        AccessApiModel? Access);
    private record AccountApiModel(
        AccountIdApiModel? AccountId,
        string? Uid,
        string? Currency,
        string? Name);
    private record AccountIdApiModel(string? Iban);
    private record AccountDetailsApiResponse(
        string? Iban,
        string? Currency,
        string? Name,
        string? OwnerName,
        string? Product);
    private record BalancesApiResponse(List<BalanceApiModel>? Balances);
    private record BalanceApiModel(
        string? BalanceType,
        AmountApiModel? BalanceAmount,
        DateTimeOffset? ReferenceDate);
    private record AmountApiModel(
        [property: JsonConverter(typeof(StringToDecimalConverter))]
        decimal Amount,
        string? Currency);

    private class StringToDecimalConverter : JsonConverter<decimal>
    {
        public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                var str = reader.GetString();
                return decimal.Parse(str!, System.Globalization.CultureInfo.InvariantCulture);
            }
            return reader.GetDecimal();
        }

        public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
        {
            writer.WriteNumberValue(value);
        }
    }
    private record TransactionsApiResponse(
        List<TransactionApiModel>? Transactions,
        string? ContinuationKey);
    private record TransactionApiModel(
        string? TransactionId,
        string? EntryReference,
        AmountApiModel? TransactionAmount,
        DateOnly? BookingDate,
        DateOnly? ValueDate,
        PartyApiModel? Creditor,
        PartyApiModel? Debtor,
        List<string>? RemittanceInformation,
        string? EndToEndId,
        string? CreditDebitIndicator)
    {
        public string? CreditorName => Creditor?.Name;
        public string? DebtorName => Debtor?.Name;
        public string? RemittanceInformationUnstructured =>
            RemittanceInformation != null && RemittanceInformation.Count > 0
                ? string.Join(" ", RemittanceInformation)
                : null;
        public bool IsDebit => CreditDebitIndicator == "DBIT";
    }

    private record PartyApiModel(string? Name);
}

public class EnableBankingOptions
{
    public string ApplicationId { get; set; } = "";
    public string KeyId { get; set; } = "";
    public string PrivateKey { get; set; } = "";
}
