import Foundation

/// Turns a shell command that's really a git operation into a short, human phrase for the
/// approval UI — so a permission request for an autoship-style pipeline reads "Commit:
/// Render bash summary lines …" instead of the raw
///
///     cd "/path" && git add -A && git commit -q -F - <<'EOF'
///     Render bash summary lines as inline Markdown (code spans)
///     …
///     EOF
///     git log --oneline -1
///
/// `summary(for:)` returns nil for anything that isn't a recognized git op, so the caller
/// falls back to showing the command verbatim. A pipeline of several git ops collapses to
/// its single most salient one (the commit above, not the `cd`, `add`, or `log`).
///
/// The command-agnostic lexing (segment splitting, tokenizing, heredocs) lives in
/// ``ShellLexer``; this type only adds the git-specific subcommand model on top. For a view
/// that also covers non-git ops (`rm`) across a mixed pipeline, see ``ShellCommandSummary``.
public enum GitCommandSummary {
    /// A friendly, imperative one-liner for the git operation a command performs, or nil
    /// when the command isn't a git op we recognize. The phrasing matches the approval
    /// title's register ("Commit: …", "Push to origin/main", "Check git status").
    public static func summary(for command: String) -> String? {
        let parsed = parse(command)
        // Across a multi-command pipeline, describe the one op that matters most — a commit
        // outweighs the `add` that staged it and the `log` that echoed it back.
        guard let op = parsed.ops.max(by: { $0.kind.salience < $1.kind.salience }) else { return nil }
        return label(for: op, heredoc: parsed.firstHeredocBody)
    }

    // MARK: - Structured block

    /// A shell command decomposed into the git operations it runs, for rendering as a GUI
    /// element instead of a raw monospaced blob. `commit` carries the parsed commit message
    /// (subject + body) when the block commits; `steps` lists every recognized git op in run
    /// order so the pipeline reads as a sequence. Nil from `block(for:)` when no git op is found.
    public struct Block: Equatable, Sendable {
        /// A commit's message, split into the subject (first line) and the remaining body
        /// (which may be empty, and may contain Markdown the renderer formats). `amend` is
        /// true for `git commit --amend`, so the UI can flag a history rewrite.
        public struct Commit: Equatable, Sendable {
            public let subject: String
            public let body: String
            public let amend: Bool
        }

        /// One git op in the pipeline, as a concise verb label plus an SF Symbol name for its
        /// glyph — the unit a step list renders ("Stage all changes", "Commit", "Show recent
        /// commits").
        public struct Step: Equatable, Sendable, Identifiable {
            public let id: Int
            public let label: String
            public let symbol: String
        }

        public let steps: [Step]
        public let commit: Commit?

        /// Whether this block is worth rendering as a commit card — it has a commit with a
        /// non-empty subject (or an amend, which may carry none).
        public var isCommit: Bool { commit != nil }
    }

    /// Decomposes a command into its git operations for the GUI, or nil when it contains no
    /// recognized git op. See ``Block``.
    public static func block(for command: String) -> Block? {
        let parsed = parse(command)
        guard !parsed.ops.isEmpty else { return nil }
        var steps: [Block.Step] = []
        var commit: Block.Commit?
        for (index, op) in parsed.ops.enumerated() {
            steps.append(Block.Step(id: index, label: stepLabel(for: op), symbol: symbol(for: op.kind)))
            if op.kind == .commit, commit == nil {
                commit = commitDetail(op.args, heredoc: parsed.firstHeredocBody)
            }
        }
        return Block(steps: steps, commit: commit)
    }

    /// A git op observed with certainty from a raw argv — the ground-truth counterpart to
    /// `summary(for:)` (which heuristically parses a Bash command string). Reported by the
    /// in-container `git` interceptor shim and classified here so Nucleic detects merges and
    /// other state changes without guessing.
    public struct ObservedGitOp: Equatable, Sendable {
        /// The recognized git subcommand, normalized (`merge`, `commit`, `switchBranch`, …).
        public let kind: String
        /// Human one-liner (e.g. "Merge feature/x", "Commit: Fix the bug").
        public let label: String
        /// SF Symbol name for the op's glyph, for a GUI step list.
        public let symbol: String
        /// How strongly this op defines a command's purpose (see `Kind.salience`) — lets a
        /// mixed-command pipeline pick the op that matters most across git and non-git ops.
        public let salience: Int
        /// True for `git merge` specifically — the op autoship/lock-release care about most.
        public let isMerge: Bool
        /// True for ops that change repo/worktree/ref state (vs read-only status/log/diff).
        public let isMutating: Bool
    }

