using Books.Api.Domain.BankConnections;
using Books.Api.EventFlow.ReadModels;
using Books.Api.EventFlow.Repositories;
using Hangfire;
using Ledger.Core.Models;
using Ledger.Core.Services;

namespace Books.Api.Banking;

/// <summary>
/// Hangfire job for reconciling bank account balances with ledger accounts.
/// Runs daily to compare balances from Enable Banking with the corresponding ledger accounts
/// and flags any discrepancies.
/// </summary>
public class BankReconciliationJob(
    IBankConnectionRepository connectionRepository,
    IAccountRepository accountRepository,
    IEnableBankingClient bankingClient,
    ILedgerService ledgerService,
    ILogger<BankReconciliationJob> logger)
{
    /// <summary>
    /// Reconcile all linked bank accounts across all companies.
    /// Called by Hangfire recurring job daily.
    /// </summary>
    [DisableConcurrentExecution(timeoutInSeconds: 600)]
    [AutomaticRetry(Attempts = 2, DelaysInSeconds = [300, 600])]
    public async Task ReconcileAllAsync(CancellationToken cancellationToken = default)
    {
        logger.LogInformation("Starting bank reconciliation for all active connections");

        var result = new BankReconciliationResult();

        try
        {
            var connections = await connectionRepository.GetAllActiveAsync(cancellationToken);
            result.TotalConnections = connections.Count;

            foreach (var connection in connections)
            {
                try
                {
                    var connectionResult = await ReconcileConnectionAsync(connection, cancellationToken);
                    result.TotalAccountsChecked += connectionResult.TotalAccountsChecked;
                    result.MatchedAccounts += connectionResult.MatchedAccounts;
                    result.Discrepancies.AddRange(connectionResult.Discrepancies);
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "Error reconciling connection {ConnectionId}", connection.Id);
                    result.Errors++;
                }
            }

            LogReconciliationResult(result);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Fatal error during bank reconciliation");
            throw;
        }
    }

    /// <summary>
    /// Reconcile bank accounts for a specific company (manual trigger from UI).
    /// </summary>
    public async Task<BankReconciliationResult> ReconcileForCompanyAsync(
        string companyId,
        CancellationToken cancellationToken = default)
    {
        logger.LogInformation("Starting manual bank reconciliation for company {CompanyId}", companyId);

        var result = new BankReconciliationResult();

        var connections = await connectionRepository.GetActiveByCompanyIdAsync(companyId, cancellationToken);
        result.TotalConnections = connections.Count;

        foreach (var connection in connections)
        {
            try
            {
                var connectionResult = await ReconcileConnectionAsync(connection, cancellationToken);
                result.TotalAccountsChecked += connectionResult.TotalAccountsChecked;
                result.MatchedAccounts += connectionResult.MatchedAccounts;
                result.Discrepancies.AddRange(connectionResult.Discrepancies);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error reconciling connection {ConnectionId}", connection.Id);
                result.Errors++;
            }
        }

        LogReconciliationResult(result);
        return result;
    }

    private async Task<BankReconciliationResult> ReconcileConnectionAsync(
        BankConnectionReadModelDto connection,
        CancellationToken cancellationToken)
    {
        var result = new BankReconciliationResult();

        if (string.IsNullOrEmpty(connection.SessionId))
        {
            logger.LogWarning("Connection {ConnectionId} has no session ID, skipping", connection.Id);
            return result;
        }

        if (connection.Accounts == null || connection.Accounts.Count == 0)
        {
            logger.LogWarning("Connection {ConnectionId} has no accounts, skipping", connection.Id);
            return result;
        }

        foreach (var bankAccount in connection.Accounts)
        {
            // Skip accounts that are not linked to a ledger account
            if (string.IsNullOrEmpty(bankAccount.LinkedAccountId))
            {
                logger.LogDebug(
                    "Bank account {BankAccountId} is not linked to a ledger account, skipping",
                    bankAccount.AccountId);
                continue;
            }

            result.TotalAccountsChecked++;

            try
            {
                var discrepancy = await ReconcileAccountAsync(
                    connection,
                    bankAccount,
                    cancellationToken);

                if (discrepancy != null)
                {
                    result.Discrepancies.Add(discrepancy);
                    logger.LogWarning(
                        "Discrepancy found for account {AccountId}: Bank={BankBalance:N2}, Ledger={LedgerBalance:N2}, Diff={Difference:N2}",
                        bankAccount.LinkedAccountId,
                        discrepancy.BankBalance,
                        discrepancy.LedgerBalance,
                        discrepancy.Difference);
                }
                else
                {
                    result.MatchedAccounts++;
                }
            }
            catch (Exception ex)
            {
                logger.LogError(ex,
                    "Error reconciling bank account {BankAccountId} with ledger account {LedgerAccountId}",
                    bankAccount.AccountId, bankAccount.LinkedAccountId);
                result.Errors++;
            }
        }

        return result;
    }

    private async Task<ReconciliationDiscrepancy?> ReconcileAccountAsync(
        BankConnectionReadModelDto connection,
        BankAccountInfo bankAccount,
        CancellationToken cancellationToken)
    {
        // 1. Get current balance from Enable Banking
        var bankBalances = await bankingClient.GetBalancesAsync(
            connection.SessionId!,
            bankAccount.AccountId,
            cancellationToken);

        // Use the "closingBooked" or "interimAvailable" balance type
        var bankBalance = bankBalances
            .Where(b => b.BalanceType is "closingBooked" or "interimAvailable" or "expected")
            .OrderByDescending(b => b.ReferenceDate)
            .FirstOrDefault();

        if (bankBalance == null)
        {
            logger.LogWarning(
                "No suitable balance found for bank account {BankAccountId}. Available types: {Types}",
                bankAccount.AccountId,
                string.Join(", ", bankBalances.Select(b => b.BalanceType)));
            return null;
        }

        // 2. Get ledger account balance
        var ledgerAccountId = ParseAccountGuid(bankAccount.LinkedAccountId!);
        if (ledgerAccountId == null)
        {
            logger.LogWarning(
                "Could not parse ledger account ID: {LinkedAccountId}",
                bankAccount.LinkedAccountId);
            return null;
        }

        // Query aggregated balance up to now
        var query = new EntriesQuery
        {
            AccountIds = [ledgerAccountId.Value],
            To = DateTimeOffset.UtcNow,
            Aggregate = true
        };

        var ledgerResult = await ledgerService.QueryEntriesAsync(query, cancellationToken);
        var ledgerAggregate = ledgerResult.Aggregates?.FirstOrDefault();

        // Calculate net balance for the ledger account
        // For asset accounts (which bank accounts are), the balance is Debits - Credits
        var ledgerBalance = (ledgerAggregate?.TotalDebits ?? 0) - (ledgerAggregate?.TotalCredits ?? 0);

        // 3. Compare balances
        var difference = bankBalance.Amount - ledgerBalance;

        // Allow for small rounding differences (1 cent)
        if (Math.Abs(difference) <= 0.01m)
        {
            logger.LogDebug(
                "Account {LedgerAccountId} reconciled: Bank={BankBalance:N2}, Ledger={LedgerBalance:N2}",
                bankAccount.LinkedAccountId,
                bankBalance.Amount,
                ledgerBalance);
            return null;
        }

        // Get account details for the discrepancy report
        var account = await accountRepository.GetByIdAsync(bankAccount.LinkedAccountId!, cancellationToken);

        return new ReconciliationDiscrepancy
        {
            CompanyId = connection.CompanyId,
            BankConnectionId = connection.Id,
            BankAccountId = bankAccount.AccountId,
            BankAccountIban = bankAccount.Iban,
            LinkedAccountId = bankAccount.LinkedAccountId!,
            LinkedAccountNumber = account?.AccountNumber ?? "Unknown",
            LinkedAccountName = account?.Name ?? "Unknown",
            BankBalance = bankBalance.Amount,
            BankBalanceType = bankBalance.BalanceType,
            BankBalanceDate = bankBalance.ReferenceDate,
            LedgerBalance = ledgerBalance,
            Difference = difference,
            Currency = bankBalance.Currency,
            CheckedAt = DateTimeOffset.UtcNow
        };
    }

    private void LogReconciliationResult(BankReconciliationResult result)
    {
        if (result.Discrepancies.Count == 0)
        {
            logger.LogInformation(
                "Bank reconciliation completed: {Connections} connections, {Checked} accounts checked, " +
                "all {Matched} accounts matched, {Errors} errors",
                result.TotalConnections,
                result.TotalAccountsChecked,
                result.MatchedAccounts,
                result.Errors);
        }
        else
        {
            logger.LogWarning(
                "Bank reconciliation completed with discrepancies: {Connections} connections, " +
                "{Checked} accounts checked, {Matched} matched, {Discrepancies} discrepancies, {Errors} errors",
                result.TotalConnections,
                result.TotalAccountsChecked,
                result.MatchedAccounts,
                result.Discrepancies.Count,
                result.Errors);

            foreach (var discrepancy in result.Discrepancies)
            {
                logger.LogWarning(
                    "DISCREPANCY: Company={CompanyId}, Account={AccountNumber} ({AccountName}), " +
                    "Bank={BankBalance:N2} {Currency}, Ledger={LedgerBalance:N2}, Diff={Difference:N2}",
                    discrepancy.CompanyId,
                    discrepancy.LinkedAccountNumber,
                    discrepancy.LinkedAccountName,
                    discrepancy.BankBalance,
                    discrepancy.Currency,
                    discrepancy.LedgerBalance,
                    discrepancy.Difference);
            }
        }
    }

    private static Guid? ParseAccountGuid(string accountId)
    {
        if (accountId.StartsWith("account-", StringComparison.Ordinal))
        {
            var guidString = accountId["account-".Length..];
            if (Guid.TryParse(guidString, out var guid))
                return guid;
        }
        else if (Guid.TryParse(accountId, out var guid))
        {
            return guid;
        }
        return null;
    }
}

