using Books.Api.Domain.JournalEntryDrafts.Events;
using EventFlow.Aggregates;

namespace Books.Api.Domain.JournalEntryDrafts;

public class JournalEntryDraftAggregate(JournalEntryDraftId id)
    : AggregateRoot<JournalEntryDraftAggregate, JournalEntryDraftId>(id),
    IEmit<JournalEntryDraftCreatedEvent>,
    IEmit<JournalEntryDraftUpdatedEvent>,
    IEmit<JournalEntryDraftPostedEvent>,
    IEmit<JournalEntryDraftDiscardedEvent>
{
    /// <summary>
    /// Tolerance for floating-point rounding when comparing debit/credit totals.
    /// </summary>
    private const decimal BalanceTolerance = 0.01m;

    private bool _isCreated;
    private DraftStatus _status = DraftStatus.Active;
    private string _companyId = string.Empty;
    private string _voucherNumber = string.Empty;
    private string? _fiscalYearId;
    private List<DraftLine> _lines = [];

    public string CompanyId => _companyId;
    public string VoucherNumber => _voucherNumber;

    /// <summary>
    /// The fiscal year ID assigned during the last update.
    /// </summary>
    public string? FiscalYearId => _fiscalYearId;

    /// <summary>
    /// The current draft lines (populated from the last update event).
    /// </summary>
    public IReadOnlyList<DraftLine> Lines => _lines.AsReadOnly();

    #region Apply Methods

    public void Apply(JournalEntryDraftCreatedEvent e)
    {
        _isCreated = true;
        _companyId = e.CompanyId;
        _voucherNumber = e.VoucherNumber;
        _status = DraftStatus.Active;
    }

    public void Apply(JournalEntryDraftUpdatedEvent e)
    {
        _fiscalYearId = e.FiscalYearId;
        _lines = e.Lines.ToList();
    }

    public void Apply(JournalEntryDraftPostedEvent e)
    {
        _status = DraftStatus.Posted;
    }

    public void Apply(JournalEntryDraftDiscardedEvent e)
    {
        _status = DraftStatus.Discarded;
    }

    #endregion

    #region Command Methods

    /// <summary>
    /// Creates a new journal entry draft with auto-generated voucher number.
    /// </summary>
    /// <param name="companyId">Company ID</param>
    /// <param name="name">Draft name</param>
    /// <param name="createdBy">User who created the draft</param>
    /// <param name="voucherNumber">Bilagsnummer - required by Bogføringsloven § 7</param>
    /// <param name="extractionData">Full AI extraction data as JSON string (optional)</param>
    public void Create(string companyId, string name, string createdBy, string voucherNumber, string? extractionData = null)
    {
        if (_isCreated)
            throw new DomainException(
                "DRAFT_ALREADY_EXISTS",
                "Journal entry draft already exists",
                "Kassekladden eksisterer allerede");

        if (string.IsNullOrWhiteSpace(companyId))
            throw new DomainException(
                "COMPANY_ID_REQUIRED",
                "Company ID is required",
                "Virksomheds-ID er påkrævet");

        if (string.IsNullOrWhiteSpace(name))
            throw new DomainException(
                "DRAFT_NAME_REQUIRED",
                "Draft name is required",
                "Kladdenavn er påkrævet");

        if (string.IsNullOrWhiteSpace(createdBy))
            throw new DomainException(
                "CREATED_BY_REQUIRED",
                "Created by is required",
                "Oprettet af er påkrævet");

        if (string.IsNullOrWhiteSpace(voucherNumber))
            throw new DomainException(
                "VOUCHER_NUMBER_REQUIRED",
                "Voucher number (bilagsnummer) is required by Danish accounting law",
                "Bilagsnummer er påkrævet iht. Bogføringsloven");

        Emit(new JournalEntryDraftCreatedEvent(
            companyId.Trim(),
            name.Trim(),
            createdBy,
            voucherNumber.Trim(),
            extractionData));
    }

    /// <summary>
    /// Updates a journal entry draft (auto-save).
    /// Validates that each line has either DebitAmount or CreditAmount (not both),
    /// and that VAT codes are valid.
    /// </summary>
    /// <param name="name">Draft name</param>
    /// <param name="documentDate">Bilagsdato - the date of the transaction/document (e.g., invoice date)</param>
    /// <param name="description">Entry description</param>
    /// <param name="fiscalYearId">Fiscal year ID</param>
    /// <param name="lines">Posting lines with VAT codes</param>
    /// <param name="attachmentIds">References to attached documents (bilag)</param>
    public void Update(
        string? name,
        DateOnly? documentDate,
        string? description,
        string? fiscalYearId,
        List<DraftLine> lines,
        List<string>? attachmentIds = null)
    {
        EnsureCanModify();

        // Validate VAT codes if provided
        foreach (var line in lines.Where(l => !string.IsNullOrEmpty(l.VatCode)))
        {
            if (!VatCodes.IsValid(line.VatCode))
            {
                throw new DomainException(
                    "INVALID_VAT_CODE",
                    $"Invalid VAT code '{line.VatCode}' on line {line.LineNumber}",
                    $"Ugyldig momskode '{line.VatCode}' på linje {line.LineNumber}");
            }
        }

        // Validate individual lines: cannot have both debit and credit amounts
        foreach (var line in lines)
        {
            ValidateDraftLine(line);
        }

        Emit(new JournalEntryDraftUpdatedEvent(
            name?.Trim(),
            documentDate,
            description?.Trim(),
            fiscalYearId,
            lines,
            attachmentIds));
    }

    /// <summary>
    /// Marks the draft as posted after validation.
    /// Enforces double-entry bookkeeping: total debits must equal total credits.
    /// Requires at least 2 lines with valid account IDs.
    /// </summary>
    /// <param name="transactionId">The ledger transaction ID</param>
    /// <param name="postedBy">User who posted the draft</param>
    public void MarkPosted(string transactionId, string postedBy)
    {
        EnsureCanModify();

        if (string.IsNullOrWhiteSpace(transactionId))
            throw new DomainException(
                "TRANSACTION_ID_REQUIRED",
                "Transaction ID is required",
                "Transaktions-ID er påkrævet");

        if (string.IsNullOrWhiteSpace(postedBy))
            throw new DomainException(
                "POSTED_BY_REQUIRED",
                "Posted by is required",
                "Bogført af er påkrævet");

        // Validate minimum number of lines for double-entry bookkeeping
        if (_lines.Count < 2)
            throw new DomainException(
                "INSUFFICIENT_LINES",
                "A journal entry must have at least 2 lines for double-entry bookkeeping",
                "En postering skal have mindst 2 linjer for dobbelt bogholderi");

        // Validate all lines have account IDs assigned
        var linesWithoutAccounts = _lines.Where(l => string.IsNullOrWhiteSpace(l.AccountId)).ToList();
        if (linesWithoutAccounts.Count > 0)
        {
            var lineNumbers = string.Join(", ", linesWithoutAccounts.Select(l => l.LineNumber));
            throw new DomainException(
                "ACCOUNT_REQUIRED",
                $"All lines must have an account. Lines without account: {lineNumbers}",
                $"Alle linjer skal have en konto. Linjer uden konto: {lineNumbers}");
        }

        // Validate debit/credit balance (fundamental double-entry accounting principle)
        var totalDebit = _lines.Sum(l => l.DebitAmount);
        var totalCredit = _lines.Sum(l => l.CreditAmount);

        if (Math.Abs(totalDebit - totalCredit) > BalanceTolerance)
            throw new DomainException(
                "UNBALANCED_ENTRY",
                $"Total debits must equal credits. Debit: {totalDebit:N2}, Credit: {totalCredit:N2}",
                $"Debet og kredit skal balancere. Debet: {totalDebit:N2}, Kredit: {totalCredit:N2}");

        Emit(new JournalEntryDraftPostedEvent(transactionId, postedBy, DateTimeOffset.UtcNow));
    }

    public void Discard(string discardedBy)
    {
        EnsureCanModify();

        if (string.IsNullOrWhiteSpace(discardedBy))
            throw new DomainException(
                "DISCARDED_BY_REQUIRED",
                "Discarded by is required",
                "Kasseret af er påkrævet");

        Emit(new JournalEntryDraftDiscardedEvent(discardedBy));
    }

    #endregion

    #region Private Methods

    private void EnsureCanModify()
    {
        if (!_isCreated)
            throw new DomainException(
                "DRAFT_NOT_FOUND",
                "Journal entry draft does not exist",
                "Kassekladden findes ikke");

        if (_status == DraftStatus.Posted)
            throw new DomainException(
                "DRAFT_ALREADY_POSTED",
                "Journal entry draft has already been posted",
                "Kassekladden er allerede bogført");

        if (_status == DraftStatus.Discarded)
            throw new DomainException(
                "DRAFT_ALREADY_DISCARDED",
                "Journal entry draft has been discarded",
                "Kassekladden er blevet kasseret");
    }

    /// <summary>
    /// Validates a single draft line.
    /// A line cannot have both DebitAmount > 0 AND CreditAmount > 0.
    /// At least one of DebitAmount or CreditAmount must be > 0.
    /// </summary>
    private static void ValidateDraftLine(DraftLine line)
    {
        if (line.DebitAmount > 0 && line.CreditAmount > 0)
        {
            throw new DomainException(
                "INVALID_LINE_AMOUNTS",
                $"Line {line.LineNumber} cannot have both debit and credit amounts. Use separate lines.",
                $"Linje {line.LineNumber} kan ikke have både debet- og kreditbeløb. Brug separate linjer.");
        }

        if (line.DebitAmount <= 0 && line.CreditAmount <= 0)
        {
            throw new DomainException(
                "MISSING_LINE_AMOUNT",
                $"Line {line.LineNumber} must have either a debit or credit amount greater than zero",
                $"Linje {line.LineNumber} skal have enten et debet- eller kreditbeløb større end nul");
        }
    }

    #endregion
}
