using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Backoffice;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Backoffice;

internal sealed class BackofficeService(
    TikuDbContext dbContext,
    IOperationAuditService auditService,
    IFeatureAccessService featureAccessService) : IBackofficeService
{
    public async Task<BackofficeUiBootstrap> GetTenantUiBootstrapAsync(
        CurrentAccessSnapshot access,
        CancellationToken cancellationToken = default)
    {
        if (!access.IsUserActive || !access.IsCurrentTenantMember ||
            access.UserId is null || access.TenantId is null)
        {
            throw new BackofficeException("Tenant backoffice access is denied.", "tenant_access_denied");
        }

        var permissionCodes = await FilterTenantPermissionCodesAsync(
            access.TenantId.Value,
            access.TenantPermissions,
            CapabilityOperation.Read,
            cancellationToken);
        var menus = await LoadEffectiveMenusAsync(
            BackendPermissionArea.Tenant,
            permissionCodes,
            cancellationToken);
        var features = await featureAccessService.GetEnabledFeaturesAsync(
            access.TenantId.Value, FeatureAccessOperation.Read, cancellationToken);
        var quotas = await featureAccessService.GetQuotaSummaryAsync(access.TenantId.Value, cancellationToken);
        return new BackofficeUiBootstrap(permissionCodes, menus, features.Order(StringComparer.Ordinal).ToArray(), quotas);
    }

    public async Task<BackofficeUiBootstrap> GetPlatformUiBootstrapAsync(
        CurrentAccessSnapshot access,
        CancellationToken cancellationToken = default)
    {
        if (!access.IsUserActive || access.UserId is null || access.PlatformPermissions.Count == 0)
        {
            throw new BackofficeException("Platform backoffice access is denied.", "platform_access_denied");
        }

        var permissionCodes = access.PlatformPermissions.Order(StringComparer.Ordinal).ToArray();
        var menus = await LoadEffectiveMenusAsync(
            BackendPermissionArea.Platform,
            permissionCodes,
            cancellationToken);
        return new BackofficeUiBootstrap(permissionCodes, menus, [], []);
    }

    public async Task<BackofficeBootstrap> GetTenantBootstrapAsync(
        BackofficeActor actor,
        CancellationToken cancellationToken = default)
    {
        var tenantId = RequireTenantAdmin(actor);
        var roles = await LoadTenantRolesAsync(tenantId, cancellationToken);
        var permissions = await dbContext.BackendPermissions.AsNoTracking()
            .Where(item => item.Area == BackendPermissionArea.Tenant || item.Area == BackendPermissionArea.Both)
            .OrderBy(item => item.PermissionModuleCode).ThenBy(item => item.SortOrder).ThenBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        var enabledPermissionCodes = await FilterTenantPermissionCodesAsync(
            tenantId,
            permissions.Select(item => item.Code),
            CapabilityOperation.Read,
            cancellationToken);
        permissions = permissions.Where(item => enabledPermissionCodes.Contains(item.Code, StringComparer.Ordinal)).ToArray();
        var menus = await dbContext.BackendMenus.AsNoTracking()
            .Where(item => item.IsActive && item.Area == BackendPermissionArea.Tenant)
            .OrderBy(item => item.SortOrder).ThenBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        menus = menus.Where(item => item.PermissionCode is null ||
            enabledPermissionCodes.Contains(item.PermissionCode, StringComparer.Ordinal)).ToArray();
        return new BackofficeBootstrap(
            permissions.Select(ToPermissionItem).ToArray(),
            menus.Select(ToMenuItem).ToArray(),
            roles);
    }

    public async Task<BackofficeBootstrap> GetPlatformBootstrapAsync(
        BackofficeActor actor,
        CancellationToken cancellationToken = default)
    {
        RequirePlatformAdmin(actor);
        var roles = await LoadPlatformRolesAsync(cancellationToken);
        var permissions = await dbContext.BackendPermissions.AsNoTracking()
            .Where(item => item.Area == BackendPermissionArea.Platform || item.Area == BackendPermissionArea.Both)
            .OrderBy(item => item.PermissionModuleCode).ThenBy(item => item.SortOrder).ThenBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        var menus = await dbContext.BackendMenus.AsNoTracking()
            .Where(item => item.IsActive && item.Area == BackendPermissionArea.Platform)
            .OrderBy(item => item.SortOrder).ThenBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        return new BackofficeBootstrap(
            permissions.Select(ToPermissionItem).ToArray(),
            menus.Select(ToMenuItem).ToArray(),
            roles);
    }

    public async Task<BackofficeRoleItem> UpsertTenantRoleAsync(
        BackofficeActor actor,
        UpsertBackofficeRoleCommand command,
        CancellationToken cancellationToken = default)
    {
        var tenantId = RequireTenantAdmin(actor);
        var role = command.Id.HasValue
            ? await dbContext.TenantBackendRoles.SingleOrDefaultAsync(
                item => item.TenantId == tenantId && item.Id == command.Id.Value,
                cancellationToken)
            : await dbContext.TenantBackendRoles.SingleOrDefaultAsync(
                item => item.TenantId == tenantId && item.Code == NormalizeCode(command.Code),
                cancellationToken);
        if (role is null)
        {
            role = new TenantBackendRole { TenantId = tenantId, Code = NormalizeCode(command.Code) };
            dbContext.TenantBackendRoles.Add(role);
        }
        else if (role.IsSystem)
        {
            throw new BackofficeException("System tenant role cannot be modified.", "system_role_locked");
        }

        role.Name = command.Name.Trim();
        role.Status = command.Status;
        role.Description = command.Description?.Trim();
        role.DataScope = command.DataScope ?? JsonDefaults.Object();
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "tenant.role.upserted", "tenant_backend_roles", role.Id, new { role.Code }, cancellationToken);
        return await LoadTenantRoleAsync(tenantId, role.Id, cancellationToken);
    }

    public async Task<BackofficeRoleItem> UpsertPlatformRoleAsync(
        BackofficeActor actor,
        UpsertBackofficeRoleCommand command,
        CancellationToken cancellationToken = default)
    {
        RequirePlatformAdmin(actor);
        var role = command.Id.HasValue
            ? await dbContext.PlatformBackendRoles.SingleOrDefaultAsync(item => item.Id == command.Id.Value, cancellationToken)
            : await dbContext.PlatformBackendRoles.SingleOrDefaultAsync(item => item.Code == NormalizeCode(command.Code), cancellationToken);
        if (role is null)
        {
            role = new PlatformBackendRole { Code = NormalizeCode(command.Code) };
            dbContext.PlatformBackendRoles.Add(role);
        }
        else if (role.IsSystem)
        {
            throw new BackofficeException("System platform role cannot be modified.", "system_role_locked");
        }

        role.Name = command.Name.Trim();
        role.Status = command.Status;
        role.Description = command.Description?.Trim();
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.role.upserted", "platform_backend_roles", role.Id, new { role.Code }, cancellationToken);
        return await LoadPlatformRoleAsync(role.Id, cancellationToken);
    }

    public async Task<BackofficeRoleItem> ReplaceTenantRoleBindingsAsync(
        BackofficeActor actor,
        ReplaceRoleBindingsCommand command,
        CancellationToken cancellationToken = default)
    {
        var tenantId = RequireTenantAdmin(actor);
        var role = await dbContext.TenantBackendRoles.SingleOrDefaultAsync(
            item => item.TenantId == tenantId && item.Id == command.RoleId,
            cancellationToken) ?? throw new BackofficeException("Tenant role was not found.", "role_not_found");
        if (role.IsSystem)
        {
            throw new BackofficeException("System tenant role bindings cannot be modified.", "system_role_locked");
        }

        await ReplaceTenantBindingsCoreAsync(tenantId, role.Id, command.PermissionCodes, command.MenuCodes, cancellationToken);
        await AuditAsync(actor, "tenant.role.bindings_replaced", "tenant_backend_roles", role.Id, new { role.Code }, cancellationToken);
        return await LoadTenantRoleAsync(tenantId, role.Id, cancellationToken);
    }

    public async Task<BackofficeRoleItem> ReplacePlatformRoleBindingsAsync(
        BackofficeActor actor,
        ReplaceRoleBindingsCommand command,
        CancellationToken cancellationToken = default)
    {
        RequirePlatformAdmin(actor);
        var role = await dbContext.PlatformBackendRoles.SingleOrDefaultAsync(
            item => item.Id == command.RoleId,
            cancellationToken) ?? throw new BackofficeException("Platform role was not found.", "role_not_found");
        if (role.IsSystem)
        {
            throw new BackofficeException("System platform role bindings cannot be modified.", "system_role_locked");
        }

        await ReplacePlatformBindingsCoreAsync(role.Id, command.PermissionCodes, command.MenuCodes, cancellationToken);
        await AuditAsync(actor, "platform.role.bindings_replaced", "platform_backend_roles", role.Id, new { role.Code }, cancellationToken);
        return await LoadPlatformRoleAsync(role.Id, cancellationToken);
    }

    public async Task ReplaceTenantUserRolesAsync(
        BackofficeActor actor,
        ReplaceUserRolesCommand command,
        CancellationToken cancellationToken = default)
    {
        var tenantId = RequireTenantAdmin(actor);
        var roleIds = command.RoleIds.Distinct().ToArray();
        var ownerRoleId = await dbContext.TenantBackendRoles
            .Where(item => item.TenantId == tenantId && item.Code == "tenant_owner" && item.IsSystem)
            .Select(item => (Guid?)item.Id)
            .SingleOrDefaultAsync(cancellationToken);
        var isActiveOwner = await dbContext.TenantMemberships.AnyAsync(
            item => item.TenantId == tenantId &&
                    item.UserId == command.UserId &&
                    item.Role == TenantRole.TenantOwner &&
                    item.Status == MembershipStatus.Active,
            cancellationToken);
        if (isActiveOwner && ownerRoleId.HasValue && !roleIds.Contains(ownerRoleId.Value))
        {
            throw new BackofficeException("Tenant owner system role cannot be removed.", "system_role_locked");
        }

        var count = await dbContext.TenantBackendRoles.CountAsync(
            item => item.TenantId == tenantId && roleIds.Contains(item.Id) && item.Status == BackendRoleStatus.Active,
            cancellationToken);
        if (count != roleIds.Length)
        {
            throw new BackofficeException("One or more tenant roles were not found.", "role_not_found");
        }

        await dbContext.TenantBackendUserRoles
            .Where(item => item.TenantId == tenantId && item.UserId == command.UserId)
            .ExecuteDeleteAsync(cancellationToken);
        dbContext.TenantBackendUserRoles.AddRange(roleIds.Select(roleId => new TenantBackendUserRole
        {
            TenantId = tenantId,
            UserId = command.UserId,
            RoleId = roleId
        }));
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "tenant.user_roles.replaced", "users", command.UserId, new { roleIds }, cancellationToken);
    }

    public async Task ReplacePlatformUserRolesAsync(
        BackofficeActor actor,
        ReplaceUserRolesCommand command,
        CancellationToken cancellationToken = default)
    {
        RequirePlatformAdmin(actor);
        var roleIds = command.RoleIds.Distinct().ToArray();
        var count = await dbContext.PlatformBackendRoles.CountAsync(
            item => roleIds.Contains(item.Id) && item.Status == BackendRoleStatus.Active,
            cancellationToken);
        if (count != roleIds.Length)
        {
            throw new BackofficeException("One or more platform roles were not found.", "role_not_found");
        }

        await dbContext.PlatformBackendUserRoles
            .Where(item => item.UserId == command.UserId)
            .ExecuteDeleteAsync(cancellationToken);
        dbContext.PlatformBackendUserRoles.AddRange(roleIds.Select(roleId => new PlatformBackendUserRole
        {
            UserId = command.UserId,
            RoleId = roleId
        }));
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor, "platform.user_roles.replaced", "users", command.UserId, new { roleIds }, cancellationToken);
    }

    private async Task ReplaceTenantBindingsCoreAsync(
        Guid tenantId,
        Guid roleId,
        IReadOnlyCollection<string> permissionCodes,
        IReadOnlyCollection<string> menuCodes,
        CancellationToken cancellationToken)
    {
        var normalizedPermissions = NormalizeCodes(permissionCodes);
        var normalizedMenus = NormalizeCodes(menuCodes);
        await ValidatePermissionCodesAsync(normalizedPermissions, BackendPermissionArea.Tenant, cancellationToken);
        var allowedPermissions = await featureAccessService.FilterPermissionCodesAsync(
            tenantId, normalizedPermissions, FeatureAccessOperation.Write, cancellationToken);
        if (allowedPermissions.Count != normalizedPermissions.Length)
        {
            throw new BackofficeException(
                "One or more permissions belong to a feature unavailable to this tenant.",
                "feature_not_available");
        }
        await ValidateMenuCodesAsync(normalizedMenus, BackendPermissionArea.Tenant, cancellationToken);
        await dbContext.TenantBackendRolePermissions.Where(item => item.TenantId == tenantId && item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
        await dbContext.TenantBackendRoleMenus.Where(item => item.TenantId == tenantId && item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
        dbContext.TenantBackendRolePermissions.AddRange(normalizedPermissions.Select(code => new TenantBackendRolePermission { TenantId = tenantId, RoleId = roleId, PermissionCode = code }));
        dbContext.TenantBackendRoleMenus.AddRange(normalizedMenus.Select(code => new TenantBackendRoleMenu { TenantId = tenantId, RoleId = roleId, MenuCode = code }));
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private async Task ReplacePlatformBindingsCoreAsync(
        Guid roleId,
        IReadOnlyCollection<string> permissionCodes,
        IReadOnlyCollection<string> menuCodes,
        CancellationToken cancellationToken)
    {
        var normalizedPermissions = NormalizeCodes(permissionCodes);
        var normalizedMenus = NormalizeCodes(menuCodes);
        await ValidatePermissionCodesAsync(normalizedPermissions, BackendPermissionArea.Platform, cancellationToken);
        await ValidateMenuCodesAsync(normalizedMenus, BackendPermissionArea.Platform, cancellationToken);
        await dbContext.PlatformBackendRolePermissions.Where(item => item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
        await dbContext.PlatformBackendRoleMenus.Where(item => item.RoleId == roleId).ExecuteDeleteAsync(cancellationToken);
        dbContext.PlatformBackendRolePermissions.AddRange(normalizedPermissions.Select(code => new PlatformBackendRolePermission { RoleId = roleId, PermissionCode = code }));
        dbContext.PlatformBackendRoleMenus.AddRange(normalizedMenus.Select(code => new PlatformBackendRoleMenu { RoleId = roleId, MenuCode = code }));
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private async Task ValidatePermissionCodesAsync(string[] codes, BackendPermissionArea area, CancellationToken cancellationToken)
    {
        var count = await dbContext.BackendPermissions.CountAsync(
            item => codes.Contains(item.Code) && (item.Area == area || item.Area == BackendPermissionArea.Both),
            cancellationToken);
        if (count != codes.Length)
        {
            throw new BackofficeException("One or more permissions were not found.", "permission_not_found");
        }
    }

    private async Task<BackofficeMenuItem[]> LoadEffectiveMenusAsync(
        BackendPermissionArea area,
        IReadOnlyCollection<string> permissionCodes,
        CancellationToken cancellationToken)
    {
        var codes = permissionCodes.ToArray();
        var menus = await dbContext.BackendMenus.AsNoTracking()
            .Where(item => item.IsActive && item.Area == area &&
                (item.PermissionCode == null || codes.Contains(item.PermissionCode)))
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        return menus.Select(ToMenuItem).ToArray();
    }

    private async Task<string[]> FilterTenantPermissionCodesAsync(
        Guid tenantId,
        IEnumerable<string> permissionCodes,
        CapabilityOperation operation,
        CancellationToken cancellationToken)
    {
        var featureOperation = operation == CapabilityOperation.Read
            ? FeatureAccessOperation.Read
            : FeatureAccessOperation.Write;
        var enabled = await featureAccessService.FilterPermissionCodesAsync(
            tenantId, permissionCodes, featureOperation, cancellationToken);
        return enabled.Order(StringComparer.Ordinal).ToArray();
    }

    private async Task ValidateMenuCodesAsync(string[] codes, BackendPermissionArea area, CancellationToken cancellationToken)
    {
        var count = await dbContext.BackendMenus.CountAsync(
            item => codes.Contains(item.Code) && item.Area == area && item.IsActive,
            cancellationToken);
        if (count != codes.Length)
        {
            throw new BackofficeException("One or more menus were not found.", "menu_not_found");
        }
    }

    private async Task<BackofficeRoleItem[]> LoadTenantRolesAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        var roles = await dbContext.TenantBackendRoles.AsNoTracking()
            .Where(item => item.TenantId == tenantId)
            .OrderBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        var roleIds = roles.Select(item => item.Id).ToArray();
        var permissions = await dbContext.TenantBackendRolePermissions.AsNoTracking()
            .Where(item => item.TenantId == tenantId && roleIds.Contains(item.RoleId))
            .ToArrayAsync(cancellationToken);
        var menus = await dbContext.TenantBackendRoleMenus.AsNoTracking()
            .Where(item => item.TenantId == tenantId && roleIds.Contains(item.RoleId))
            .ToArrayAsync(cancellationToken);
        return roles.Select(role => ToTenantRoleItem(role, permissions, menus)).ToArray();
    }

    private async Task<BackofficeRoleItem[]> LoadPlatformRolesAsync(CancellationToken cancellationToken)
    {
        var roles = await dbContext.PlatformBackendRoles.AsNoTracking()
            .OrderBy(item => item.Code)
            .ToArrayAsync(cancellationToken);
        var roleIds = roles.Select(item => item.Id).ToArray();
        var permissions = await dbContext.PlatformBackendRolePermissions.AsNoTracking()
            .Where(item => roleIds.Contains(item.RoleId))
            .ToArrayAsync(cancellationToken);
        var menus = await dbContext.PlatformBackendRoleMenus.AsNoTracking()
            .Where(item => roleIds.Contains(item.RoleId))
            .ToArrayAsync(cancellationToken);
        return roles.Select(role => ToPlatformRoleItem(role, permissions, menus)).ToArray();
    }

    private async Task<BackofficeRoleItem> LoadTenantRoleAsync(Guid tenantId, Guid roleId, CancellationToken cancellationToken)
    {
        return (await LoadTenantRolesAsync(tenantId, cancellationToken)).Single(item => item.Id == roleId);
    }

    private async Task<BackofficeRoleItem> LoadPlatformRoleAsync(Guid roleId, CancellationToken cancellationToken)
    {
        return (await LoadPlatformRolesAsync(cancellationToken)).Single(item => item.Id == roleId);
    }

    private async Task AuditAsync(BackofficeActor actor, string action, string targetType, Guid targetId, object details, CancellationToken cancellationToken)
    {
        await auditService.WriteAsync(new BackofficeOperationAuditCommand(
            actor.TenantId,
            actor.UserId,
            action,
            targetType,
            targetId.ToString(),
            JsonSerializer.SerializeToElement(details)), cancellationToken);
    }

    private static Guid RequireTenantAdmin(BackofficeActor actor)
    {
        if (actor.TenantId is not { } tenantId)
        {
            throw new BackofficeException("Tenant backoffice requires a resolved tenant.", "tenant_required");
        }

        return tenantId;
    }

    private static void RequirePlatformAdmin(BackofficeActor actor)
    {
        if (!actor.IsPlatform)
        {
            throw new BackofficeException("Platform backoffice access is denied.", "platform_access_denied");
        }
    }

    private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant();

    private static string[] NormalizeCodes(IEnumerable<string> codes)
    {
        return codes.Select(NormalizeCode).Where(item => item.Length > 0).Distinct(StringComparer.Ordinal).ToArray();
    }

    private static BackofficePermissionItem ToPermissionItem(BackendPermission item)
    {
        return new BackofficePermissionItem(item.Id, item.Code, item.Name, item.Area, item.PermissionModuleCode, item.Description, item.SortOrder);
    }

    private static BackofficeMenuItem ToMenuItem(BackendMenu item)
    {
        return new BackofficeMenuItem(item.Id, item.Code, item.ParentCode, item.Title, item.Area, item.Path, item.Icon, item.PermissionCode, item.SortOrder, item.IsActive);
    }

    private static BackofficeRoleItem ToTenantRoleItem(TenantBackendRole role, TenantBackendRolePermission[] permissions, TenantBackendRoleMenu[] menus)
    {
        return new BackofficeRoleItem(
            role.Id,
            role.Code,
            role.Name,
            role.Status,
            role.IsSystem,
            role.Description,
            permissions.Where(item => item.RoleId == role.Id).Select(item => item.PermissionCode).Order().ToArray(),
            menus.Where(item => item.RoleId == role.Id).Select(item => item.MenuCode).Order().ToArray(),
            role.DataScope);
    }

    private static BackofficeRoleItem ToPlatformRoleItem(PlatformBackendRole role, PlatformBackendRolePermission[] permissions, PlatformBackendRoleMenu[] menus)
    {
        return new BackofficeRoleItem(
            role.Id,
            role.Code,
            role.Name,
            role.Status,
            role.IsSystem,
            role.Description,
            permissions.Where(item => item.RoleId == role.Id).Select(item => item.PermissionCode).Order().ToArray(),
            menus.Where(item => item.RoleId == role.Id).Select(item => item.MenuCode).Order().ToArray());
    }

}

public sealed class BackofficeException(string message, string code) : InvalidOperationException(message)
{
    public string Code { get; } = code;
}
