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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class OnboardingCaseRepository : IOnboardingCaseRepository
{
    private const long PublicSubmissionActorUserId = 0;

    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<OnboardingCaseSummaryView> GetCases(string? subjectTypeCode = null, string? caseStatusCode = null, string? queueCode = null, long? submittedByUserId = null)
    {
        var normalizedSubjectTypeCode = Normalize(subjectTypeCode);
        var normalizedCaseStatusCode = Normalize(caseStatusCode);
        var normalizedQueueCode = Normalize(queueCode);
        var resolvedSubmittedByUserId = submittedByUserId.GetValueOrDefault() > 0 ? submittedByUserId : null;

        return BuildCases()
            .Where(item => normalizedSubjectTypeCode is null || string.Equals(item.SubjectTypeCode, normalizedSubjectTypeCode, StringComparison.OrdinalIgnoreCase))
            .Where(item => normalizedCaseStatusCode is null || string.Equals(item.CaseStatusCode, normalizedCaseStatusCode, StringComparison.OrdinalIgnoreCase))
            .Where(item => normalizedQueueCode is null || string.Equals(item.QueueCode, normalizedQueueCode, StringComparison.OrdinalIgnoreCase))
            .Where(item => resolvedSubmittedByUserId is null || item.SubmittedByUserId == resolvedSubmittedByUserId)
            .OrderByDescending(item => item.SubmittedAtUtc ?? item.LastReviewActivityAtUtc)
            .ThenBy(item => item.SubjectTypeCode)
            .ThenBy(item => item.ProfileCode)
            .Select(MapSummary)
            .ToArray();
    }

    public OnboardingCaseDetailView? GetCase(string subjectTypeCode, long subjectProfileId, long? submittedByUserId = null)
    {
        var normalizedSubjectTypeCode = NormalizeRequiredSubjectType(subjectTypeCode);
        var resolvedSubmittedByUserId = submittedByUserId.GetValueOrDefault() > 0 ? submittedByUserId : null;
        return BuildCases()
            .SingleOrDefault(item =>
                string.Equals(item.SubjectTypeCode, normalizedSubjectTypeCode, StringComparison.OrdinalIgnoreCase)
                && item.SubjectProfileId == subjectProfileId
                && (resolvedSubmittedByUserId is null || item.SubmittedByUserId == resolvedSubmittedByUserId))
            ?.ToDetailView();
    }

    private IReadOnlyCollection<OnboardingCaseAggregate> BuildCases()
    {
        var partiesById = _dbContext.BusinessParties.AsNoTracking().ToDictionary(entry => entry.Id);
        var usersById = _dbContext.Users.AsNoTracking().ToDictionary(entry => entry.Id);
        var rolesById = _dbContext.Roles
            .AsNoTracking()
            .Where(entry => entry.IsActive)
            .ToDictionary(entry => entry.Id);
        var userRoleCodesByUserId = _dbContext.UserRoles
            .AsNoTracking()
            .Where(entry => entry.IsActive)
            .ToArray()
            .GroupBy(entry => entry.UserId)
            .ToDictionary(
                group => group.Key,
                group => (IReadOnlyCollection<string>)group
                    .Select(entry => rolesById.TryGetValue(entry.RoleId, out var role) ? role.RoleCode : null)
                    .Where(roleCode => !string.IsNullOrWhiteSpace(roleCode))
                    .Cast<string>()
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToArray());
        var workflows = _dbContext.WorkflowInstances.AsNoTracking().ToArray();
        var workflowsByKey = workflows
            .GroupBy(entry => BuildWorkflowKey(entry.DocumentType, entry.DocumentId))
            .ToDictionary(
                group => group.Key,
                group => group
                    .OrderByDescending(entry => entry.UpdatedAtUtc ?? entry.SubmittedAtUtc ?? entry.CreatedAtUtc)
                    .ThenByDescending(entry => entry.Id)
                    .First());

        var checklistByWorkflowId = _dbContext.OnboardingReviewChecklistItems
            .AsNoTracking()
            .ToArray()
            .GroupBy(entry => entry.WorkflowInstanceId)
            .ToDictionary(
                group => group.Key,
                group => (IReadOnlyCollection<OnboardingReviewChecklistItemView>)group
                    .OrderBy(entry => entry.ChecklistItemCode)
                    .Select(entry => new OnboardingReviewChecklistItemView(
                        entry.Id,
                        entry.ChecklistItemCode,
                        entry.DisplayName,
                        entry.StatusCode,
                        entry.CoverageCode,
                        entry.ReviewerNotesText,
                        entry.ReviewedAtUtc,
                        entry.ReviewedByUserId))
                    .ToArray());

        var findingsByWorkflowId = _dbContext.OnboardingReviewFindings
            .AsNoTracking()
            .ToArray()
            .GroupBy(entry => entry.WorkflowInstanceId)
            .ToDictionary(
                group => group.Key,
                group => (IReadOnlyCollection<OnboardingReviewFindingView>)group
                    .OrderBy(entry => entry.LoggedAtUtc)
                    .Select(entry => new OnboardingReviewFindingView(
                        entry.Id,
                        entry.FindingCode,
                        entry.SeverityCode,
                        entry.RelatedItemCode,
                        entry.StatusCode,
                        entry.FindingText,
                        entry.LoggedAtUtc,
                        entry.LoggedByUserId))
                    .ToArray());

        var supplierCases = _dbContext.SupplierProfiles
            .AsNoTracking()
            .ToArray()
            .Select(profile => BuildSupplierCase(profile, partiesById, usersById, userRoleCodesByUserId, workflowsByKey, checklistByWorkflowId, findingsByWorkflowId));

        var customerCases = _dbContext.CustomerProfiles
            .AsNoTracking()
            .ToArray()
            .Select(profile => BuildCustomerCase(profile, partiesById, usersById, userRoleCodesByUserId, workflowsByKey, checklistByWorkflowId, findingsByWorkflowId));

        return supplierCases
            .Concat(customerCases)
            .ToArray();
    }

    private static OnboardingCaseAggregate BuildSupplierCase(
        SupplierProfile profile,
        IReadOnlyDictionary<long, BusinessParty> partiesById,
        IReadOnlyDictionary<long, CashLineERP.Domain.Identity.ApplicationUser> usersById,
        IReadOnlyDictionary<long, IReadOnlyCollection<string>> userRoleCodesByUserId,
        IReadOnlyDictionary<string, WorkflowInstance> workflowsByKey,
        IReadOnlyDictionary<long, IReadOnlyCollection<OnboardingReviewChecklistItemView>> checklistByWorkflowId,
        IReadOnlyDictionary<long, IReadOnlyCollection<OnboardingReviewFindingView>> findingsByWorkflowId)
    {
        var party = partiesById[profile.BusinessPartyId];
        workflowsByKey.TryGetValue(BuildWorkflowKey("SUPPLIER_PROFILE", profile.Id), out var workflow);
        var checklistItems = workflow is not null && checklistByWorkflowId.TryGetValue(workflow.Id, out var checklist)
            ? checklist
            : [];
        var findings = workflow is not null && findingsByWorkflowId.TryGetValue(workflow.Id, out var findingSet)
            ? findingSet
            : [];

        return BuildAggregate(
            subjectTypeCode: "SUPPLIER",
            subjectProfileId: profile.Id,
            businessPartyId: party.Id,
            partyCode: party.PartyCode,
            partyName: party.PartyName,
            partyTypeCode: party.PartyType,
            profileCode: profile.SupplierProfileCode,
            countryCode: party.CountryCode,
            taxRegistrationNumber: party.TaxRegistrationNumber,
            email: party.Email,
            mobileNumber: party.MobileNumber,
            addressLine1: party.AddressLine1,
            domainStatusCode: profile.ComplianceStatusCode,
            isActive: profile.IsActive,
            isOnHold: profile.IsOnHold,
            holdReason: profile.HoldReason,
            reviewDecisionNotes: profile.ReviewDecisionNotes,
            submittedAtUtc: profile.SubmittedAtUtc,
            submittedByUserId: profile.SubmittedByUserId,
            approvedAtUtc: profile.ApprovedAtUtc,
            approvedByUserId: profile.ApprovedByUserId,
            activatedAtUtc: profile.ActivatedAtUtc,
            activatedByUserId: profile.ActivatedByUserId,
            workflowStatusCode: workflow?.CurrentStatusCode ?? WorkflowStatusSeedIds.ToCode(profile.StatusId),
            checklistItems: checklistItems,
            findings: findings,
            usersById: usersById,
            userRoleCodesByUserId: userRoleCodesByUserId);
    }

    private static OnboardingCaseAggregate BuildCustomerCase(
        CustomerProfile profile,
        IReadOnlyDictionary<long, BusinessParty> partiesById,
        IReadOnlyDictionary<long, CashLineERP.Domain.Identity.ApplicationUser> usersById,
        IReadOnlyDictionary<long, IReadOnlyCollection<string>> userRoleCodesByUserId,
        IReadOnlyDictionary<string, WorkflowInstance> workflowsByKey,
        IReadOnlyDictionary<long, IReadOnlyCollection<OnboardingReviewChecklistItemView>> checklistByWorkflowId,
        IReadOnlyDictionary<long, IReadOnlyCollection<OnboardingReviewFindingView>> findingsByWorkflowId)
    {
        var party = partiesById[profile.BusinessPartyId];
        workflowsByKey.TryGetValue(BuildWorkflowKey("CUSTOMER_PROFILE", profile.Id), out var workflow);
        var checklistItems = workflow is not null && checklistByWorkflowId.TryGetValue(workflow.Id, out var checklist)
            ? checklist
            : [];
        var findings = workflow is not null && findingsByWorkflowId.TryGetValue(workflow.Id, out var findingSet)
            ? findingSet
            : [];

        return BuildAggregate(
            subjectTypeCode: "CUSTOMER",
            subjectProfileId: profile.Id,
            businessPartyId: party.Id,
            partyCode: party.PartyCode,
            partyName: party.PartyName,
            partyTypeCode: party.PartyType,
            profileCode: profile.CustomerProfileCode,
            countryCode: party.CountryCode,
            taxRegistrationNumber: party.TaxRegistrationNumber,
            email: party.Email,
            mobileNumber: party.MobileNumber,
            addressLine1: party.AddressLine1,
            domainStatusCode: profile.TaxProfileStatusCode,
            isActive: profile.IsActive,
            isOnHold: profile.IsOnHold,
            holdReason: profile.HoldReason,
            reviewDecisionNotes: profile.ReviewDecisionNotes,
            submittedAtUtc: profile.SubmittedAtUtc,
            submittedByUserId: profile.SubmittedByUserId,
            approvedAtUtc: profile.ApprovedAtUtc,
            approvedByUserId: profile.ApprovedByUserId,
            activatedAtUtc: profile.ActivatedAtUtc,
            activatedByUserId: profile.ActivatedByUserId,
            workflowStatusCode: workflow?.CurrentStatusCode ?? WorkflowStatusSeedIds.ToCode(profile.StatusId),
            checklistItems: checklistItems,
            findings: findings,
            usersById: usersById,
            userRoleCodesByUserId: userRoleCodesByUserId);
    }

    private static OnboardingCaseAggregate BuildAggregate(
        string subjectTypeCode,
        long subjectProfileId,
        long businessPartyId,
        string partyCode,
        string partyName,
        string partyTypeCode,
        string profileCode,
        string countryCode,
        string? taxRegistrationNumber,
        string? email,
        string? mobileNumber,
        string? addressLine1,
        string? domainStatusCode,
        bool isActive,
        bool isOnHold,
        string? holdReason,
        string? reviewDecisionNotes,
        DateTime? submittedAtUtc,
        long? submittedByUserId,
        DateTime? approvedAtUtc,
        long? approvedByUserId,
        DateTime? activatedAtUtc,
        long? activatedByUserId,
        string workflowStatusCode,
        IReadOnlyCollection<OnboardingReviewChecklistItemView> checklistItems,
        IReadOnlyCollection<OnboardingReviewFindingView> findings,
        IReadOnlyDictionary<long, CashLineERP.Domain.Identity.ApplicationUser> usersById,
        IReadOnlyDictionary<long, IReadOnlyCollection<string>> userRoleCodesByUserId)
    {
        var unresolvedChecklistItemCount = checklistItems.Count(item => !IsChecklistResolved(item.StatusCode));
        var openFindingCount = findings.Count(item => !string.Equals(item.StatusCode, "CLOSED", StringComparison.OrdinalIgnoreCase));
        var readyForApproval = string.Equals(workflowStatusCode, WorkflowStatusCode.Submitted, StringComparison.OrdinalIgnoreCase)
            && !isOnHold
            && unresolvedChecklistItemCount == 0
            && openFindingCount == 0;
        var readyForActivation = string.Equals(workflowStatusCode, WorkflowStatusCode.Approved, StringComparison.OrdinalIgnoreCase)
            && !isActive;

        var caseStatusCode = DeriveCaseStatus(
            workflowStatusCode,
            isActive,
            isOnHold,
            checklistItems.Count > 0 || findings.Count > 0,
            readyForApproval);
        var queueCode = DeriveQueueCode(caseStatusCode);

        var lastChecklistReview = checklistItems
            .OrderByDescending(item => item.ReviewedAtUtc)
            .FirstOrDefault();
        var lastFinding = findings
            .OrderByDescending(item => item.LoggedAtUtc)
            .FirstOrDefault();

        var lastReviewActivityAtUtc = Max(lastChecklistReview?.ReviewedAtUtc, lastFinding?.LoggedAtUtc);
        var lastReviewActorLoginName = ResolveUserLoginName(
            lastChecklistReview?.ReviewedAtUtc >= lastFinding?.LoggedAtUtc
                ? lastChecklistReview?.ReviewedByUserId
                : lastFinding?.LoggedByUserId,
            usersById);
        var submissionSource = ResolveSubmissionSource(submittedByUserId, userRoleCodesByUserId);

        return new OnboardingCaseAggregate(
            subjectTypeCode,
            subjectProfileId,
            businessPartyId,
            partyCode,
            partyName,
            partyTypeCode,
            profileCode,
            submissionSource.InitiationPathCode,
            submissionSource.SubmissionSourceCode,
            submissionSource.SubmissionSourceDisplayName,
            caseStatusCode,
            queueCode,
            workflowStatusCode,
            countryCode,
            taxRegistrationNumber,
            email,
            mobileNumber,
            addressLine1,
            domainStatusCode,
            isActive,
            isOnHold,
            holdReason,
            reviewDecisionNotes,
            checklistItems.Count,
            unresolvedChecklistItemCount,
            findings.Count,
            openFindingCount,
            readyForApproval,
            readyForActivation,
            submittedAtUtc,
            submittedByUserId,
            ResolveUserLoginName(submittedByUserId, usersById),
            ResolveUserDisplayName(submittedByUserId, usersById),
            approvedAtUtc,
            ResolveUserLoginName(approvedByUserId, usersById),
            activatedAtUtc,
            ResolveUserLoginName(activatedByUserId, usersById),
            lastReviewActivityAtUtc,
            lastReviewActorLoginName,
            checklistItems,
            findings);
    }

    private static string DeriveCaseStatus(string workflowStatusCode, bool isActive, bool isOnHold, bool hasReviewActivity, bool readyForApproval)
    {
        if (isActive)
        {
            return "ACTIVATED";
        }

        if (isOnHold)
        {
            return "ON_HOLD";
        }

        if (string.Equals(workflowStatusCode, WorkflowStatusCode.Approved, StringComparison.OrdinalIgnoreCase))
        {
            return "APPROVED";
        }

        if (string.Equals(workflowStatusCode, WorkflowStatusCode.Rejected, StringComparison.OrdinalIgnoreCase))
        {
            return "REJECTED";
        }

        if (string.Equals(workflowStatusCode, WorkflowStatusCode.Returned, StringComparison.OrdinalIgnoreCase))
        {
            return "RETURNED_FOR_REWORK";
        }

        if (string.Equals(workflowStatusCode, WorkflowStatusCode.Submitted, StringComparison.OrdinalIgnoreCase))
        {
            if (readyForApproval)
            {
                return "PENDING_APPROVAL";
            }

            return hasReviewActivity ? "UNDER_REVIEW" : "SUBMITTED";
        }

        return "DRAFT";
    }

    private static string DeriveQueueCode(string caseStatusCode) => caseStatusCode switch
    {
        "SUBMITTED" => "ONBOARDING_INTAKE",
        "UNDER_REVIEW" => "ONBOARDING_REVIEW",
        "PENDING_APPROVAL" => "ONBOARDING_APPROVAL",
        "ON_HOLD" => "ONBOARDING_HOLD",
        "RETURNED_FOR_REWORK" or "REJECTED" => "ONBOARDING_EXCEPTION",
        "APPROVED" => "ONBOARDING_ACTIVATION_READY",
        "ACTIVATED" => "ONBOARDING_COMPLETED",
        _ => "ONBOARDING_DRAFT"
    };

    private static bool IsChecklistResolved(string statusCode) =>
        string.Equals(statusCode, "ACCEPTED", StringComparison.OrdinalIgnoreCase)
        || string.Equals(statusCode, "WAIVED_WITH_REASON", StringComparison.OrdinalIgnoreCase);

    private static string BuildWorkflowKey(string documentType, long documentId) => $"{documentType}:{documentId}";

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

    private static string NormalizeRequiredSubjectType(string subjectTypeCode)
    {
        var normalized = Normalize(subjectTypeCode);
        return normalized switch
        {
            "SUPPLIER" or "CUSTOMER" => normalized,
            _ => throw new InvalidOperationException("Unsupported onboarding subject type.")
        };
    }

    private static DateTime? Max(DateTime? left, DateTime? right)
    {
        if (!left.HasValue)
        {
            return right;
        }

        if (!right.HasValue)
        {
            return left;
        }

        return left.Value >= right.Value ? left : right;
    }

    private static string? ResolveUserLoginName(long? userId, IReadOnlyDictionary<long, CashLineERP.Domain.Identity.ApplicationUser> usersById)
    {
        if (!userId.HasValue)
        {
            return null;
        }

        if (userId.Value == PublicSubmissionActorUserId)
        {
            return "PUBLIC_APPLICANT";
        }

        return usersById.TryGetValue(userId.Value, out var user) ? user.LoginName : null;
    }

    private static string? ResolveUserDisplayName(long? userId, IReadOnlyDictionary<long, CashLineERP.Domain.Identity.ApplicationUser> usersById)
    {
        if (!userId.HasValue)
        {
            return null;
        }

        if (userId.Value == PublicSubmissionActorUserId)
        {
            return "Public applicant";
        }

        return usersById.TryGetValue(userId.Value, out var user) ? user.DisplayName : null;
    }

    private static (string InitiationPathCode, string SubmissionSourceCode, string SubmissionSourceDisplayName) ResolveSubmissionSource(
        long? submittedByUserId,
        IReadOnlyDictionary<long, IReadOnlyCollection<string>> userRoleCodesByUserId)
    {
        if (!submittedByUserId.HasValue || submittedByUserId.Value == PublicSubmissionActorUserId)
        {
            return ("PUBLIC_SELF_INITIATED", "PUBLIC_APPLICANT", "Public self-service applicant");
        }

        if (userRoleCodesByUserId.TryGetValue(submittedByUserId.Value, out var roleCodes)
            && roleCodes.Any(roleCode =>
                string.Equals(roleCode, "ACCOUNTING_OFFICE_USER", StringComparison.OrdinalIgnoreCase)
                || string.Equals(roleCode, "ACCOUNTING_OFFICE_SUPERVISOR", StringComparison.OrdinalIgnoreCase)))
        {
            return ("ACCOUNTING_OFFICE_MANAGED", "ACCOUNTING_OFFICE_SUBMISSION", "Accounting Office submission");
        }

        return ("INTERNAL_MANUAL", "CASHLINE_INTERNAL_SUBMISSION", "CashLine internal submission");
    }

    private sealed record OnboardingCaseAggregate(
        string SubjectTypeCode,
        long SubjectProfileId,
        long BusinessPartyId,
        string PartyCode,
        string PartyName,
        string PartyTypeCode,
        string ProfileCode,
        string InitiationPathCode,
        string SubmissionSourceCode,
        string SubmissionSourceDisplayName,
        string CaseStatusCode,
        string QueueCode,
        string WorkflowStatusCode,
        string CountryCode,
        string? TaxRegistrationNumber,
        string? Email,
        string? MobileNumber,
        string? AddressLine1,
        string? DomainStatusCode,
        bool IsActive,
        bool IsOnHold,
        string? HoldReason,
        string? ReviewDecisionNotes,
        int ChecklistItemCount,
        int UnresolvedChecklistItemCount,
        int FindingCount,
        int OpenFindingCount,
        bool ReadyForApproval,
        bool ReadyForActivation,
        DateTime? SubmittedAtUtc,
        long? SubmittedByUserId,
        string? SubmittedByLoginName,
        string? SubmittedByDisplayName,
        DateTime? ApprovedAtUtc,
        string? ApprovedByLoginName,
        DateTime? ActivatedAtUtc,
        string? ActivatedByLoginName,
        DateTime? LastReviewActivityAtUtc,
        string? LastReviewActorLoginName,
        IReadOnlyCollection<OnboardingReviewChecklistItemView> ChecklistItems,
        IReadOnlyCollection<OnboardingReviewFindingView> Findings)
    {
        public OnboardingCaseSummaryView ToSummaryView() =>
            new(
                SubjectTypeCode,
                SubjectProfileId,
                BusinessPartyId,
                PartyCode,
                PartyName,
                ProfileCode,
                InitiationPathCode,
                SubmissionSourceCode,
                SubmissionSourceDisplayName,
                CaseStatusCode,
                QueueCode,
                WorkflowStatusCode,
                ChecklistItemCount,
                UnresolvedChecklistItemCount,
                FindingCount,
                OpenFindingCount,
                ReadyForApproval,
                ReadyForActivation,
                SubmittedAtUtc,
                SubmittedByLoginName,
                SubmittedByDisplayName,
                LastReviewActivityAtUtc,
                LastReviewActorLoginName);

        public OnboardingCaseDetailView ToDetailView() =>
            new(
                SubjectTypeCode,
                SubjectProfileId,
                BusinessPartyId,
                PartyCode,
                PartyName,
                PartyTypeCode,
                ProfileCode,
                InitiationPathCode,
                SubmissionSourceCode,
                SubmissionSourceDisplayName,
                CaseStatusCode,
                QueueCode,
                WorkflowStatusCode,
                CountryCode,
                TaxRegistrationNumber,
                Email,
                MobileNumber,
                AddressLine1,
                DomainStatusCode,
                IsActive,
                IsOnHold,
                HoldReason,
                ReviewDecisionNotes,
                ChecklistItemCount,
                UnresolvedChecklistItemCount,
                FindingCount,
                OpenFindingCount,
                ReadyForApproval,
                ReadyForActivation,
                SubmittedAtUtc,
                SubmittedByLoginName,
                SubmittedByDisplayName,
                ApprovedAtUtc,
                ApprovedByLoginName,
                ActivatedAtUtc,
                ActivatedByLoginName,
                LastReviewActivityAtUtc,
                LastReviewActorLoginName,
                ChecklistItems,
                Findings);
    }

    private static OnboardingCaseSummaryView MapSummary(OnboardingCaseAggregate aggregate) => aggregate.ToSummaryView();
}
