import Foundation

/// Programmatic resistance for `host_exec` — the code-enforced companion to the system-prompt
/// steering in ``SessionController/sandboxBuildGuidance(allowHostExec:)``. A prompt only *asks*
/// the agent to keep work in the container; this classifier *decides*, deterministically, from
/// the command string alone.
///
/// `host_exec` escapes the Linux sandbox onto the shared macOS host and exists only for the
/// macOS-only toolchain the container genuinely cannot run (Swift/Xcode). Every other use is
/// either better done in the container or worth a fresh human look. The gate (in
/// `ClaudeCodeBackend.handleHostExecCall`) uses this to sort a command into three buckets:
///
///   - ``Decision/hostOnly``            — a genuine macOS-only tool is in the pipeline; proceed
///                                         through the normal approval (and "Allow for session"
///                                         may blanket it).
///   - ``Decision/readOnly``            — a read-only inspection utility (`cat`, `find`, `grep`, …)
///                                         that can look at, but not mutate, the host; **allowed**
///                                         through the normal approval, same as ``hostOnly``.
///   - ``Decision/containerRunnable``   — a program that definitely runs in the container; **deny**
///                                         with guidance to run it there, before any approval.
///   - ``Decision/ambiguous``           — nothing proves it needs the host (`make`, `bash -c "…"`,
///                                         a bare script path); **force a fresh approval prompt**
///                                         every time, so "Allow for session" can never wave it
///                                         through unseen.
///   - ``Decision/secretFileBlocked``   — the command reads a `.env` secrets file; **hard-denied**,
///                                         unconditionally, before anything else — it never reaches
///                                         an approval prompt and no session grant can wave it past.
///
/// The classification reuses ``HostCommandSummary`` (the same tested parser the approval UI uses)
/// so it sees the *real* programs a pipeline runs — heredoc-, `sudo`-, and `cd`-aware — not a
/// naive first token.
public enum HostExecPolicy {
    public enum Decision: Equatable, Sendable {
        /// A macOS-only toolchain command — the one legitimate reason to escape the sandbox.
        case hostOnly
        /// A read-only inspection utility (`cat`, `find`, `grep`, …) — permitted on the host, since
        /// it can look at but not mutate the machine. The named program is the recognized utility.
        case readOnly(program: String)
        /// A command that definitely runs inside the container; the named program is the offender.
        case containerRunnable(program: String)
        /// Can't be shown to need the host — force a fresh human decision rather than block or allow.
        case ambiguous
        /// The command reads a `.env` secrets file — forbidden outright, no matter what else it does.
        case secretFileBlocked
    }

    /// The macOS-only toolchain: Swift compilation, Xcode build/sign/package, the simulator. These
    /// are absent from the Linux container (see containers/nucleic-sandbox/Dockerfile), so they are
    /// the genuine reason `host_exec` exists.
    static let hostOnlyPrograms: Set<String> = [
        "swift", "swiftc", "xcodebuild", "xcrun", "xcode-select", "xcodegen", "tuist",
        "codesign", "notarytool", "altool", "stapler", "spctl", "actool", "ibtool",
        "momc", "mapc", "swiftformat", "swiftlint", "instruments", "simctl",
        "xcresulttool", "dsymutil", "lipo", "otool", "install_name_tool",
        "pkgbuild", "productbuild", "agvtool",
    ]

    /// Programs that plainly run inside the sandbox: the container's own language toolchains (Node,
    /// Python, Ruby, Go, Rust) and the VCS shims. There is no reason to run these on the host, so a
    /// command whose only recognized program is one of these is denied with a pointer back to the
    /// container. `git`/`gh` are here for a consistent message; they are already hard-blocked by the
    /// `block-host-shims.mjs` PreToolUse shield before they reach this gate.
    static let containerRunnablePrograms: Set<String> = [
        "node", "nodejs", "npm", "npx", "pnpm", "yarn", "bun", "deno",
        "python", "python2", "python3", "pip", "pip3", "pipx", "poetry", "pytest", "tox",
        "ruby", "gem", "bundle", "rake", "php", "composer",
        "go", "cargo", "rustc", "rustup",
        "git", "gh",
    ]

