import Foundation

/// Classifies a raw `gh` (GitHub CLI) argv into the agentic action it performs — the ground-truth
/// counterpart to ``GitCommandSummary`` for the `git` interceptor. The in-container `gh` shim
/// reports each invocation (POST `/gh-event`); this type decides, from the noun + verb, whether it
/// was a *mutating* action worth observing (open/merge/close a PR, cut a release, hit the API with a
/// write method, …) and gives it a human label + glyph. Read-only `gh` (list/view/status) and
/// commands we don't model classify to `nil`, so the host ignores them.
///
/// Like ``GitCommandSummary``, parsing is deliberately structural (noun, verb, a few flag values)
/// rather than a full gh grammar — the host only needs to recognize the action, not re-run it.
public enum GhCommandSummary {
    /// A `gh` action observed with certainty from a raw argv. `kind` is the normalized
    /// `noun.verb` (`pr.create`, `pr.merge`, `release.create`, `api`, …).
    public struct ObservedGhOp: Equatable, Sendable {
        public let kind: String
        /// Human one-liner (e.g. "Open PR: Fix the bug", "Merge pull request #42").
        public let label: String
        /// SF Symbol name for the op's glyph (shared with the Control activity feed).
        public let symbol: String
        /// True for state-changing actions (the only ones we classify); reads return `nil` instead.
        public let isMutating: Bool
        /// `gh pr merge` specifically — the closest gh analog to a `git merge` landing.
        public let isMerge: Bool

        public init(kind: String, label: String, symbol: String, isMutating: Bool, isMerge: Bool) {
            self.kind = kind
            self.label = label
            self.symbol = symbol
            self.isMutating = isMutating
            self.isMerge = isMerge
        }
    }

    /// Classify a raw gh argv — the tokens after `gh` as the shim reports them (a leading `gh` or
    /// path-to-gh is tolerated). Returns `nil` for reads and commands we don't model.
    public static func classify(argv: [String]) -> ObservedGhOp? {
        var rest = ShellLexer.stripLeadingPrefixes(argv)[...]
        if let first = rest.first, first == "gh" || first.hasSuffix("/gh") { rest = rest.dropFirst() }
        // gh's pre-command global flags (`--version`, `--help`, …) are all self-contained.
        while let opt = rest.first, opt.hasPrefix("-") { rest = rest.dropFirst() }
        guard let noun = rest.first else { return nil }
        let after = Array(rest.dropFirst())

        switch noun {
        case "pr": return pullRequest(after)
        case "issue": return issue(after)
        case "release": return release(after)
        case "repo": return repo(after)
        case "gist":
            return table("gist", after, symbol: "doc.text", verbs: [
                "create": "Create a gist", "delete": "Delete a gist", "edit": "Edit a gist",
                "rename": "Rename a gist file",
            ])
        case "workflow":
            return table("workflow", after, symbol: "play.circle", verbs: [
                "run": "Run a workflow", "enable": "Enable a workflow", "disable": "Disable a workflow",
            ])
        case "run":
            return table("run", after, symbol: "arrow.clockwise", verbs: [
                "rerun": "Re-run a workflow run", "cancel": "Cancel a workflow run",
                "delete": "Delete a workflow run",
            ])
        case "secret":
            return table("secret", after, symbol: "key", verbs: [
                "set": "Set a secret", "delete": "Delete a secret", "remove": "Delete a secret",
            ])
        case "variable":
            return table("variable", after, symbol: "character.textbox", verbs: [
                "set": "Set a variable", "delete": "Delete a variable", "remove": "Delete a variable",
            ])
        case "label":
            return table("label", after, symbol: "tag", verbs: [
                "create": "Create a label", "delete": "Delete a label", "edit": "Edit a label",
                "clone": "Clone labels",
            ])
        case "cache":
            return table("cache", after, symbol: "trash", verbs: ["delete": "Delete a cache"])
        case "ssh-key":
            return table("ssh-key", after, symbol: "key", verbs: [
                "add": "Add an SSH key", "delete": "Delete an SSH key",
            ])
        case "gpg-key":
            return table("gpg-key", after, symbol: "key", verbs: [
                "add": "Add a GPG key", "delete": "Delete a GPG key",
            ])
        case "api": return api(after)
        default: return nil
        }
    }

    // MARK: - Nouns with enriched labels

    private static func pullRequest(_ a: [String]) -> ObservedGhOp? {
        guard let verb = verb(a) else { return nil }
        switch verb {
        case "create":
            let title = ShellLexer.flagValue(a, short: "-t", long: "--title").map(clip)
            return op("pr.create", title.map { "Open PR: \($0)" } ?? "Open a pull request",
                      "arrow.triangle.branch")
        case "merge":
            return op("pr.merge", numbered("Merge pull request", a), "arrow.triangle.merge", isMerge: true)
        case "close": return op("pr.close", numbered("Close pull request", a), "xmark.circle")
        case "reopen": return op("pr.reopen", numbered("Reopen pull request", a), "arrow.uturn.backward.circle")
        case "ready": return op("pr.ready", numbered("Mark pull request ready", a), "checkmark.circle")
        case "edit": return op("pr.edit", numbered("Edit pull request", a), "pencil")
        case "comment": return op("pr.comment", numbered("Comment on pull request", a), "text.bubble")
        case "review": return op("pr.review", numbered("Review pull request", a), "checkmark.bubble")
        default: return nil  // list/view/status/checks/diff/checkout → reads or local-only
        }
    }