/// <summary>
/// Result of a bank reconciliation run.
/// </summary>
public class BankReconciliationResult
{
    public int TotalConnections { get; set; }
    public int TotalAccountsChecked { get; set; }
    public int MatchedAccounts { get; set; }
    public int Errors { get; set; }
    public List<ReconciliationDiscrepancy> Discrepancies { get; set; } = [];
}

/// <summary>
/// Details of a discrepancy between bank and ledger balances.
/// </summary>
public class ReconciliationDiscrepancy
{
    public string CompanyId { get; set; } = string.Empty;
    public string BankConnectionId { get; set; } = string.Empty;
    public string BankAccountId { get; set; } = string.Empty;
    public string BankAccountIban { get; set; } = string.Empty;
    public string LinkedAccountId { get; set; } = string.Empty;
    public string LinkedAccountNumber { get; set; } = string.Empty;
    public string LinkedAccountName { get; set; } = string.Empty;
    public decimal BankBalance { get; set; }
    public string BankBalanceType { get; set; } = string.Empty;
    public DateTimeOffset? BankBalanceDate { get; set; }
    public decimal LedgerBalance { get; set; }
    public decimal Difference { get; set; }
    public string Currency { get; set; } = "DKK";
    public DateTimeOffset CheckedAt { get; set; }
}
