namespace Sdt.Core;

public enum CommandResolutionSource
{
    Exact,
    Path,
    Shim,
    NodeAdjacentShim,
    ConfiguredOverride,
    Fallback,
}

public sealed record CommandResolutionResult(
    string Requested,
    string Resolved,
    CommandResolutionSource Source);

public static class CommandResolver
{
    public static CommandResolutionResult ResolveWithTrace(string command, Config.DevToolConfig? config = null, string? tool = null)
    {
        if (string.IsNullOrWhiteSpace(command))
            return new CommandResolutionResult(command, command, CommandResolutionSource.Exact);

        if (!OperatingSystem.IsWindows())
            return new CommandResolutionResult(command, command, CommandResolutionSource.Exact);

        if (command.Contains(Path.DirectorySeparatorChar) || command.Contains(Path.AltDirectorySeparatorChar))
            return new CommandResolutionResult(command, command, CommandResolutionSource.Exact);

        var normalized = command.ToLowerInvariant();
        if (Path.HasExtension(command))
        {
            var extensionResolved = ResolveFromPath(command);
            return extensionResolved is null
                ? new CommandResolutionResult(command, command, CommandResolutionSource.Fallback)
                : new CommandResolutionResult(command, extensionResolved, CommandResolutionSource.Path);
        }

        var overrideTool = string.IsNullOrWhiteSpace(tool) ? normalized : tool.ToLowerInvariant();
        var configuredCandidates = config?.Tooling?.Tools
            .FirstOrDefault(t => string.Equals(t.Tool, overrideTool, StringComparison.OrdinalIgnoreCase))
            ?.Executables
            ?.Where(x => !string.IsNullOrWhiteSpace(x))
            .ToList();

        if (configuredCandidates is not null)
        {
            foreach (var configured in configuredCandidates)
            {
                var resolvedConfigured = ResolveFromPath(configured!) ?? configured!;
                if (IsUsableExecutable(resolvedConfigured))
                    return new CommandResolutionResult(command, resolvedConfigured, CommandResolutionSource.ConfiguredOverride);
            }
        }

        foreach (var candidate in BuildWindowsCandidates(command, normalized))
        {
            var resolved = ResolveFromPath(candidate);
            if (!string.IsNullOrWhiteSpace(resolved))
            {
                var source = candidate.EndsWith(".cmd", StringComparison.OrdinalIgnoreCase) ||
                             candidate.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
                             candidate.EndsWith(".bat", StringComparison.OrdinalIgnoreCase)
                    ? CommandResolutionSource.Shim
                    : CommandResolutionSource.Path;
                return new CommandResolutionResult(command, resolved!, source);
            }
        }

        if (normalized is "npm" or "npx" or "pnpm" or "yarn")
        {
            var nodePath = ResolveFromPath("node.exe") ?? ResolveFromPath("node");
            if (!string.IsNullOrWhiteSpace(nodePath))
            {
                var nodeDir = Path.GetDirectoryName(nodePath);
                if (!string.IsNullOrWhiteSpace(nodeDir))
                {
                    var shim = Path.Combine(nodeDir, normalized + ".cmd");
                    if (File.Exists(shim))
                        return new CommandResolutionResult(command, shim, CommandResolutionSource.NodeAdjacentShim);
                }
            }
        }

        var fallback = BuildWindowsCandidates(command, normalized).LastOrDefault() ?? command;
        return new CommandResolutionResult(command, fallback, CommandResolutionSource.Fallback);
    }

    public static string Resolve(string command)
    {
        return ResolveWithTrace(command).Resolved;
    }

    private static string? ResolveFromPath(string executable)
    {
        var pathValue = Environment.GetEnvironmentVariable("PATH");
        if (string.IsNullOrWhiteSpace(pathValue))
            return null;

        foreach (var segment in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
        {
            try
            {
                var expandedSegment = ExpandWindowsPathTokens(segment.Trim());
                var candidate = Path.Combine(expandedSegment, executable);
                if (File.Exists(candidate))
                {
                    // If PATH lookup hit extensionless npm but npm.cmd exists beside it, prefer npm.cmd.
                    var fileName = Path.GetFileName(candidate).ToLowerInvariant();
                    if (fileName is "npm" or "npx" or "pnpm" or "yarn" or "tauri")
                    {
                        var shim = candidate + ".cmd";
                        if (File.Exists(shim))
                            return shim;
                    }
                    return candidate;
                }
            }
            catch
            {
                // Ignore malformed PATH segments.
            }
        }

        return null;
    }

    private static string ExpandWindowsPathTokens(string segment)
    {
        if (string.IsNullOrWhiteSpace(segment) || !OperatingSystem.IsWindows())
            return segment;

        var expanded = segment;
        for (var i = 0; i < 4; i++)
        {
            var next = Environment.ExpandEnvironmentVariables(expanded);
            if (string.Equals(next, expanded, StringComparison.Ordinal))
                break;
            expanded = next;
        }
        return expanded;
    }

    private static List<string> BuildWindowsCandidates(string command, string normalized)
    {
        var candidates = new List<string>();
        if (normalized is "npm" or "npx" or "pnpm" or "yarn" or "tauri")
        {
            candidates.Add(command + ".cmd");
            candidates.Add(command + ".exe");
            candidates.Add(command + ".bat");
            candidates.Add(command);
        }
        else
        {
            candidates.Add(command);
        }

        return candidates;
    }

    private static bool IsUsableExecutable(string resolved)
    {
        if (string.IsNullOrWhiteSpace(resolved))
            return false;

        if (Path.IsPathRooted(resolved))
            return File.Exists(resolved);

        return ResolveFromPath(resolved) is not null;
    }
}
