using CashLineERP.Application.Suppliers;
using CashLineERP.Domain.Audit;
using CashLineERP.Domain.Parties;
using CashLineERP.Domain.Suppliers;
using CashLineERP.Domain.Workflow;
using CashLineERP.SharedKernel.Documents;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class SupplierProfileRepository : ISupplierProfileRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<SupplierProfileDetailView> GetSupplierProfiles(bool activeOnly = false, string? statusCode = null, long? businessPartyId = null)
    {
        var normalizedStatusCode = string.IsNullOrWhiteSpace(statusCode) ? null : statusCode.Trim().ToUpperInvariant();
        long? normalizedStatusId = normalizedStatusCode is null ? null : MapStatusCodeToId(normalizedStatusCode);

        var profiles = _dbContext.SupplierProfiles
            .AsNoTracking()
            .Where(entry => !activeOnly || entry.IsActive)
            .Where(entry => !normalizedStatusId.HasValue || entry.StatusId == normalizedStatusId.Value)
            .Where(entry => !businessPartyId.HasValue || entry.BusinessPartyId == businessPartyId.Value)
            .OrderBy(entry => entry.SupplierProfileCode)
            .ToArray();

        return BuildDetailViews(profiles);
    }

    public SupplierProfileDetailView? GetSupplierProfile(long supplierProfileId)
    {
        var profile = _dbContext.SupplierProfiles
            .AsNoTracking()
            .SingleOrDefault(entry => entry.Id == supplierProfileId);

        return profile is null ? null : BuildDetailViews([profile]).Single();
    }

    public SupplierProfileDetailView Create(CreateSupplierProfileCommand command)
    {
        var party = EnsureSupplierPartyActive(command.BusinessPartyId);

        if (!string.IsNullOrWhiteSpace(command.FirmNationalId))
        {
            var normalized = NormalizeFirmNationalId(command.FirmNationalId);
            if (normalized is not null)
            {
                var nationalIdDuplicateExists = _dbContext.BusinessParties
                    .AsNoTracking()
                    .Where(entry => entry.IsActive && entry.Id != party.Id)
                    .Any(entry => entry.FirmNationalId != null && NormalizeFirmNationalId(entry.FirmNationalId) == normalized);

                if (nationalIdDuplicateExists)
                {
                    throw new InvalidOperationException("A client already exists with the same firm national ID (Egypt). Duplicate registration is not allowed.");
                }

                // Optional for MVP: only set when provided; can be followed up later by Ops.
                if (string.IsNullOrWhiteSpace(party.FirmNationalId))
                {
                    party.SetFirmNationalId(normalized);
                    _dbContext.SaveChanges();
                }
            }
        }

        var profileDuplicateExists = _dbContext.SupplierProfiles
            .AsNoTracking()
            .Any(entry => entry.BusinessPartyId == command.BusinessPartyId);
        if (profileDuplicateExists)
        {
            throw new InvalidOperationException("Supplier profile already exists for the selected supplier party.");
        }

        var nextSequence = (_dbContext.SupplierProfiles.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var profile = new SupplierProfile(
            supplierProfileCode: $"SUPR-{nextSequence:D6}",
            businessPartyId: party.Id,
            supplierCategoryCode: command.SupplierCategoryCode,
            defaultCurrencyCode: command.DefaultCurrencyCode,
            preferredPaymentMethodCode: command.PreferredPaymentMethodCode,
            paymentTermsDays: command.PaymentTermsDays,
            leadTimeDays: command.LeadTimeDays,
            minimumOrderAmount: command.MinimumOrderAmount,
            bankName: command.BankName,
            bankAccountNumber: command.BankAccountNumber,
            bankSwiftCode: command.BankSwiftCode,
            notes: command.Notes);

        _dbContext.SupplierProfiles.Add(profile);
        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    private static string? NormalizeFirmNationalId(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var buffer = new List<char>(value.Length);
        foreach (var ch in value.Trim())
        {
            if (ch >= '0' && ch <= '9')
            {
                buffer.Add(ch);
                continue;
            }

            if (ch >= '\u0660' && ch <= '\u0669')
            {
                buffer.Add((char)('0' + (ch - '\u0660')));
                continue;
            }

            if (ch >= '\u06F0' && ch <= '\u06F9')
            {
                buffer.Add((char)('0' + (ch - '\u06F0')));
                continue;
            }
        }

        return buffer.Count == 0 ? null : new string(buffer.ToArray());
    }

    public SupplierProfileDetailView Submit(long supplierProfileId, long actorUserId)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);
        var oldStatusCode = WorkflowStatusSeedIds.ToCode(profile.StatusId);

        profile.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "PARTY",
            documentType: "SUPPLIER_PROFILE",
            documentId: profile.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Supplier profile submitted for controlled review.");
        AddAuditEvent(profile.Id, actorUserId, "SUBMITTED", oldStatusCode, WorkflowStatusSeedIds.ToCode(profile.StatusId));

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView Approve(long supplierProfileId, long actorUserId)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        EnsureActivationPrerequisites(profile.BusinessPartyId);
        var oldStatusCode = WorkflowStatusSeedIds.ToCode(profile.StatusId);

        profile.Approve(actorUserId);

        var workflowInstance = ResolveLatestWorkflowInstance(supplierProfileId);
        workflowInstance?.Approve(actorUserId, "Supplier profile approved after compliance review.");
        AddAuditEvent(profile.Id, actorUserId, "APPROVED", oldStatusCode, WorkflowStatusSeedIds.ToCode(profile.StatusId));

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView Return(long supplierProfileId, long actorUserId, string reviewNotes)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);
        var oldStatusCode = WorkflowStatusSeedIds.ToCode(profile.StatusId);

        profile.Return(actorUserId, reviewNotes);

        var workflowInstance = ResolveLatestWorkflowInstance(supplierProfileId);
        workflowInstance?.Return(actorUserId, reviewNotes);
        AddAuditEvent(profile.Id, actorUserId, "RETURNED", oldStatusCode, $"{WorkflowStatusSeedIds.ToCode(profile.StatusId)}|{reviewNotes}");

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView Reject(long supplierProfileId, long actorUserId, string reviewNotes)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);
        var oldStatusCode = WorkflowStatusSeedIds.ToCode(profile.StatusId);

        profile.Reject(actorUserId, reviewNotes);

        var workflowInstance = ResolveLatestWorkflowInstance(supplierProfileId);
        workflowInstance?.Reject(actorUserId, reviewNotes);
        AddAuditEvent(profile.Id, actorUserId, "REJECTED", oldStatusCode, $"{WorkflowStatusSeedIds.ToCode(profile.StatusId)}|{reviewNotes}");

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView Hold(long supplierProfileId, long actorUserId, string holdReason)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);
        var oldValue = $"{WorkflowStatusSeedIds.ToCode(profile.StatusId)}|{profile.HoldReason}";

        profile.Hold(actorUserId, holdReason);

        var workflowInstance = ResolveLatestWorkflowInstance(supplierProfileId);
        workflowInstance?.Hold(actorUserId, holdReason);
        AddAuditEvent(profile.Id, actorUserId, "ON_HOLD", oldValue, $"{WorkflowStatusSeedIds.ToCode(profile.StatusId)}|{profile.HoldReason}");

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView Reopen(long supplierProfileId, long actorUserId, string reopenReason)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);
        var oldValue = $"{WorkflowStatusSeedIds.ToCode(profile.StatusId)}|{profile.HoldReason}";

        profile.Reopen(actorUserId, reopenReason);

        var workflowInstance = ResolveLatestWorkflowInstance(supplierProfileId);
        workflowInstance?.Reopen(actorUserId, reopenReason);
        AddAuditEvent(profile.Id, actorUserId, "REOPENED", oldValue, $"{WorkflowStatusSeedIds.ToCode(profile.StatusId)}|{reopenReason}");

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView Activate(long supplierProfileId, long actorUserId)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        var party = EnsureActivationPrerequisites(profile.BusinessPartyId);
        var oldStatusCode = WorkflowStatusSeedIds.ToCode(profile.StatusId);

        profile.Activate(actorUserId);

        OutboxEventWriter.Enqueue(
            _dbContext,
            sourceModuleCode: "PARTY",
            aggregateType: "SUPPLIER_PROFILE",
            aggregateId: profile.Id,
            eventType: "SUPPLIER_PROFILE_ACTIVATED",
            payload: new
            {
                profile.SupplierProfileCode,
                profile.BusinessPartyId,
                party.PartyCode,
                party.PartyName,
                profile.SupplierCategoryCode,
                profile.DefaultCurrencyCode,
                profile.PreferredPaymentMethodCode
            },
            actorUserId: actorUserId,
            correlationKey: profile.SupplierProfileCode);
        AddAuditEvent(profile.Id, actorUserId, "ACTIVATED", oldStatusCode, $"ACTIVE|{WorkflowStatusSeedIds.ToCode(profile.StatusId)}");

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    public SupplierProfileDetailView VerifyBank(long supplierProfileId, long actorUserId, string verificationReference)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);

        profile.VerifyBank(actorUserId, verificationReference);
        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    private WorkflowInstance? ResolveLatestWorkflowInstance(long supplierProfileId) =>
        _dbContext.WorkflowInstances
            .Include(entry => entry.Actions)
            .Where(entry => entry.DocumentType == "SUPPLIER_PROFILE" && entry.DocumentId == supplierProfileId)
            .OrderByDescending(entry => entry.Id)
            .FirstOrDefault();

    public SupplierProfileDetailView RecordPerformanceReview(
        long supplierProfileId,
        long actorUserId,
        string riskRatingCode,
        decimal performanceScore,
        decimal onTimeDeliveryScore,
        decimal qualityScore,
        string? performanceNotes)
    {
        var profile = _dbContext.SupplierProfiles.Single(entry => entry.Id == supplierProfileId);
        _ = EnsureSupplierPartyActive(profile.BusinessPartyId);

        profile.RecordPerformanceReview(
            actorUserId,
            riskRatingCode,
            performanceScore,
            onTimeDeliveryScore,
            qualityScore,
            performanceNotes);

        _dbContext.SaveChanges();
        return GetSupplierProfile(profile.Id)!;
    }

    private IReadOnlyCollection<SupplierProfileDetailView> BuildDetailViews(IReadOnlyCollection<SupplierProfile> profiles)
    {
        if (profiles.Count == 0)
        {
            return [];
        }

        var businessPartyIds = profiles
            .Select(entry => entry.BusinessPartyId)
            .Distinct()
            .ToList();

        var partiesById = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => businessPartyIds.Contains(entry.Id))
            .ToDictionary(entry => entry.Id);

        var complianceCountsByPartyId = _dbContext.DocumentLinks
            .AsNoTracking()
            .Where(link =>
                businessPartyIds.Contains(link.EntityId)
                && link.ModuleCode == "PARTY"
                && link.EntityName == "BUSINESS_PARTY"
                && link.LifecycleStatusCode == DocumentLifecycleStatusCode.Active)
            .ToArray()
            .GroupBy(link => link.EntityId)
            .ToDictionary(group => group.Key, group => group.Count());

        return profiles
            .Select(entry =>
            {
                var party = partiesById[entry.BusinessPartyId];
                complianceCountsByPartyId.TryGetValue(entry.BusinessPartyId, out var complianceCount);

                return new SupplierProfileDetailView(
                    entry.Id,
                    entry.SupplierProfileCode,
                    entry.BusinessPartyId,
                    party.PartyCode,
                    party.PartyName,
                    party.CountryCode,
                    party.TaxRegistrationNumber,
                    entry.SupplierCategoryCode,
                    entry.DefaultCurrencyCode,
                    entry.PreferredPaymentMethodCode,
                    entry.PaymentTermsDays,
                    entry.LeadTimeDays,
                    entry.MinimumOrderAmount,
                    entry.BankName,
                    entry.BankAccountNumber,
                    entry.BankSwiftCode,
                    entry.BankVerificationStatusCode,
                    entry.BankVerifiedAtUtc,
                    entry.BankVerifiedByUserId,
                    entry.BankVerificationReference,
                    entry.RiskRatingCode,
                    entry.PerformanceScore,
                    entry.OnTimeDeliveryScore,
                    entry.QualityScore,
                    entry.LastPerformanceReviewAtUtc,
                    entry.LastPerformanceReviewedByUserId,
                    entry.PerformanceNotes,
                    entry.ComplianceStatusCode,
                    complianceCount,
                    WorkflowStatusSeedIds.ToCode(entry.StatusId),
                    entry.IsActive,
                    entry.IsOnHold,
                    entry.HoldReason,
                    entry.ReviewDecisionNotes,
                    entry.Notes,
                    entry.SubmittedAtUtc,
                    entry.SubmittedByUserId,
                    entry.ApprovedAtUtc,
                    entry.ApprovedByUserId,
                    entry.ActivatedAtUtc,
                    entry.ActivatedByUserId);
            })
            .ToArray();
    }

    private BusinessParty EnsureSupplierPartyActive(long businessPartyId)
    {
        var party = _dbContext.BusinessParties.SingleOrDefault(entry => entry.Id == businessPartyId)
            ?? throw new InvalidOperationException("Supplier profile requires a valid supplier business party.");

        if (!party.IsActive)
        {
            throw new InvalidOperationException("Supplier profile requires an active supplier business party.");
        }

        if (!string.Equals(party.PartyType, "SUPPLIER", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Supplier profile can only be created for parties of type SUPPLIER.");
        }

        return party;
    }

    private BusinessParty EnsureActivationPrerequisites(long businessPartyId)
    {
        var party = EnsureSupplierPartyActive(businessPartyId);

        if (string.IsNullOrWhiteSpace(party.TaxRegistrationNumber))
        {
            throw new InvalidOperationException("Supplier approval requires supplier tax registration data.");
        }

        var hasComplianceDocument = _dbContext.DocumentLinks
            .AsNoTracking()
            .Any(link =>
                link.ModuleCode == "PARTY"
                && link.EntityName == "BUSINESS_PARTY"
                && link.EntityId == businessPartyId
                && link.LifecycleStatusCode == DocumentLifecycleStatusCode.Active);

        if (!hasComplianceDocument)
        {
            throw new InvalidOperationException("Supplier approval requires at least one active compliance document.");
        }

        return party;
    }

    private static long MapStatusCodeToId(string statusCode) => statusCode switch
    {
        WorkflowStatusCode.Draft => WorkflowStatusSeedIds.Draft,
        WorkflowStatusCode.Submitted => WorkflowStatusSeedIds.Submitted,
        WorkflowStatusCode.Returned => WorkflowStatusSeedIds.Returned,
        WorkflowStatusCode.Approved => WorkflowStatusSeedIds.Approved,
        WorkflowStatusCode.Rejected => WorkflowStatusSeedIds.Rejected,
        WorkflowStatusCode.Closed => WorkflowStatusSeedIds.Closed,
        WorkflowStatusCode.Cancelled => WorkflowStatusSeedIds.Cancelled,
        _ => throw new InvalidOperationException($"Supplier status code '{statusCode}' is not supported.")
    };

    private void AddAuditEvent(long supplierProfileId, long actorUserId, string actionCode, string? oldValue, string? newValue)
    {
        _dbContext.AuditEvents.Add(new AuditEvent(
            moduleCode: "PARTY",
            entityName: "SUPPLIER_PROFILE",
            entityId: supplierProfileId,
            actionCode: actionCode,
            actorUserId: actorUserId,
            oldValue: oldValue,
            newValue: newValue));
    }
}
