using CashLineERP.Application.Parties;
using CashLineERP.Domain.Documents;
using CashLineERP.Domain.Parties;
using CashLineERP.SharedKernel.Documents;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class BusinessPartyRepository : IBusinessPartyRepository
{
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<BusinessPartyView> GetParties(string? partyType = null)
    {
        var query = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(party => party.IsActive);

        if (!string.IsNullOrWhiteSpace(partyType))
        {
            query = query.Where(party => party.PartyType == partyType);
        }

        return query
            .OrderBy(party => party.PartyCode)
            .Select(party => new BusinessPartyView(
                party.Id,
                party.PartyCode,
                party.PartyName,
                party.PartyType,
                party.CountryCode,
                party.FirmNationalId,
                party.TaxRegistrationNumber,
                party.Email,
                party.MobileNumber,
                party.CreditLimitAmount,
                party.CreditTermsDays,
                party.IsCreditHoldEnabled,
                party.CreditHoldReason,
                party.IsActive))
            .ToArray();
    }

    public BusinessPartyView Create(CreateBusinessPartyCommand command)
    {
        var normalizedPartyType = command.PartyType.Trim().ToUpperInvariant();
        var normalizedPartyCode = Normalize(command.PartyCode);
        var normalizedPartyName = Normalize(command.PartyName);
        var normalizedFirmNationalId = NormalizeFirmNationalId(command.FirmNationalId);
        var normalizedTaxRegistrationNumber = Normalize(command.TaxRegistrationNumber);
        var normalizedEmail = Normalize(command.Email);
        var normalizedMobileNumber = Normalize(command.MobileNumber);

        var existing = _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.IsActive && entry.PartyType == normalizedPartyType)
            .ToArray()
            .FirstOrDefault(entry =>
                (normalizedPartyCode != null && Normalize(entry.PartyCode) == normalizedPartyCode)
                || (normalizedFirmNationalId != null && NormalizeFirmNationalId(entry.FirmNationalId) == normalizedFirmNationalId)
                || (normalizedTaxRegistrationNumber != null && Normalize(entry.TaxRegistrationNumber) == normalizedTaxRegistrationNumber)
                || (normalizedEmail != null && Normalize(entry.Email) == normalizedEmail)
                || (normalizedMobileNumber != null && Normalize(entry.MobileNumber) == normalizedMobileNumber));

        // Idempotent safety for demo/QA flows: if the exact same party is submitted again, return the existing
        // party instead of throwing, while still preventing true duplicates from being created.
        if (existing is not null && normalizedPartyName is not null && Normalize(existing.PartyName) == normalizedPartyName)
        {
            return new BusinessPartyView(
                existing.Id,
                existing.PartyCode,
                existing.PartyName,
                existing.PartyType,
                existing.CountryCode,
                existing.FirmNationalId,
                existing.TaxRegistrationNumber,
                existing.Email,
                existing.MobileNumber,
                existing.CreditLimitAmount,
                existing.CreditTermsDays,
                existing.IsCreditHoldEnabled,
                existing.CreditHoldReason,
                existing.IsActive);
        }

        EnsureNoActiveDuplicate(command);

        var party = new BusinessParty(
            command.PartyCode,
            command.PartyName,
            command.PartyType,
            command.CountryCode,
            normalizedFirmNationalId,
            command.TaxRegistrationNumber,
            command.TaxBranchCode,
            command.Email,
            command.MobileNumber,
            command.AddressLine1,
            command.CreditLimitAmount,
            command.CreditTermsDays,
            command.IsCreditHoldEnabled,
            command.CreditHoldReason);

        _dbContext.BusinessParties.Add(party);
        _dbContext.SaveChanges();

        return new BusinessPartyView(
            party.Id,
            party.PartyCode,
            party.PartyName,
            party.PartyType,
            party.CountryCode,
            party.FirmNationalId,
            party.TaxRegistrationNumber,
            party.Email,
            party.MobileNumber,
            party.CreditLimitAmount,
            party.CreditTermsDays,
            party.IsCreditHoldEnabled,
            party.CreditHoldReason,
            party.IsActive);
    }

    public bool ExistsActiveDuplicate(string partyType, string? taxRegistrationNumber, string? email, string? mobileNumber, long? excludingPartyId = null)
    {
        var normalizedPartyType = partyType.Trim().ToUpperInvariant();
        var normalizedTaxRegistrationNumber = Normalize(taxRegistrationNumber);
        var normalizedEmail = Normalize(email);
        var normalizedMobileNumber = Normalize(mobileNumber);

        return _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.IsActive && entry.PartyType == normalizedPartyType)
            .Where(entry => !excludingPartyId.HasValue || entry.Id != excludingPartyId.Value)
            .ToArray()
            .Any(entry =>
                (normalizedTaxRegistrationNumber != null && Normalize(entry.TaxRegistrationNumber) == normalizedTaxRegistrationNumber)
                || (normalizedEmail != null && Normalize(entry.Email) == normalizedEmail)
                || (normalizedMobileNumber != null && Normalize(entry.MobileNumber) == normalizedMobileNumber));
    }

    public bool ExistsActiveFirmNationalIdDuplicate(string firmNationalId, long? excludingPartyId = null)
    {
        var normalizedFirmNationalId = NormalizeFirmNationalId(firmNationalId);
        if (normalizedFirmNationalId is null)
        {
            return false;
        }

        return _dbContext.BusinessParties
            .AsNoTracking()
            .Where(entry => entry.IsActive)
            .Where(entry => entry.FirmNationalId != null && entry.FirmNationalId != string.Empty)
            .Where(entry => !excludingPartyId.HasValue || entry.Id != excludingPartyId.Value)
            .ToArray()
            .Any(entry => NormalizeFirmNationalId(entry.FirmNationalId) == normalizedFirmNationalId);
    }

    public BusinessPartyView SetFirmNationalId(long partyId, string? firmNationalId, long actorUserId)
    {
        var normalizedFirmNationalId = NormalizeFirmNationalId(firmNationalId);
        if (normalizedFirmNationalId is null)
        {
            // MVP: allow leaving the field unset, and allow clearing too (kept for admin follow-up).
            var party = _dbContext.BusinessParties.Single(entry => entry.Id == partyId && entry.IsActive);
            party.SetFirmNationalId(null);
            _dbContext.SaveChanges();
            return new BusinessPartyView(
                party.Id,
                party.PartyCode,
                party.PartyName,
                party.PartyType,
                party.CountryCode,
                party.FirmNationalId,
                party.TaxRegistrationNumber,
                party.Email,
                party.MobileNumber,
                party.CreditLimitAmount,
                party.CreditTermsDays,
                party.IsCreditHoldEnabled,
                party.CreditHoldReason,
                party.IsActive);
        }

        if (ExistsActiveFirmNationalIdDuplicate(normalizedFirmNationalId, excludingPartyId: partyId))
        {
            throw new InvalidOperationException("A client already exists with the same firm national ID (Egypt). Duplicate registration is not allowed.");
        }

        var partyEntity = _dbContext.BusinessParties.Single(entry => entry.Id == partyId && entry.IsActive);
        partyEntity.SetFirmNationalId(normalizedFirmNationalId);
        _dbContext.SaveChanges();

        return new BusinessPartyView(
            partyEntity.Id,
            partyEntity.PartyCode,
            partyEntity.PartyName,
            partyEntity.PartyType,
            partyEntity.CountryCode,
            partyEntity.FirmNationalId,
            partyEntity.TaxRegistrationNumber,
            partyEntity.Email,
            partyEntity.MobileNumber,
            partyEntity.CreditLimitAmount,
            partyEntity.CreditTermsDays,
            partyEntity.IsCreditHoldEnabled,
            partyEntity.CreditHoldReason,
            partyEntity.IsActive);
    }

    public IReadOnlyCollection<BusinessPartyView> GetActiveClientsMissingFirmNationalId()
    {
        return _dbContext.BusinessParties
            .AsNoTracking()
            .Where(party => party.IsActive)
            .Where(party => party.PartyType == "SUPPLIER" || party.PartyType == "CUSTOMER")
            .Where(party => party.FirmNationalId == null || party.FirmNationalId == string.Empty)
            .OrderBy(party => party.PartyType)
            .ThenBy(party => party.PartyCode)
            .Select(party => new BusinessPartyView(
                party.Id,
                party.PartyCode,
                party.PartyName,
                party.PartyType,
                party.CountryCode,
                party.FirmNationalId,
                party.TaxRegistrationNumber,
                party.Email,
                party.MobileNumber,
                party.CreditLimitAmount,
                party.CreditTermsDays,
                party.IsCreditHoldEnabled,
                party.CreditHoldReason,
                party.IsActive))
            .ToArray();
    }

    public IReadOnlyCollection<BusinessPartyComplianceDocumentView> GetComplianceDocuments(long partyId)
    {
        EnsureActiveParty(partyId);

        return _dbContext.DocumentLinks
            .AsNoTracking()
            .Where(link =>
                link.ModuleCode == "PARTY" &&
                link.EntityName == "BUSINESS_PARTY" &&
                link.EntityId == partyId &&
                link.LifecycleStatusCode == DocumentLifecycleStatusCode.Active)
            .OrderByDescending(link => link.UploadedAtUtc)
            .ThenByDescending(link => link.Id)
            .Select(link => new BusinessPartyComplianceDocumentView(
                link.Id,
                partyId,
                link.DocumentType,
                link.FileName,
                link.StorageReference,
                link.ContentType,
                link.FileSizeBytes,
                link.UploadedAtUtc,
                link.UploadedByUserId))
            .ToArray();
    }

    public BusinessPartyComplianceDocumentView RecordComplianceDocument(long actorUserId, RecordBusinessPartyComplianceDocumentCommand command)
    {
        EnsureActiveParty(command.PartyId);

        var document = new DocumentLink(
            moduleCode: "PARTY",
            entityName: "BUSINESS_PARTY",
            entityId: command.PartyId,
            documentType: command.DocumentType.Trim().ToUpperInvariant(),
            attachmentCategoryCode: DocumentAttachmentCategoryCode.Compliance,
            fileName: command.FileName.Trim(),
            storageReference: command.StorageReference.Trim(),
            storageProviderCode: DocumentStorageProviderCode.ManagedStorage,
            contentType: string.IsNullOrWhiteSpace(command.ContentType) ? null : command.ContentType.Trim(),
            fileSizeBytes: command.FileSizeBytes,
            checksumText: null,
            retentionUntilUtc: null,
            versionNumber: 1,
            uploadedByUserId: actorUserId);

        _dbContext.DocumentLinks.Add(document);
        _dbContext.SaveChanges();

        return new BusinessPartyComplianceDocumentView(
            document.Id,
            command.PartyId,
            document.DocumentType,
            document.FileName,
            document.StorageReference,
            document.ContentType,
            document.FileSizeBytes,
            document.UploadedAtUtc,
            document.UploadedByUserId);
    }

    private void EnsureActiveParty(long partyId)
    {
        if (!_dbContext.BusinessParties.AsNoTracking().Any(party => party.Id == partyId && party.IsActive))
        {
            throw new InvalidOperationException($"Active business party '{partyId}' was not found.");
        }
    }

    private void EnsureNoActiveDuplicate(CreateBusinessPartyCommand command)
    {
        var normalizedFirmNationalId = NormalizeFirmNationalId(command.FirmNationalId);
        if (normalizedFirmNationalId is not null && ExistsActiveFirmNationalIdDuplicate(normalizedFirmNationalId))
        {
            throw new InvalidOperationException("A client already exists with the same firm national ID (Egypt). Duplicate registration is not allowed.");
        }

        if (!ExistsActiveDuplicate(command.PartyType, command.TaxRegistrationNumber, command.Email, command.MobileNumber))
        {
            return;
        }

        throw new InvalidOperationException($"An active {command.PartyType.Trim().ToUpperInvariant()} party already exists with the same tax registration number, email, or mobile number.");
    }

    private static string? Normalize(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToUpperInvariant();

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

        // Normalize to ASCII digits only so uniqueness is enforced consistently
        // even if users type Arabic-Indic digits or add separators/spaces.
        var buffer = new List<char>(value.Length);
        foreach (var ch in value.Trim())
        {
            // Western digits
            if (ch >= '0' && ch <= '9')
            {
                buffer.Add(ch);
                continue;
            }

            // Arabic-Indic digits: ٠١٢٣٤٥٦٧٨٩
            if (ch >= '\u0660' && ch <= '\u0669')
            {
                buffer.Add((char)('0' + (ch - '\u0660')));
                continue;
            }

            // Eastern Arabic-Indic digits: ۰۱۲۳۴۵۶۷۸۹
            if (ch >= '\u06F0' && ch <= '\u06F9')
            {
                buffer.Add((char)('0' + (ch - '\u06F0')));
                continue;
            }
        }

        if (buffer.Count == 0)
        {
            return null;
        }

        return new string(buffer.ToArray());
    }
}
