using Books.Api.EventFlow.ReadModels;
using Books.Api.EventFlow.Repositories;
using Books.Api.Saft.Models;
using Ledger.Core.Models;
using Ledger.Core.Services;
using Microsoft.Extensions.Logging;

namespace Books.Api.Saft.Services;

/// <summary>
/// Service for generating SAF-T (Standard Audit File for Tax) exports.
/// Implements the Danish SAF-T DK standard based on OECD SAF-T 2.0.
/// </summary>
public class SaftExportService(
    ICompanyRepository companyRepository,
    IFiscalYearRepository fiscalYearRepository,
    IAccountRepository accountRepository,
    ICustomerRepository customerRepository,
    ILedgerService ledgerService,
    ILogger<SaftExportService> logger) : ISaftExportService
{
    private const string AuditFileVersion = "1.0";
    private const string SoftwareCompanyName = "Books ApS";
    private const string SoftwareId = "books-api";
    private const string SoftwareVersion = "1.0.0";

    public async Task<SaftExportResult> GenerateAsync(
        string companyId,
        string fiscalYearId,
        CancellationToken ct = default)
    {
        try
        {
            logger.LogInformation(
                "Generating SAF-T export for company {CompanyId}, fiscal year {FiscalYearId}",
                companyId, fiscalYearId);

            // 1. Load company data
            var company = await companyRepository.GetByIdAsync(companyId, ct);
            if (company == null)
            {
                return SaftExportResult.Error("COMPANY_NOT_FOUND", $"Company '{companyId}' not found");
            }

            // Validate CVR number (required for SAF-T DK)
            if (string.IsNullOrWhiteSpace(company.Cvr) || !IsValidCvr(company.Cvr))
            {
                return SaftExportResult.Error("INVALID_CVR",
                    "Et gyldigt CVR-nummer (8 cifre) er påkrævet for SAF-T eksport. Opdater venligst virksomhedens CVR-nummer i indstillinger.");
            }

            // 2. Load fiscal year
            var fiscalYear = await fiscalYearRepository.GetByIdAsync(fiscalYearId, ct);
            if (fiscalYear == null)
            {
                return SaftExportResult.Error("FISCAL_YEAR_NOT_FOUND", $"Fiscal year '{fiscalYearId}' not found");
            }

            if (fiscalYear.CompanyId != companyId)
            {
                return SaftExportResult.Error("FISCAL_YEAR_MISMATCH",
                    "Fiscal year does not belong to the specified company");
            }

            // 3. Load accounts
            var accounts = await accountRepository.GetByCompanyIdAsync(companyId, ct);

            // 4. Load customers
            var customers = await customerRepository.GetByCompanyIdAsync(companyId, ct);

            // 5. Query ledger entries for the fiscal year period
            var periodStart = new DateTimeOffset(fiscalYear.StartDate);
            var periodEnd = new DateTimeOffset(fiscalYear.EndDate.AddDays(1).AddTicks(-1)); // End of day

            var accountGuids = accounts
                .Select(a => ParseAccountGuid(a.Id))
                .Where(g => g.HasValue)
                .Select(g => g!.Value)
                .ToList();

            var ledgerEntries = new List<LedgerEntry>();
            var accountBalances = new Dictionary<Guid, AccountPeriodBalance>();
            var openingBalances = new Dictionary<Guid, AccountPeriodBalance>();

            if (accountGuids.Any())
            {
                // Query individual entries (Aggregate=false)
                var entriesQuery = new EntriesQuery
                {
                    AccountIds = accountGuids,
                    From = periodStart,
                    To = periodEnd,
                    Aggregate = false,
                    Limit = 100000 // High limit for full export
                };

                var entriesResult = await ledgerService.QueryEntriesAsync(entriesQuery, ct);
                ledgerEntries = entriesResult.Entries?.ToList() ?? [];

                // Query aggregates for closing balances (within period)
                var aggregatesQuery = new EntriesQuery
                {
                    AccountIds = accountGuids,
                    From = periodStart,
                    To = periodEnd,
                    Aggregate = true
                };

                var aggregatesResult = await ledgerService.QueryEntriesAsync(aggregatesQuery, ct);
                accountBalances = aggregatesResult.Aggregates?
                    .ToDictionary(a => a.AccountId, a => a)
                    ?? [];

                // Query historical aggregates for opening balances (all transactions BEFORE period start)
                var openingQuery = new EntriesQuery
                {
                    AccountIds = accountGuids,
                    To = periodStart.AddTicks(-1), // End just before period starts
                    Aggregate = true
                };

                var openingResult = await ledgerService.QueryEntriesAsync(openingQuery, ct);
                openingBalances = openingResult.Aggregates?
                    .ToDictionary(a => a.AccountId, a => a)
                    ?? [];
            }

            // 6. Build SAF-T document
            var document = BuildSaftDocument(
                company,
                fiscalYear,
                accounts.ToList(),
                customers.ToList(),
                ledgerEntries,
                accountBalances,
                openingBalances);

            // 7. Generate XML
            var xmlBuilder = new SaftXmlBuilder();
            var xmlContent = xmlBuilder.Build(document);

            // 8. Generate filename
            var fileName = GenerateFileName(company, fiscalYear);

            logger.LogInformation(
                "SAF-T export generated successfully: {FileName}, {AccountCount} accounts, {EntryCount} entries",
                fileName, accounts.Count(), ledgerEntries.Count);

            return SaftExportResult.Ok(xmlContent, fileName);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to generate SAF-T export for company {CompanyId}", companyId);
            return SaftExportResult.Error("EXPORT_FAILED", ex.Message);
        }
    }

    private SaftDocument BuildSaftDocument(
        CompanyReadModelDto company,
        FiscalYearReadModelDto fiscalYear,
        List<AccountReadModelDto> accounts,
        List<CustomerReadModelDto> customers,
        List<LedgerEntry> ledgerEntries,
        Dictionary<Guid, AccountPeriodBalance> accountBalances,
        Dictionary<Guid, AccountPeriodBalance> openingBalances)
    {
        var header = BuildHeader(company, fiscalYear);
        var masterFiles = BuildMasterFiles(accounts, customers, accountBalances, openingBalances);
        var entries = BuildGeneralLedgerEntries(ledgerEntries, accounts);

        return new SaftDocument(header, masterFiles, entries);
    }

    private SaftHeader BuildHeader(CompanyReadModelDto company, FiscalYearReadModelDto fiscalYear)
    {
        var address = new SaftAddress(
            company.Address,
            company.City,
            company.PostalCode,
            company.Country);

        var saftCompany = new SaftCompany(
            company.Cvr ?? "",
            company.Name,
            address,
            null); // Contact info not stored yet

        var selectionCriteria = new SaftSelectionCriteria(
            fiscalYear.StartDate.ToString("yyyy-MM-dd"),
            fiscalYear.EndDate.ToString("yyyy-MM-dd"));

        return new SaftHeader(
            AuditFileVersion,
            DateTime.UtcNow.ToString("yyyy-MM-dd"),
            SoftwareCompanyName,
            SoftwareId,
            SoftwareVersion,
            saftCompany,
            selectionCriteria);
    }

    private SaftMasterFiles BuildMasterFiles(
        List<AccountReadModelDto> accounts,
        List<CustomerReadModelDto> customers,
        Dictionary<Guid, AccountPeriodBalance> accountBalances,
        Dictionary<Guid, AccountPeriodBalance> openingBalances)
    {
        var saftAccounts = accounts.Select(acc =>
        {
            var guid = ParseAccountGuid(acc.Id);
            accountBalances.TryGetValue(guid ?? Guid.Empty, out var periodBalance);
            openingBalances.TryGetValue(guid ?? Guid.Empty, out var openingBalance);

            // Opening balance = net balance from all transactions before period start
            // Net balance is calculated as: Debits - Credits for Asset/Expense, Credits - Debits for Liability/Equity/Income
            var openingDebit = openingBalance.TotalDebits;
            var openingCredit = openingBalance.TotalCredits;

            // Closing balance = Opening balance + Period movements
            var closingDebit = openingDebit + periodBalance.TotalDebits;
            var closingCredit = openingCredit + periodBalance.TotalCredits;

            return new SaftAccount(
                acc.AccountNumber,
                acc.Name,
                acc.StandardAccountNumber,
                MapAccountType(acc.AccountType),
                openingDebit,
                openingCredit,
                closingDebit,
                closingCredit);
        }).ToList();

        var saftCustomers = customers.Select(cust =>
        {
            var address = new SaftAddress(
                cust.Address,
                cust.City,
                cust.PostalCode,
                cust.Country);

            var contact = new SaftContact(
                cust.Phone,
                cust.Email,
                null);

            return new SaftCustomer(
                cust.CustomerNumber,
                null, // AccountID - could map to sub-ledger
                cust.Name,
                cust.Cvr,
                address,
                contact);
        }).ToList();

        // Suppliers not implemented yet
        var suppliers = new List<SaftSupplier>();

        return new SaftMasterFiles(saftAccounts, saftCustomers, suppliers);
    }

    private SaftGeneralLedgerEntries BuildGeneralLedgerEntries(
        List<LedgerEntry> ledgerEntries,
        List<AccountReadModelDto> accounts)
    {
        // Group entries by End2EndId (transaction grouping)
        var transactionGroups = ledgerEntries
            .GroupBy(e => e.End2EndId)
            .OrderBy(g => g.Min(e => e.CreatedAt));

        var accountLookup = accounts.ToDictionary(
            a => ParseAccountGuid(a.Id) ?? Guid.Empty,
            a => a.AccountNumber);

        var journals = new List<SaftJournal>();
        var transactions = new List<SaftTransaction>();
        var totalDebit = 0m;
        var totalCredit = 0m;

        foreach (var group in transactionGroups)
        {
            var firstEntry = group.First();
            var transactionDate = firstEntry.CreatedAt;
            var period = transactionDate.Month.ToString("D2");

            var lines = group.Select((entry, idx) =>
            {
                accountLookup.TryGetValue(entry.AccountId, out var accountNumber);

                var isDebit = entry.EntryType.Equals("Debit", StringComparison.OrdinalIgnoreCase);
                var debitAmount = isDebit ? entry.Amount : (decimal?)null;
                var creditAmount = !isDebit ? entry.Amount : (decimal?)null;

                if (isDebit) totalDebit += entry.Amount;
                else totalCredit += entry.Amount;

                return new SaftTransactionLine(
                    (idx + 1).ToString(),
                    accountNumber ?? entry.AccountId.ToString(),
                    entry.Description,
                    debitAmount,
                    creditAmount,
                    null, // CustomerID - could parse from reference
                    null, // SupplierID
                    null); // TaxInfo - would need VAT code tracking
            }).ToList();

            transactions.Add(new SaftTransaction(
                group.Key,
                period,
                transactionDate.ToString("yyyy-MM-dd"),
                firstEntry.Description ?? "Postering",
                transactionDate.ToString("yyyy-MM-ddTHH:mm:ss"),
                transactionDate.ToString("yyyy-MM-dd"),
                lines));
        }

        // Single journal for general ledger
        if (transactions.Any())
        {
            journals.Add(new SaftJournal(
                "GL",
                "Hovedbog",
                "GL",
                transactions));
        }

        return new SaftGeneralLedgerEntries(
            ledgerEntries.Count,
            totalDebit,
            totalCredit,
            journals);
    }

    /// <summary>
    /// Maps internal account types to SAF-T standard account classifications.
    /// Note: The "financial" type is ambiguous in SAF-T mapping. Financial accounts
    /// can represent either income (e.g., interest income, account 8000-8499) or
    /// expense (e.g., interest expense, account 8500-8999). Without the account
    /// number or balance direction, we cannot determine the correct mapping.
    /// A future improvement should inspect the account number range or actual
    /// balance direction to choose between "Income" and "Expense".
    /// </summary>
    private static string MapAccountType(string accountType)
    {
        return accountType.ToLowerInvariant() switch
        {
            "asset" => "Asset",
            "liability" => "Liability",
            "equity" => "Equity",
            "revenue" => "Income",
            "cogs" => "Expense",
            "expense" => "Expense",
            "personnel" => "Expense",
            // Financial accounts are ambiguous: could be income (8000-8499) or expense (8500-8999).
            // Defaulting to "Expense" is safer since most financial items are costs (interest, fees).
            // TODO: Determine mapping based on account number range or balance direction.
            "financial" => "Expense",
            "extraordinary" => "Expense",
            _ => "Asset"
        };
    }

    private static string GenerateFileName(CompanyReadModelDto company, FiscalYearReadModelDto fiscalYear)
    {
        // SAF-T DK naming convention: <CVR>_<YYYYMMDD>_<YYYYMMDD>_SAF-T.xml
        var cvr = company.Cvr ?? "00000000";
        var startDate = fiscalYear.StartDate.ToString("yyyyMMdd");
        var endDate = fiscalYear.EndDate.ToString("yyyyMMdd");
        return $"{cvr}_{startDate}_{endDate}_SAF-T.xml";
    }

    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>
    /// Validates a Danish CVR number.
    /// A valid CVR is exactly 8 digits.
    /// </summary>
    private static bool IsValidCvr(string cvr)
    {
        if (string.IsNullOrWhiteSpace(cvr))
            return false;

        // Remove any spaces or dashes
        var cleanCvr = cvr.Replace(" ", "").Replace("-", "");

        // Must be exactly 8 digits
        return cleanCvr.Length == 8 && cleanCvr.All(char.IsDigit);
    }
}
