import Foundation

/// The command-agnostic half of parsing a Bash command for the approval UI: splitting a
/// command line into segments, tokenizing a segment with just enough shell-awareness to read
/// a subcommand/flags/operands, and pulling heredoc bodies out of the way. Shared by
/// ``GitCommandSummary`` (git ops) and ``ShellCommandSummary`` (the general `rm`/`git`
/// pipeline view) so there's one tokenizer, not two that drift.
enum ShellLexer {
    /// Removes heredoc bodies from a command, returning the command with each heredoc region
    /// (the `<<'EOF'` operator through its closing delimiter) elided, plus the captured body
    /// text of each heredoc in order. This both keeps a heredoc body from being parsed as
    /// shell and recovers a commit message piped in via `git commit -F -`.
    ///
    /// Handles the common forms: `<<EOF`, `<<'EOF'`, `<<"EOF"`, and the indented `<<-EOF`
    /// (whose closing delimiter may be tab-indented).
    static func stripHeredocs(_ command: String) -> (flattened: String, bodies: [String]) {
        let lines = command.components(separatedBy: "\n")
        var kept: [String] = []
        var bodies: [String] = []
        var index = 0
        while index < lines.count {
            let line = lines[index]
            guard let here = heredocStart(in: line) else {
                kept.append(line)
                index += 1
                continue
            }
            // Keep the operator line minus the `<<…DELIM` operator, so a `git commit … <<'EOF'`
            // still parses as a commit segment.
            kept.append(here.lineWithoutOperator)
            // Consume body lines up to the closing delimiter.
            var body: [String] = []
            index += 1
            while index < lines.count {
                let candidate = here.dashIndented
                    ? lines[index].trimmingCharacters(in: CharacterSet(charactersIn: " \t"))
                    : lines[index]
                if candidate == here.delimiter { index += 1; break }
                body.append(lines[index])
                index += 1
            }
            bodies.append(body.joined(separator: "\n"))
        }
        return (kept.joined(separator: "\n"), bodies)
    }

    /// A heredoc operator found on a line: its delimiter, whether it's the `<<-` indented
    /// form, and the line with the `<<…DELIM` operator removed.
    private struct Heredoc {
        let delimiter: String
        let dashIndented: Bool
        let lineWithoutOperator: String
    }

    /// Detects a heredoc redirection (`<<EOF`, `<<-'EOF'`, …) in a line and extracts its
    /// delimiter, or nil when the line opens no heredoc.
    private static func heredocStart(in line: String) -> Heredoc? {
        guard let opRange = line.range(of: #"<<-?\s*(['"]?)[A-Za-z_][A-Za-z0-9_]*\1"#,
                                       options: .regularExpression) else { return nil }
        let op = String(line[opRange])
        let dashIndented = op.hasPrefix("<<-")
        // Strip the leading `<<` / `<<-` and any quotes to recover the bare delimiter word.
        let delimiter = op
            .replacingOccurrences(of: "<<-", with: "")
            .replacingOccurrences(of: "<<", with: "")
            .trimmingCharacters(in: CharacterSet(charactersIn: " \t'\""))
        let without = line.replacingCharacters(in: opRange, with: " ")
        return Heredoc(delimiter: delimiter, dashIndented: dashIndented, lineWithoutOperator: without)
    }

    /// Splits a (heredoc-stripped) command into individual command segments on the shell
    /// separators that chain commands — `&&`, `||`, `;`, `|`, and newlines — so each
    /// invocation in a pipeline is parsed on its own. Separators inside quotes are left alone.
    static func splitSegments(_ command: String) -> [String] {
        var segments: [String] = []
        var current = ""
        var quote: Character? = nil
        let chars = Array(command)
        var i = 0
        func flush() {
            let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
            if !trimmed.isEmpty { segments.append(trimmed) }
            current = ""
        }
        while i < chars.count {
            let c = chars[i]
            if let q = quote {
                current.append(c)
                if c == q { quote = nil }
                i += 1
                continue
            }
            switch c {
            case "'", "\"":
                quote = c; current.append(c); i += 1
            case "\n", ";":
                flush(); i += 1
            case "&" where i + 1 < chars.count && chars[i + 1] == "&":
                flush(); i += 2
            case "|" where i + 1 < chars.count && chars[i + 1] == "|":
                flush(); i += 2
            case "|":
                flush(); i += 1
            default:
                current.append(c); i += 1
            }
        }
        flush()
        return segments
    }

    /// Splits one command segment into tokens, honoring single and double quotes (the quotes
    /// themselves are dropped). Enough shell-awareness to read a subcommand, flags, and a
    /// quoted commit message without a full parser.
    static func tokenize(_ segment: String) -> [String] {
        var tokens: [String] = []
        var current = ""
        var quote: Character? = nil
        var sawChar = false
        for c in segment {
            if let q = quote {
                if c == q { quote = nil } else { current.append(c) }
                continue
            }
            switch c {
            case "'", "\"":
                quote = c; sawChar = true
            case " ", "\t":
                if sawChar { tokens.append(current); current = ""; sawChar = false }
            default:
                current.append(c); sawChar = true
            }
        }
        if sawChar { tokens.append(current) }
        return tokens
    }

    /// Drops a leading `sudo` and any `VAR=value` environment assignments from a tokenized
    /// segment, so the real command (`git`, `rm`, …) lands at the front. `sudo foo=1 git …`
    /// → `git …`.
    static func stripLeadingPrefixes(_ tokens: [String]) -> [String] {
        var rest = tokens[...]
        while let first = rest.first, first == "sudo" || isEnvAssignment(first) {
            rest = rest.dropFirst()
        }
        return Array(rest)
    }

    /// Whether a token is a `NAME=value` shell environment assignment (so it can be skipped
    /// when looking for the command word).
    static func isEnvAssignment(_ token: String) -> Bool {
        guard let eq = token.firstIndex(of: "="), eq != token.startIndex else { return false }
        return token[..<eq].allSatisfy { $0.isLetter || $0.isNumber || $0 == "_" }
    }

    /// The value of a flag given either as `--flag value` / `-f value` or `--flag=value`.
    static func flagValue(_ args: [String], short: String, long: String) -> String? {
        for (i, a) in args.enumerated() {
            if a == short || a == long, i + 1 < args.count { return args[i + 1] }
            for prefix in ["\(long)=", "\(short)="] where a.hasPrefix(prefix) {
                return String(a.dropFirst(prefix.count))
            }
        }
        return nil
    }

    /// The positional operands of a command: tokens that aren't flags and aren't the value
    /// consumed by a value-taking flag (`valueFlags`). A bare `--` ends option parsing, so
    /// everything after it counts as a positional even when it starts with `-`. Good enough to
    /// recover a branch, remote, or path operand.
    static func positionals(_ args: [String], valueFlags: Set<String>) -> [String] {
        var out: [String] = []
        var skipNext = false
        var endOfOptions = false
        for a in args {
            if skipNext { skipNext = false; continue }
            if endOfOptions { out.append(a); continue }
            if a == "--" { endOfOptions = true; continue }
            if a.hasPrefix("-") {
                if valueFlags.contains(a) { skipNext = true }
                continue
            }
            out.append(a)
        }
        return out
    }
}
