import Foundation

/// Recovers the user's *login-shell* *environment delta* — the non-`PATH` exported state their rc
/// files set up — so a host spawn under a non-zsh shell keeps working (docs/NASH.md §7.7.2).
///
/// **Why this exists.** `zsh -lc` re-sources the user's login files on every host command, so
/// Homebrew/toolchain variables (`HOMEBREW_PREFIX`, `NVM_DIR`, `rbenv`/`asdf` state, `ANDROID_HOME`,
/// `JAVA_HOME`, …) are simply *there*. nash's `-l` reads `/etc/profile` + `~/.profile`, which on a
/// typical Mac don't exist — so flipping the host argv to `nash -c` would silently drop all of that
/// and commands would fail `command not found` (the PATH half is already solved by
/// ``LoginShellPATH/augment(_:)``; this type is the other half). The generalization also improves the
/// status quo: with the snapshot injected, host spawns are deterministic and skip per-command
/// rc-sourcing latency.
///
/// **Shape.** The exact ``LoginShellPATH`` recipe, generalized: run `$SHELL -ilc 'env -0'` ONCE per
/// app launch (memoized, sentinel-fenced, bounded wait, injectable runner for tests), split NUL-
/// delimited entries, DIFF against the app's own environment, and keep only the delta — filtered of
/// `PATH` (already handled), shell-internal noise, and credential-shaped names. ``inject(into:)``
/// merges the delta *beneath* an existing environment: a key already present (the app's own env, an
/// explicit `ProcessSpec.env` value, the Managed Git pins) always wins.
enum LoginShellEnv {
    typealias ShellRunner = LoginShellPATH.ShellRunner

    /// Sentinels fencing the `env -0` payload off from any banner/noise an rc file prints.
    private static let begin = "___NUCLEIC_LOGIN_ENV_BEGIN___"
    private static let end = "___NUCLEIC_LOGIN_ENV_END___"

    /// Keys the delta must never carry: `PATH` rides ``LoginShellPATH``; the rest is shell/session
    /// plumbing that describes the *probe* shell, not the user's toolchain intent.
    static let excludedKeys: Set<String> = [
        "PATH", "SHLVL", "PWD", "OLDPWD", "_", "SHELL", "HOME", "USER", "LOGNAME", "TMPDIR",
        "TERM", "TERM_PROGRAM", "TERM_PROGRAM_VERSION", "TERM_SESSION_ID",
        "SECURITYSESSIONID", "XPC_SERVICE_NAME", "XPC_FLAGS",
    ]

    /// Key prefixes dropped wholesale: zsh internals and per-process Apple session plumbing.
    static let excludedPrefixes = ["ZSH", "__CF", "__ETC", "LC_TERMINAL"]

    /// Credential-shaped names never forwarded from rc files into agent-visible spawns — the same
    /// posture as `ProcessSpec.removeFromEnvironment` at the spawn sites (`ClaudeCodeBackend`), kept
    /// here too so the snapshot cannot re-introduce what a spec asked to drop.
    static let credentialKeys: Set<String> = [
        "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY", "AWS_SECRET_ACCESS_KEY",
        "AWS_SESSION_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "NPM_TOKEN",
    ]

    // MARK: - Public surface

    /// The filtered login-shell env delta, resolved once per process and memoized. Empty when the
    /// probe fails (spawn error, timeout, sentinels absent) — injection then changes nothing.
    static let cached: [String: String] =
        delta(
            snapshot: resolve() ?? [:],
            appEnvironment: ProcessInfo.processInfo.environment)

    /// Whether host spawns inject the snapshot (`nucleic.host.loginEnvSnapshot`, NASH.md §7.7.2 /
    /// §9.6) — its own lever, shippable and revertible independently of the host shell flip.
    static var injectionEnabled: Bool {
        ContainerServiceSettings.defaults.bool(forKey: loginEnvSnapshotKey)
    }
    static let loginEnvSnapshotKey = "nucleic.host.loginEnvSnapshot"

    /// Merge the memoized delta beneath `environment`: only keys `environment` lacks are added, so
    /// an explicit spec value / app-env value / Managed Git pin always wins. No-op when the setting
    /// is off or the probe failed.
    static func inject(into environment: [String: String]) -> [String: String] {
        guard injectionEnabled else { return environment }
        return merged(delta: cached, into: environment)
    }

    // MARK: - Pure pieces (unit-tested directly)

    /// The snapshot minus the app's own environment (identical values carry no signal), minus the
    /// exclusions: `PATH`/shell noise/Apple session plumbing/credential-shaped names.
    static func delta(
        snapshot: [String: String], appEnvironment: [String: String]
    ) -> [String: String] {
        snapshot.filter { key, value in
            guard appEnvironment[key] != value else { return false }
            return !isExcluded(key)
        }
    }

    static func isExcluded(_ key: String) -> Bool {
        if excludedKeys.contains(key) || credentialKeys.contains(key) { return true }
        return excludedPrefixes.contains { key.hasPrefix($0) }
    }

    /// Underlay merge: `environment` wins on every collision.
    static func merged(
        delta: [String: String], into environment: [String: String]
    ) -> [String: String] {
        var env = environment
        for (key, value) in delta where env[key] == nil { env[key] = value }
        return env
    }

    // MARK: - Resolution

    /// Run the user's login+interactive shell once and capture its exported environment, NUL-
    /// delimited between sentinels (`env -0` — values may contain newlines; NUL is unambiguous).
    /// `nil` on any failure; the caller degrades to no injection.
    static func resolve(
        shell: String? = ProcessInfo.processInfo.environment["SHELL"],
        runner: ShellRunner = LoginShellPATH.defaultRunner
    ) -> [String: String]? {
        #if os(Windows)
        // Nothing to recover: a Windows process is launched with the merged machine + user
        // environment already applied, so the "delta an rc file would have exported" is empty by
        // construction (docs/WINDOWS_PORT.md §4.3). The PATH half — which *can* go stale against
        // the registry — is handled by ``LoginShellPATH``.
        _ = shell
        _ = runner
        return nil
        #else
        let shellPath = (shell?.isEmpty == false ? shell! : "/bin/zsh")
        let command =
            "printf '%s' '\(begin)'; /usr/bin/env -0; printf '%s' '\(end)'"
        guard let result = runner(shellPath, ["-ilc", command]), result.status == 0 else {
            return nil
        }
        return parse(output: result.stdout)
        #endif
    }

    /// Extract and split the fenced `env -0` payload. Entries without `=` are ignored; on a
    /// duplicate key the last entry wins (matching `env` semantics).
    static func parse(output: String) -> [String: String]? {
        guard let b = output.range(of: begin),
            let e = output.range(of: end, range: b.upperBound..<output.endIndex)
        else { return nil }
        var env: [String: String] = [:]
        for entry in output[b.upperBound..<e.lowerBound].split(separator: "\0") {
            guard let eq = entry.firstIndex(of: "=") else { continue }
            env[String(entry[entry.startIndex..<eq])] = String(entry[entry.index(after: eq)...])
        }
        return env.isEmpty ? nil : env
    }
}
