using System.Text.Json;
using System.Text.RegularExpressions;
using Books.Api.Banking;
using Books.Api.EventFlow.Repositories;
using Dapper;
using Microsoft.Extensions.Logging;
using Npgsql;

namespace Books.Api.Invoicing.Services;

/// <summary>
/// Implementation of payment matching service.
/// Uses multiple signals to suggest matches between bank transactions and invoices.
/// </summary>
public partial class PaymentMatchingService(
    NpgsqlDataSource dataSource,
    IBankTransactionRepository bankTransactionRepo,
    IInvoiceRepository invoiceRepo,
    ICustomerRepository customerRepo,
    IInvoicePostingService invoicePostingService,
    ILogger<PaymentMatchingService> logger) : IPaymentMatchingService
{
    // Confidence thresholds
    private const decimal AutoMatchThreshold = 0.95m;
    private const decimal HighConfidenceThreshold = 0.80m;

    // Scoring weights
    private const decimal ExactAmountScore = 0.50m;
    private const decimal CloseAmountScore = 0.30m;
    private const decimal ReferenceMatchScore = 0.40m;
    private const decimal NameMatchScore = 0.30m;
    private const decimal RecentInvoiceBonus = 0.10m;

    public async Task<IReadOnlyList<SuggestedPaymentMatch>> SuggestMatchesAsync(
        string bankTransactionId,
        CancellationToken cancellationToken = default)
    {
        var transaction = await bankTransactionRepo.GetByIdAsync(bankTransactionId, cancellationToken);
        if (transaction == null)
        {
            logger.LogWarning("Bank transaction {TransactionId} not found", bankTransactionId);
            return [];
        }

        // Only match incoming payments (positive amounts)
        if (transaction.Amount <= 0)
        {
            return [];
        }

        // Get unpaid invoices for the company
        var unpaidInvoices = await invoiceRepo.GetUnpaidByCompanyIdAsync(transaction.CompanyId, cancellationToken);
        if (unpaidInvoices.Count == 0)
        {
            return [];
        }

        var suggestions = new List<SuggestedPaymentMatch>();

        foreach (var invoice in unpaidInvoices)
        {
            var reasons = new List<MatchReason>();
            decimal totalScore = 0;

            // 1. Amount matching
            var amountScore = CalculateAmountScore(transaction.Amount, invoice.AmountRemaining);
            if (amountScore > 0)
            {
                reasons.Add(new MatchReason
                {
                    Reason = amountScore >= ExactAmountScore ? "exact_amount" : "close_amount",
                    Description = amountScore >= ExactAmountScore
                        ? "Beløbet matcher præcist"
                        : $"Beløbet er tæt på ({transaction.Amount:N2} vs {invoice.AmountRemaining:N2})",
                    Score = amountScore
                });
                totalScore += amountScore;
            }

            // 2. Reference matching (look for invoice number in transaction reference)
            var referenceScore = CalculateReferenceScore(
                transaction.Reference,
                transaction.Description,
                invoice.InvoiceNumber);
            if (referenceScore > 0)
            {
                reasons.Add(new MatchReason
                {
                    Reason = "reference_match",
                    Description = $"Fakturanummer '{invoice.InvoiceNumber}' fundet i reference",
                    Score = referenceScore
                });
                totalScore += referenceScore;
            }

            // 3. Name matching (compare counterparty to customer name)
            var customer = await customerRepo.GetByIdAsync(invoice.CustomerId, cancellationToken);
            if (customer != null)
            {
                var nameScore = CalculateNameScore(transaction.CounterpartyName, customer.Name);
                if (nameScore > 0)
                {
                    reasons.Add(new MatchReason
                    {
                        Reason = "name_match",
                        Description = $"Modpartsnavn ligner kundenavn",
                        Score = nameScore
                    });
                    totalScore += nameScore;
                }
            }

            // 4. Recent invoice bonus (invoices from last 30 days get a small boost)
            if (invoice.InvoiceDate.HasValue)
            {
                var invoiceDate = DateOnly.FromDateTime(invoice.InvoiceDate.Value);
                var daysSinceInvoice = (DateOnly.FromDateTime(DateTime.Today).DayNumber - invoiceDate.DayNumber);
                if (daysSinceInvoice <= 30)
                {
                    reasons.Add(new MatchReason
                    {
                        Reason = "recent_invoice",
                        Description = "Faktura udstedt inden for de sidste 30 dage",
                        Score = RecentInvoiceBonus
                    });
                    totalScore += RecentInvoiceBonus;
                }
            }

            // Only suggest if we have at least one matching signal
            if (reasons.Count > 0 && totalScore >= 0.20m)
            {
                // Cap confidence at 1.0
                var confidence = Math.Min(totalScore, 1.0m);

                suggestions.Add(new SuggestedPaymentMatch
                {
                    Id = Guid.NewGuid().ToString(),
                    CompanyId = transaction.CompanyId,
                    BankTransactionId = bankTransactionId,
                    InvoiceId = invoice.Id,
                    BankTransactionAmount = transaction.Amount,
                    BankTransactionDate = DateOnly.FromDateTime(transaction.BookingDate ?? transaction.ValueDate ?? DateTime.Today),
                    BankTransactionDescription = transaction.Description,
                    BankTransactionCounterparty = transaction.CounterpartyName,
                    InvoiceNumber = invoice.InvoiceNumber,
                    CustomerName = invoice.CustomerName,
                    InvoiceAmountRemaining = invoice.AmountRemaining,
                    InvoiceDueDate = invoice.DueDate.HasValue ? DateOnly.FromDateTime(invoice.DueDate.Value) : null,
                    Confidence = confidence,
                    SuggestedAmount = Math.Min(transaction.Amount, invoice.AmountRemaining),
                    MatchReasons = reasons,
                    Status = "pending",
                    CreatedAt = DateTimeOffset.UtcNow
                });
            }
        }

        // Sort by confidence descending
        return suggestions.OrderByDescending(s => s.Confidence).ToList();
    }

    public async Task<IReadOnlyList<SuggestedPaymentMatch>> SuggestMatchesForCompanyAsync(
        string companyId,
        CancellationToken cancellationToken = default)
    {
        // Get all pending bank transactions
        var pendingTransactions = await bankTransactionRepo.GetPendingByCompanyIdAsync(companyId, cancellationToken);

        var allSuggestions = new List<SuggestedPaymentMatch>();

        foreach (var transaction in pendingTransactions)
        {
            var suggestions = await SuggestMatchesAsync(transaction.Id, cancellationToken);
            allSuggestions.AddRange(suggestions);
        }

        // Sort by confidence and store in database
        var sortedSuggestions = allSuggestions
            .OrderByDescending(s => s.Confidence)
            .ToList();

        // Store suggestions in database for later retrieval
        await StoreSuggestionsAsync(sortedSuggestions, cancellationToken);

        return sortedSuggestions;
    }

    public async Task<PaymentAllocationResult> ConfirmMatchAsync(
        string bankTransactionId,
        string invoiceId,
        decimal amount,
        string matchMethod,
        string userId,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // Validate bank transaction
            var transaction = await bankTransactionRepo.GetByIdAsync(bankTransactionId, cancellationToken);
            if (transaction == null)
            {
                return PaymentAllocationResult.Failed("TRANSACTION_NOT_FOUND", "Bank transaction not found");
            }

            if (transaction.Status != "pending")
            {
                return PaymentAllocationResult.Failed("TRANSACTION_NOT_PENDING",
                    $"Bank transaction is not pending (status: {transaction.Status})");
            }

            // Validate invoice
            var invoice = await invoiceRepo.GetByIdAsync(invoiceId, cancellationToken);
            if (invoice == null)
            {
                return PaymentAllocationResult.Failed("INVOICE_NOT_FOUND", "Invoice not found");
            }

            if (invoice.Status != "sent" && invoice.Status != "partially_paid")
            {
                return PaymentAllocationResult.Failed("INVOICE_NOT_PAYABLE",
                    $"Invoice cannot receive payment (status: {invoice.Status})");
            }

            if (amount > invoice.AmountRemaining)
            {
                return PaymentAllocationResult.Failed("AMOUNT_EXCEEDS_REMAINING",
                    $"Amount ({amount:N2}) exceeds remaining ({invoice.AmountRemaining:N2})");
            }

            // Get bank account for posting
            var bankAccountId = transaction.BankAccountId;
            if (string.IsNullOrEmpty(bankAccountId))
            {
                return PaymentAllocationResult.Failed("NO_BANK_ACCOUNT", "Bank transaction has no associated account");
            }

            // Post payment to ledger
            var postingResult = await invoicePostingService.PostPaymentAsync(
                invoiceId,
                invoice.CustomerId,
                bankAccountId,
                amount,
                invoice.FiscalYearId!,
                transaction.Reference,
                cancellationToken);

            if (!postingResult.Success)
            {
                return PaymentAllocationResult.Failed(
                    postingResult.ErrorCode ?? "POSTING_FAILED",
                    postingResult.ErrorMessage ?? "Failed to post payment");
            }

            // Create allocation record
            var allocationId = Guid.NewGuid().ToString();
            await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);

            await connection.ExecuteAsync("""
                INSERT INTO payment_allocations (
                    id, company_id, bank_transaction_id, invoice_id, amount,
                    allocation_type, match_method, ledger_transaction_id, created_by
                ) VALUES (
                    @Id, @CompanyId, @BankTransactionId, @InvoiceId, @Amount,
                    'payment', @MatchMethod, @LedgerTransactionId, @CreatedBy
                )
                """, new
            {
                Id = allocationId,
                CompanyId = transaction.CompanyId,
                BankTransactionId = bankTransactionId,
                InvoiceId = invoiceId,
                Amount = amount,
                MatchMethod = matchMethod,
                LedgerTransactionId = postingResult.TransactionId,
                CreatedBy = userId
            });

            // Mark bank transaction as booked (with journal entry draft ID for tracking)
            await bankTransactionRepo.UpdateStatusAsync(bankTransactionId, "booked", null, cancellationToken);

            // Update suggestion status if exists
            await connection.ExecuteAsync("""
                UPDATE suggested_payment_matches
                SET status = 'accepted', reviewed_at = NOW(), reviewed_by = @UserId
                WHERE bank_transaction_id = @BankTransactionId
                AND invoice_id = @InvoiceId
                AND status = 'pending'
                """, new { BankTransactionId = bankTransactionId, InvoiceId = invoiceId, UserId = userId });

            logger.LogInformation(
                "Confirmed match: Bank transaction {TransactionId} -> Invoice {InvoiceId}, Amount: {Amount:N2}",
                bankTransactionId, invoiceId, amount);

            return PaymentAllocationResult.Succeeded(new PaymentAllocation
            {
                Id = allocationId,
                CompanyId = transaction.CompanyId,
                BankTransactionId = bankTransactionId,
                InvoiceId = invoiceId,
                Amount = amount,
                AllocationType = "payment",
                MatchMethod = matchMethod,
                LedgerTransactionId = postingResult.TransactionId,
                CreatedAt = DateTimeOffset.UtcNow,
                CreatedBy = userId
            });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error confirming match for transaction {TransactionId}", bankTransactionId);
            return PaymentAllocationResult.Failed("CONFIRM_FAILED", ex.Message);
        }
    }

    public async Task RejectMatchAsync(
        string bankTransactionId,
        string invoiceId,
        string userId,
        CancellationToken cancellationToken = default)
    {
        await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);

        await connection.ExecuteAsync("""
            UPDATE suggested_payment_matches
            SET status = 'rejected', reviewed_at = NOW(), reviewed_by = @UserId
            WHERE bank_transaction_id = @BankTransactionId
            AND invoice_id = @InvoiceId
            AND status = 'pending'
            """, new { BankTransactionId = bankTransactionId, InvoiceId = invoiceId, UserId = userId });

        logger.LogInformation(
            "Rejected match: Bank transaction {TransactionId} -> Invoice {InvoiceId}",
            bankTransactionId, invoiceId);
    }

    public async Task<bool> RemoveAllocationAsync(
        string allocationId,
        string userId,
        CancellationToken cancellationToken = default)
    {
        await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);

        // Get allocation details first
        var allocation = await connection.QuerySingleOrDefaultAsync<PaymentAllocation>("""
            SELECT id AS Id, company_id AS CompanyId, bank_transaction_id AS BankTransactionId,
                   invoice_id AS InvoiceId, amount AS Amount
            FROM payment_allocations
            WHERE id = @AllocationId
            """, new { AllocationId = allocationId });

        if (allocation == null)
        {
            return false;
        }

        // Delete allocation
        await connection.ExecuteAsync(
            "DELETE FROM payment_allocations WHERE id = @AllocationId",
            new { AllocationId = allocationId });

        // Mark bank transaction as pending again
        if (!string.IsNullOrEmpty(allocation.BankTransactionId))
        {
            await bankTransactionRepo.UpdateStatusAsync(allocation.BankTransactionId, "pending", null, cancellationToken);
        }

        logger.LogInformation(
            "Removed allocation {AllocationId}: Bank transaction {TransactionId} -> Invoice {InvoiceId}",
            allocationId, allocation.BankTransactionId, allocation.InvoiceId);

        return true;
    }

    public async Task<IReadOnlyList<SuggestedPaymentMatch>> GetPendingSuggestionsAsync(
        string companyId,
        CancellationToken cancellationToken = default)
    {
        await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);

        var results = await connection.QueryAsync<SuggestedPaymentMatchDto>("""
            SELECT
                spm.id, spm.company_id, spm.bank_transaction_id, spm.invoice_id,
                spm.confidence, spm.match_reasons, spm.suggested_amount, spm.status, spm.created_at,
                bt.amount AS bank_amount, bt.booking_date AS bank_date,
                bt.description AS bank_description, bt.counterparty_name AS bank_counterparty,
                inv.invoice_number, inv.customer_name, inv.amount_remaining AS invoice_remaining,
                inv.due_date AS invoice_due_date
            FROM suggested_payment_matches spm
            JOIN bank_transactions bt ON bt.id = spm.bank_transaction_id
            JOIN invoice_read_models inv ON inv.aggregate_id = spm.invoice_id
            WHERE spm.company_id = @CompanyId
            AND spm.status = 'pending'
            ORDER BY spm.confidence DESC
            """, new { CompanyId = companyId });

        return results.Select(MapToSuggestedMatch).ToList();
    }

    public async Task<IReadOnlyList<PaymentAllocation>> GetAllocationsForInvoiceAsync(
        string invoiceId,
        CancellationToken cancellationToken = default)
    {
        await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);

        var results = await connection.QueryAsync<PaymentAllocation>("""
            SELECT
                id AS Id, company_id AS CompanyId, bank_transaction_id AS BankTransactionId,
                invoice_id AS InvoiceId, credit_note_id AS CreditNoteId, amount AS Amount,
                allocation_type AS AllocationType, match_method AS MatchMethod,
                match_confidence AS MatchConfidence, ledger_transaction_id AS LedgerTransactionId,
                created_at AS CreatedAt, created_by AS CreatedBy
            FROM payment_allocations
            WHERE invoice_id = @InvoiceId
            ORDER BY created_at DESC
            """, new { InvoiceId = invoiceId });

        return results.ToList();
    }

    #region Private Methods

    private static decimal CalculateAmountScore(decimal transactionAmount, decimal invoiceRemaining)
    {
        if (transactionAmount == invoiceRemaining)
        {
            return ExactAmountScore;
        }

        // Allow 5% tolerance for close matches
        var tolerance = invoiceRemaining * 0.05m;
        if (Math.Abs(transactionAmount - invoiceRemaining) <= tolerance)
        {
            return CloseAmountScore;
        }

        // Partial payment matching
        if (transactionAmount < invoiceRemaining && transactionAmount > 0)
        {
            return CloseAmountScore * 0.5m; // Lower score for partial
        }

        return 0;
    }

    private decimal CalculateReferenceScore(string? reference, string? description, string invoiceNumber)
    {
        if (string.IsNullOrWhiteSpace(invoiceNumber))
            return 0;

        var searchText = $"{reference} {description}".ToUpperInvariant();

        // Look for exact invoice number
        if (searchText.Contains(invoiceNumber.ToUpperInvariant()))
        {
            return ReferenceMatchScore;
        }

        // Look for invoice number without prefix (e.g., "2024-0001" instead of "INV-2024-0001")
        var numberMatch = InvoiceNumberRegex().Match(invoiceNumber);
        if (numberMatch.Success)
        {
            var shortNumber = numberMatch.Groups[1].Value;
            if (searchText.Contains(shortNumber))
            {
                return ReferenceMatchScore * 0.8m;
            }
        }

        return 0;
    }

    private static decimal CalculateNameScore(string? counterpartyName, string customerName)
    {
        if (string.IsNullOrWhiteSpace(counterpartyName) || string.IsNullOrWhiteSpace(customerName))
            return 0;

        var normalizedCounterparty = NormalizeName(counterpartyName);
        var normalizedCustomer = NormalizeName(customerName);

        // Exact match
        if (normalizedCounterparty == normalizedCustomer)
        {
            return NameMatchScore;
        }

        // Contains match
        if (normalizedCounterparty.Contains(normalizedCustomer) ||
            normalizedCustomer.Contains(normalizedCounterparty))
        {
            return NameMatchScore * 0.8m;
        }

        // Word overlap
        var counterpartyWords = normalizedCounterparty.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        var customerWords = normalizedCustomer.Split(' ', StringSplitOptions.RemoveEmptyEntries);

        var overlap = counterpartyWords.Intersect(customerWords).Count();
        if (overlap > 0)
        {
            var overlapRatio = (decimal)overlap / Math.Max(counterpartyWords.Length, customerWords.Length);
            return NameMatchScore * overlapRatio * 0.6m;
        }

        return 0;
    }

    private static string NormalizeName(string name)
    {
        // Remove common suffixes and normalize
        return name
            .ToUpperInvariant()
            .Replace("APS", "")
            .Replace("A/S", "")
            .Replace("ApS", "")
            .Replace("IVS", "")
            .Replace("K/S", "")
            .Trim();
    }

    private async Task StoreSuggestionsAsync(
        IReadOnlyList<SuggestedPaymentMatch> suggestions,
        CancellationToken cancellationToken)
    {
        if (suggestions.Count == 0) return;

        await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);

        foreach (var suggestion in suggestions)
        {
            await connection.ExecuteAsync("""
                INSERT INTO suggested_payment_matches (
                    id, company_id, bank_transaction_id, invoice_id,
                    confidence, match_reasons, suggested_amount, status
                ) VALUES (
                    @Id, @CompanyId, @BankTransactionId, @InvoiceId,
                    @Confidence, @MatchReasons::jsonb, @SuggestedAmount, 'pending'
                )
                ON CONFLICT (bank_transaction_id, invoice_id) WHERE status = 'pending'
                DO UPDATE SET
                    confidence = EXCLUDED.confidence,
                    match_reasons = EXCLUDED.match_reasons,
                    suggested_amount = EXCLUDED.suggested_amount
                """, new
            {
                suggestion.Id,
                suggestion.CompanyId,
                suggestion.BankTransactionId,
                suggestion.InvoiceId,
                suggestion.Confidence,
                MatchReasons = JsonSerializer.Serialize(suggestion.MatchReasons),
                suggestion.SuggestedAmount
            });
        }
    }

    private static SuggestedPaymentMatch MapToSuggestedMatch(SuggestedPaymentMatchDto dto)
    {
        var reasons = string.IsNullOrEmpty(dto.MatchReasons)
            ? []
            : JsonSerializer.Deserialize<List<MatchReason>>(dto.MatchReasons) ?? [];

        return new SuggestedPaymentMatch
        {
            Id = dto.Id,
            CompanyId = dto.CompanyId,
            BankTransactionId = dto.BankTransactionId,
            InvoiceId = dto.InvoiceId,
            BankTransactionAmount = dto.BankAmount,
            BankTransactionDate = dto.BankDate.HasValue
                ? DateOnly.FromDateTime(dto.BankDate.Value)
                : DateOnly.FromDateTime(DateTime.Today),
            BankTransactionDescription = dto.BankDescription,
            BankTransactionCounterparty = dto.BankCounterparty,
            InvoiceNumber = dto.InvoiceNumber,
            CustomerName = dto.CustomerName,
            InvoiceAmountRemaining = dto.InvoiceRemaining,
            InvoiceDueDate = dto.InvoiceDueDate.HasValue
                ? DateOnly.FromDateTime(dto.InvoiceDueDate.Value)
                : null,
            Confidence = dto.Confidence,
            SuggestedAmount = dto.SuggestedAmount,
            MatchReasons = reasons,
            Status = dto.Status,
            CreatedAt = dto.CreatedAt
        };
    }

    [GeneratedRegex(@"(\d{4}-\d+)")]
    private static partial Regex InvoiceNumberRegex();

    #endregion
}

internal class SuggestedPaymentMatchDto
{
    public string Id { get; set; } = string.Empty;
    public string CompanyId { get; set; } = string.Empty;
    public string BankTransactionId { get; set; } = string.Empty;
    public string InvoiceId { get; set; } = string.Empty;
    public decimal Confidence { get; set; }
    public string? MatchReasons { get; set; }
    public decimal SuggestedAmount { get; set; }
    public string Status { get; set; } = "pending";
    public DateTimeOffset CreatedAt { get; set; }

    // Bank transaction fields
    public decimal BankAmount { get; set; }
    public DateTime? BankDate { get; set; }
    public string? BankDescription { get; set; }
    public string? BankCounterparty { get; set; }

    // Invoice fields
    public string InvoiceNumber { get; set; } = string.Empty;
    public string CustomerName { get; set; } = string.Empty;
    public decimal InvoiceRemaining { get; set; }
    public DateTime? InvoiceDueDate { get; set; }
}