    private static func issue(_ a: [String]) -> ObservedGhOp? {
        guard let verb = verb(a) else { return nil }
        switch verb {
        case "create":
            let title = ShellLexer.flagValue(a, short: "-t", long: "--title").map(clip)
            return op("issue.create", title.map { "Open issue: \($0)" } ?? "Open an issue",
                      "exclamationmark.circle")
        case "close": return op("issue.close", numbered("Close issue", a), "xmark.circle")
        case "reopen": return op("issue.reopen", numbered("Reopen issue", a), "arrow.uturn.backward.circle")
        case "edit": return op("issue.edit", numbered("Edit issue", a), "pencil")
        case "comment": return op("issue.comment", numbered("Comment on issue", a), "text.bubble")
        case "delete": return op("issue.delete", numbered("Delete issue", a), "trash")
        case "transfer": return op("issue.transfer", numbered("Transfer issue", a), "arrow.right.circle")
        default: return nil
        }
    }

    private static func release(_ a: [String]) -> ObservedGhOp? {
        guard let verb = verb(a) else { return nil }
        let tag = positionals(a).dropFirst().first  // the token after the verb, e.g. the tag
        func tagged(_ phrase: String) -> String { tag.map { "\(phrase) \($0)" } ?? "\(phrase)" }
        switch verb {
        case "create": return op("release.create", tag.map { "Create release \($0)" } ?? "Create a release", "tag")
        case "delete": return op("release.delete", tagged("Delete release"), "trash")
        case "edit": return op("release.edit", tagged("Edit release"), "pencil")
        case "upload": return op("release.upload", tagged("Upload assets to release"), "arrow.up.doc")
        case "delete-asset": return op("release.delete-asset", "Delete a release asset", "trash")
        default: return nil
        }
    }

    private static func repo(_ a: [String]) -> ObservedGhOp? {
        guard let verb = verb(a) else { return nil }
        let name = positionals(a).dropFirst().first
        func named(_ phrase: String) -> String { name.map { "\(phrase) \($0)" } ?? "\(phrase)" }
        switch verb {
        case "create": return op("repo.create", name.map { "Create repository \($0)" } ?? "Create a repository", "folder.badge.plus")
        case "delete": return op("repo.delete", named("Delete repository"), "trash")
        case "fork": return op("repo.fork", named("Fork repository"), "tuningfork")
        case "rename": return op("repo.rename", "Rename repository", "pencil")
        case "edit": return op("repo.edit", "Edit repository settings", "pencil")
        case "archive": return op("repo.archive", named("Archive repository"), "archivebox")
        case "unarchive": return op("repo.unarchive", named("Unarchive repository"), "archivebox")
        case "sync": return op("repo.sync", named("Sync repository"), "arrow.triangle.2.circlepath")
        default: return nil  // clone/view/list/set-default → reads or local (git covers clone)
        }
    }

    /// `gh api`: mutating when an explicit write method is given (`-X POST|PUT|PATCH|DELETE`) or a
    /// field flag forces a POST; a bare `gh api <endpoint>` is a GET (read) → `nil`.
    private static func api(_ a: [String]) -> ObservedGhOp? {
        let method = (ShellLexer.flagValue(a, short: "-X", long: "--method") ?? "").uppercased()
        let hasField = a.contains {
            $0 == "-f" || $0 == "--raw-field" || $0 == "-F" || $0 == "--field" || $0 == "--input"
        }
        let effective = method.isEmpty ? (hasField ? "POST" : "GET") : method
        guard ["POST", "PUT", "PATCH", "DELETE"].contains(effective) else { return nil }
        let endpoint = positionals(a).first
        let label = endpoint.map { "GitHub API \(effective) \($0)" } ?? "GitHub API \(effective)"
        return op("api", label, "network")
    }

    // MARK: - Helpers

    /// A noun whose verbs map 1:1 to a fixed label — mutating verbs only; anything else → `nil`.
    private static func table(
        _ noun: String, _ a: [String], symbol: String, verbs: [String: String]
    ) -> ObservedGhOp? {
        guard let verb = verb(a), let phrase = verbs[verb] else { return nil }
        return op("\(noun).\(verb)", phrase, symbol)
    }

    private static func op(
        _ kind: String, _ label: String, _ symbol: String, isMerge: Bool = false
    ) -> ObservedGhOp {
        ObservedGhOp(kind: kind, label: label, symbol: symbol, isMutating: true, isMerge: isMerge)
    }

    /// The verb — the first positional after the noun (`create`, `merge`, …).
    private static func verb(_ a: [String]) -> String? { positionals(a).first }

    /// "<phrase> #<n>" when a numeric operand (a PR/issue number) follows the verb, else "<phrase>".
    private static func numbered(_ phrase: String, _ a: [String]) -> String {
        let n = positionals(a).dropFirst().first { $0.allSatisfy(\.isNumber) && !$0.isEmpty }
        return n.map { "\(phrase) #\($0)" } ?? "\(phrase)"
    }

    private static func positionals(_ a: [String]) -> [String] {
        ShellLexer.positionals(a, valueFlags: valueFlags)
    }

    private static func clip(_ s: String) -> String { HeuristicTitle.clip(s, max: 60) }

    /// gh flags whose following token is a value (not an operand), so `positionals` finds the verb
    /// and key operand without swallowing a `--title`/`--body`/`--method` value.
    private static let valueFlags: Set<String> = [
        "-t", "--title", "-b", "--body", "-F", "--body-file", "--notes", "--notes-file",
        "-B", "--base", "-H", "--head", "-R", "--repo", "-X", "--method", "-f", "--raw-field",
        "--field", "--input", "-l", "--label", "-a", "--assignee", "-m", "--milestone",
        "-p", "--project", "--target", "--template",
    ]
}
