import Foundation

/// Turns a general Bash command — the kind an agent asks permission to run — into the
/// notable operations it performs, for the approval UI. Where ``GitCommandSummary`` sees
/// only git, this also surfaces the destructive `rm`/`rmdir` deletes that matter most in a
/// permission request, so a worktree-setup pipeline like
///
///     cd "/repo" && WT=/tmp/x && rm -rf "$WT" && git worktree add --detach "$WT" HEAD \
///       && cd "$WT" && git switch -c work/x && git rev-parse HEAD
///
/// reads as "Delete $WT (recursive, forced) · Add worktree at $WT · Create branch work/x ·
/// Resolve a git revision" instead of one opaque blob — and the leading `rm -rf` can't hide
/// behind the git ops. The shell plumbing (`cd`, env assignments, `echo`, `tail`) is noise
/// and dropped; only `rm` and `git` are recognized (git via ``GitCommandSummary``).
///
/// `summary(for:)` returns the single most salient op for a one-line title (a recursive `rm`
/// outranks the git ops around it); `block(for:)` returns every notable op in run order for a
/// step list. Both return nil when the command contains no recognized op, so the caller falls
/// back to the verbatim command.
public enum ShellCommandSummary {
    /// A shell command decomposed into its notable (`rm`/`git`) operations, for rendering as
    /// a GUI step list instead of a raw monospaced blob. Nil from `block(for:)` when none.
    public struct Block: Equatable, Sendable {
        /// One notable op in the pipeline: a concise label, an SF Symbol glyph, and whether
        /// it's a destructive delete the UI should emphasize.
        public struct Step: Equatable, Sendable, Identifiable {
            public let id: Int
            public let label: String
            public let symbol: String
            /// True for a `rm`/`rmdir` delete — the op a permission request should flag in red.
            public let destructive: Bool
        }

        public let steps: [Step]
    }

    /// A friendly one-liner for the command's single most important operation, or nil when no
    /// `rm`/`git` op is recognized. A recursive/forced `rm` outranks the git ops around it so
    /// the scariest part of a mixed pipeline leads the title.
    public static func summary(for command: String) -> String? {
        notableOps(command).max(by: { $0.salience < $1.salience })?.label
    }

    /// Every notable op in run order, for a step list, or nil when none is recognized.
    public static func block(for command: String) -> Block? {
        let ops = notableOps(command)
        guard !ops.isEmpty else { return nil }
        let steps = ops.enumerated().map { index, op in
            Block.Step(id: index, label: op.label, symbol: op.symbol, destructive: op.destructive)
        }
        return Block(steps: steps)
    }

    // MARK: - Parsing

    /// A recognized op, with the salience that ranks it against the others for the one-liner.
    private struct NotableOp {
        let label: String
        let symbol: String
        let destructive: Bool
        let salience: Int
    }

    /// The `rm`/`git` ops in the command, in run order. Heredocs are pulled out first (so a
    /// commit message piped to `git commit -F -` titles the commit, not parses as commands),
    /// and each segment's leading `sudo`/env prefix is stripped before the command word.
    private static func notableOps(_ command: String) -> [NotableOp] {
        let (flattened, bodies) = ShellLexer.stripHeredocs(command)
        let heredoc = bodies.first
        var ops: [NotableOp] = []
        for segment in ShellLexer.splitSegments(flattened) {
            let rest = ShellLexer.stripLeadingPrefixes(ShellLexer.tokenize(segment))
            guard let head = rest.first else { continue }
            let args = Array(rest.dropFirst())
            if head == "git" || head.hasSuffix("/git") {
                // Reuse the git model so every git subcommand reads identically here and in the
                // git-only views; heredoc lets a `commit -F -` keep its subject.
                if let git = GitCommandSummary.classify(argv: args, heredoc: heredoc) {
                    ops.append(NotableOp(label: git.label, symbol: git.symbol,
                                         destructive: false, salience: git.salience))
                }
            } else if head == "rm" || head.hasSuffix("/rm") {
                ops.append(removeOp(args))
            } else if head == "rmdir" || head.hasSuffix("/rmdir") {
                ops.append(rmdirOp(args))
            }
        }
        return ops
    }

    /// "Delete <target> (recursive, forced)" — the notes name the `-r`/`-f` flags that make a
    /// delete dangerous. A recursive or forced `rm` is ranked above most git ops so it leads a
    /// mixed pipeline's one-liner; a plain `rm` still outranks read-only ops.
    private static func removeOp(_ args: [String]) -> NotableOp {
        let recursive = hasFlag(args, shortChars: ["r", "R"], long: ["--recursive"])
        let force = hasFlag(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: ", ")))"
        return NotableOp(label: "Delete \(target(args))\(suffix)", symbol: "trash",
                         destructive: true, salience: (recursive || force) ? 95 : 88)
    }

    private static func rmdirOp(_ args: [String]) -> NotableOp {
        NotableOp(label: "Remove directory \(target(args))", symbol: "trash",
                  destructive: true, salience: 80)
    }

    /// The thing a delete targets: the single path, a count when several, or a bare "files"
    /// when only flags are present.
    private static func target(_ args: [String]) -> String {
        let paths = ShellLexer.positionals(args, valueFlags: [])
        switch paths.count {
        case 0: return "files"
        case 1: return paths[0]
        default: return "\(paths.count) paths"
        }
    }

    /// Whether any token sets one of `shortChars` (as a bundled short flag like `-rf`) or one
    /// of the `long` flags. Stops at `--` (the end-of-options marker) and ignores operands.
    private static func hasFlag(_ 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
    }
}