    /// Read-only inspection utilities that are *permitted* on the host. These only look at the
    /// machine — list, read, search, slice, and compare files — they can't mutate it, so letting the
    /// agent run them (with the usual approval) is safe and often genuinely useful for inspecting
    /// host-only state the container can't see. A command whose recognized programs are all in this
    /// set classifies as ``Decision/readOnly`` and goes through the normal approval, not a denial.
    /// (Reading a `.env` secrets file is still blocked outright — see ``referencesDotenvFile(_:)`` —
    /// regardless of which of these tools would do the reading.)
    static let readOnlyPrograms: Set<String> = [
        "ls", "cat", "bat", "grep", "rg", "egrep", "fgrep", "find", "fd",
        "head", "tail", "wc", "sort", "uniq", "cut", "tr", "nl", "od", "xxd", "strings",
        "echo", "printf", "jq", "yq", "tree", "diff", "stat", "file", "du", "df",
        "pwd", "which", "whoami", "date", "printenv", "hostname", "uname", "less", "more",
    ]

    /// Classify a `host_exec` command from its text alone.
    public static func classify(_ command: String) -> Decision {
        // Absolute, and checked first: reading a `.env` secrets file is forbidden outright, ahead of
        // every other consideration — so even `cat .env` piped through a genuine host tool, or a
        // `.env` read hidden inside an otherwise-approvable command, is denied and never prompted.
        if referencesDotenvFile(command) { return .secretFileBlocked }

        guard let summary = HostCommandSummary.summary(for: command) else { return .ambiguous }
        let programs = summary.invocations.map { $0.program.lowercased() }
        guard !programs.isEmpty else { return .ambiguous }

        // A genuine host-only tool anywhere in the pipeline justifies the host: an incidental
        // `| grep`, `| head`, or `| tee` after `xcrun`/`xcodebuild` doesn't demote a real build.
        if programs.contains(where: hostOnlyPrograms.contains) { return .hostOnly }

        // No host-only tool at all, but a program we know runs in the container → deny with guidance.
        if let offender = programs.first(where: containerRunnablePrograms.contains) {
            return .containerRunnable(program: offender)
        }

        // No container toolchain either, but a recognized read-only inspection utility → allow it
        // on the host (through the normal approval), rather than forcing it back to the container.
        if let tool = programs.first(where: readOnlyPrograms.contains) {
            return .readOnly(program: tool)
        }

        // Nothing recognized either way — `make` (might invoke swift, might not), `bash -c "…"`,
        // a bare `./script.sh`. Can't prove it needs the host, so force a fresh human decision.
        return .ambiguous
    }

    /// Whether the command text references a `.env` secrets file — the dotenv family (`.env`,
    /// `.env.local`, `.env.production`, …), at any path (`.env`, `./config/.env`, `/app/.env`).
    /// Detection is a deterministic scan of the raw command for a `.env` filename token: a `.env`
    /// that starts a filename (preceded by start-of-string, whitespace, `/`, a quote, `=`, or a
    /// shell operator) and isn't part of a longer name — so `cat .env`, `grep X ./config/.env`, and
    /// `head /app/.env.local` all match, while `.environment`, `.envrc`, and `foo.env` (basenames
    /// that merely contain the letters) do not.
    ///
    /// This is a deterministic guardrail, not a sandbox: an obfuscated read (variable expansion,
    /// `$(…)`, base64, a copy-then-read) can still slip past, exactly like every other text-level
    /// check on this gate. It reliably stops the direct, common forms of reading a `.env`.
    static func referencesDotenvFile(_ command: String) -> Bool {
        guard let regex = dotenvReference else { return false }
        let range = NSRange(command.startIndex..., in: command)
        return regex.firstMatch(in: command, range: range) != nil
    }

    /// Matches a `.env` filename token: `\.env` not preceded by a name char (so it's the *start* of
    /// a hidden filename, not the `.env` tail of `foo.env`) and not followed by a name char other
    /// than `.` (so `.env` and `.env.local` match but `.environment`/`.envrc` don't). Case-
    /// insensitive to also catch `.ENV`. A nil regex (never, for this static pattern) fails open.
    private static let dotenvReference = try? NSRegularExpression(
        pattern: "(?<![A-Za-z0-9_.\\-])\\.env(?![A-Za-z0-9_\\-])",
        options: [.caseInsensitive])

