using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using Books.Api.Domain.Invoices;
using Books.Api.Domain.Invoices.Events;
using EventFlow.Aggregates;
using EventFlow.PostgreSql.ReadStores.Attributes;
using EventFlow.ReadStores;

namespace Books.Api.EventFlow.ReadModels;

[Table("invoice_read_models")]
public class InvoiceReadModel : IReadModel,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceCreatedEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceLineAddedEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceLineUpdatedEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceLineRemovedEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceSentEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoicePaymentReceivedEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceVoidedEvent>,
    IAmReadModelFor<InvoiceAggregate, InvoiceId, InvoiceCreditAppliedEvent>
{
    // EventFlow standard columns
    [PostgreSqlReadModelIdentityColumn]
    public string AggregateId { get; set; } = string.Empty;

    public DateTimeOffset CreateTime { get; set; }
    public DateTimeOffset UpdateTime { get; set; }

    [PostgreSqlReadModelVersionColumn]
    public int LastAggregateSequenceNumber { get; set; }

    // Company and fiscal year
    public string CompanyId { get; set; } = string.Empty;
    public string? FiscalYearId { get; set; }

    // Customer reference
    public string CustomerId { get; set; } = string.Empty;
    public string CustomerName { get; set; } = string.Empty;
    public string CustomerNumber { get; set; } = string.Empty;

    // Invoice identification
    public string InvoiceNumber { get; set; } = string.Empty;
    public DateTime? InvoiceDate { get; set; }
    public DateTime? DueDate { get; set; }

    // Document type (invoice or credit_note)
    public string InvoiceType { get; set; } = "invoice";

    // Credit note specific fields
    public string? OriginalInvoiceId { get; set; }
    public string? OriginalInvoiceNumber { get; set; }
    public string? CreditReason { get; set; }

    // Status
    public string Status { get; set; } = "draft";

    // Amounts
    public decimal AmountExVat { get; set; }
    public decimal AmountVat { get; set; }
    public decimal AmountTotal { get; set; }
    public decimal AmountPaid { get; set; }
    public decimal AmountApplied { get; set; } // For credit notes: total applied
    public decimal AmountRemaining { get; set; }

    // Currency and VAT
    public string Currency { get; set; } = "DKK";
    public string? VatCode { get; set; }

    // Payment terms
    public int PaymentTermsDays { get; set; } = 30;

    // Lines (stored as JSON string - PostgreSQL column is text, not jsonb)
    public string Lines { get; set; } = "[]";

    // Ledger integration
    public string? LedgerTransactionId { get; set; }

    // Notes and reference
    public string? Notes { get; set; }
    public string? Reference { get; set; }

    // Status timestamps
    public DateTimeOffset? SentAt { get; set; }
    public DateTimeOffset? IssuedAt { get; set; } // For credit notes
    public DateTimeOffset? PaidAt { get; set; }
    public DateTimeOffset? VoidedAt { get; set; }
    public string? VoidedReason { get; set; }
    public string? VoidedBy { get; set; }

    // Audit
    public string CreatedBy { get; set; } = string.Empty;
    public string? UpdatedBy { get; set; }

    // Internal list for line tracking
    [NotMapped]
    private List<InvoiceLineDto>? _linesList;

    private List<InvoiceLineDto> GetLinesList()
    {
        if (_linesList == null)
        {
            _linesList = string.IsNullOrEmpty(Lines)
                ? []
                : JsonSerializer.Deserialize<List<InvoiceLineDto>>(Lines) ?? [];
        }
        return _linesList;
    }

    /// <summary>
    /// Returns the invoice lines for external access.
    /// </summary>
    public IReadOnlyList<InvoiceLineDto> GetLines() => GetLinesList();

    private void SaveLines()
    {
        Lines = JsonSerializer.Serialize(_linesList ?? []);
        RecalculateAmounts();
    }

    private void RecalculateAmounts()
    {
        var lines = GetLinesList();
        AmountExVat = lines.Sum(l => l.AmountExVat);
        AmountVat = lines.Sum(l => l.AmountVat);
        AmountTotal = lines.Sum(l => l.AmountTotal);

        // For credit notes, use AmountApplied; for invoices, use AmountPaid
        if (InvoiceType == "credit_note")
        {
            AmountRemaining = Math.Abs(AmountTotal) - AmountApplied;
        }
        else
        {
            AmountRemaining = AmountTotal - AmountPaid;
        }
    }

    #region Apply Methods

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceCreatedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        AggregateId = domainEvent.AggregateIdentity.Value;
        CreateTime = domainEvent.Timestamp;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;
        CompanyId = e.CompanyId;
        FiscalYearId = e.FiscalYearId;
        CustomerId = e.CustomerId;
        CustomerName = e.CustomerName;
        CustomerNumber = e.CustomerNumber;
        InvoiceNumber = e.InvoiceNumber;
        InvoiceDate = e.InvoiceDate.ToDateTime(TimeOnly.MinValue);
        DueDate = e.DueDate.ToDateTime(TimeOnly.MinValue);
        PaymentTermsDays = e.PaymentTermsDays;
        Currency = e.Currency;
        VatCode = e.VatCode;
        Notes = e.Notes;
        Reference = e.Reference;
        CreatedBy = e.CreatedBy;
        Status = "draft";

        // Credit note specific fields
        InvoiceType = e.Type.ToStringValue();
        OriginalInvoiceId = e.OriginalInvoiceId;
        OriginalInvoiceNumber = e.OriginalInvoiceNumber;
        CreditReason = e.CreditReason;

        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceLineAddedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;

        var lines = GetLinesList();
        lines.Add(new InvoiceLineDto
        {
            LineNumber = e.LineNumber,
            Description = e.Description,
            Quantity = e.Quantity,
            Unit = e.Unit,
            UnitPrice = e.UnitPrice,
            DiscountPercent = e.DiscountPercent,
            VatCode = e.VatCode,
            AccountId = e.AccountId
        });
        SaveLines();

        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceLineUpdatedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;

        var lines = GetLinesList();
        var existingIndex = lines.FindIndex(l => l.LineNumber == e.LineNumber);
        if (existingIndex >= 0)
        {
            lines[existingIndex] = new InvoiceLineDto
            {
                LineNumber = e.LineNumber,
                Description = e.Description,
                Quantity = e.Quantity,
                Unit = e.Unit,
                UnitPrice = e.UnitPrice,
                DiscountPercent = e.DiscountPercent,
                VatCode = e.VatCode,
                AccountId = e.AccountId
            };
        }
        SaveLines();

        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceLineRemovedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;

        var lines = GetLinesList();
        lines.RemoveAll(l => l.LineNumber == e.LineNumber);
        SaveLines();

        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceSentEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;

        // Credit notes are "issued", invoices are "sent"
        if (InvoiceType == "credit_note")
        {
            Status = "issued";
            IssuedAt = e.SentAt;
        }
        else
        {
            Status = "sent";
            SentAt = e.SentAt;
        }

        AmountExVat = e.AmountExVat;
        AmountVat = e.AmountVat;
        AmountTotal = e.AmountTotal;
        AmountRemaining = Math.Abs(e.AmountTotal); // Use absolute for remaining calculation
        LedgerTransactionId = e.LedgerTransactionId;
        UpdatedBy = e.SentBy;
        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoicePaymentReceivedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;
        AmountPaid = e.NewAmountPaid;
        AmountRemaining = e.NewAmountRemaining;
        Status = e.NewStatus.ToStringValue();
        UpdatedBy = e.RecordedBy;

        if (e.NewStatus == InvoiceStatus.Paid)
        {
            PaidAt = domainEvent.Timestamp;
        }

        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceVoidedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;
        Status = "voided";
        VoidedAt = e.VoidedAt;
        VoidedReason = e.Reason;
        VoidedBy = e.VoidedBy;
        return Task.CompletedTask;
    }

    public Task ApplyAsync(
        IReadModelContext context,
        IDomainEvent<InvoiceAggregate, InvoiceId, InvoiceCreditAppliedEvent> domainEvent,
        CancellationToken cancellationToken)
    {
        var e = domainEvent.AggregateEvent;
        UpdateTime = domainEvent.Timestamp;
        LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber;
        AmountApplied = e.NewAmountApplied;
        AmountRemaining = e.NewAmountRemaining;
        Status = e.NewStatus.ToStringValue();
        UpdatedBy = e.AppliedBy;
        return Task.CompletedTask;
    }

    #endregion
}

/// <summary>
/// DTO for invoice lines stored as JSON in the read model.
/// </summary>
public class InvoiceLineDto
{
    public int LineNumber { get; set; }
    public string Description { get; set; } = string.Empty;
    public decimal Quantity { get; set; }
    public string? Unit { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal DiscountPercent { get; set; }
    public string VatCode { get; set; } = "U25";
    public string? AccountId { get; set; }

    public decimal AmountExVat => Math.Round(UnitPrice * Quantity * (1 - DiscountPercent / 100m), 2);
    public decimal AmountVat => Math.Round(AmountExVat * GetVatRate(), 2);
    public decimal AmountTotal => AmountExVat + AmountVat;

    private decimal GetVatRate() => VatCode switch
    {
        "U25" or "I25" => 0.25m,
        "UEU" or "IEU" => 0m,
        "UEXP" or "IEXP" => 0m,
        "INGEN" => 0m,
        _ => 0.25m
    };
}
