using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

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


namespace AIRuntime.Runtime.Filesystem
{
    /// <summary>
    /// Reads a UTF-8 text file from the current workspace.
    /// </summary>
    public sealed class ReadFileCapability
        : CapabilityBase<ReadFileArguments>,
          ICapabilityObservationSemantics
    {
        private const int MaximumRecoveryEntries =
            10000;


        private static readonly HashSet<string> GeneratedRootDirectories =
            new HashSet<string>(
                new[]
                {
                    ".git",
                    ".idea",
                    ".vs",
                    "Library",
                    "Logs",
                    "obj",
                    "Temp"
                },
                StringComparer.OrdinalIgnoreCase);


        public override string Name =>
            "read_file";


        public override string Description =>
            "Reads and returns the actual UTF-8 contents of a workspace text file. You must call this tool before answering what a named file is, says, contains, documents, or defines. Never infer file contents from its path or filename.";


        public override CapabilityPermission Permission =>
            CapabilityPermission.ReadOnly;


        public CapabilityObservationBehavior ObservationBehavior =>
            CapabilityObservationBehavior.StableUntilMutation;


        protected override async Task<object> ExecuteAsync(
            ReadFileArguments args,
            CapabilityContext context)
        {
            try
            {
                return await context.FileSystem.ReadFileAsync(
                    args.Path);
            }
            catch (FileNotFoundException)
            {
                if (args.RequireExactPath)
                {
                    // Verification callers must never substitute a different
                    // same-named file for a missing declared artifact. The
                    // ordinary agent-facing read remains forgiving by default.
                    throw;
                }


                string duplicateExtensionRecovery =
                    RemoveDuplicateFinalExtension(
                        args.Path);


                if (!string.IsNullOrEmpty(
                        duplicateExtensionRecovery))
                {
                    try
                    {
                        return await context.FileSystem.ReadFileAsync(
                            duplicateExtensionRecovery);
                    }
                    catch (FileNotFoundException)
                    {
                        // Continue to the existing encoded-path recovery.
                    }
                }


                string recoveredPath =
                    null;


                if (MayContainEncodedDirectorySegments(
                        args.Path))
                {
                    recoveredPath =
                        await FindUniqueEncodedPathMatchAsync(
                            context.FileSystem,
                            args.Path,
                            context.CancellationToken);
                }


                if (string.IsNullOrEmpty(
                        recoveredPath))
                {
                    throw;
                }


                return await context.FileSystem.ReadFileAsync(
                    recoveredPath);
            }
        }


        private static string RemoveDuplicateFinalExtension(
            string path)
        {
            if (string.IsNullOrWhiteSpace(
                    path))
            {
                return null;
            }


            string extension =
                Path.GetExtension(
                    path);

            if (string.IsNullOrEmpty(
                    extension))
            {
                return null;
            }


            string withoutFinalExtension =
                path.Substring(
                    0,
                    path.Length -
                    extension.Length);

            return string.Equals(
                    Path.GetExtension(
                        withoutFinalExtension),
                    extension,
                    StringComparison.OrdinalIgnoreCase)
                ? withoutFinalExtension
                : null;
        }


        private static bool MayContainEncodedDirectorySegments(
            string path)
        {
            if (string.IsNullOrWhiteSpace(
                    path) ||
                path.IndexOf(
                    '_') <
                0)
            {
                return false;
            }

            string[] segments =
                path.Split(
                    new[]
                    {
                        '/',
                        '\\'
                    },
                    StringSplitOptions.RemoveEmptyEntries);

            if (segments.Length ==
                1)
            {
                // Preserve support for the original fully encoded model
                // form, for example Assets_Runtime_Docs_README_md.
                return path.IndexOf(
                           '.') <
                       0;
            }

            // Mixed malformed paths preserve the final filename but encode
            // one or more directory separators as underscores, for example
            // Assets_Runtime_Docs/DEPENDENCY_RULES.md.
            return segments
                .Take(
                    segments.Length -
                    1)
                .Any(
                    segment =>
                        segment.IndexOf(
                            '_') >=
                        0);
        }


        private static async Task<string> FindUniqueEncodedPathMatchAsync(
            IFileSystem fileSystem,
            string encodedPath,
            CancellationToken cancellationToken)
        {
            string expectedKey =
                CreatePathKey(
                    encodedPath);

            return await FindUniqueFileMatchAsync(
                fileSystem,
                path =>
                    string.Equals(
                        CreatePathKey(
                            path),
                        expectedKey,
                        StringComparison.OrdinalIgnoreCase),
                cancellationToken);
        }


        private static async Task<string> FindUniqueFileMatchAsync(
            IFileSystem fileSystem,
            Func<string, bool> isMatch,
            CancellationToken cancellationToken)
        {

            string match =
                null;

            int inspectedEntries =
                0;

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

            pending.Enqueue(
                ".");


            while (pending.Count > 0 &&
                   inspectedEntries <
                   MaximumRecoveryEntries)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                string directory =
                    pending.Dequeue();

                IReadOnlyList<string> names =
                    await fileSystem.ListDirectoryAsync(
                        directory);


                foreach (
                    string name
                    in names)
                {
                    cancellationToken
                        .ThrowIfCancellationRequested();


                    if (directory == "." &&
                        GeneratedRootDirectories.Contains(
                            name))
                    {
                        continue;
                    }


                    if (inspectedEntries >=
                        MaximumRecoveryEntries)
                    {
                        return null;
                    }


                    inspectedEntries++;


                    string path =
                        CombinePath(
                            directory,
                            name);

                    FileInfoModel info =
                        await fileSystem.GetFileInfoAsync(
                            path);


                    if (!info.Exists)
                        continue;


                    if (info.IsDirectory)
                    {
                        if (!info.IsSymbolicLink)
                        {
                            pending.Enqueue(
                                path);
                        }


                        continue;
                    }


                    if (!isMatch(
                            path))
                    {
                        continue;
                    }


                    if (match != null)
                    {
                        return null;
                    }


                    match =
                        path;
                }
            }


            return match;
        }


        private static string CombinePath(
            string parent,
            string child)
        {
            return parent == "."
                ? child
                : parent +
                  "/" +
                  child;
        }


        private static string CreatePathKey(
            string path)
        {
            StringBuilder key =
                new StringBuilder();


            foreach (
                char character
                in path ??
                   string.Empty)
            {
                if (char.IsLetterOrDigit(
                        character))
                {
                    key.Append(
                        char.ToLowerInvariant(
                            character));
                }
            }


            return key.ToString();
        }
    }
}
