using System.Text.Json;

namespace Sdt.Config;

public sealed record BootstrapScanResult(
    string ProjectRoot,
    string ProjectName,
    IReadOnlyList<string> ToolFamilies,
    string? NodeWorkingDir,
    string? PythonRequirementsFile,
    bool HasDockerCompose,
    IReadOnlyList<string> RootHints);

public static class ConfigBootstrapper
{
    private const int MaxScanDepth = 4;

    private static readonly HashSet<string> ExcludedDirectories = new(StringComparer.OrdinalIgnoreCase)
    {
        ".git",
        "node_modules",
        ".venv",
        "venv",
        "bin",
        "obj",
        ".idea",
        ".vscode",
        "dist",
        "build",
        ".sdt",
    };

    private static readonly string[] RequirementCandidates =
    [
        "requirements.txt",
        "requirements-dev.txt",
        "requirements_cpu_only.txt",
        "requirements_gpu.txt",
    ];

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };

    public static BootstrapScanResult Scan(string startDir)
    {
        var root = FindProjectRoot(startDir);
        var toolFamilies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var rootHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        if (Directory.Exists(Path.Combine(root, ".git")))
        {
            toolFamilies.Add("git");
            rootHints.Add(".git");
        }

        var hasTopLevelSln = Directory.EnumerateFiles(root, "*.sln", SearchOption.TopDirectoryOnly).Any();
        var hasCsproj = hasTopLevelSln || EnumerateFilesBounded(root, "*.csproj", MaxScanDepth).Any();
        if (hasCsproj)
        {
            toolFamilies.Add("dotnet");
            rootHints.Add("*.sln");
        }

        var topLevelPackageJson = Path.Combine(root, "package.json");
        var packageJson = File.Exists(topLevelPackageJson)
            ? topLevelPackageJson
            : EnumerateFilesBounded(root, "package.json", MaxScanDepth)
            .OrderBy(p => p.Length)
            .FirstOrDefault();
        string? nodeWorkingDir = null;
        if (packageJson is not null)
        {
            toolFamilies.Add("node");
            toolFamilies.Add("npm");
            nodeWorkingDir = Path.GetRelativePath(root, Path.GetDirectoryName(packageJson)!);
            rootHints.Add("package.json");
        }

        string? requirements = RequirementCandidates
            .Select(name => Path.Combine(root, name))
            .FirstOrDefault(File.Exists);
        if (requirements is null)
        {
            var pyproject = Path.Combine(root, "pyproject.toml");
            if (File.Exists(pyproject))
            {
                toolFamilies.Add("python");
                rootHints.Add("pyproject.toml");
            }
        }
        else
        {
            toolFamilies.Add("python");
            rootHints.Add(Path.GetFileName(requirements)!);
        }

        var hasCargo = File.Exists(Path.Combine(root, "Cargo.toml")) ||
                       EnumerateFilesBounded(root, "Cargo.toml", MaxScanDepth).Any();
        if (hasCargo)
        {
            toolFamilies.Add("cargo");
            rootHints.Add("Cargo.toml");
        }

        var hasTauri = File.Exists(Path.Combine(root, "tauri.conf.json")) ||
                       EnumerateFilesBounded(root, "tauri.conf.json", MaxScanDepth).Any();
        if (hasTauri)
        {
            toolFamilies.Add("tauri");
            toolFamilies.Add("cargo");
            toolFamilies.Add("node");
            toolFamilies.Add("npm");
            rootHints.Add("tauri.conf.json");
        }

        var hasDockerCompose = File.Exists(Path.Combine(root, "docker-compose.yml")) ||
                               File.Exists(Path.Combine(root, "docker-compose.yaml"));
        if (hasDockerCompose || File.Exists(Path.Combine(root, "Dockerfile")))
        {
            toolFamilies.Add("docker");
            rootHints.Add(hasDockerCompose ? "docker-compose.yml" : "Dockerfile");
        }

        var scriptsDir = Path.Combine(root, "scripts");
        if (Directory.Exists(scriptsDir) &&
            Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly).Any())
        {
            toolFamilies.Add("python");
            rootHints.Add("scripts");
        }

        if (rootHints.Count == 0)
            rootHints.Add("devtool.json");

        return new BootstrapScanResult(
            ProjectRoot: root,
            ProjectName: new DirectoryInfo(root).Name,
            ToolFamilies: toolFamilies.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(),
            NodeWorkingDir: nodeWorkingDir,
            PythonRequirementsFile: requirements is null ? null : Path.GetRelativePath(root, requirements),
            HasDockerCompose: hasDockerCompose,
            RootHints: rootHints.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList());
    }

    public static DevToolConfig BuildDefaultConfig(BootstrapScanResult scan)
    {
        var workflows = BuildWorkflows(scan).ToList();

        var toolingTools = scan.ToolFamilies
            .Select(t => new ToolInstallDefinition { Tool = t, PreferredInstallCommands = [] })
            .ToList();

        var toolchains = new ToolchainConfig
        {
            Python = scan.ToolFamilies.Contains("python", StringComparer.OrdinalIgnoreCase)
                ? new PythonToolchain
                {
                    Executable = "python",
                    WindowsExecutable = "py",
                    VenvDir = ".venv",
                    Profiles = []
                }
                : null,
            Node = scan.ToolFamilies.Contains("node", StringComparer.OrdinalIgnoreCase)
                ? new NodeToolchain
                {
                    PackageManager = "npm",
                    WorkingDir = string.IsNullOrWhiteSpace(scan.NodeWorkingDir) ? "." : scan.NodeWorkingDir
                }
                : null
        };

        var debugProfiles = BuildDebugProfiles(scan).ToList();

        return new DevToolConfig
        {
            Name = scan.ProjectName,
            Version = "0.1.0",
            Project = new ProjectMetadata
            {
                Type = "generic",
                RootHints = scan.RootHints.ToList(),
                Artifacts = ["bin", "obj", ".sdt/debug"]
            },
            Toolchains = toolchains,
            Tooling = new ToolingConfig { Tools = toolingTools },
            Workflows = workflows,
            Debug = new DebugConfig
            {
                Profiles = debugProfiles,
                Diagnostics = new DebugDiagnosticsOptions
                {
                    Enabled = true,
                    OutputDir = ".sdt/debug",
                    IncludeAllEnv = false,
                    CaptureEnvKeys =
                    [
                        "SDT_LOG_LEVEL",
                        "DOTNET_CLI_HOME",
                        "NUGET_PACKAGES",
                        "PIP_CACHE_DIR",
                        "NVM_HOME",
                        "NVM_SYMLINK",
                    ],
                    BundleOnFailure = true
                }
            },
            Env =
            [
                new EnvVarDef
                {
                    Key = "SDT_LOG_LEVEL",
                    Description = "CLI log verbosity",
                    DefaultValue = "information",
                    Options = ["trace", "debug", "information", "warning", "error", "critical"]
                }
            ]
        };
    }

    public static string ToJson(DevToolConfig config) => JsonSerializer.Serialize(config, JsonOptions) + Environment.NewLine;

    public static string WriteDefaultConfig(string projectRoot, DevToolConfig config, bool overwrite = false)
    {
        var path = Path.Combine(projectRoot, "devtool.json");
        if (File.Exists(path) && !overwrite)
            throw new InvalidOperationException($"devtool.json already exists at {path}");

        File.WriteAllText(path, ToJson(config));
        return path;
    }

    private static IEnumerable<WorkflowDefinition> BuildWorkflows(BootstrapScanResult scan)
    {
        var has = new Func<string, bool>(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase));
        var scripts = DetectScriptHelpers(scan.ProjectRoot);

        if (scripts.Contains("publish-sidecar.py") ||
            scripts.Contains("publish-app.py") ||
            scripts.Contains("publish-webgateway.py") ||
            scripts.Contains("publish-output.py") ||
            scripts.Contains("sync-output.py") ||
            scripts.Contains("run-webgateway.py"))
        {
            foreach (var workflow in BuildScriptDrivenWorkflows(scripts))
                yield return workflow;
        }

        var buildSteps = new List<WorkflowStep>();
        if (has("dotnet")) buildSteps.Add(StepAction("dotnet-build", "dotnet build", "dotnet-build"));
        if (has("npm")) buildSteps.Add(StepAction("npm-build", "npm run build", "npm-build", scan.NodeWorkingDir));
        if (has("cargo")) buildSteps.Add(StepAction("cargo-build", "cargo build", "cargo-build"));
        if (has("tauri")) buildSteps.Add(StepAction("tauri-build", "tauri build", "tauri-build", scan.NodeWorkingDir ?? "."));
        if (buildSteps.Count > 0)
        {
            yield return new WorkflowDefinition
            {
                Id = "build",
                Label = "Build",
                Description = "Build detected project stacks",
                Group = "Build",
                Steps = buildSteps
            };
        }

        var depsSteps = new List<WorkflowStep>();
        if (has("dotnet")) depsSteps.Add(StepAction("dotnet-restore", "dotnet restore", "dotnet-restore"));
        if (has("npm")) depsSteps.Add(StepAction("npm-ci", "npm ci", "npm-ci", scan.NodeWorkingDir));
        if (has("python") && !string.IsNullOrWhiteSpace(scan.PythonRequirementsFile))
        {
            depsSteps.Add(StepAction("python-pip-sync", "python pip sync", "python-pip-sync", ".", ["--requirements", scan.PythonRequirementsFile!]));
        }
        if (depsSteps.Count > 0)
        {
            yield return new WorkflowDefinition
            {
                Id = "deps-refresh",
                Label = "Refresh Dependencies",
                Description = "Restore/install dependency stacks",
                Group = "Deps",
                Steps = depsSteps
            };
        }

        var testSteps = new List<WorkflowStep>();
        if (has("dotnet")) testSteps.Add(StepAction("dotnet-test", "dotnet test", "dotnet-test"));
        if (has("npm")) testSteps.Add(StepAction("npm-test", "npm test", "npm-test", scan.NodeWorkingDir));
        if (has("python")) testSteps.Add(StepAction("python-pytest", "python -m pytest", "python-pytest"));
        if (has("cargo")) testSteps.Add(StepAction("cargo-test", "cargo test", "cargo-test"));
        if (testSteps.Count > 0)
        {
            yield return new WorkflowDefinition
            {
                Id = "test",
                Label = "Run Tests",
                Description = "Run detected test stacks",
                Group = "Test",
                Steps = testSteps
            };
        }

        if (has("git"))
        {
            yield return new WorkflowDefinition
            {
                Id = "repo-health",
                Label = "Repo Health",
                Description = "Check repo status and fetch remotes",
                Group = "Repo",
                Steps =
                [
                    StepAction("git-status", "git status", "git-status"),
                    StepAction("git-fetch", "git fetch", "git-fetch")
                ]
            };
        }

        if (has("docker"))
        {
            yield return new WorkflowDefinition
            {
                Id = "containers",
                Label = "Containers",
                Description = scan.HasDockerCompose ? "Manage docker compose stack" : "Build docker image",
                Group = "Containers",
                Steps = scan.HasDockerCompose
                    ? [StepAction("docker-compose-up", "docker compose up -d", "docker-compose-up")]
                    : [StepAction("docker-build", "docker build .", "docker-build")]
            };
        }
    }

    private static IEnumerable<DebugProfileDefinition> BuildDebugProfiles(BootstrapScanResult scan)
    {
        var has = new Func<string, bool>(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase));
        if (has("dotnet"))
        {
            yield return new DebugProfileDefinition
            {
                Id = "dotnet-run",
                Label = "Run .NET app",
                Type = "dotnet",
                Command = "dotnet",
                Args = ["run"],
                WorkingDir = ".",
                Requires = [new ToolRequirement { Tool = "dotnet", InstallPolicy = InstallPolicy.Prompt }],
                Attach = new DebugAttachConfig
                {
                    Kind = "manual",
                    Note = "Attach your IDE debugger to the running dotnet process."
                }
            };
        }

        if (has("npm"))
        {
            yield return new DebugProfileDefinition
            {
                Id = "npm-dev",
                Label = "Run npm dev server",
                Type = "node",
                Command = "npm",
                Args = ["run", "dev"],
                WorkingDir = string.IsNullOrWhiteSpace(scan.NodeWorkingDir) ? "." : scan.NodeWorkingDir!,
                Requires =
                [
                    new ToolRequirement { Tool = "node", InstallPolicy = InstallPolicy.Prompt },
                    new ToolRequirement { Tool = "npm", InstallPolicy = InstallPolicy.Prompt }
                ]
            };
        }
    }

    private static WorkflowStep StepAction(
        string id,
        string label,
        string action,
        string? workingDir = null,
        IReadOnlyList<string>? actionArgs = null)
    {
        return new WorkflowStep
        {
            Id = id,
            Label = label,
            Action = action,
            ActionArgs = actionArgs?.ToList() ?? [],
            WorkingDir = string.IsNullOrWhiteSpace(workingDir) ? "." : workingDir
        };
    }

    private static HashSet<string> DetectScriptHelpers(string projectRoot)
    {
        var scriptsDir = Path.Combine(projectRoot, "scripts");
        if (!Directory.Exists(scriptsDir))
            return new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        return Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly)
            .Select(Path.GetFileName)
            .Where(f => !string.IsNullOrWhiteSpace(f))
            .Cast<string>()
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
    }

    private static IEnumerable<WorkflowDefinition> BuildScriptDrivenWorkflows(HashSet<string> scripts)
    {
        static WorkflowStep ScriptStep(string id, string label, params string[] scriptArgs) => new()
        {
            Id = id,
            Label = label,
            Command = "python",
            Args = scriptArgs.ToList(),
            WorkingDir = ".",
            Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }]
        };

        if (scripts.Contains("publish-sidecar.py"))
        {
            yield return new WorkflowDefinition
            {
                Id = "sidecar",
                Label = "Publish Sidecar",
                Description = "Publish sidecar service",
                Group = "Build",
                Steps = [ScriptStep("sidecar:run", "python scripts/publish-sidecar.py", "scripts/publish-sidecar.py")]
            };
        }

        if (scripts.Contains("publish-app.py"))
        {
            yield return new WorkflowDefinition
            {
                Id = "web",
                Label = "Build Web UI",
                Description = "Build frontend assets",
                Group = "Build",
                Steps =
                [
                    ScriptStep("web:run", "python scripts/publish-app.py --target web", "scripts/publish-app.py", "--target", "web")
                ]
            };

            yield return new WorkflowDefinition
            {
                Id = "tauri",
                Label = "Build Tauri Desktop App",
                Description = "Build desktop binary",
                Group = "Build",
                DependsOn = scripts.Contains("publish-sidecar.py") ? ["sidecar"] : [],
                Steps =
                [
                    ScriptStep("tauri:run", "python scripts/publish-app.py --target tauri --tauri-bundles none",
                        "scripts/publish-app.py", "--target", "tauri", "--tauri-bundles", "none")
                ]
            };
        }

        if (scripts.Contains("publish-webgateway.py"))
        {
            yield return new WorkflowDefinition
            {
                Id = "webgateway",
                Label = "Publish WebGateway",
                Description = "Publish ASP.NET gateway",
                Group = "Build",
                DependsOn = scripts.Contains("publish-app.py") ? ["web"] : [],
                Steps = [ScriptStep("webgateway:run", "python scripts/publish-webgateway.py", "scripts/publish-webgateway.py")]
            };
        }

        if (scripts.Contains("sync-output.py"))
        {
            yield return new WorkflowDefinition
            {
                Id = "sync-output",
                Label = "Sync Output",
                Description = "Sync newest artifacts to output",
                Group = "Build",
                Steps = [ScriptStep("sync-output:run", "python scripts/sync-output.py", "scripts/sync-output.py")]
            };
        }

        if (scripts.Contains("publish-output.py"))
        {
            yield return new WorkflowDefinition
            {
                Id = "stage-output",
                Label = "Stage Output Bundle",
                Description = "Publish and stage distributable output",
                Group = "Build",
                Steps = [ScriptStep("stage-output:run", "python scripts/publish-output.py", "scripts/publish-output.py")]
            };
        }

        if (scripts.Contains("run-webgateway.py"))
        {
            yield return new WorkflowDefinition
            {
                Id = "run-gateway-dev",
                Label = "Run WebGateway Server (Dev)",
                Description = "Run gateway in development mode",
                Group = "Dev",
                Steps =
                [
                    ScriptStep("run-gateway-dev:run", "python scripts/run-webgateway.py --mode Dev",
                        "scripts/run-webgateway.py", "--mode", "Dev")
                ]
            };
        }
    }

    private static string FindProjectRoot(string startDir)
    {
        var start = Path.GetFullPath(startDir);
        var gitRoot = TryGetGitRoot(start);
        if (!string.IsNullOrWhiteSpace(gitRoot))
            return gitRoot!;

        var best = start;
        var bestScore = ScoreRoot(start);
        var cursor = new DirectoryInfo(start);
        while (cursor.Parent is not null)
        {
            cursor = cursor.Parent;
            var score = ScoreRoot(cursor.FullName);
            if (score > bestScore)
            {
                best = cursor.FullName;
                bestScore = score;
            }
        }

        return best;
    }

    private static int ScoreRoot(string path)
    {
        var score = 0;
        if (File.Exists(Path.Combine(path, "package.json"))) score += 2;
        if (File.Exists(Path.Combine(path, "pyproject.toml"))) score += 2;
        if (File.Exists(Path.Combine(path, "Cargo.toml"))) score += 2;
        if (File.Exists(Path.Combine(path, "docker-compose.yml")) ||
            File.Exists(Path.Combine(path, "docker-compose.yaml")) ||
            File.Exists(Path.Combine(path, "Dockerfile"))) score += 1;
        if (Directory.EnumerateFiles(path, "*.sln", SearchOption.TopDirectoryOnly).Any()) score += 3;
        if (Directory.Exists(Path.Combine(path, ".git"))) score += 1;
        return score;
    }

    private static string? TryGetGitRoot(string start)
    {
        try
        {
            var psi = new System.Diagnostics.ProcessStartInfo
            {
                FileName = "git",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
                WorkingDirectory = start
            };
            psi.ArgumentList.Add("rev-parse");
            psi.ArgumentList.Add("--show-toplevel");
            using var process = System.Diagnostics.Process.Start(psi);
            if (process is null)
                return null;
            var stdout = process.StandardOutput.ReadToEnd();
            process.WaitForExit(2000);
            if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(stdout))
                return stdout.Trim();
            return null;
        }
        catch
        {
            return null;
        }
    }

    private static IEnumerable<string> EnumerateFilesBounded(string root, string pattern, int maxDepth)
    {
        var queue = new Queue<(string Dir, int Depth)>();
        queue.Enqueue((root, 0));

        while (queue.Count > 0)
        {
            var (dir, depth) = queue.Dequeue();
            IEnumerable<string> files = [];
            try
            {
                files = Directory.EnumerateFiles(dir, pattern, SearchOption.TopDirectoryOnly);
            }
            catch
            {
                // Ignore unreadable directories.
            }

            foreach (var file in files)
                yield return file;

            if (depth >= maxDepth)
                continue;

            IEnumerable<string> subdirs = [];
            try
            {
                subdirs = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly);
            }
            catch
            {
                // Ignore unreadable directories.
            }

            foreach (var subdir in subdirs)
            {
                var name = Path.GetFileName(subdir);
                if (ExcludedDirectories.Contains(name))
                    continue;

                queue.Enqueue((subdir, depth + 1));
            }
        }
    }
}