    /// Classify a raw git argv — the tokens after `git`, as the interceptor shim reports them
    /// (it may still include global options like `-C <path>`/`-c <cfg>` before the subcommand).
    /// Returns nil when it isn't a git subcommand we model.
    public static func classify(argv: [String]) -> ObservedGitOp? {
        classify(argv: argv, heredoc: nil)
    }

    /// As `classify(argv:)`, but threading a heredoc body so a `git commit -F -` reading its
    /// message from stdin can title itself with the message's subject. Used by
    /// ``ShellCommandSummary`` when it has already pulled the pipeline's heredoc out.
    public static func classify(argv: [String], heredoc: String?) -> ObservedGitOp? {
        guard let op = parseGit(["git"] + argv) else { return nil }
        return ObservedGitOp(
            kind: op.kind.rawValue,
            label: label(for: op, heredoc: heredoc),
            symbol: symbol(for: op.kind),
            salience: op.kind.salience,
            isMerge: op.kind == .merge,
            isMutating: op.kind.isMutating)
    }

    /// The concise verb label for a step list — like `label(for:)` but without a commit's
    /// subject (the subject lives in ``Block/Commit`` and would only duplicate it here).
    private static func stepLabel(for op: Op) -> String {
        if op.kind == .commit { return op.args.contains("--amend") ? "Amend commit" : "Commit" }
        return label(for: op, heredoc: nil)
    }

    /// The parsed commit message (subject + body) for a commit op, or — for an `--amend`
    /// with no new message — an amend marker with empty text. Nil for a non-message commit.
    private static func commitDetail(_ args: [String], heredoc: String?) -> Block.Commit? {
        let amend = args.contains("--amend")
        guard let message = commitMessage(args, heredoc: heredoc),
              let (subject, body) = splitMessage(message) else {
            return amend ? Block.Commit(subject: "", body: "", amend: true) : nil
        }
        return Block.Commit(subject: subject, body: body, amend: amend)
    }

    /// An SF Symbol name for a git op's glyph in the step list.
    private static func symbol(for kind: Kind) -> String {
        switch kind {
        case .commit: return "checkmark.seal"
        case .merge: return "arrow.triangle.merge"
        case .rebase, .cherryPick, .checkout, .switchBranch, .branch: return "arrow.triangle.branch"
        case .revert, .reset: return "arrow.uturn.backward"
        case .push: return "arrow.up.circle"
        case .pull, .fetch, .clone: return "arrow.down.circle"
        case .tag: return "tag"
        case .stash: return "tray"
        case .restore: return "arrow.counterclockwise"
        case .remove: return "trash"
        case .move: return "arrow.left.arrow.right"
        case .add: return "plus.circle"
        case .initialize: return "sparkles"
        case .worktree: return "square.split.2x1"
        case .status: return "info.circle"
        case .log: return "list.bullet.rectangle"
        case .diff: return "plusminus"
        case .show: return "doc.text.magnifyingglass"
        case .blame: return "person.crop.circle.badge.questionmark"
        case .revParse: return "number"
        }
    }

    // MARK: - Parsing

    /// One recognized git invocation within a command line: its subcommand and the tokens
    /// that followed it (flags + operands), so a label builder can pull out a branch, remote,
    /// or message without re-tokenizing.
    private struct Op {
        let kind: Kind
        let args: [String]
    }

    private struct Parsed {
        var ops: [Op] = []
        /// The body of the first heredoc in the command — the commit message when a `git
        /// commit -F -` reads its message from stdin.
        var firstHeredocBody: String?
    }

    /// The git subcommands we phrase, ordered by `salience` so a pipeline reports the op
    /// that carries the most intent. Anything outside this set isn't recognized as a git op.
    private enum Kind: String {
        case commit, merge, rebase, cherryPick, revert, reset, push, pull, fetch, clone
        case checkout, switchBranch, branch, tag, stash, restore, remove, move, add, initialize
        case worktree, status, log, diff, show, blame, revParse

