using Books.Api.Domain.BankConnections.Events;
using EventFlow.Aggregates;
using EventFlow.Core;

namespace Books.Api.Domain.BankConnections;

public class BankConnectionAggregate(BankConnectionId id)
    : AggregateRoot<BankConnectionAggregate, BankConnectionId>(id),
    IEmit<BankConnectionInitiatedEvent>,
    IEmit<BankConnectionEstablishedEvent>,
    IEmit<BankConnectionFailedEvent>,
    IEmit<BankConnectionDisconnectedEvent>,
    IEmit<BankConnectionRefreshedEvent>,
    IEmit<BankAccountLinkedEvent>,
    IEmit<BankConnectionReInitiatedEvent>,
    IEmit<BankConnectionArchivedEvent>
{
    private bool _isInitiated;
    private bool _isEstablished;
    private bool _isDisconnected;
    private bool _isArchived;
    private string? _sessionId;
    private DateTimeOffset? _validUntil;
    private List<BankAccountInfo> _accounts = [];

    public string? SessionId => _sessionId;
    public bool IsActive => _isEstablished && !_isDisconnected && _validUntil > DateTimeOffset.UtcNow;
    public IReadOnlyList<BankAccountInfo> Accounts => _accounts;

    public void Apply(BankConnectionInitiatedEvent e)
    {
        _isInitiated = true;
    }

    public void Apply(BankConnectionEstablishedEvent e)
    {
        _isEstablished = true;
        _sessionId = e.SessionId;
        _validUntil = e.ValidUntil;
        _accounts = e.Accounts.ToList();
    }

    public void Apply(BankConnectionFailedEvent e)
    {
        _isDisconnected = true;
    }

    public void Apply(BankConnectionDisconnectedEvent e)
    {
        _isDisconnected = true;
        _sessionId = null;
    }

    public void Apply(BankConnectionRefreshedEvent e)
    {
        _sessionId = e.NewSessionId;
        _validUntil = e.ValidUntil;
    }

    public void Apply(BankAccountLinkedEvent e)
    {
        // Update the account with the linked account ID and import from date
        var accountIndex = _accounts.FindIndex(a => a.AccountId == e.BankAccountId);
        if (accountIndex >= 0)
        {
            var account = _accounts[accountIndex];
            _accounts[accountIndex] = account with
            {
                LinkedAccountId = e.LinkedAccountId,
                ImportFromDate = e.ImportFromDate
            };
        }
    }

    public void Apply(BankConnectionReInitiatedEvent e)
    {
        // Reset to initiated state, ready for new authorization
        _isEstablished = false;
        _isDisconnected = false;
        _sessionId = null;
        _validUntil = null;
    }

    public void Apply(BankConnectionArchivedEvent e)
    {
        _isArchived = true;
    }

    /// <summary>
    /// Initiate a bank connection authorization flow
    /// </summary>
    public void Initiate(
        string companyId,
        string aspspName,
        string authorizationId,
        string redirectUrl,
        string state)
    {
        if (_isInitiated)
            throw new DomainException("BANK_CONNECTION_ALREADY_INITIATED",
                "Bank connection has already been initiated",
                "Bankforbindelse er allerede påbegyndt");

        if (string.IsNullOrWhiteSpace(aspspName))
            throw new DomainException("ASPSP_NAME_REQUIRED",
                "Bank name (ASPSP) is required",
                "Banknavn er påkrævet");

        Emit(new BankConnectionInitiatedEvent(
            companyId,
            aspspName,
            authorizationId,
            redirectUrl,
            state));
    }

    /// <summary>
    /// Establish the bank connection after successful authorization
    /// </summary>
    public void Establish(
        string sessionId,
        DateTimeOffset validUntil,
        IReadOnlyList<BankAccountInfo> accounts)
    {
        if (!_isInitiated)
            throw new DomainException("BANK_CONNECTION_NOT_INITIATED",
                "Bank connection must be initiated before establishing",
                "Bankforbindelse skal påbegyndes før den kan etableres");

        if (_isEstablished && !_isDisconnected)
            throw new DomainException("BANK_CONNECTION_ALREADY_ESTABLISHED",
                "Bank connection is already established",
                "Bankforbindelse er allerede etableret");

        if (string.IsNullOrWhiteSpace(sessionId))
            throw new DomainException("SESSION_ID_REQUIRED",
                "Session ID is required",
                "Session ID er påkrævet");

        // Reject connections without accounts - the Enable Banking session response
        // is the ONLY source for account information. If empty, user should try
        // different psuType (personal vs business)
        if (!accounts.Any())
        {
            throw new DomainException("NO_ACCOUNTS_FOUND",
                "No bank accounts found. Try switching between personal/business account type.",
                "Ingen bankkonti fundet. Prøv at skifte mellem personlig/erhverv kontotype.");
        }

        Emit(new BankConnectionEstablishedEvent(sessionId, validUntil, accounts));
    }

    /// <summary>
    /// Mark the connection as failed
    /// </summary>
    public void Fail(string reason)
    {
        if (!_isInitiated)
            throw new DomainException("BANK_CONNECTION_NOT_INITIATED",
                "Cannot fail a connection that was not initiated",
                "Kan ikke fejle en forbindelse der ikke er påbegyndt");

        Emit(new BankConnectionFailedEvent(reason));
    }

    /// <summary>
    /// Disconnect the bank connection (idempotent - safe to call on already disconnected connections)
    /// </summary>
    public void Disconnect(string reason = "User requested disconnection")
    {
        // Idempotent: if already disconnected, do nothing
        if (_isDisconnected)
            return;

        // Allow disconnecting failed connections that were never fully established
        // This enables cleanup of connections stuck in "initiated" or "failed" state
        if (!_isEstablished && !_isInitiated)
            throw new DomainException("BANK_CONNECTION_NOT_INITIATED",
                "Cannot disconnect a connection that was never initiated",
                "Kan ikke afbryde en forbindelse der aldrig blev påbegyndt");

        Emit(new BankConnectionDisconnectedEvent(reason));
    }

    /// <summary>
    /// Refresh the bank connection session
    /// </summary>
    public void Refresh(string newSessionId, DateTimeOffset validUntil)
    {
        if (!_isEstablished || _isDisconnected)
            throw new DomainException("BANK_CONNECTION_NOT_ACTIVE",
                "Cannot refresh an inactive connection",
                "Kan ikke genopfriske en inaktiv forbindelse");

        Emit(new BankConnectionRefreshedEvent(newSessionId, validUntil));
    }

    /// <summary>
    /// Link a bank account to a chart of accounts account
    /// </summary>
    public void LinkBankAccount(string bankAccountId, string linkedAccountId, DateOnly? importFromDate = null)
    {
        if (!_isEstablished || _isDisconnected)
            throw new DomainException("BANK_CONNECTION_NOT_ACTIVE",
                "Cannot link accounts on an inactive connection",
                "Kan ikke koble konti på en inaktiv forbindelse");

        if (string.IsNullOrWhiteSpace(bankAccountId))
            throw new DomainException("BANK_ACCOUNT_ID_REQUIRED",
                "Bank account ID is required",
                "Bankkonto ID er påkrævet");

        if (string.IsNullOrWhiteSpace(linkedAccountId))
            throw new DomainException("LINKED_ACCOUNT_ID_REQUIRED",
                "Linked account ID is required",
                "Finanskonto ID er påkrævet");

        var account = _accounts.FirstOrDefault(a => a.AccountId == bankAccountId);
        if (account == null)
            throw new DomainException("BANK_ACCOUNT_NOT_FOUND",
                $"Bank account with ID '{bankAccountId}' not found in this connection",
                $"Bankkonto med ID '{bankAccountId}' blev ikke fundet i denne forbindelse");

        Emit(new BankAccountLinkedEvent(bankAccountId, linkedAccountId, importFromDate));
    }

    /// <summary>
    /// Re-initiate the bank connection authorization flow (reconnect).
    /// Used for connections that are disconnected, failed, or expired.
    /// Active connections are automatically disconnected before re-initiation.
    /// </summary>
    public void ReInitiate(string authorizationId, string redirectUrl, string state)
    {
        if (!_isInitiated)
            throw new DomainException("BANK_CONNECTION_NOT_INITIATED",
                "Cannot re-initiate a connection that was never initiated",
                "Kan ikke genoptage en forbindelse der aldrig blev påbegyndt");

        if (string.IsNullOrWhiteSpace(authorizationId))
            throw new DomainException("AUTHORIZATION_ID_REQUIRED",
                "Authorization ID is required",
                "Autorisations ID er påkrævet");

        // Auto-disconnect active connections before re-initiation
        if (!_isDisconnected && _isEstablished)
        {
            Emit(new BankConnectionDisconnectedEvent("Auto-disconnected for re-connection"));
        }

        Emit(new BankConnectionReInitiatedEvent(authorizationId, redirectUrl, state));
    }

    /// <summary>
    /// Archive the bank connection (hide from UI)
    /// </summary>
    public void Archive(string reason = "User requested archival")
    {
        if (!_isInitiated)
            throw new DomainException("BANK_CONNECTION_NOT_INITIATED",
                "Cannot archive a connection that was never initiated",
                "Kan ikke arkivere en forbindelse der aldrig blev påbegyndt");

        if (_isArchived)
            return; // Idempotent - already archived

        // Don't allow archiving active connections
        if (IsActive)
            throw new DomainException("BANK_CONNECTION_STILL_ACTIVE",
                "Cannot archive an active connection. Disconnect first.",
                "Kan ikke arkivere en aktiv forbindelse. Afbryd først.");

        Emit(new BankConnectionArchivedEvent(reason));
    }
}
