using System.Collections;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;

namespace PlotLine.Services;

public sealed class ProjectAccessFilter(IProjectAccessService access, ICurrentUserService currentUser) : IAsyncActionFilter
{
    private static readonly HashSet<string> SkippedControllers = new(StringComparer.OrdinalIgnoreCase)
    {
        "Account",
        "Home",
        "Help",
        "HelpDiagnostics",
        "HelpDrawer",
        "ManageAccount"
    };

    private static readonly Dictionary<string, HashSet<string>> SkippedActions = new(StringComparer.OrdinalIgnoreCase)
    {
        ["Projects"] = new(StringComparer.OrdinalIgnoreCase) { "Restore", "HardDeleteArchivedProject" },
        ["Archives"] = new(StringComparer.OrdinalIgnoreCase) { "DeleteForever" }
    };

    private static readonly Dictionary<string, string> ModelPropertyEntities = new(StringComparer.OrdinalIgnoreCase)
    {
        ["ProjectID"] = "Project",
        ["ReturnProjectID"] = "Project",
        ["BookID"] = "Book",
        ["ReturnBookID"] = "Book",
        ["InitialBookID"] = "Book",
        ["ChapterID"] = "Chapter",
        ["TargetChapterID"] = "Chapter",
        ["SceneID"] = "Scene",
        ["CurrentSceneID"] = "Scene",
        ["ReturnSelectedSceneID"] = "Scene",
        ["SelectedSceneID"] = "Scene",
        ["AnchorSceneID"] = "Scene",
        ["SourceSceneID"] = "Scene",
        ["TargetSceneID"] = "Scene",
        ["CharacterID"] = "Character",
        ["RelatedCharacterID"] = "Character",
        ["CharacterAID"] = "Character",
        ["CharacterBID"] = "Character",
        ["POVCharacterID"] = "Character",
        ["FocusCharacterID"] = "Character",
        ["StoryAssetID"] = "StoryAsset",
        ["SourceAssetID"] = "StoryAsset",
        ["TargetAssetID"] = "StoryAsset",
        ["LocationID"] = "Location",
        ["ParentLocationID"] = "Location",
        ["FromLocationID"] = "Location",
        ["ToLocationID"] = "Location",
        ["FloorPlanID"] = "FloorPlan",
        ["FloorPlanFloorID"] = "FloorPlanFloor",
        ["FloorPlanBlockID"] = "FloorPlanBlock",
        ["PlotLineID"] = "PlotLine",
        ["ParentPlotLineID"] = "PlotLine",
        ["PlotThreadID"] = "PlotThread",
        ["CharacterRelationshipID"] = "CharacterRelationship",
        ["CharacterKnowledgeID"] = "CharacterKnowledge",
        ["RelationshipEventID"] = "RelationshipEvent",
        ["TimelineViewPresetID"] = "TimelineViewPreset",
        ["ScenarioID"] = "Scenario"
    };

