import Foundation

/// Turns a non-git shell command an agent ran — `grep`, `cat`, `find`, `ls`, `sed`, `npm`, … —
/// into a short, human phrase + glyph for the Control panel's command-activity feed and the
/// structured log. This is the non-git counterpart to ``GitCommandSummary``: where that one
/// models git subcommands, this models the ordinary commands agents lean on in Bash
/// (file inspection, search, text munging, build/run), as reported by the in-container
/// command-interceptor shim and the bash command tracer.
///
/// Unlike `GitCommandSummary.classify` (which returns nil for anything it doesn't recognize),
/// this ALWAYS yields an ``ObservedCommand`` for a non-empty argv: every command the shim/tracer
/// bothered to report is worth a feed row, so an unrecognized command falls back to a generic
/// "Run <cmd>" entry rather than being dropped. The recognized cases get a friendlier label,
/// a category, and a fitting SF Symbol.
public enum CommandSummary {
    /// A command observed with certainty from a raw argv (or tokenized command line). Carries a
    /// glanceable label, a coarse `category` (for grouping/coloring), an SF Symbol for the feed
    /// glyph, and whether the op mutates the filesystem (so a consumer can flag writes/deletes).
    public struct ObservedCommand: Equatable, Sendable {
        /// The resolved command name (argv[0]'s basename), e.g. `grep`, `cat`, `npm`.
        public let command: String
        /// A coarse bucket: `read`, `search`, `list`, `text`, `fs`, `delete`, `build`, `run`,
        /// `network`, `archive`, `info`, or `other`.
        public let category: String
        /// Human one-liner (e.g. `Search for "TODO"`, `Read file.txt`, `List src/`).
        public let label: String
        /// SF Symbol name for the op's glyph in the feed.
        public let symbol: String
        /// True for commands that write/delete on the filesystem (`rm`, `cp`, `mv`, `mkdir`, …),
        /// so the UI can emphasize them; false for the read/search/inspect majority.
        public let isMutating: Bool
        /// True when the command word matched a case we specifically model (vs. the generic
        /// "Run <cmd>" fallback) — lets a consumer treat recognized ops with more confidence.
        public let recognized: Bool

        public init(
            command: String, category: String, label: String, symbol: String,
            isMutating: Bool, recognized: Bool
        ) {
            self.command = command
            self.category = category
            self.label = label
            self.symbol = symbol
            self.isMutating = isMutating
            self.recognized = recognized
        }
    }

    /// Classify a tokenized argv (the command word at index 0, then its arguments — exactly what
    /// the interceptor shim reports). Returns nil only for an empty argv.
    public static func classify(argv: [String]) -> ObservedCommand? {
        let stripped = ShellLexer.stripLeadingPrefixes(argv)
        guard let head = stripped.first, !head.isEmpty else { return nil }
        let name = basename(head)
        let args = Array(stripped.dropFirst())
        return classify(name: name, args: args)
    }

    /// As `classify(argv:)`, but from a raw command line (the form the bash tracer reports). The
    /// line is tokenized with the shared ``ShellLexer`` so quoting/heredocs match the rest of the
    /// summarizers; only the first segment's leading command is described.
    public static func classify(commandLine: String) -> ObservedCommand? {
        let (flattened, _) = ShellLexer.stripHeredocs(commandLine)
        guard let segment = ShellLexer.splitSegments(flattened).first else { return nil }
        return classify(argv: ShellLexer.tokenize(segment))
    }

    // MARK: - Core mapping

