using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Permissions;
using AIRuntime.Runtime.Filesystem.Models;


namespace AIRuntime.Runtime.Filesystem
{
    /// <summary>
    /// Recursively lists structured entries beneath a workspace directory.
    /// </summary>
    public sealed class ListDirectoryTreeCapability
        : CapabilityBase<ListDirectoryTreeArguments>
    {
        private static readonly string[] GeneratedRootDirectories =
        {
            ".git",
            ".idea",
            ".vs",
            "Library",
            "Logs",
            "obj",
            "Temp"
        };


        private sealed class PendingDirectory
        {
            public string Path;

            public int Depth;
        }


        public override string Name =>
            "list_directory_tree";


        public override string Description =>
            "Lists the recursive Unity project folder tree. For 'all folders in this project', use path '.' and the defaults; they return project folders only and omit generated caches such as Library. Set includeGenerated true only for a literal full workspace tree, or directoriesOnly false when files are requested. Unity .meta sidecars remain excluded unless includeMetaFiles is explicitly true. Use list_directory for one directory.";


        public override CapabilityPermission Permission =>
            CapabilityPermission.ReadOnly;


        protected override async Task<object> ExecuteAsync(
            ListDirectoryTreeArguments args,
            CapabilityContext context)
        {
            context.CancellationToken
                .ThrowIfCancellationRequested();


            string root =
                NormalizePath(
                    args.Path);

            HashSet<string> exclusions =
                BuildExclusions(
                    args.Exclude,
                    root,
                    args.IncludeGenerated);

            DirectoryTreeResult result =
                new DirectoryTreeResult
                {
                    Root =
                        root,

                    MaxDepth =
                        args.MaxDepth
                };

            Queue<PendingDirectory> pending =
                new Queue<PendingDirectory>();

            pending.Enqueue(
                new PendingDirectory
                {
                    Path =
                        root,

                    Depth =
                        0
                });


            while (pending.Count > 0)
            {
                context.CancellationToken
                    .ThrowIfCancellationRequested();


                PendingDirectory directory =
                    pending.Dequeue();

                IReadOnlyList<string> names =
                    await context.FileSystem.ListDirectoryAsync(
                        directory.Path);


                foreach (
                    string name
                    in names)
                {
                    context.CancellationToken
                        .ThrowIfCancellationRequested();


                    string path =
                        CombinePath(
                            directory.Path,
                            name);


                    if (IsExcluded(
                            exclusions,
                            name,
                            path))
                    {
                        continue;
                    }


                    FileInfoModel info =
                        await context.FileSystem.GetFileInfoAsync(
                            path);


                    if (!info.Exists)
                        continue;

                    if (!args.IncludeMetaFiles &&
                        !info.IsDirectory &&
                        name.EndsWith(
                            ".meta",
                            StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }


                    result.InspectedEntries++;


                    int depth =
                        directory.Depth +
                        1;


                    if (info.IsDirectory ||
                        !args.DirectoriesOnly)
                    {
                        if (result.Entries.Count >=
                            args.MaxEntries)
                        {
                            result.Truncated =
                                true;

                            return result;
                        }


                        result.Entries.Add(
                            new DirectoryTreeEntry
                            {
                                Path =
                                    NormalizePath(
                                        info.Path ??
                                        path),

                                Name =
                                    info.Name ??
                                    name,

                                IsDirectory =
                                    info.IsDirectory,

                                IsSymbolicLink =
                                    info.IsSymbolicLink,

                                Depth =
                                    depth
                            });
                    }


                    if (info.IsDirectory &&
                        !info.IsSymbolicLink &&
                        depth <
                        args.MaxDepth)
                    {
                        pending.Enqueue(
                            new PendingDirectory
                            {
                                Path =
                                    path,

                                Depth =
                                    depth
                            });
                    }
                }
            }


            return result;
        }


        private static HashSet<string> BuildExclusions(
            IEnumerable<string> exclusions,
            string root,
            bool includeGenerated)
        {
            HashSet<string> result =
                new HashSet<string>(
                    StringComparer.OrdinalIgnoreCase);


            if (!includeGenerated &&
                root == ".")
            {
                foreach (
                    string generatedDirectory
                    in GeneratedRootDirectories)
                {
                    result.Add(
                        generatedDirectory);
                }
            }


            if (exclusions != null)
            {
                foreach (
                    string exclusion
                    in exclusions)
                {
                    if (!string.IsNullOrWhiteSpace(
                            exclusion))
                    {
                        result.Add(
                            NormalizePath(
                                exclusion));
                    }
                }
            }


            return result;
        }


        private static bool IsExcluded(
            HashSet<string> exclusions,
            string name,
            string path)
        {
            return exclusions.Contains(
                       NormalizePath(
                           name)) ||
                   exclusions.Contains(
                       NormalizePath(
                           path));
        }


        private static string CombinePath(
            string parent,
            string child)
        {
            string normalizedParent =
                NormalizePath(
                    parent);


            if (normalizedParent == ".")
            {
                return NormalizePath(
                    child);
            }


            return normalizedParent +
                   "/" +
                   NormalizePath(
                       child);
        }


        private static string NormalizePath(
            string path)
        {
            string normalized =
                (path ??
                 string.Empty)
                .Trim()
                .Replace(
                    '\\',
                    '/')
                .TrimEnd(
                    '/');


            return string.IsNullOrEmpty(
                normalized)
                ? "."
                : normalized;
        }
    }
}
