using CashLineERP.Application.Finance;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.Workflow;
using CashLineERP.SharedKernel.Finance;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class CardTokenReferenceRepository : ICardTokenReferenceRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public CardTokenReferenceRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IReadOnlyCollection<CardTokenReferenceDetailView> GetCardTokenReferences(bool activeOnly = false, long? partyId = null, string? integrationProviderCode = null, string? usageCode = null)
    {
        var normalizedProviderCode = string.IsNullOrWhiteSpace(integrationProviderCode)
            ? null
            : CardTokenReferenceCatalog.NormalizeIntegrationProviderCode(integrationProviderCode);
        var normalizedUsageCode = string.IsNullOrWhiteSpace(usageCode)
            ? null
            : CardTokenReferenceCatalog.NormalizeUsageCode(usageCode);

        return _dbContext.CardTokenReferences
            .AsNoTracking()
            .ToArray()
            .Where(entry => !activeOnly || entry.IsActive)
            .Where(entry => !partyId.HasValue || entry.PartyId == partyId.Value)
            .Where(entry => normalizedProviderCode is null || string.Equals(entry.IntegrationProviderCode, normalizedProviderCode, StringComparison.OrdinalIgnoreCase))
            .Where(entry => normalizedUsageCode is null || string.Equals(entry.UsageCode, normalizedUsageCode, StringComparison.OrdinalIgnoreCase))
            .OrderByDescending(entry => entry.IsDefault)
            .ThenByDescending(entry => entry.IsActive)
            .ThenBy(entry => entry.CardTokenReferenceNumber)
            .Select(Map)
            .ToArray();
    }

    public CardTokenReferenceDetailView? GetCardTokenReference(long cardTokenReferenceId)
    {
        var reference = _dbContext.CardTokenReferences
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == cardTokenReferenceId);

        return reference is null ? null : Map(reference);
    }

    public CardTokenReferenceDetailView Create(CreateCardTokenReferenceCommand command)
    {
        var party = _dbContext.BusinessParties
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == command.PartyId);
        if (party is null || !party.IsActive)
        {
            throw new InvalidOperationException("Card token reference requires an active business party.");
        }

        var normalizedPartyTypeCode = CardTokenReferenceCatalog.NormalizePartyTypeCode(command.PartyTypeCode);
        if (!string.Equals(party.PartyType, normalizedPartyTypeCode, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Card token reference party type must match the linked business party.");
        }

        var normalizedProviderCode = CardTokenReferenceCatalog.NormalizeIntegrationProviderCode(command.IntegrationProviderCode);
        var normalizedUsageCode = CardTokenReferenceCatalog.NormalizeUsageCode(command.UsageCode);
        var normalizedTokenReference = CardTokenReferenceCatalog.NormalizeTokenReference(command.TokenReference);

        var duplicateExists = _dbContext.CardTokenReferences
            .AsNoTracking()
            .Any(entry =>
                entry.IntegrationProviderCode == normalizedProviderCode
                && entry.TokenReference == normalizedTokenReference);

        if (duplicateExists)
        {
            throw new InvalidOperationException("Card token reference already exists for the selected provider token.");
        }

        var nextSequence = (_dbContext.CardTokenReferences.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var reference = new CardTokenReference(
            cardTokenReferenceNumber: $"CTR-{nextSequence:D6}",
            partyId: command.PartyId,
            partyTypeCode: normalizedPartyTypeCode,
            integrationProviderCode: normalizedProviderCode,
            usageCode: normalizedUsageCode,
            cardBrandCode: command.CardBrandCode,
            maskedPan: command.MaskedPan,
            expiryMonth: command.ExpiryMonth,
            expiryYear: command.ExpiryYear,
            tokenReference: normalizedTokenReference,
            externalCustomerReference: command.ExternalCustomerReference,
            isDefault: command.IsDefault,
            notes: command.Notes);

        _dbContext.CardTokenReferences.Add(reference);
        _dbContext.SaveChanges();
        return GetCardTokenReference(reference.Id)!;
    }

    public CardTokenReferenceDetailView Submit(long cardTokenReferenceId, long actorUserId)
    {
        var reference = _dbContext.CardTokenReferences.Single(entry => entry.Id == cardTokenReferenceId);
        reference.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "FINANCE",
            documentType: "CARD_TOKEN_REFERENCE",
            documentId: reference.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Card token reference submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetCardTokenReference(reference.Id)!;
    }

    public CardTokenReferenceDetailView Approve(long cardTokenReferenceId, long actorUserId)
    {
        var reference = _dbContext.CardTokenReferences.Single(entry => entry.Id == cardTokenReferenceId);
        reference.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .SingleOrDefault(entry => entry.DocumentType == "CARD_TOKEN_REFERENCE" && entry.DocumentId == cardTokenReferenceId);
        workflowInstance?.Approve(actorUserId, "Card token reference approved.");

        _dbContext.SaveChanges();
        return GetCardTokenReference(reference.Id)!;
    }

    public CardTokenReferenceDetailView Activate(long cardTokenReferenceId, long actorUserId)
    {
        var reference = _dbContext.CardTokenReferences.Single(entry => entry.Id == cardTokenReferenceId);
        reference.Activate(actorUserId);

        if (reference.IsDefault)
        {
            var peers = _dbContext.CardTokenReferences
                .Where(entry =>
                    entry.Id != reference.Id
                    && entry.PartyId == reference.PartyId
                    && entry.IntegrationProviderCode == reference.IntegrationProviderCode
                    && entry.UsageCode == reference.UsageCode
                    && entry.IsDefault)
                .ToArray();

            foreach (var peer in peers)
            {
                peer.UnsetDefault(actorUserId);
            }
        }

        _dbContext.SaveChanges();
        return GetCardTokenReference(reference.Id)!;
    }

    public CardTokenReferenceDetailView Suspend(long cardTokenReferenceId, long actorUserId, SuspendCardTokenReferenceCommand command)
    {
        var reference = _dbContext.CardTokenReferences.Single(entry => entry.Id == cardTokenReferenceId);
        reference.Suspend(actorUserId, command.ReasonCode, command.Notes);
        _dbContext.SaveChanges();
        return GetCardTokenReference(reference.Id)!;
    }

    private static CardTokenReferenceDetailView Map(CardTokenReference reference) =>
        new(
            reference.Id,
            reference.CardTokenReferenceNumber,
            reference.PartyId,
            reference.PartyTypeCode,
            reference.IntegrationProviderCode,
            reference.UsageCode,
            reference.CardBrandCode,
            reference.MaskedPan,
            reference.ExpiryMonth,
            reference.ExpiryYear,
            reference.TokenReference,
            reference.ExternalCustomerReference,
            reference.IsDefault,
            WorkflowStatusSeedIds.ToCode(reference.StatusId),
            reference.IsActive,
            reference.Notes,
            reference.SubmittedAtUtc,
            reference.SubmittedByUserId,
            reference.ApprovedAtUtc,
            reference.ApprovedByUserId,
            reference.ActivatedAtUtc,
            reference.ActivatedByUserId,
            reference.SuspendedAtUtc,
            reference.SuspendedByUserId,
            reference.SuspensionReasonCode,
            reference.SuspensionNotes);
}