    private static func classify(name: String, args: [String]) -> ObservedCommand {
        switch name {
        // ── Reading file contents ────────────────────────────────────────────────────────────
        case "cat", "bat", "less", "more", "tac":
            return read("Read \(fileOperand(args) ?? "input")", name)
        case "head":
            return read(fileOperand(args).map { "Show start of \($0)" } ?? "Show file head", name)
        case "tail":
            return read(fileOperand(args).map { "Show end of \($0)" } ?? "Show file tail", name)
        case "od", "xxd", "hexdump", "strings":
            return read(fileOperand(args).map { "Inspect bytes of \($0)" } ?? "Inspect bytes", name)

        // ── Searching ────────────────────────────────────────────────────────────────────────
        case "grep", "egrep", "fgrep", "rg", "ag", "ack", "ugrep":
            return op("search", searchLabel(args), "magnifyingglass", name, mutating: false)
        case "find", "fd", "fdfind":
            return op("search", findLabel(args), "folder.badge.questionmark", name, mutating: false)
        case "locate", "mdfind", "which", "whereis", "type", "command":
            return op("search", "Locate \(firstPositional(args) ?? "a command")", "magnifyingglass", name, mutating: false)

        // ── Listing ──────────────────────────────────────────────────────────────────────────
        case "ls", "exa", "eza", "ll", "tree", "lsd":
            return op("list", listLabel(args), "list.bullet", name, mutating: false)
        case "pwd", "dirname", "basename", "realpath", "readlink":
            return op("info", "Resolve a path", "folder", name, mutating: false)

        // ── Text processing ──────────────────────────────────────────────────────────────────
        case "sed":
            return op("text", "Transform text (sed)", "text.append", name, mutating: false)
        case "awk", "gawk", "mawk":
            return op("text", "Process text (awk)", "text.append", name, mutating: false)
        case "wc":
            return op("text", fileOperand(args).map { "Count lines in \($0)" } ?? "Count lines", "number", name, mutating: false)
        case "sort", "uniq", "cut", "tr", "paste", "join", "comm", "column", "fold", "fmt", "rev", "nl", "expand", "tee":
            return op("text", "Process text (\(name))", "text.alignleft", name, mutating: false)
        case "jq", "yq", "xmllint":
            return op("text", "Query structured data (\(name))", "curlybraces", name, mutating: false)
        case "diff", "cmp", "comm-diff", "delta":
            return op("text", "Compare files", "plusminus", name, mutating: false)
        case "echo", "printf", "yes", "seq":
            return op("text", "Print text", "text.bubble", name, mutating: false)
        case "xargs":
            return op("run", "Run a command over input (xargs)", "arrow.triangle.branch", name, mutating: false)

        // ── Filesystem mutations ─────────────────────────────────────────────────────────────
        case "rm":
            return op("delete", deleteLabel(args), "trash", name, mutating: true)
        case "rmdir":
            return op("delete", "Remove directory \(firstPositional(args) ?? "")".trimmingCharacters(in: .whitespaces), "trash", name, mutating: true)
        case "mkdir":
            return op("fs", "Create directory \(firstPositional(args) ?? "")".trimmingCharacters(in: .whitespaces), "folder.badge.plus", name, mutating: true)
        case "touch":
            return op("fs", "Touch \(firstPositional(args) ?? "a file")", "doc.badge.plus", name, mutating: true)
        case "cp", "rsync", "install":
            return op("fs", "Copy files", "doc.on.doc", name, mutating: true)
        case "mv", "rename":
            return op("fs", "Move/rename files", "arrow.left.arrow.right", name, mutating: true)
        case "ln":
            return op("fs", "Create a link", "link", name, mutating: true)
        case "chmod", "chown", "chgrp", "umask":
            return op("fs", "Change permissions", "lock.shield", name, mutating: true)

        // ── Build / package / run ────────────────────────────────────────────────────────────
        case "npm", "pnpm", "yarn", "bun", "npx", "pip", "pip3", "poetry", "uv", "gem", "bundle", "cargo", "go", "mvn", "gradle", "brew", "apt", "apt-get":
            return op("build", "Package/build: \(name) \(firstPositional(args) ?? "")".trimmingCharacters(in: .whitespaces), "shippingbox", name, mutating: true)
        case "make", "cmake", "ninja", "swift", "xcodebuild", "tsc", "webpack", "vite", "rollup", "esbuild":
            return op("build", "Build (\(name))", "hammer", name, mutating: true)
        case "gcc", "g++", "clang", "clang++", "cc", "rustc", "javac", "ld":
            return op("build", "Compile (\(name))", "hammer", name, mutating: true)
        case "node", "deno", "python", "python3", "ruby", "perl", "php", "bash", "sh", "zsh":
            return op("run", runLabel(name, args), "terminal", name, mutating: false)
        case "pytest", "jest", "vitest", "mocha", "rspec", "tox":
            return op("run", "Run tests (\(name))", "checkmark.diamond", name, mutating: false)

        // ── Network ──────────────────────────────────────────────────────────────────────────
        case "curl", "wget", "http", "https":
            return op("network", networkLabel(name, args), "network", name, mutating: false)
        case "ssh", "scp", "sftp", "rsync-remote", "nc", "ncat", "telnet", "ping", "dig", "nslookup", "host":
            return op("network", "Network (\(name))", "network", name, mutating: false)

        // ── Archives ─────────────────────────────────────────────────────────────────────────
        case "tar", "zip", "unzip", "gzip", "gunzip", "bzip2", "xz", "zstd", "7z", "unrar":
            return op("archive", "Archive (\(name))", "archivebox", name, mutating: true)

        // ── Process / system info ────────────────────────────────────────────────────────────
        case "ps", "top", "htop", "kill", "pkill", "killall", "jobs", "wait", "nohup", "timeout":
            return op("info", "Process control (\(name))", "gauge", name, mutating: false)
        case "df", "du", "free", "uname", "hostname", "whoami", "id", "uptime", "date", "env", "printenv", "lsof", "stat", "file":
            return op("info", "System info (\(name))", "info.circle", name, mutating: false)
        case "cd", "pushd", "popd", "export", "set", "unset", "source", "alias", "true", "false", "test", "[", "[[", "read":
            return op("other", "Shell builtin (\(name))", "chevron.left.forwardslash.chevron.right", name, mutating: false)

        default:
            // Unknown command: keep it in the feed with a generic label so nothing the agent ran
            // is silently invisible. Include the first operand for a touch of context.
            let detail = firstPositional(args).map { " \($0)" } ?? ""
            return ObservedCommand(
                command: name, category: "other",
                label: "Run \(name)\(clip(detail, 32))", symbol: "terminal",
                isMutating: false, recognized: false)
        }
    }