        /// How strongly this op defines the command's purpose. The highest-salience op in a
        /// pipeline wins, so write-class ops (a commit) beat read-class ones (a log/status)
        /// and the plumbing (`add`) that set them up.
        var salience: Int {
            switch self {
            case .commit: return 100
            case .merge, .rebase, .cherryPick, .revert: return 90
            case .reset: return 85
            case .push: return 80
            case .pull, .fetch: return 75
            case .clone: return 70
            case .checkout, .switchBranch: return 60
            case .worktree: return 58
            case .branch, .tag: return 55
            case .stash: return 50
            case .restore, .remove, .move: return 45
            case .add: return 40
            case .initialize: return 35
            case .status, .diff, .show, .blame: return 20
            case .log: return 15
            case .revParse: return 12
            }
        }

        /// Whether this op changes repo/worktree/ref state — everything except the pure reads
        /// (status, log, diff, show, blame, rev-parse). Lets the git-interceptor consumer
        /// ignore the agent's read-only git without re-deriving intent.
        var isMutating: Bool {
            switch self {
            case .status, .log, .diff, .show, .blame, .revParse: return false
            default: return true
            }
        }
    }

    /// Maps a git subcommand token to its `Kind`. Returns nil for an unknown subcommand so
    /// the op is ignored rather than mislabeled.
    private static func kind(forSubcommand sub: String) -> Kind? {
        switch sub {
        case "commit": return .commit
        case "merge": return .merge
        case "rebase": return .rebase
        case "cherry-pick": return .cherryPick
        case "revert": return .revert
        case "reset": return .reset
        case "push": return .push
        case "pull": return .pull
        case "fetch": return .fetch
        case "clone": return .clone
        case "checkout": return .checkout
        case "switch": return .switchBranch
        case "branch": return .branch
        case "tag": return .tag
        case "stash": return .stash
        case "restore": return .restore
        case "rm": return .remove
        case "mv": return .move
        case "add": return .add
        case "init": return .initialize
        case "worktree": return .worktree
        case "status": return .status
        case "log": return .log
        case "diff": return .diff
        case "show": return .show
        case "blame": return .blame
        case "rev-parse": return .revParse
        default: return nil
        }
    }

    /// Splits the command into segments, pulls any heredoc bodies out first so their contents
    /// can't be mistaken for commands, and parses each segment that begins with `git`. The
    /// lexing is delegated to ``ShellLexer``.
    private static func parse(_ command: String) -> Parsed {
        var result = Parsed()
        let (flattened, heredocs) = ShellLexer.stripHeredocs(command)
        result.firstHeredocBody = heredocs.first
        for segment in ShellLexer.splitSegments(flattened) {
            let tokens = ShellLexer.tokenize(segment)
            guard let op = parseGit(tokens) else { continue }
            result.ops.append(op)
        }
        return result
    }

    /// Parses a single tokenized segment into an `Op` when it's a git invocation, skipping a
    /// leading `sudo`/env-style prefix and git's pre-subcommand global options (`-C <path>`,
    /// `-c <cfg>`, `--git-dir=…`). Returns nil for a non-git segment or an unknown subcommand.
    private static func parseGit(_ tokens: [String]) -> Op? {
        var rest = ShellLexer.stripLeadingPrefixes(tokens)[...]
        guard let command = rest.first, command == "git" || command.hasSuffix("/git") else { return nil }
        rest = rest.dropFirst()
        // Skip git's global options that precede the subcommand. `-C` and `-c` take a value
        // as the following token; `--foo` / `--foo=bar` are self-contained.
        while let opt = rest.first, opt.hasPrefix("-") {
            rest = rest.dropFirst()
            if opt == "-C" || opt == "-c" { rest = rest.dropFirst() }  // consume its argument
        }
        guard let sub = rest.first, let kind = kind(forSubcommand: sub) else { return nil }
        return Op(kind: kind, args: Array(rest.dropFirst()))
    }

    // MARK: - Labels

