using System.Diagnostics;

namespace Fast.Shared.Logic.FileService;

/// <summary>
/// Environment type for folder resolution logic.
/// </summary>
public enum EnvType
{
    Production,
    Test,
    Dev,
    DevDeployed,
    Legacy,
    Unknown
}

/// <summary>
/// Utility class for resolving local folder paths based on environment type.
/// Centralizes the folder searching logic used by TemplateController, JobsHelper, and other components.
/// </summary>
public static class FolderResolver
{
    private static readonly List<string> DefaultExcludedKeys = new()
    {
        Path.Join("inetpub", "custerr"),
        Path.Join("inetpub", "ftproot"),
        Path.Join("inetpub", "history"),
        Path.Join("inetpub", "logs"),
        Path.Join("inetpub", "temp"),
        @"code/superior/sngc/", // old Panton site, needs trailing slash and must be lowercase
    };

    /// <summary>
    /// Detects the environment type from the database AppSettings.
    /// </summary>
    /// <param name="db">Database context</param>
    /// <param name="isDevelopment">Whether the host environment is Development</param>
    /// <returns>The detected environment type</returns>
    public static EnvType DetectEnvironmentType(MyDbContext db, bool isDevelopment)
    {
#pragma warning disable CA1862 // Prefer 'string.Equals(string, StringComparison)' for case-insensitive comparison
        string envMetadata = db.AppSettings.FirstOrDefault(x => x.Name.ToLower() == "homeurl")?.Metadata ?? "";
#pragma warning restore CA1862

        if (isDevelopment)
            return EnvType.Dev;
        else if (envMetadata.Contains("dev", StringComparison.OrdinalIgnoreCase))
            return EnvType.DevDeployed;
        else if (envMetadata.Contains("prod", StringComparison.OrdinalIgnoreCase))
            return EnvType.Production;
        else if (envMetadata.Contains("test", StringComparison.OrdinalIgnoreCase))
            return EnvType.Test;
        else if (envMetadata.Contains("legacy", StringComparison.OrdinalIgnoreCase))
            return EnvType.Legacy;
        else
            return EnvType.Unknown;
    }

    /// <summary>
    /// Resolves folder paths for the given folder names based on environment type.
    /// Searches the filesystem starting from the content root path and returns a dictionary
    /// mapping logical folder names to their actual filesystem paths.
    /// </summary>
    /// <param name="contentRootPath">The application's content root path</param>
    /// <param name="envType">The environment type</param>
    /// <param name="folderNames">The logical folder names to resolve</param>
    /// <param name="excludedKeys">Optional additional paths to exclude from search (combined with defaults)</param>
    /// <param name="throwOnNotFound">If true, throws when a folder is not found. Default is true.</param>
    /// <returns>Dictionary mapping folder names to their resolved paths</returns>
    public static Dictionary<string, string> ResolveFolderPaths(
        string contentRootPath,
        EnvType envType,
        IEnumerable<string> folderNames,
        IEnumerable<string>? excludedKeys = null,
        bool throwOnNotFound = true)
    {
        static string GetTimestamp() => $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]";
        Debug.WriteLine($"{GetTimestamp()} Executing ResolveFolderPaths()...");
        var sw = Stopwatch.StartNew();

        var result = new Dictionary<string, string>();

        if (envType == EnvType.Unknown)
        {
            if (throwOnNotFound)
                throw new InvalidOperationException("Unknown environment type. Check the HomeURL metadata in the database Settings table.");
            return result;
        }

        // Combine default and custom excluded keys
        var allExcludedKeys = DefaultExcludedKeys.ToList();
        if (excludedKeys != null)
            allExcludedKeys.AddRange(excludedKeys);

        // Determine how many directories to traverse up
        int numDirectoriesToTraverseUp = envType == EnvType.Dev ? 2 : 1;
        DirectoryInfo? topDirectoryToSearch = new DirectoryInfo(contentRootPath);

        for (int i = 0; i < numDirectoriesToTraverseUp; i++)
        {
            if (topDirectoryToSearch?.Parent != null)
                topDirectoryToSearch = topDirectoryToSearch.Parent;
        }