    /// The denial returned when a command tries to read a `.env` secrets file. This is a hard,
    /// unconditional block — not friction to retry past — so the message says so plainly.
    public static func secretFileBlockedDenial() -> String {
        "Blocked host_exec: reading `.env` files is not permitted. `.env` files hold secrets "
            + "(API keys, tokens, credentials), so host_exec refuses to read them — there is no "
            + "reason and no approval that lifts this. Remove the `.env` reference; if you need a "
            + "specific non-secret value, obtain it another way."
    }

    /// The denial returned when a clearly-container-runnable command is attempted on the host.
    public static func containerRunnableDenial(program: String) -> String {
        "Blocked host_exec: `\(program)` runs inside the sandbox container — run it there, not on "
            + "the host. host_exec is a last-resort escape hatch reserved for the macOS-only "
            + "toolchain (Swift/Xcode: swift, xcodebuild, xcrun, codesign, …) that the container "
            + "genuinely cannot run. If only part of this command needs the host, split that part out."
    }

    /// Whether the agent's justification is substantive enough to accept. Empty, whitespace, a few
    /// obvious placeholders, or anything too short is rejected — the field exists to make the agent
    /// state a real reason the container can't do the work, not to be rubber-stamped. Kept lenient
    /// (a short genuine reason passes) because a rejected call self-corrects: the agent simply
    /// retries with a better reason, guided by ``missingReasonMessage()``.
    public static func isAcceptableReason(_ reason: String) -> Bool {
        let trimmed = reason.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.count < 10 { return false }
        let placeholders: Set<String> = [
            "n/a", "na", "none", "null", "test", "testing", "reason", "because",
            "host exec", "host_exec", "needed", "required", "build it",
        ]
        return !placeholders.contains(trimmed.lowercased())
    }

    /// The denial returned when `reason` is missing or too vague.
    public static func missingReasonMessage() -> String {
        "host_exec requires a `reason` argument stating specifically why this command cannot run "
            + "inside the sandbox container (e.g. \"builds a SwiftUI app that only compiles with "
            + "Xcode on macOS\"). The reason given was empty or too vague. host_exec is the right "
            + "tool for the macOS-only toolchain; retry with a specific reason. If the work can run "
            + "in the container, run it there instead."
    }
}

/// The on-device model's cross-check of a host_exec `reason` against the actual `command`: does
/// the stated justification honestly and accurately explain THIS command's need for the macOS
/// host? Surfaced as a trust indicator beside the agent's reason so a deceptive reason — one that
/// describes different work than the command performs — can't slip past the user.
///
/// The verdict is produced by a SEPARATE local model from the raw command + reason, never by the
/// agent, so the agent cannot fabricate its own trust badge. Absent a model it is ``notChecked``
/// (fail toward caution: shown as "not verified", never as trusted). This mirrors how
/// ``HostCommandSummary`` gives the user a command-derived breakdown to check the agent against —
/// but where that is deterministic parsing, this is a semantic judgement only a model can make.
public struct HostExecReasonAudit: Sendable, Equatable, Codable {
    public enum Trust: String, Sendable, Codable {
        /// The reason accurately describes the command and it genuinely needs the host.
        case consistent
        /// Partly accurate, or too vague to fully support running on the host.
        case partial
        /// The reason describes different work than the command does, or claims host-necessity for
        /// a container-runnable command — the agent may be deceiving the user.
        case contradictory
        /// The model couldn't tell from the given text.
        case unclear
        /// No on-device model was available/enabled to check — shown neutrally as "not verified".
        case notChecked
    }

    public let trust: Trust
    /// A short, model-produced line of why (fixed text for ``notChecked``). Shown under the reason.
    public let rationale: String

    public init(trust: Trust, rationale: String) {
        self.trust = trust
        self.rationale = rationale
    }

    /// The neutral verdict when no model backed the check.
    public static let notChecked = HostExecReasonAudit(
        trust: .notChecked,
        rationale: "No on-device model was available to verify this reason.")

    /// Map a model verdict token (from guided generation) to a ``Trust`` level. Defensive: an
    /// unrecognized token becomes ``Trust/unclear``, never a silent ``Trust/consistent`` — a
    /// confused or manipulated model can't mint trust.
    public static func trust(fromModelToken raw: String) -> Trust {
        switch raw.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() {
        case "CONSISTENT": return .consistent
        case "PARTIAL": return .partial
        case "CONTRADICTORY": return .contradictory
        default: return .unclear
        }
    }
}