    /// The human phrase for a parsed op. `heredoc` carries the first heredoc body so a commit
    /// reading its message from stdin (`-F -`) can title itself with the message's subject.
    private static func label(for op: Op, heredoc: String?) -> String {
        switch op.kind {
        case .commit: return commitLabel(op.args, heredoc: heredoc)
        case .merge: return ref(op.args).map { "Merge \($0)" } ?? "Merge branches"
        case .rebase: return ref(op.args).map { "Rebase onto \($0)" } ?? "Rebase commits"
        case .cherryPick: return "Cherry-pick commits"
        case .revert: return "Revert commits"
        case .reset: return "Reset the working tree"
        case .push: return remoteLabel("Push", preposition: "to", op.args, fallback: "Push changes")
        case .pull: return remoteLabel("Pull", preposition: "from", op.args, fallback: "Pull changes")
        case .fetch: return ref(op.args).map { "Fetch from \($0)" } ?? "Fetch changes"
        case .clone: return ref(op.args).map { "Clone \(repoName($0))" } ?? "Clone a repository"
        case .checkout: return branchLabel(op.args, verb: "Check out", fallback: "Check out a branch")
        case .switchBranch: return branchLabel(op.args, verb: "Switch to", fallback: "Switch branches")
        case .branch: return gitBranchLabel(op.args)
        case .tag: return ref(op.args).map { "Tag \($0)" } ?? "List tags"
        case .stash: return stashLabel(op.args)
        case .restore: return "Restore files"
        case .remove: return "Remove files"
        case .move: return "Move files"
        case .add: return addLabel(op.args)
        case .initialize: return "Initialize a git repository"
        case .worktree: return worktreeLabel(op.args)
        case .status: return "Check git status"
        case .log: return "Show recent commits"
        case .diff: return "Show changes"
        case .show: return "Show a commit"
        case .blame: return "Show file blame"
        case .revParse: return "Resolve a git revision"
        }
    }

    /// "Commit: <subject>" when a message is available — from `-m`/`--message` or, for `git
    /// commit -F -`, the first line of the piped heredoc — else a bare "Commit changes".
    /// Amends read as "Amend" so the user sees the history rewrite.
    private static func commitLabel(_ args: [String], heredoc: String?) -> String {
        let verb = args.contains("--amend") ? "Amend commit" : "Commit"
        guard let subject = commitSubject(args, heredoc: heredoc) else {
            return args.contains("--amend") ? "Amend commit" : "Commit changes"
        }
        return "\(verb): \(subject)"
    }

    /// The commit's subject line, clipped for the one-line title: the first line of the
    /// message from `-m`/`--message` or, for `git commit -F -`, the piped heredoc.
    private static func commitSubject(_ args: [String], heredoc: String?) -> String? {
        guard let message = commitMessage(args, heredoc: heredoc),
              let (subject, _) = splitMessage(message) else { return nil }
        return clipSubject(subject)
    }

    /// The raw commit message text: an inline `-m`/`--message` value, or the heredoc body
    /// when the commit reads its message from stdin (`-F -`). Nil when neither is present.
    private static func commitMessage(_ args: [String], heredoc: String?) -> String? {
        if let inline = flagValue(args, short: "-m", long: "--message") { return inline }
        if readsMessageFromStdin(args), let heredoc { return heredoc }
        return nil
    }

    /// Splits a commit message into its subject (the first non-empty line, trimmed) and body
    /// (everything after it, trimmed of surrounding blank lines — interior blanks are kept so
    /// the renderer can format paragraphs and bullet lists). Nil when the message is blank.
    private static func splitMessage(_ message: String) -> (subject: String, body: String)? {
        let lines = message.components(separatedBy: "\n")
        guard let index = lines.firstIndex(where: { !$0.trimmingCharacters(in: .whitespaces).isEmpty })
        else { return nil }
        let subject = lines[index].trimmingCharacters(in: .whitespaces)
        let body = lines[(index + 1)...].joined(separator: "\n")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return (subject, body)
    }

    /// Whether a `git commit` takes its message from stdin — `-F -`, `--file -`, or
    /// `--file=-` — in which case the heredoc body is that message.
    private static func readsMessageFromStdin(_ args: [String]) -> Bool {
        for (i, a) in args.enumerated() {
            if (a == "-F" || a == "--file"), i + 1 < args.count, args[i + 1] == "-" { return true }
            if a == "--file=-" || a == "-F-" { return true }
        }
        return false
    }

    /// "Push to origin/main" / "Pull from origin/main" from a `[remote] [branch]`, falling
    /// back to the remote alone or a bare verb. Skips flags to find the positional operands.
    private static func remoteLabel(_ verb: String, preposition: String, _ args: [String], fallback: String) -> String {
        let operands = positionals(args)
        guard let remote = operands.first else { return fallback }
        if operands.count >= 2 { return "\(verb) \(preposition) \(remote)/\(operands[1])" }
        return "\(verb) \(preposition) \(remote)"
    }

    /// "Check out <branch>" / "Switch to <branch>" / "Create branch <name>" (the last when
    /// `-b`/`-c` creates one), falling back to the bare verb when no branch operand is present.
    private static func branchLabel(_ args: [String], verb: String, fallback: String) -> String {
        if let created = flagValue(args, short: "-b", long: "-c") ?? flagValue(args, short: "-B", long: "-C") {
            return "Create branch \(created)"
        }
        guard let branch = positionals(args).first else { return fallback }
        return "\(verb) \(branch)"
    }