        if (topDirectoryToSearch == null)
            return result;

        // Determine search pattern based on environment
        string searchPattern = envType switch
        {
            EnvType.Production => "*",
            EnvType.Test => "*test*",
            EnvType.Legacy => "*legacy*",
            EnvType.Dev or EnvType.DevDeployed => "*sngc*",
            _ => "*"
        };

        // Find top-level eligible folders
        var topEligibleFolders = topDirectoryToSearch.GetDirectories(searchPattern, SearchOption.TopDirectoryOnly).ToList();
        var eligibleFolders = new List<DirectoryInfo>();

        foreach (var topEligibleFolder in topEligibleFolders)
        {
            string folderName = topEligibleFolder.FullName.ToLower();
            if (allExcludedKeys.Any(x => folderName.Contains(x, StringComparison.OrdinalIgnoreCase)))
                continue;

            // In dev the search pattern is * and we don't need to exclude any folders
            if (envType == EnvType.Dev)
            {
                eligibleFolders.AddRange(topEligibleFolder.GetDirectories("*", SearchOption.AllDirectories));
            }
            // In production the search pattern is *, so we need to exclude folders with "test" or "legacy" in the name
            else if (envType == EnvType.Production && !folderName.Contains("test") && !folderName.Contains("legacy"))
            {
                eligibleFolders.AddRange(topEligibleFolder.GetDirectories("*", SearchOption.AllDirectories));
            }
            // In test, legacy, and devdeployed the search patterns are *test*, *legacy*, or *sngc*, so we don't need to exclude any folders
            else if (envType == EnvType.Test || envType == EnvType.Legacy || envType == EnvType.DevDeployed)
            {
                eligibleFolders.AddRange(topEligibleFolder.GetDirectories("*", SearchOption.AllDirectories));
            }
        }

        // Filter out excluded folders
        eligibleFolders = eligibleFolders
            .Where(e => !allExcludedKeys.Any(x => e.FullName.Contains(x, StringComparison.OrdinalIgnoreCase)))
            .ToList();

        var eligibleFoldersLookup = eligibleFolders.ToLookup(x => x.Name);

        // Resolve each folder name
        foreach (var docFolderName in folderNames)
        {
            // Handle nested folder names like "Invoices\Gas"
            var docFolderSplit = docFolderName.Split(Path.DirectorySeparatorChar);
            string docFolderSubName = docFolderName;
            string? docFolderParentName = null;
            if (docFolderSplit.Length > 1)
            {
                docFolderParentName = docFolderSplit[0];
                docFolderSubName = docFolderSplit[1];
            }

            if (eligibleFoldersLookup.Contains(docFolderSubName))
            {
                // Check for ambiguous matches
                if (eligibleFoldersLookup[docFolderSubName].Count() > 1)
                {
                    var foundPlaces = new List<string>();
                    foreach (var eligibleFolder in eligibleFoldersLookup[docFolderSubName])
                    {
                        if (docFolderParentName == null || eligibleFolder?.Parent?.Name == docFolderParentName)
                            foundPlaces.Add(eligibleFolder!.FullName);
                    }
                    if (foundPlaces.Count >= 2)
                        throw new InvalidOperationException($"The {docFolderSubName} folder was found in two places.\r\n{foundPlaces[0]}\r\n{foundPlaces[1]}");
                }

                string path;
                if (docFolderParentName == null)
                    path = eligibleFoldersLookup[docFolderSubName].First().FullName;
                else
                    path = eligibleFoldersLookup[docFolderSubName].Where(x => x.Parent?.Name == docFolderParentName).First().FullName;

                result.Add(docFolderName, path);
            }
            else if (throwOnNotFound)
            {
                throw new InvalidOperationException($"The {docFolderSubName} folder was not found in any subfolder starting from {topDirectoryToSearch?.FullName ?? ""}");
            }
        }

        Debug.WriteLine($"{GetTimestamp()} ResolveFolderPaths(): {sw.ElapsedMilliseconds} ms");
        return result;
    }
}
