using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using ROLAC.API.Authorization;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Permissions;
using ROLAC.API.Entities;
using ROLAC.API.Entities.Logging;
using ROLAC.API.Services.Logging;

namespace ROLAC.API.Services;

public interface IPermissionService
{
    /// <summary>True if any of the given roles grants the module/action.</summary>
    Task<bool> HasPermissionAsync(IEnumerable<string> roles, string module, string action);

    /// <summary>Effective permissions for a user (union across roles). super_admin ⇒ all.</summary>
    Task<Dictionary<string, ModuleActions>> GetEffectivePermissionsAsync(IEnumerable<string> roles);

    /// <summary>Dense matrix (every role × every module) for the admin UI.</summary>
    Task<PermissionMatrixDto> GetMatrixAsync();

    /// <summary>Replaces a role's grants. Rejects super_admin. Invalidates the cache.</summary>
    Task UpsertRoleAsync(string roleName, IEnumerable<ModulePermissionDto> rows);

    /// <summary>Drops the cached matrix so the next check rebuilds from the database.</summary>
    void Invalidate();
}

/// <summary>
/// Resolves the configurable RBAC matrix. Registered as a singleton; the in-memory
/// snapshot is shared across requests and rebuilt on demand. Database access goes
/// through a scoped <see cref="AppDbContext"/> obtained from <see cref="IServiceScopeFactory"/>.
/// </summary>
public class PermissionService : IPermissionService
{
    private const string CacheKey = "rbac:matrix";

    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IMemoryCache         _cache;
    private readonly SystemLogQueue       _logQueue;
    private readonly IHttpContextAccessor _http;

    public PermissionService(
        IServiceScopeFactory scopeFactory,
        IMemoryCache         cache,
        SystemLogQueue       logQueue,
        IHttpContextAccessor http)
    {
        _scopeFactory = scopeFactory;
        _cache        = cache;
        _logQueue     = logQueue;
        _http         = http;
    }

    public async Task<bool> HasPermissionAsync(IEnumerable<string> roles, string module, string action)
    {
        var snapshot = await GetSnapshotAsync();

        foreach (var role in roles)
        {
            if (snapshot.TryGetValue(role, out var modules)
                && modules.TryGetValue(module, out var actions)
                && Grants(actions, action))
            {
                return true;
            }
        }
        return false;
    }

    public async Task<Dictionary<string, ModuleActions>> GetEffectivePermissionsAsync(IEnumerable<string> roles)
    {
        var roleList = roles.ToList();

        if (roleList.Contains(PermissionAuthorizationHandler.SuperAdminRole))
            return AllModulesFull();

        var snapshot = await GetSnapshotAsync();
        var effective = new Dictionary<string, ModuleActions>();

        foreach (var role in roleList)
        {
            if (!snapshot.TryGetValue(role, out var modules))
                continue;

            foreach (var (module, actions) in modules)
            {
                if (!effective.TryGetValue(module, out var merged))
                    effective[module] = merged = new ModuleActions();

                merged.Read    |= actions.Read;
                merged.Write   |= actions.Write;
                merged.Delete  |= actions.Delete;
                merged.Approve |= actions.Approve;
            }
        }

        // Only surface modules the user can actually touch.
        return effective.Where(kvp => kvp.Value.Any)
                        .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }

    public async Task<PermissionMatrixDto> GetMatrixAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var roles = await db.Roles.OrderBy(r => r.Name).ToListAsync();
        var perms = await db.RolePermissions.ToListAsync();
        var byRoleId = perms.ToLookup(p => p.RoleId);

        var rows = new List<RolePermissionRow>();
        foreach (var role in roles)
        {
            var isSuperAdmin = role.Name == PermissionAuthorizationHandler.SuperAdminRole;
            var existing = byRoleId[role.Id].ToDictionary(p => p.Module);

            var moduleRows = new List<ModulePermissionDto>();
            foreach (var module in Modules.All)
            {
                existing.TryGetValue(module, out var rp);
                moduleRows.Add(new ModulePermissionDto
                {
                    Module     = module,
                    // super_admin is always-full (display only — never persisted).
                    CanRead    = isSuperAdmin || (rp?.CanRead    ?? false),
                    CanWrite   = isSuperAdmin || (rp?.CanWrite   ?? false),
                    CanDelete  = isSuperAdmin || (rp?.CanDelete  ?? false),
                    CanApprove = isSuperAdmin || (rp?.CanApprove ?? false),
                });
            }

            rows.Add(new RolePermissionRow
            {
                RoleName     = role.Name!,
                Description  = role.Description,
                IsSuperAdmin = isSuperAdmin,
                Modules      = moduleRows,
            });
        }