    /// `git branch` itself: "Delete branch <name>" for `-d`/`-D`/`--delete`, "Create branch
    /// <name>" when given a name to create, else "List branches".
    private static func gitBranchLabel(_ args: [String]) -> String {
        let deleteFlags: Set<String> = ["-d", "-D", "--delete"]
        let isDelete = args.contains { deleteFlags.contains($0) }
        guard let name = positionals(args).first else { return "List branches" }
        return isDelete ? "Delete branch \(name)" : "Create branch \(name)"
    }

    /// `git worktree <sub>`: a concise phrase per sub-subcommand, naming the path for an
    /// `add`/`remove` when one is given ("Add worktree at /tmp/x", "List worktrees").
    private static func worktreeLabel(_ args: [String]) -> String {
        let operands = positionals(args)
        guard let sub = operands.first else { return "Manage worktrees" }
        let target = operands.count >= 2 ? operands[1] : nil
        switch sub {
        case "add": return target.map { "Add worktree at \($0)" } ?? "Add a worktree"
        case "remove": return target.map { "Remove worktree \($0)" } ?? "Remove a worktree"
        case "move": return "Move a worktree"
        case "prune": return "Prune worktrees"
        case "lock": return "Lock a worktree"
        case "unlock": return "Unlock a worktree"
        case "repair": return "Repair worktrees"
        case "list": return "List worktrees"
        default: return "Manage worktrees"
        }
    }

    /// "Stage all changes" for `add -A`/`add .`, "Stage <path>" for a single path, else a
    /// plain "Stage changes".
    private static func addLabel(_ args: [String]) -> String {
        if args.contains("-A") || args.contains("--all") || args.contains(".") { return "Stage all changes" }
        let paths = positionals(args)
        if paths.count == 1 { return "Stage \(paths[0])" }
        return "Stage changes"
    }

    private static func stashLabel(_ args: [String]) -> String {
        guard let sub = positionals(args).first else { return "Stash changes" }
        switch sub {
        case "pop": return "Pop stashed changes"
        case "apply": return "Apply stashed changes"
        case "drop": return "Drop a stash"
        case "list": return "List stashes"
        case "push", "save": return "Stash changes"
        default: return "Stash changes"
        }
    }

    // MARK: - Token helpers

    /// git's value-taking flags — the ones whose following token is a value, not an operand —
    /// so `positionals` doesn't mistake a message/branch/remote value for a positional. Includes
    /// the `clone`/`fetch` value options that commonly precede the repo URL (`--depth 1`,
    /// `--branch x`, `--filter …`), without which their value is read as the first positional —
    /// e.g. `git clone --depth 1 <url>` would mislabel as "Clone 1" instead of "Clone <repo>".
    /// `--recurse-submodules` is deliberately omitted: its value is optional, so consuming the
    /// next token would swallow the URL when it's given bare.
    private static let valueFlags: Set<String> = ["-m", "--message", "-F", "--file", "-b", "-B",
                                                  "-c", "-C", "-u", "--set-upstream", "-t", "--track",
                                                  "--depth", "--branch", "-j", "--jobs", "--filter",
                                                  "-o", "--origin", "--reference", "--reference-if-able"]

    /// The value of a flag given either as `--flag value` / `-f value` or `--flag=value`.
    private static func flagValue(_ args: [String], short: String, long: String) -> String? {
        ShellLexer.flagValue(args, short: short, long: long)
    }

    /// The positional operands of a git subcommand — tokens that aren't flags or flag values.
    private static func positionals(_ args: [String]) -> [String] {
        ShellLexer.positionals(args, valueFlags: valueFlags)
    }

    /// The first ref-like positional operand (a branch, remote, tag, or repo URL).
    private static func ref(_ args: [String]) -> String? { positionals(args).first }

    /// The repository name from a clone URL/path — its last path component, minus a `.git`
    /// suffix ("https://host/org/repo.git" → "repo").
    private static func repoName(_ url: String) -> String {
        let last = url.split(separator: "/").last.map(String.init) ?? url
        return last.hasSuffix(".git") ? String(last.dropLast(4)) : last
    }

    /// A commit subject trimmed to a glanceable length without splitting a word.
    private static func clipSubject(_ subject: String?) -> String? {
        guard let subject, !subject.isEmpty else { return nil }
        return HeuristicTitle.clip(subject, max: 60)
    }
}