    // MARK: - Label builders

    /// `Search for "<pattern>"` — the first non-flag operand is the pattern for grep-family tools.
    private static func searchLabel(_ args: [String]) -> String {
        guard let pattern = firstPositional(args) else { return "Search files" }
        return "Search for \(quote(clip(pattern, 40)))"
    }

    /// `Find files matching <glob>` for `find -name <pat>`, else `Find in <dir>` / `Find files`.
    private static func findLabel(_ args: [String]) -> String {
        if let glob = ShellLexer.flagValue(args, short: "-name", long: "-iname")
            ?? ShellLexer.flagValue(args, short: "-path", long: "-ipath") {
            return "Find files matching \(quote(clip(glob, 36)))"
        }
        if let dir = firstPositional(args) { return "Find files in \(clip(dir, 36))" }
        return "Find files"
    }

    /// `List <dir>` naming the directory operand when present, else a bare `List directory`.
    private static func listLabel(_ args: [String]) -> String {
        guard let dir = firstPositional(args) else { return "List directory" }
        return "List \(clip(dir, 40))"
    }

    /// `Delete <target>` with `(recursive, forced)` notes for the dangerous flags — mirrors
    /// ``ShellCommandSummary`` so an `rm` reads the same everywhere it surfaces.
    private static func deleteLabel(_ args: [String]) -> String {
        let recursive = hasShortOrLong(args, shortChars: ["r", "R"], long: ["--recursive"])
        let force = hasShortOrLong(args, shortChars: ["f"], long: ["--force"])
        var notes: [String] = []
        if recursive { notes.append("recursive") }
        if force { notes.append("forced") }
        let suffix = notes.isEmpty ? "" : " (\(notes.joined(separator: ", ")))"
        let target: String
        let paths = ShellLexer.positionals(args, valueFlags: [])
        switch paths.count {
        case 0: target = "files"
        case 1: target = clip(paths[0], 40)
        default: target = "\(paths.count) paths"
        }
        return "Delete \(target)\(suffix)"
    }

    /// `Run <script> (python)` naming the first script-like operand for interpreters.
    private static func runLabel(_ name: String, _ args: [String]) -> String {
        if let script = firstPositional(args) { return "Run \(clip(script, 36)) (\(name))" }
        return "Run \(name)"
    }

    /// `Fetch <url>` naming the first URL-ish operand for curl/wget.
    private static func networkLabel(_ name: String, _ args: [String]) -> String {
        if let url = ShellLexer.positionals(args, valueFlags: []).first(where: { $0.contains("://") || $0.contains(".") }) {
            return "Fetch \(clip(url, 44))"
        }
        return "Network (\(name))"
    }

    // MARK: - Small helpers

    private static func op(
        _ category: String, _ label: String, _ symbol: String, _ command: String, mutating: Bool
    ) -> ObservedCommand {
        ObservedCommand(
            command: command, category: category, label: label, symbol: symbol,
            isMutating: mutating, recognized: true)
    }

    private static func read(_ label: String, _ command: String) -> ObservedCommand {
        op("read", label, "doc.text", command, mutating: false)
    }

    /// The first operand that looks like a file (the search/read target), preferring an operand
    /// with a path separator or extension so `grep -n foo bar.txt` names `bar.txt`, not `foo`.
    private static func fileOperand(_ args: [String]) -> String? {
        let positionals = ShellLexer.positionals(args, valueFlags: [])
        if let pathLike = positionals.first(where: { $0.contains("/") || $0.contains(".") }) {
            return clip(pathLike, 40)
        }
        return positionals.first.map { clip($0, 40) }
    }

    private static func firstPositional(_ args: [String]) -> String? {
        ShellLexer.positionals(args, valueFlags: []).first
    }

    /// The command word's basename, sans any path (`/usr/bin/grep` → `grep`).
    private static func basename(_ command: String) -> String {
        command.split(separator: "/").last.map(String.init) ?? command
    }

    private static func quote(_ s: String) -> String { "\"\(s)\"" }

    private static func clip(_ s: String, _ max: Int) -> String {
        s.count <= max ? s : String(s.prefix(max - 1)) + "…"
    }

    /// Whether any token sets one of `shortChars` (bundled like `-rf`) or one of the `long` flags.
    private static func hasShortOrLong(_ args: [String], shortChars: Set<Character>, long: Set<String>) -> Bool {
        for a in args {
            if a == "--" { break }
            if long.contains(a) { return true }
            if a.hasPrefix("--") { continue }
            if a.hasPrefix("-"), a.count > 1, a.dropFirst().contains(where: { shortChars.contains($0) }) {
                return true
            }
        }
        return false
    }
}