        return new PermissionMatrixDto
        {
            AllModules = Modules.All,
            AllActions = PermissionActions.All,
            Roles      = rows,
        };
    }

    public async Task UpsertRoleAsync(string roleName, IEnumerable<ModulePermissionDto> rows)
    {
        if (roleName == PermissionAuthorizationHandler.SuperAdminRole)
            throw new InvalidOperationException("super_admin permissions are fixed and cannot be edited.");

        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var role = await db.Roles.FirstOrDefaultAsync(r => r.Name == roleName)
            ?? throw new KeyNotFoundException($"Role '{roleName}' not found.");

        var existing = await db.RolePermissions
            .Where(p => p.RoleId == role.Id)
            .ToListAsync();
        db.RolePermissions.RemoveRange(existing);

        foreach (var row in rows)
        {
            if (!Modules.IsValid(row.Module))
                continue;
            if (!(row.CanRead || row.CanWrite || row.CanDelete || row.CanApprove))
                continue; // don't store all-false rows

            db.RolePermissions.Add(new RolePermission
            {
                RoleId     = role.Id,
                Module     = row.Module,
                CanRead    = row.CanRead,
                CanWrite   = row.CanWrite,
                CanDelete  = row.CanDelete,
                CanApprove = row.CanApprove,
            });
        }

        await db.SaveChangesAsync();
        Invalidate();

        // Singleton service can't use the scoped IAuditLogger — enqueue directly.
        var user = _http.HttpContext?.User;
        _logQueue.TryEnqueue(new AuditLog
        {
            Timestamp     = DateTimeOffset.UtcNow,
            Level         = LogLevelEnum.Warning,
            Action        = AuditActions.PermissionChanged,
            Category      = AuditCategories.Security,
            EntityName    = "Role",
            EntityId      = roleName,
            Summary       = $"Permissions updated for role '{roleName}'",
            Changes       = AuditChangeSerializer.BuildChanges(null, new { Role = roleName, Modules = rows }),
            UserId        = user?.FindFirstValue(ClaimTypes.NameIdentifier) ?? user?.FindFirstValue("sub"),
            UserEmail     = user?.FindFirstValue("email"),
            IpAddress     = _http.HttpContext?.Connection.RemoteIpAddress?.ToString(),
            CorrelationId = _http.HttpContext?.TraceIdentifier,
        });
    }

    public void Invalidate() => _cache.Remove(CacheKey);

    // ── Internals ────────────────────────────────────────────────────────────

    /// <summary>roleName → (module → ModuleActions). Cached until invalidated.</summary>
    private async Task<Dictionary<string, Dictionary<string, ModuleActions>>> GetSnapshotAsync()
    {
        if (_cache.TryGetValue(CacheKey, out Dictionary<string, Dictionary<string, ModuleActions>>? cached)
            && cached is not null)
        {
            return cached;
        }

        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var rows = await (
            from rp in db.RolePermissions
            join r in db.Roles on rp.RoleId equals r.Id
            select new { RoleName = r.Name!, rp.Module, rp.CanRead, rp.CanWrite, rp.CanDelete, rp.CanApprove }
        ).ToListAsync();

        var snapshot = new Dictionary<string, Dictionary<string, ModuleActions>>();
        foreach (var row in rows)
        {
            if (!snapshot.TryGetValue(row.RoleName, out var modules))
                snapshot[row.RoleName] = modules = new Dictionary<string, ModuleActions>();

            modules[row.Module] = new ModuleActions
            {
                Read    = row.CanRead,
                Write   = row.CanWrite,
                Delete  = row.CanDelete,
                Approve = row.CanApprove,
            };
        }

        _cache.Set(CacheKey, snapshot);
        return snapshot;
    }

    private static bool Grants(ModuleActions actions, string action) => action switch
    {
        PermissionActions.Read    => actions.Read,
        PermissionActions.Write   => actions.Write,
        PermissionActions.Delete  => actions.Delete,
        PermissionActions.Approve => actions.Approve,
        _                          => false,
    };

    private static Dictionary<string, ModuleActions> AllModulesFull()
    {
        var all = new Dictionary<string, ModuleActions>();
        foreach (var module in Modules.All)
            all[module] = new ModuleActions { Read = true, Write = true, Delete = true, Approve = true };
        return all;
    }
}
