using CashLineERP.Application.Identity;
using CashLineERP.Contracts.Identity;
using CashLineERP.Domain.Audit;
using CashLineERP.Domain.Identity;
using CashLineERP.SharedKernel.Identity;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class AdminUserRepository : IAdminUserRepository
{
    private const string ModuleCode = "IAM";
    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<AdminUserView> GetUsers(string? userTypeCode = null, bool includeInactive = true)
    {
        var normalizedUserTypeCode = NormalizeOptionalCode(userTypeCode);
        return _dbContext.Users
            .AsNoTracking()
            .Where(user => includeInactive || user.IsActive)
            .Where(user => normalizedUserTypeCode == null || user.UserTypeCode == normalizedUserTypeCode)
            .OrderBy(user => user.UserTypeCode)
            .ThenBy(user => user.LoginName)
            .ToArray()
            .Select(Map)
            .ToArray();
    }

    public AdminUserView? GetUser(long userId)
    {
        var user = _dbContext.Users.AsNoTracking().SingleOrDefault(entry => entry.Id == userId);
        return user is null ? null : Map(user);
    }

    public IReadOnlyCollection<AdminRoleFunctionOptionResponse> GetRoleFunctionOptions(string? userTypeCode = null)
    {
        EnsureFunctionGovernanceCatalog();
        var normalizedUserTypeCode = NormalizeOptionalCode(userTypeCode);
        return _dbContext.Roles
            .AsNoTracking()
            .Where(role => role.IsActive)
            .OrderBy(role => role.PortalCode)
            .ThenBy(role => role.RoleName)
            .ToArray()
            .Where(role => normalizedUserTypeCode == null || RoleUserType(role.PortalCode) == normalizedUserTypeCode)
            .Select(role =>
            {
                var functionItems = GetFunctionItemsForRole(role.RoleCode, includeDisabled: false);
                return new AdminRoleFunctionOptionResponse(
                    role.RoleCode,
                    role.RoleName,
                    RoleUserType(role.PortalCode),
                    ResolveFunctionSetCode(role.RoleCode),
                    functionItems.Select(item => item.FunctionCode).ToArray(),
                    functionItems.Select(item => item.FunctionLabel).ToArray(),
                    functionItems);
            })
            .ToArray();
    }

    public IReadOnlyCollection<AdminPortalMenuSectionResponse> GetPortalMenuSections(bool includeDisabled = true)
    {
        EnsureFunctionGovernanceCatalog();
        var sections = _dbContext.PortalMenuSections
            .AsNoTracking()
            .ToArray()
            .Where(section => includeDisabled || section.IsEnabled)
            .OrderBy(section => section.UserTypeCode)
            .ThenBy(section => section.DisplayOrder)
            .ThenBy(section => section.SectionLabel)
            .ToArray();
        var functions = _dbContext.FunctionGovernanceDefinitions
            .AsNoTracking()
            .ToArray();

        return sections
            .Select(section => new AdminPortalMenuSectionResponse(
                section.PortalCode,
                section.UserTypeCode,
                section.SectionCode,
                section.SectionLabel,
                section.DisplayOrder,
                section.IsEnabled,
                functions
                    .Where(function => string.Equals(function.PortalCode, section.PortalCode, StringComparison.OrdinalIgnoreCase)
                        && string.Equals(function.UserTypeCode, section.UserTypeCode, StringComparison.OrdinalIgnoreCase)
                        && string.Equals(function.MenuSectionCode, section.SectionCode, StringComparison.OrdinalIgnoreCase))
                    .Where(function => includeDisabled || function.IsEnabled)
                    .OrderBy(function => function.DisplayOrder)
                    .ThenBy(function => function.FunctionLabel)
                    .Select(function => MapFunctionItem(function, section))
                    .ToArray()))
            .ToArray();
    }

    public AdminFunctionConfigurationResponse UpdateFunctionGovernance(UpdateAdminFunctionGovernanceRequest request, long actorUserId)
    {
        EnsureFunctionGovernanceCatalog();
        var portalCode = NormalizeRequiredCode(request.PortalCode, "Portal code");
        var userTypeCode = NormalizeRequiredCode(request.UserTypeCode, "User type");
        var sectionCode = NormalizeRequiredCode(request.SectionCode, "Section code");
        var section = _dbContext.PortalMenuSections.SingleOrDefault(entry =>
                entry.PortalCode == portalCode
                && entry.UserTypeCode == userTypeCode
                && entry.SectionCode == sectionCode)
            ?? throw new InvalidOperationException($"Menu section '{sectionCode}' was not found.");
        section.Update(
            NormalizeOptionalText(request.SectionLabel) ?? section.SectionLabel,
            request.DisplayOrder ?? section.DisplayOrder,
            request.SectionEnabled,
            actorUserId);

        if (!string.IsNullOrWhiteSpace(request.FunctionCode))
        {
            var functionCode = NormalizeRequiredCode(request.FunctionCode, "Function code");
            var function = _dbContext.FunctionGovernanceDefinitions.SingleOrDefault(entry =>
                    entry.FunctionCode == functionCode
                    && entry.PortalCode == portalCode
                    && entry.UserTypeCode == userTypeCode)
                ?? throw new InvalidOperationException($"Function '{functionCode}' was not found.");
            function.Update(
                NormalizeOptionalText(request.FunctionLabel) ?? function.FunctionLabel,
                section.SectionCode,
                request.DisplayOrder ?? function.DisplayOrder,
                request.FunctionEnabled ?? function.IsEnabled,
                actorUserId);

            if (request.FunctionEnabled == false)
            {
                DeactivateFunctionAssignments(function.FunctionCode);
            }
        }
        else if (!request.SectionEnabled)
        {
            var functionCodes = _dbContext.FunctionGovernanceDefinitions
                .Where(function => function.PortalCode == section.PortalCode
                    && function.UserTypeCode == section.UserTypeCode
                    && function.MenuSectionCode == section.SectionCode)
                .Select(function => function.FunctionCode)
                .ToArray();
            foreach (var functionCode in functionCodes)
            {
                DeactivateFunctionAssignments(functionCode);
            }
        }

        AddAudit(0, actorUserId, "FUNCTION_GOVERNANCE_UPDATED", sectionCode, request.FunctionCode);
        _dbContext.SaveChanges();
        return new AdminFunctionConfigurationResponse(
            GetRoleFunctionOptions("INTERNAL"),
            GetRoleFunctionOptions("EXTERNAL"),
            GetPortalMenuSections(includeDisabled: true));
    }

    public bool AnyFunctionEnabled(IReadOnlyCollection<string> functionCodes)
    {
        EnsureFunctionGovernanceCatalog();
        var requested = functionCodes
            .Select(code => NormalizeRequiredCode(code, "Function code"))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        var enabledSections = _dbContext.PortalMenuSections
            .AsNoTracking()
            .Where(section => section.IsEnabled)
            .Select(section => SectionKey(section))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        return _dbContext.FunctionGovernanceDefinitions
            .AsNoTracking()
            .Where(function => function.IsEnabled)
            .ToArray()
            .Any(function => requested.Contains(function.FunctionCode)
                && enabledSections.Contains(SectionKey(function.PortalCode, function.UserTypeCode, function.MenuSectionCode)));
    }

    public void RecordUserAdministrationReportExport(long actorUserId, string formatCode, string? userTypeCode, string? roleCode)
    {
        var normalizedFormatCode = NormalizeRequiredCode(formatCode, "Export format");
        var exportFilter = $"format={normalizedFormatCode}|userType={NormalizeOptionalCode(userTypeCode) ?? "ALL"}|role={NormalizeOptionalCode(roleCode) ?? "ALL"}";
        AddAudit(0, actorUserId, "USER_ADMIN_REPORT_EXPORTED", null, exportFilter);
        _dbContext.SaveChanges();
    }

    public AdminUserView CreateUser(CreateAdminManagedUserRequest request, long actorUserId)
    {
        var loginName = NormalizeRequiredText(request.LoginName, "Login name");
        var displayName = NormalizeRequiredText(request.DisplayName, "Display name");
        var userTypeCode = NormalizeRequiredCode(request.UserTypeCode, "User type");
        var passwordHash = PasswordSecurity.HashPassword(NormalizeRequiredText(request.Password, "Password"));
        var roles = ResolveActiveRolesForUserType(request.RoleCodes, userTypeCode);
        var functionCodes = ResolveAssignableFunctionCodes(roles, request.FunctionCodes);

        if (_dbContext.Users
            .AsNoTracking()
            .ToArray()
            .Any(entry => string.Equals(entry.LoginName, loginName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException($"User login '{loginName}' already exists.");
        }

        var nextUserId = (_dbContext.Users.Max(entry => (long?)entry.Id) ?? 0L) + 1L;
        var user = new ApplicationUser(
            nextUserId,
            loginName,
            displayName,
            userTypeCode,
            passwordHash,
            NormalizeOptionalText(request.Email),
            request.BranchId,
            request.PartyId);

        _dbContext.Users.Add(user);
        AddActiveRoles(user.Id, roles);
        AddActiveFunctions(user.Id, functionCodes);
        AddAudit(user.Id, actorUserId, "CREATED", null, BuildUserSnapshot(user));
        _dbContext.SaveChanges();
        return Map(user);
    }

    public AdminUserView ReplaceRoles(long userId, IReadOnlyCollection<string> roleCodes, IReadOnlyCollection<string>? functionCodes, long actorUserId)
    {
        var user = GetTrackedUser(userId);
        var oldRoles = string.Join(',', GetRoleCodes(userId));
        var oldFunctions = string.Join(',', GetFunctionCodes(userId));
        var roles = ResolveActiveRolesForUserType(roleCodes, user.UserTypeCode);
        var selectedFunctions = ResolveAssignableFunctionCodes(roles, functionCodes);
        DeactivateRoles(userId);
        DeactivateFunctions(userId);
        AddActiveRoles(userId, roles);
        AddActiveFunctions(userId, selectedFunctions);
        _dbContext.SaveChanges();
        var newRoles = string.Join(',', GetRoleCodes(userId));
        var newFunctions = string.Join(',', GetFunctionCodes(userId));
        AddAudit(user.Id, actorUserId, "ROLES_REPLACED", $"{oldRoles}|functions={oldFunctions}", $"{newRoles}|functions={newFunctions}");
        _dbContext.SaveChanges();
        return Map(user);
    }

    public AdminUserView SuspendUser(long userId, long actorUserId, string reason)
    {
        var user = GetTrackedUser(userId);
        var oldValue = BuildUserSnapshot(user);
        user.Deactivate();
        user.Lock();
        AddAudit(user.Id, actorUserId, "SUSPENDED", oldValue, NormalizeRequiredText(reason, "Suspension reason"));
        _dbContext.SaveChanges();
        return Map(user);
    }

    public AdminUserView HoldUser(long userId, long actorUserId, string reason)
    {
        var user = GetTrackedUser(userId);
        var oldValue = BuildUserSnapshot(user);
        user.Lock();
        AddAudit(user.Id, actorUserId, "ON_HOLD", oldValue, NormalizeRequiredText(reason, "Hold reason"));
        _dbContext.SaveChanges();
        return Map(user);
    }

    public AdminUserView ReinstateUser(long userId, long actorUserId, string reason)
    {
        var user = GetTrackedUser(userId);
        var oldValue = BuildUserSnapshot(user);
        user.Activate();
        user.Unlock();
        AddAudit(user.Id, actorUserId, "REINSTATED", oldValue, NormalizeRequiredText(reason, "Reinstatement reason"));
        _dbContext.SaveChanges();
        return Map(user);
    }

    public AdminUserView RevokeUser(long userId, long actorUserId, string reason)
    {
        var user = GetTrackedUser(userId);
        var oldRoles = string.Join(',', GetRoleCodes(userId));
        DeactivateRoles(userId);
        DeactivateFunctions(userId);
        user.Lock();
        AddAudit(user.Id, actorUserId, "ACCESS_REVOKED", oldRoles, NormalizeRequiredText(reason, "Revocation reason"));
        _dbContext.SaveChanges();
        return Map(user);
    }

    public AdminUserView DeleteUser(long userId, long actorUserId, string reason)
    {
        var user = GetTrackedUser(userId);
        var oldValue = BuildUserSnapshot(user);
        DeactivateRoles(userId);
        DeactivateFunctions(userId);
        user.Deactivate();
        user.Lock();
        AddAudit(user.Id, actorUserId, "DELETED", oldValue, NormalizeRequiredText(reason, "Delete reason"));
        _dbContext.SaveChanges();
        return Map(user);
    }

    private ApplicationUser GetTrackedUser(long userId) =>
        _dbContext.Users.SingleOrDefault(entry => entry.Id == userId)
        ?? throw new InvalidOperationException("Managed user was not found.");

    private Role[] ResolveActiveRolesForUserType(IReadOnlyCollection<string> roleCodes, string userTypeCode)
    {
        if (roleCodes.Count == 0)
        {
            throw new InvalidOperationException("At least one role code is required.");
        }

        var normalizedRoleCodes = roleCodes
            .Select(roleCode => NormalizeRequiredCode(roleCode, "Role code"))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
        var roles = _dbContext.Roles
            .Where(role => role.IsActive && normalizedRoleCodes.Contains(role.RoleCode))
            .ToArray();

        if (roles.Length != normalizedRoleCodes.Length)
        {
            var resolved = roles.Select(role => role.RoleCode).ToHashSet(StringComparer.OrdinalIgnoreCase);
            var missing = normalizedRoleCodes.Where(roleCode => !resolved.Contains(roleCode)).Order().ToArray();
            throw new InvalidOperationException($"Role code(s) not found or inactive: {string.Join(", ", missing)}.");
        }

        var invalidRoles = roles
            .Where(role => !string.Equals(RoleUserType(role.PortalCode), userTypeCode, StringComparison.OrdinalIgnoreCase))
            .Select(role => role.RoleCode)
            .Order()
            .ToArray();
        if (invalidRoles.Length > 0)
        {
            throw new InvalidOperationException($"Role code(s) are not valid for user type {userTypeCode}: {string.Join(", ", invalidRoles)}.");
        }

        return roles;
    }

    private string[] ResolveAssignableFunctionCodes(IReadOnlyCollection<Role> roles, IReadOnlyCollection<string>? requestedFunctionCodes)
    {
        var allowedFunctionCodes = roles
            .SelectMany(role => GetFunctionItemsForRole(role.RoleCode, includeDisabled: false).Select(item => item.FunctionCode))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Order(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var normalizedRequestedCodes = requestedFunctionCodes?
            .Select(code => NormalizeRequiredCode(code, "Function code"))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray() ?? [];

        if (normalizedRequestedCodes.Length == 0)
        {
            return allowedFunctionCodes;
        }

        var allowed = allowedFunctionCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
        var invalid = normalizedRequestedCodes
            .Where(code => !allowed.Contains(code))
            .Order(StringComparer.OrdinalIgnoreCase)
            .ToArray();
        if (invalid.Length > 0)
        {
            throw new InvalidOperationException($"Function code(s) are not valid for the selected role set: {string.Join(", ", invalid)}.");
        }

        return normalizedRequestedCodes.Order(StringComparer.OrdinalIgnoreCase).ToArray();
    }

    private void AddActiveRoles(long userId, IReadOnlyCollection<Role> roles)
    {
        foreach (var role in roles.OrderBy(role => role.RoleCode))
        {
            _dbContext.UserRoles.Add(new UserRole(userId, role.Id, DateTime.UtcNow));
        }
    }

    private void AddActiveFunctions(long userId, IReadOnlyCollection<string> functionCodes)
    {
        foreach (var functionCode in functionCodes.Order(StringComparer.OrdinalIgnoreCase))
        {
            _dbContext.UserFunctionAssignments.Add(new UserFunctionAssignment(userId, functionCode, DateTime.UtcNow));
        }
    }

    private void DeactivateRoles(long userId)
    {
        foreach (var assignment in _dbContext.UserRoles.Where(entry => entry.UserId == userId && entry.IsActive).ToArray())
        {
            assignment.Deactivate(DateTime.UtcNow);
        }
    }

    private void DeactivateFunctions(long userId)
    {
        foreach (var assignment in _dbContext.UserFunctionAssignments.Where(entry => entry.UserId == userId && entry.IsActive).ToArray())
        {
            assignment.Deactivate(DateTime.UtcNow);
        }
    }

    private void DeactivateFunctionAssignments(string functionCode)
    {
        foreach (var assignment in _dbContext.UserFunctionAssignments
            .Where(entry => entry.FunctionCode == functionCode && entry.IsActive)
            .ToArray())
        {
            assignment.Deactivate(DateTime.UtcNow);
        }
    }

    private AdminUserView Map(ApplicationUser user) =>
        new(
            user.Id,
            user.LoginName,
            user.DisplayName,
            user.UserTypeCode,
            user.Email,
            user.BranchId,
            user.PartyId,
            user.IsActive,
            user.IsLocked,
            GetRoleCodes(user.Id),
            GetFunctionCodes(user.Id),
            user.CreatedAtUtc,
            GetLatestSuspensionDate(user.Id),
            GetLatestAdminAction(user.Id)?.ActionCode,
            GetLatestAdminAction(user.Id)?.NewValue);

    private string[] GetRoleCodes(long userId) =>
        _dbContext.UserRoles
            .AsNoTracking()
            .Where(entry => entry.UserId == userId && entry.IsActive)
            .Join(
                _dbContext.Roles.AsNoTracking().Where(role => role.IsActive),
                userRole => userRole.RoleId,
                role => role.Id,
                (_, role) => role.RoleCode)
            .OrderBy(roleCode => roleCode)
            .ToArray();

    private string[] GetFunctionCodes(long userId) =>
        _dbContext.UserFunctionAssignments
            .AsNoTracking()
            .Where(entry => entry.UserId == userId && entry.IsActive)
            .Select(entry => entry.FunctionCode)
            .OrderBy(functionCode => functionCode)
            .ToArray();

    private void AddAudit(long userId, long actorUserId, string actionCode, string? oldValue, string? newValue)
    {
        _dbContext.AuditEvents.Add(new AuditEvent(
            ModuleCode,
            "SEC_USER",
            userId,
            actionCode,
            actorUserId,
            oldValue: oldValue,
            newValue: newValue));
    }

    private static string BuildUserSnapshot(ApplicationUser user) =>
        $"{user.LoginName}|active={user.IsActive}|locked={user.IsLocked}";

    private static string NormalizeRequiredCode(string value, string label)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new InvalidOperationException($"{label} is required.");
        }

        return value.Trim().ToUpperInvariant();
    }

    private static string NormalizeRequiredText(string value, string label)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new InvalidOperationException($"{label} is required.");
        }

        return value.Trim();
    }

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

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

    private DateTime? GetLatestSuspensionDate(long userId) =>
        _dbContext.AuditEvents
            .AsNoTracking()
            .Where(entry => entry.ModuleCode == ModuleCode && entry.EntityName == "SEC_USER" && entry.EntityId == userId && entry.ActionCode == "SUSPENDED")
            .OrderByDescending(entry => entry.OccurredAtUtc)
            .Select(entry => (DateTime?)entry.OccurredAtUtc)
            .FirstOrDefault();

    private AuditEvent? GetLatestAdminAction(long userId) =>
        _dbContext.AuditEvents
            .AsNoTracking()
            .Where(entry => entry.ModuleCode == ModuleCode && entry.EntityName == "SEC_USER" && entry.EntityId == userId)
            .OrderByDescending(entry => entry.OccurredAtUtc)
            .FirstOrDefault();

    private static string RoleUserType(string portalCode) =>
        string.Equals(portalCode, PortalCode.External, StringComparison.OrdinalIgnoreCase)
            ? UserTypeCode.External
            : UserTypeCode.Internal;

    private static string ResolveFunctionSetCode(string roleCode) =>
        roleCode.ToUpperInvariant() switch
        {
            "SUPER_ADMIN" => "USER_ADMINISTRATION",
            "SYSTEM_OWNER" => "ADMINISTRATOR_GOVERNANCE",
            "ACCOUNTING_OFFICE_USER" or "ACCOUNTING_OFFICE_SUPERVISOR" => "ACCOUNTING_OFFICE_GOVERNANCE",
            "SUPPLIER_ADMIN" or "EXTERNAL_SUPPLIER_USER" => "SUPPLIER_PORTAL",
            "CUSTOMER_ADMIN" or "EXTERNAL_CUSTOMER_USER" => "CUSTOMER_PORTAL",
            "FINANCE_MAKER" => "FINANCE_MAKER",
            "FINANCE_CHECKER" => "FINANCE_CHECKER",
            _ => "INTERNAL_OPERATIONS"
        };

    private static IReadOnlyCollection<string> ResolveFunctions(string roleCode) =>
        roleCode.ToUpperInvariant() switch
        {
            "SUPER_ADMIN" => new[] { "Create users", "Assign roles", "Configure user functions", "Suspend users", "Hold users", "Revoke users", "Delete users", "Export user administration reports" },
            "SYSTEM_OWNER" => new[] { "Create Administrator users", "Govern Administrator users", "Export Administrator activity reports" },
            "ACCOUNTING_OFFICE_USER" => new[] { "Accounting office dashboard", "Client onboarding intake", "Approved client finance tasks", "ETA and government submissions", "Accounting office reports" },
            "ACCOUNTING_OFFICE_SUPERVISOR" => new[] { "Accounting office dashboard", "Client onboarding intake", "Approved client finance tasks", "ETA and government submissions", "Accounting office reports" },
            "SUPPLIER_ADMIN" or "EXTERNAL_SUPPLIER_USER" => new[] { "Supplier onboarding", "Supplier documents", "Supplier purchase orders", "Supplier invoices", "Supplier statements", "Supplier notifications" },
            "CUSTOMER_ADMIN" or "EXTERNAL_CUSTOMER_USER" => new[] { "Customer onboarding", "Customer documents", "Customer orders", "Customer payments", "Customer statements", "Customer notifications" },
            "FINANCE_MAKER" => new[] { "Finance preparation", "Payment initiation", "Statement preparation", "Finance reporting" },
            "FINANCE_CHECKER" => new[] { "Finance review", "Payment approval", "Statement approval", "Finance reporting" },
            _ => new[] { "Operations workbench", "Onboarding review", "Workflow monitoring", "Operational reporting" }
        };

    private static IReadOnlyCollection<string> ResolveFunctionCodes(string roleCode) =>
        roleCode.ToUpperInvariant() switch
        {
            "SUPER_ADMIN" => new[] { "CREATE_USERS", "ASSIGN_ROLES", "CONFIGURE_USER_FUNCTIONS", "SUSPEND_USERS", "HOLD_USERS", "REVOKE_USERS", "DELETE_USERS", "EXPORT_USER_ADMIN_REPORTS" },
            "SYSTEM_OWNER" => new[] { "CREATE_ADMINISTRATORS", "GOVERN_ADMINISTRATORS", "EXPORT_ADMINISTRATOR_REPORTS" },
            "ACCOUNTING_OFFICE_USER" => new[] { "ACCOUNTING_OFFICE_DASHBOARD", "ACCOUNTING_OFFICE_CLIENT_ONBOARDING", "ACCOUNTING_OFFICE_CLIENT_FINANCE", "ACCOUNTING_OFFICE_ETA_SUBMISSIONS", "ACCOUNTING_OFFICE_REPORTING" },
            "ACCOUNTING_OFFICE_SUPERVISOR" => new[] { "ACCOUNTING_OFFICE_DASHBOARD", "ACCOUNTING_OFFICE_CLIENT_ONBOARDING", "ACCOUNTING_OFFICE_CLIENT_FINANCE", "ACCOUNTING_OFFICE_ETA_SUBMISSIONS", "ACCOUNTING_OFFICE_REPORTING" },
            "SUPPLIER_ADMIN" or "EXTERNAL_SUPPLIER_USER" => new[] { "SUPPLIER_ONBOARDING", "SUPPLIER_DOCUMENTS", "SUPPLIER_PURCHASE_ORDERS", "SUPPLIER_INVOICES", "SUPPLIER_STATEMENTS", "SUPPLIER_NOTIFICATIONS" },
            "CUSTOMER_ADMIN" or "EXTERNAL_CUSTOMER_USER" => new[] { "CUSTOMER_ONBOARDING", "CUSTOMER_DOCUMENTS", "CUSTOMER_ORDERS", "CUSTOMER_PAYMENTS", "CUSTOMER_STATEMENTS", "CUSTOMER_NOTIFICATIONS" },
            "FINANCE_MAKER" => new[] { "FINANCE_PREPARATION", "PAYMENT_INITIATION", "STATEMENT_PREPARATION", "FINANCE_REPORTING" },
            "FINANCE_CHECKER" => new[] { "FINANCE_REVIEW", "PAYMENT_APPROVAL", "STATEMENT_APPROVAL", "FINANCE_REPORTING" },
            _ => new[] { "OPERATIONS_WORKBENCH", "ONBOARDING_REVIEW", "WORKFLOW_MONITORING", "OPERATIONAL_REPORTING" }
        };

    private IReadOnlyCollection<AdminFunctionGovernanceItemResponse> GetFunctionItemsForRole(string roleCode, bool includeDisabled)
    {
        EnsureFunctionGovernanceCatalog();
        var allowedCodes = ResolveFunctionCodes(roleCode).ToHashSet(StringComparer.OrdinalIgnoreCase);
        var enabledSections = _dbContext.PortalMenuSections
            .AsNoTracking()
            .ToArray()
            .Where(section => includeDisabled || section.IsEnabled)
            .ToDictionary(SectionKey, StringComparer.OrdinalIgnoreCase);

        return _dbContext.FunctionGovernanceDefinitions
            .AsNoTracking()
            .ToArray()
            .Where(function => allowedCodes.Contains(function.FunctionCode))
            .Where(function => includeDisabled || function.IsEnabled)
            .Where(function => enabledSections.ContainsKey(SectionKey(function.PortalCode, function.UserTypeCode, function.MenuSectionCode)))
            .OrderBy(function => function.DisplayOrder)
            .ThenBy(function => function.FunctionLabel)
            .Select(function => MapFunctionItem(function, enabledSections[SectionKey(function.PortalCode, function.UserTypeCode, function.MenuSectionCode)]))
            .ToArray();
    }

    private void EnsureFunctionGovernanceCatalog()
    {
        var defaultItems = _dbContext.Roles
            .AsNoTracking()
            .Where(role => role.IsActive)
            .ToArray()
            .SelectMany(role => ResolveFunctionItems(role.RoleCode, role.PortalCode))
            .GroupBy(item => item.FunctionCode, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToArray();
        var existingSections = _dbContext.PortalMenuSections
            .ToArray()
            .ToDictionary(SectionKey, StringComparer.OrdinalIgnoreCase);
        var nextSectionId = (_dbContext.PortalMenuSections.Max(section => (long?)section.Id) ?? 0L) + 1L;

        foreach (var sectionGroup in defaultItems.GroupBy(item => SectionKey(item.PortalCode, item.UserTypeCode, item.MenuSectionCode), StringComparer.OrdinalIgnoreCase))
        {
            var first = sectionGroup.First();
            if (existingSections.ContainsKey(sectionGroup.Key))
            {
                continue;
            }

            var section = new PortalMenuSectionDefinition(
                nextSectionId++,
                first.PortalCode,
                first.UserTypeCode,
                first.MenuSectionCode,
                first.MenuSectionLabel,
                sectionGroup.Min(item => item.DisplayOrder),
                DateTime.UtcNow);
            _dbContext.PortalMenuSections.Add(section);
            existingSections[SectionKey(section)] = section;
        }

        var existingFunctions = _dbContext.FunctionGovernanceDefinitions
            .ToArray()
            .ToDictionary(function => function.FunctionCode, StringComparer.OrdinalIgnoreCase);
        var nextFunctionId = (_dbContext.FunctionGovernanceDefinitions.Max(function => (long?)function.Id) ?? 0L) + 1L;

        foreach (var item in defaultItems.Where(item => !existingFunctions.ContainsKey(item.FunctionCode)))
        {
            _dbContext.FunctionGovernanceDefinitions.Add(new FunctionGovernanceDefinition(
                nextFunctionId++,
                item.FunctionCode,
                item.FunctionLabel,
                item.MenuSectionCode,
                item.PortalCode,
                item.UserTypeCode,
                item.Route,
                item.StepId,
                item.PermissionCode,
                item.RuleCode,
                item.DisplayOrder,
                DateTime.UtcNow));
        }

        _dbContext.SaveChanges();
    }

    private static AdminFunctionGovernanceItemResponse MapFunctionItem(
        FunctionGovernanceDefinition function,
        PortalMenuSectionDefinition section) =>
        new(
            function.FunctionCode,
            function.FunctionLabel,
            section.SectionCode,
            section.SectionLabel,
            function.PortalCode,
            function.UserTypeCode,
            function.Route,
            function.StepId,
            function.PermissionCode,
            function.RuleCode,
            function.DisplayOrder,
            function.IsEnabled);

    private static string SectionKey(PortalMenuSectionDefinition section) =>
        SectionKey(section.PortalCode, section.UserTypeCode, section.SectionCode);

    private static string SectionKey(string portalCode, string userTypeCode, string sectionCode) =>
        $"{portalCode.ToUpperInvariant()}|{userTypeCode.ToUpperInvariant()}|{sectionCode.ToUpperInvariant()}";

    private static IReadOnlyCollection<AdminFunctionGovernanceItemResponse> ResolveFunctionItems(string roleCode, string portalCode)
    {
        var functionCodes = ResolveFunctionCodes(roleCode).ToArray();
        var labels = ResolveFunctions(roleCode).ToArray();
        return functionCodes
            .Select((functionCode, index) => BuildFunctionItem(functionCode, labels.ElementAtOrDefault(index) ?? functionCode, portalCode, index + 1))
            .ToArray();
    }

    private static AdminFunctionGovernanceItemResponse BuildFunctionItem(string functionCode, string label, string portalCode, int order)
    {
        var userTypeCode = RoleUserType(portalCode);
        var normalizedPortalCode = string.Equals(userTypeCode, UserTypeCode.External, StringComparison.OrdinalIgnoreCase)
            ? PortalCode.External
            : PortalCode.Internal;
        var (sectionCode, sectionLabel, route, stepId) = ResolveMenuBinding(functionCode, userTypeCode);
        return new AdminFunctionGovernanceItemResponse(
            functionCode,
            label,
            sectionCode,
            sectionLabel,
            normalizedPortalCode,
            userTypeCode,
            route,
            stepId,
            $"PERM_{functionCode}",
            $"ALLOW_{userTypeCode}_{functionCode}",
            order,
            true);
    }

    private static (string SectionCode, string SectionLabel, string Route, string StepId) ResolveMenuBinding(string functionCode, string userTypeCode) =>
        functionCode.ToUpperInvariant() switch
        {
            "SUPPLIER_ONBOARDING" => ("ACCESS_IDENTITY", "Access & Identity", "/supplier", "supplier-entry"),
            "SUPPLIER_DOCUMENTS" => ("ONBOARDING_KYC", "New Onboarding & KYC", "/supplier", "supplier-profile"),
            "SUPPLIER_PURCHASE_ORDERS" => ("OFFERS_ORDERS", "Offers and Orders", "/supplier", "supplier-orders"),
            "SUPPLIER_INVOICES" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/supplier", "supplier-payments"),
            "SUPPLIER_STATEMENTS" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/supplier", "supplier-reports"),
            "SUPPLIER_NOTIFICATIONS" => ("ACCESS_IDENTITY", "Access & Identity", "/supplier", "supplier-notifications"),
            "CUSTOMER_ONBOARDING" => ("ACCESS_IDENTITY", "Access & Identity", "/customer", "customer-entry"),
            "CUSTOMER_DOCUMENTS" => ("ONBOARDING_KYC", "New Onboarding & KYC", "/customer", "customer-profile"),
            "CUSTOMER_ORDERS" => ("REQUESTS_ORDERS", "Requests & Orders", "/customer", "customer-orders"),
            "CUSTOMER_PAYMENTS" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/customer", "customer-payments"),
            "CUSTOMER_STATEMENTS" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/customer", "customer-reports"),
            "CUSTOMER_NOTIFICATIONS" => ("ACCESS_IDENTITY", "Access & Identity", "/customer", "customer-notifications"),
            "FINANCE_PREPARATION" => ("FINANCE_OPERATIONS", "Finance Operations", "/internal/finance-approvals", "fa-intake"),
            "PAYMENT_INITIATION" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/internal/finance-approvals", "fa-gateway"),
            "STATEMENT_PREPARATION" => ("REPORTS_STATEMENTS", "Reports & Statements", "/internal/reporting-center", "reports-financial-statements"),
            "FINANCE_REPORTING" => ("REPORTS_STATEMENTS", "Reports & Statements", "/internal/reporting-center", "reports-financial-statements"),
            "FINANCE_REVIEW" => ("FINANCE_OPERATIONS", "Finance Operations", "/internal/finance-approvals", "fa-review"),
            "PAYMENT_APPROVAL" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/internal/finance-approvals", "fa-gateway"),
            "STATEMENT_APPROVAL" => ("REPORTS_STATEMENTS", "Reports & Statements", "/internal/reporting-center", "reports-financial-statements"),
            "ACCOUNTING_OFFICE_DASHBOARD" => ("ACCOUNTING_OFFICE", "Accounting Office", "/internal/accounting-office", "ao-command"),
            "ACCOUNTING_OFFICE_CLIENT_ONBOARDING" => ("ONBOARDING_KYC", "New Onboarding & KYC", "/internal/accounting-office", "ao-onboarding"),
            "ACCOUNTING_OFFICE_CLIENT_FINANCE" => ("PAYMENTS_SETTLEMENT", "Payments & Settlement", "/internal/accounting-office", "ao-clients"),
            "ACCOUNTING_OFFICE_ETA_SUBMISSIONS" => ("GOVERNMENT_SUBMISSIONS", "Government & ETA Submissions", "/internal/accounting-office", "ao-submissions"),
            "ACCOUNTING_OFFICE_REPORTING" => ("REPORTS_STATEMENTS", "Reports & Statements", "/internal/accounting-office", "ao-reports"),
            "OPERATIONS_WORKBENCH" => ("OPERATIONS_CONTROL", "Operations Control", "/internal/operations-control", "ops-intake"),
            "ONBOARDING_REVIEW" => ("ONBOARDING_KYC", "New Onboarding & KYC", "/internal/operations-control", "ops-onboarding"),
            "WORKFLOW_MONITORING" => ("WORKFLOW_MONITORING", "Workflow Monitoring", "/internal/workflow-monitoring", "monitor-live"),
            "OPERATIONAL_REPORTING" => ("REPORTS_STATEMENTS", "Reports & Statements", "/internal/reporting-center", "reports-operational"),
            "CREATE_USERS" => ("USER_ADMINISTRATION", "User Administration", "/internal/user-administration", "create-user"),
            "ASSIGN_ROLES" => ("USER_ADMINISTRATION", "User Administration", "/internal/user-administration", "assign-roles"),
            "CONFIGURE_USER_FUNCTIONS" => ("FUNCTION_GOVERNANCE", "Function Governance", "/internal/user-administration", "function-governance"),
            "SUSPEND_USERS" or "HOLD_USERS" or "REVOKE_USERS" or "DELETE_USERS" => ("USER_LIFECYCLE", "User Lifecycle Controls", "/internal/user-administration", "user-lifecycle"),
            "EXPORT_USER_ADMIN_REPORTS" => ("USER_REPORTING", "User Administration Reports", "/internal/user-administration", "user-reports"),
            "CREATE_ADMINISTRATORS" => ("ADMINISTRATOR_GOVERNANCE", "Administrator Governance", "/internal/user-administration", "create-administrator"),
            "GOVERN_ADMINISTRATORS" => ("ADMINISTRATOR_GOVERNANCE", "Administrator Governance", "/internal/user-administration", "govern-administrators"),
            "EXPORT_ADMINISTRATOR_REPORTS" => ("ADMINISTRATOR_REPORTING", "Administrator Activity Reports", "/internal/user-administration", "administrator-reports"),
            _ when string.Equals(userTypeCode, UserTypeCode.External, StringComparison.OrdinalIgnoreCase) => ("PORTAL_SERVICES", "Portal Services", "/supplier", functionCode.ToLowerInvariant().Replace('_', '-')),
            _ => ("INTERNAL_SERVICES", "Internal Services", "/internal/operations-control", functionCode.ToLowerInvariant().Replace('_', '-'))
        };
}
