import Foundation

// MARK: - Build/Run config detection

/// One detected way to build or run a project, derived from a manifest file
/// (Makefile, package.json, Package.swift, …). The `command` is a shell string
/// run via the login shell in the project root; `label`/`source` describe it for
/// the UI.
public struct BuildRunCommand: Identifiable, Sendable, Hashable, Codable {
    public enum Kind: String, Sendable, Codable {
        case build
        case run
    }

    public let kind: Kind
    /// Short label shown on the menu item, e.g. `make build` or `npm run dev`.
    public let label: String
    /// The shell command to execute (passed to `zsh -lc`).
    public let command: String
    /// The manifest the command came from, e.g. `Makefile` or `package.json`.
    public let source: String

    /// Stable identity so SwiftUI lists don't churn: source + command is unique per project.
    public var id: String { "\(source)|\(kind.rawValue)|\(command)" }

    public init(kind: Kind, label: String, command: String, source: String) {
        self.kind = kind
        self.label = label
        self.command = command
        self.source = source
    }
}

/// Scans a project root for common build systems and extracts candidate build/run
/// commands. Pure and synchronous (just file reads + parsing) so it's trivially
/// testable and cheap to run off the main actor.
public enum BuildRunDetector {
    /// Detect build/run commands in `rootPath`, de-duplicated and ordered build-first.
    /// Returns an empty array when nothing recognizable is found.
    public static func detect(in rootPath: String) -> [BuildRunCommand] {
        let fm = FileManager.default
        var commands: [BuildRunCommand] = []

        func fileExists(_ name: String) -> Bool {
            fm.fileExists(atPath: (rootPath as NSString).appendingPathComponent(name))
        }
        func read(_ name: String) -> String? {
            try? String(contentsOfFile: (rootPath as NSString).appendingPathComponent(name),
                        encoding: .utf8)
        }

        // Makefile targets → `make <target>`.
        for name in ["Makefile", "makefile", "GNUmakefile"] where fileExists(name) {
            if let text = read(name) {
                for target in makeTargets(text) {
                    if let kind = classify(target) {
                        commands.append(BuildRunCommand(
                            kind: kind, label: "make \(target)",
                            command: "make \(target)", source: name))
                    }
                }
            }
            break  // only the first Makefile variant present
        }

        // package.json `scripts` → `npm run <script>`.
        if fileExists("package.json"), let text = read("package.json") {
            for script in npmScripts(text) {
                if let kind = classify(script) {
                    // `npm run start` works, but `npm start` is the idiomatic form.
                    let command = script == "start" ? "npm start" : "npm run \(script)"
                    commands.append(BuildRunCommand(
                        kind: kind, label: command, command: command, source: "package.json"))
                }
            }
        }

        // SwiftPM.
        if fileExists("Package.swift") {
            commands.append(BuildRunCommand(
                kind: .build, label: "swift build", command: "swift build", source: "Package.swift"))
            commands.append(BuildRunCommand(
                kind: .run, label: "swift run", command: "swift run", source: "Package.swift"))
        }

        // Cargo (Rust).
        if fileExists("Cargo.toml") {
            commands.append(BuildRunCommand(
                kind: .build, label: "cargo build", command: "cargo build", source: "Cargo.toml"))
            commands.append(BuildRunCommand(
                kind: .run, label: "cargo run", command: "cargo run", source: "Cargo.toml"))
        }

        // Go modules.
        if fileExists("go.mod") {
            commands.append(BuildRunCommand(
                kind: .build, label: "go build", command: "go build ./...", source: "go.mod"))
            commands.append(BuildRunCommand(
                kind: .run, label: "go run", command: "go run .", source: "go.mod"))
        }

        return dedupe(commands)
    }

    // MARK: - Parsing

    /// Target names from a Makefile: the word before `:` on a rule line. Skips
    /// pattern rules (`%`), special `.PHONY`-style dot targets, variable assignments,
    /// indented recipe lines, and comments.
    static func makeTargets(_ text: String) -> [String] {
        var targets: [String] = []
        var seen = Set<String>()
        for raw in text.components(separatedBy: "\n") {
            // Recipe lines are tab-indented; rule lines start at column 0.
            if raw.first == "\t" { continue }
            let line = raw.trimmingCharacters(in: .whitespaces)
            if line.isEmpty || line.hasPrefix("#") { continue }
            guard let colon = line.firstIndex(of: ":") else { continue }
            let head = String(line[..<colon]).trimmingCharacters(in: .whitespaces)
            // A variable assignment like `FOO := bar` has `=` before the colon's `:=`.
            if head.contains("=") { continue }
            // One target per rule is the common case; `a b:` declares two but is rare —
            // splitting on whitespace keeps it correct.
            for target in head.split(separator: " ") {
                let name = String(target)
                if name.isEmpty || name.hasPrefix(".") || name.contains("%") || name.contains("$") {
                    continue
                }
                if seen.insert(name).inserted { targets.append(name) }
            }
        }
        return targets
    }

    /// Script names from a package.json's top-level `scripts` object.
    static func npmScripts(_ text: String) -> [String] {
        guard let data = text.data(using: .utf8),
              let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let scripts = root["scripts"] as? [String: Any]
        else { return [] }
        // Preserve a stable, predictable order (JSON objects are unordered).
        return scripts.keys.sorted()
    }

    // MARK: - Classification

    /// Map a target/script name to build or run by keyword, or `nil` to ignore it
    /// (test/lint/clean/etc. aren't build or run actions).
    static func classify(_ name: String) -> BuildRunCommand.Kind? {
        let n = name.lowercased()
        let runKeywords = ["run", "start", "serve", "dev", "watch", "preview", "launch"]
        let buildKeywords = ["build", "compile", "dist", "bundle", "release", "package", "make"]
        if runKeywords.contains(where: { n == $0 || n.contains($0) }) { return .run }
        if buildKeywords.contains(where: { n == $0 || n.contains($0) }) { return .build }
        if n == "all" { return .build }
        return nil
    }

    /// Drop duplicate commands (same shell string) and order build-first, run-second,
    /// each preserving discovery order.
    private static func dedupe(_ commands: [BuildRunCommand]) -> [BuildRunCommand] {
        var seen = Set<String>()
        let unique = commands.filter { seen.insert($0.command).inserted }
        return unique.filter { $0.kind == .build } + unique.filter { $0.kind == .run }
    }
}