    private static readonly Dictionary<string, Dictionary<string, string>> ControllerParameterEntities = new(StringComparer.OrdinalIgnoreCase)
    {
        ["Projects"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Project", ["projectId"] = "Project" },
        ["ProjectCollaborators"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
        ["Books"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Book", ["bookId"] = "Book", ["projectId"] = "Project" },
        ["Chapters"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Chapter", ["chapterId"] = "Chapter", ["bookId"] = "Book" },
        ["Characters"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Character", ["characterId"] = "Character", ["projectId"] = "Project" },
        ["Locations"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Location", ["locationId"] = "Location", ["returnLocationId"] = "Location", ["projectId"] = "Project" },
        ["FloorPlans"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "FloorPlan", ["floorPlanId"] = "FloorPlan", ["floorPlanFloorId"] = "FloorPlanFloor", ["floorPlanBlockId"] = "FloorPlanBlock", ["projectId"] = "Project" },
        ["PlotLines"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "PlotLine", ["plotLineId"] = "PlotLine", ["projectId"] = "Project" },
        ["PlotThreads"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "PlotThread", ["plotThreadId"] = "PlotThread", ["plotLineId"] = "PlotLine", ["projectId"] = "Project" },
        ["ProjectMetrics"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
        ["ProjectRestorePoints"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["id"] = "ProjectRestorePoint" },
        ["RelationshipMap"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["bookId"] = "Book", ["chapterId"] = "Chapter", ["sceneId"] = "Scene", ["focusCharacterId"] = "Character" },
        ["Scenarios"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Scenario", ["scenarioId"] = "Scenario", ["projectId"] = "Project", ["bookId"] = "Book", ["sceneId"] = "Scene", ["anchorSceneId"] = "Scene", ["targetChapterId"] = "Chapter" },
        ["StoryAssets"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "StoryAsset", ["returnAssetId"] = "StoryAsset", ["projectId"] = "Project" },
        ["Timeline"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "TimelineViewPreset", ["projectId"] = "Project", ["bookId"] = "Book" },
        ["Writer"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Chapter", ["projectId"] = "Project" },
        ["Warnings"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "ContinuityWarning", ["projectId"] = "Project", ["bookId"] = "Book", ["sceneId"] = "Scene" },
        ["Analytics"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["bookId"] = "Book" },
        ["Archives"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
        ["Dynamics"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["bookId"] = "Book", ["sceneId"] = "Scene" },
        ["Exports"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["sceneId"] = "Scene" },
        ["Scenes"] = new(StringComparer.OrdinalIgnoreCase) { ["chapterId"] = "Chapter", ["sceneId"] = "Scene", ["projectId"] = "Project", ["bookId"] = "Book" },
        ["Acceptance"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
        ["StoryBible"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" }
    };

    private static readonly Dictionary<string, HashSet<string>> OwnerOnlyProjectActions = new(StringComparer.OrdinalIgnoreCase)
    {
        ["Projects"] = new(StringComparer.OrdinalIgnoreCase) { "Archive" },
        ["ProjectCollaborators"] = new(StringComparer.OrdinalIgnoreCase) { "Index", "Add", "Remove" },
        ["Exports"] = new(StringComparer.OrdinalIgnoreCase) { "ProjectBackup" }
    };

    private static readonly Dictionary<string, Dictionary<string, string>> ActionIdEntities = new(StringComparer.OrdinalIgnoreCase)
    {
        ["Characters"] = new(StringComparer.OrdinalIgnoreCase) { ["ArchiveRelationship"] = "CharacterRelationship" },
        ["Dynamics"] = new(StringComparer.OrdinalIgnoreCase) { ["Character"] = "Character", ["Knowledge"] = "Character", ["Relationship"] = "CharacterRelationship" },
        ["Exports"] = new(StringComparer.OrdinalIgnoreCase) { ["Character"] = "Character", ["SceneBrief"] = "Scene", ["ChapterBrief"] = "Chapter" },
        ["Locations"] = new(StringComparer.OrdinalIgnoreCase) { ["ArchiveRelationship"] = "LocationRelationship" },
        ["Scenes"] = new(StringComparer.OrdinalIgnoreCase)
        {
            ["Create"] = "Chapter",
            ["Edit"] = "Scene",
            ["Archive"] = "Scene",
            ["MoveUp"] = "Scene",
            ["MoveDown"] = "Scene",
            ["DeleteThreadEvent"] = "ThreadEvent",
            ["DeleteAssetEvent"] = "AssetEvent",
            ["DeleteSceneAssetLocation"] = "SceneAssetLocation",
            ["DeleteAssetCustodyEvent"] = "AssetCustodyEvent",
            ["DeleteSceneCharacter"] = "SceneCharacter",
            ["DeleteSceneDependency"] = "SceneDependency",
            ["DeleteCharacterKnowledge"] = "CharacterKnowledge",
            ["DeleteRelationshipEvent"] = "RelationshipEvent",
            ["DeleteNote"] = "SceneNote",
            ["DeleteChecklistItem"] = "SceneChecklistItem",
            ["DeleteAttachment"] = "SceneAttachment"
        },
        ["StoryAssets"] = new(StringComparer.OrdinalIgnoreCase) { ["DeleteDependency"] = "AssetDependency" },
        ["Timeline"] = new(StringComparer.OrdinalIgnoreCase) { ["LoadPreset"] = "TimelineViewPreset", ["DeletePreset"] = "TimelineViewPreset" },
        ["Warnings"] = new(StringComparer.OrdinalIgnoreCase) { ["Dismiss"] = "ContinuityWarning", ["MarkIntentional"] = "ContinuityWarning", ["Restore"] = "ContinuityWarning" }
    };

    private static readonly Dictionary<string, string> ArchiveEntityTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        ["Project"] = "Project",
        ["Book"] = "Book",
        ["Chapter"] = "Chapter",
        ["Scene"] = "Scene",
        ["PlotLine"] = "PlotLine",
        ["PlotThread"] = "PlotThread",
        ["StoryAsset"] = "StoryAsset",
        ["Character"] = "Character",
        ["Location"] = "Location",
        ["Scenario"] = "Scenario",
        ["Relationship"] = "CharacterRelationship",
        ["CharacterRelationship"] = "CharacterRelationship"
    };

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (!currentUser.IsAuthenticated || context.ActionDescriptor is not ControllerActionDescriptor descriptor)
        {
            await next();
            return;
        }

        var controllerName = descriptor.ControllerName;
        if (SkippedControllers.Contains(controllerName))
        {
            await next();
            return;
        }

        if (SkippedActions.TryGetValue(controllerName, out var skippedActions) && skippedActions.Contains(descriptor.ActionName))
        {
            await next();
            return;
        }

        var requirements = CollectRequirements(controllerName, descriptor.ActionName, context.ActionArguments);
        foreach (var requirement in requirements)
        {
            var allowed = requirement.EntityType == "Project"
                ? await CheckProjectRequirementAsync(controllerName, descriptor.ActionName, requirement.EntityId)
                : await access.CanAccessEntityAsync(requirement.EntityType, requirement.EntityId);

            if (!allowed)
            {
                context.Result = new NotFoundResult();
                return;
            }
        }

        await next();
    }

    private Task<bool> CheckProjectRequirementAsync(string controllerName, string actionName, int projectId)
    {
        return OwnerOnlyProjectActions.TryGetValue(controllerName, out var ownerOnlyActions) && ownerOnlyActions.Contains(actionName)
            ? access.CanOwnProjectAsync(projectId)
            : access.CanAccessProjectAsync(projectId);
    }

    private static HashSet<AccessRequirement> CollectRequirements(string controllerName, string actionName, IDictionary<string, object?> arguments)
    {
        var requirements = new HashSet<AccessRequirement>();
        var parameterMap = ControllerParameterEntities.GetValueOrDefault(controllerName);
        var actionIdEntity = ActionIdEntities.GetValueOrDefault(controllerName)?.GetValueOrDefault(actionName);

        if (string.Equals(controllerName, "Archives", StringComparison.OrdinalIgnoreCase)
            && arguments.TryGetValue("entityType", out var archiveEntityType)
            && arguments.TryGetValue("entityId", out var archiveEntityId)
            && archiveEntityType is string archiveEntityTypeValue
            && ArchiveEntityTypes.TryGetValue(archiveEntityTypeValue, out var archiveAccessEntityType)
            && TryGetInt(archiveEntityId, out var archiveId)
            && archiveId > 0)
        {
            requirements.Add(new AccessRequirement(archiveAccessEntityType, archiveId));
        }

        foreach (var argument in arguments)
        {
            if (TryGetInt(argument.Value, out var id) && id > 0)
            {
                if (string.Equals(argument.Key, "id", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(actionIdEntity))
                {
                    requirements.Add(new AccessRequirement(actionIdEntity, id));
                    continue;
                }

                if (parameterMap is not null && parameterMap.TryGetValue(argument.Key, out var entityType))
                {
                    requirements.Add(new AccessRequirement(entityType, id));
                    continue;
                }
            }

            CollectModelRequirements(argument.Value, requirements);
        }

        return requirements;
    }

    private static void CollectModelRequirements(object? value, HashSet<AccessRequirement> requirements)
    {
        if (value is null || value is string || value.GetType().IsPrimitive || value is IFormFile)
        {
            return;
        }

        if (value is IEnumerable enumerable && value is not IDictionary)
        {
            foreach (var item in enumerable)
            {
                CollectModelRequirements(item, requirements);
            }

            return;
        }

        foreach (var property in value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            if (property.GetIndexParameters().Length > 0 || !ModelPropertyEntities.TryGetValue(property.Name, out var entityType))
            {
                continue;
            }

            if (TryGetInt(property.GetValue(value), out var id) && id > 0)
            {
                requirements.Add(new AccessRequirement(entityType, id));
            }
        }
    }

    private static bool TryGetInt(object? value, out int id)
    {
        id = 0;
        return value switch
        {
            int intValue => (id = intValue) > 0,
            string stringValue => int.TryParse(stringValue, out id) && id > 0,
            _ => false
        };
    }

    private readonly record struct AccessRequirement(string EntityType, int EntityId);
}
