/**
 * Actor provenance -- who initiated a run.
 *
 * One shared account means every session, commit, and event otherwise shows up
 * as the same person. `resolveActor()` answers "which human is behind this run"
 * by looking at how the process was reached:
 *
 *   - Over SSH (the shared-fleet case): `tailscale whois` the SSH client IP to
 *     the connecting tailnet identity -- a real name + login email.
 *   - Locally (non-SSH): the run belongs to whoever owns this device on the
 *     tailnet, so we read that owner from `tailscale status` (`.Self.UserID` ->
 *     `.User[]`). Only if tailscale can't name the device owner either do we fall
 *     back to the honest `UNRESOLVED@<host>` with no personal git identity claimed.
 *   - Inherited: a child spawn trusts the `AGENTS_ACTOR*` env its parent
 *     stamped rather than re-resolving, so the whole spawn tree shares one actor.
 *
 * The resolved actor rides the child-process env (`actorEnv()`, merged into
 * `buildExecEnv`). When the actor is a resolved human, that env also carries
 * `GIT_AUTHOR_*` / `GIT_COMMITTER_*`, so the agent's own `git commit` is credited
 * to the person instead of the shared account.
 *
 * The optional `actors:` map in agents.yaml enriches or overrides a resolved
 * identity (pin a work email, add a github handle, mark an entry as an agent).
 */
import { spawnSync } from 'child_process';
import { machineId } from './machine-id.js';
import { parseSshConnection } from './session/provenance.js';
import { readMeta } from './state.js';
/** Hard cap on the whois shell-out — it runs on the spawn hot path. */
const WHOIS_TIMEOUT_MS = 2000;
/**
 * Resolve the tailnet identity behind an IP via `tailscale whois`. Returns
 * undefined when tailscale is absent, the peer is unknown, the call fails, or it
 * exceeds WHOIS_TIMEOUT_MS -- every one of those falls back to an unresolved
 * actor, never an error and never a hang (a wedged tailscaled is timed out, not
 * waited on, since this sits in buildExecEnv on every agent spawn).
 */
function tailscaleWhois(ip) {
    try {
        const res = spawnSync('tailscale', ['whois', '--json', ip], {
            encoding: 'utf-8',
            windowsHide: true,
            timeout: WHOIS_TIMEOUT_MS,
        });
        // A timeout leaves status null (child killed by SIGTERM) -- the status guard
        // below treats it as an unresolved actor, same as any other failure.
        if (res.status !== 0 || !res.stdout)
            return undefined;
        const data = JSON.parse(res.stdout);
        const up = data.UserProfile;
        if (!up)
            return undefined;
        return { login: up.LoginName, displayName: up.DisplayName };
    }
    catch {
        return undefined;
    }
}
/**
 * Resolve this device's own tailnet owner via `tailscale status --json`:
 * `.Self.UserID` indexes into the `.User` map for the owner's login + display
 * name. Used for a LOCAL run, where there is no SSH client to whois — the run
 * belongs to whoever owns the box on the tailnet. Same graceful-undefined +
 * timeout discipline as `tailscaleWhois`: tailscale absent, a wedged daemon, a
 * tagged (owner-less) device, or a parse failure all yield undefined, never an
 * error and never a hang on the spawn hot path.
 */
function tailscaleSelf() {
    try {
        const res = spawnSync('tailscale', ['status', '--json'], {
            encoding: 'utf-8',
            windowsHide: true,
            timeout: WHOIS_TIMEOUT_MS,
        });
        if (res.status !== 0 || !res.stdout)
            return undefined;
        const data = JSON.parse(res.stdout);
        const uid = data.Self?.UserID;
        if (uid == null)
            return undefined;
        // The User map is keyed by the UserID rendered as a string.
        const u = data.User?.[String(uid)];
        if (!u?.LoginName)
            return undefined;
        return { login: u.LoginName, displayName: u.DisplayName };
    }
    catch {
        return undefined;
    }
}
/** Read the actors map from config, tolerant of a missing/unreadable config. */
function readActors() {
    try {
        return readMeta().actors ?? {};
    }
    catch {
        return {};
    }
}
/**
 * Find the actors-map entry for a resolved tailnet login. Matches on an entry's
 * explicit `login`, its map key, or its `email` (all case-insensitive).
 */
function findActorConfig(login, actors) {
    const needle = login.toLowerCase();
    for (const [key, cfg] of Object.entries(actors)) {
        const candidates = [cfg.login, key, cfg.email].filter((v) => !!v);
        if (candidates.some((c) => c.toLowerCase() === needle))
            return cfg;
    }
    return undefined;
}
/**
 * Map a resolved tailnet identity (+ the actors map) to a ResolvedActor. Pure --
 * the impure whois/config reads happen in computeActor -- so the enrich/override
 * path is fully testable. No login (local, or an unnameable peer) yields the
 * honest `UNRESOLVED@<host>`; a login without a config entry still credits git
 * from the tailnet DisplayName + login email.
 */
export function actorFromIdentity(who, host, actors) {
    const login = who?.login;
    if (!login) {
        return { id: `UNRESOLVED@${host}`, kind: 'human' };
    }
    const cfg = findActorConfig(login, actors);
    const emailFromLogin = login.includes('@') ? login : undefined;
    return {
        id: login,
        kind: cfg?.kind ?? 'human',
        name: cfg?.name ?? who?.displayName,
        email: cfg?.email ?? emailFromLogin,
        github: cfg?.github,
        phoenixId: cfg?.phoenixId,
    };
}
/** Reconstruct an actor an ancestor process already resolved into the env. */
function inheritedActor(env) {
    const id = env.AGENTS_ACTOR;
    if (!id)
        return undefined;
    return {
        id,
        kind: env.AGENTS_ACTOR_KIND === 'agent' ? 'agent' : 'human',
        name: env.AGENTS_ACTOR_NAME || undefined,
        email: env.AGENTS_ACTOR_EMAIL || undefined,
        github: env.AGENTS_ACTOR_GITHUB || undefined,
        phoenixId: env.AGENTS_ACTOR_PHOENIX_ID || undefined,
    };
}
const defaultResolvers = { whois: tailscaleWhois, self: tailscaleSelf };
/**
 * Compute the actor for a given environment. The only impurity is the tailscale
 * shell-out (injectable via `resolvers`), so tests drive every branch explicitly.
 *
 * Resolution order: an inherited env actor wins; otherwise an SSH run whois-es
 * its client IP; a local run (no SSH) credits the device's own tailnet owner;
 * and anything unresolvable degrades to `UNRESOLVED@<host>`. Note the self
 * fallback fires ONLY for a truly local run — an SSH run whose whois fails must
 * NOT be credited to the box owner (that would misattribute a remote human to
 * whoever owns the machine).
 */
export function computeActor(env = process.env, resolvers = defaultResolvers) {
    const inherited = inheritedActor(env);
    if (inherited)
        return inherited;
    const sshRaw = env.SSH_CONNECTION;
    const ssh = sshRaw ? parseSshConnection(sshRaw) : undefined;
    let who = ssh?.clientIp ? resolvers.whois(ssh.clientIp) : undefined;
    // Self-credit only for a genuinely LOCAL run (no SSH_CONNECTION at all). An
    // SSH session whose connection is unparseable or unresolvable stays
    // UNRESOLVED rather than being misattributed to the box's owner.
    if (!who && !sshRaw)
        who = resolvers.self();
    return actorFromIdentity(who, machineId(), readActors());
}
let cached;
let resolverOverride;
/**
 * Resolve the actor for the current process, cached for the process lifetime
 * (the SSH `whois` shell-out runs at most once).
 */
export function resolveActor() {
    if (!cached)
        cached = computeActor(process.env, resolverOverride ?? defaultResolvers);
    return cached;
}
/**
 * Test-only: pin the tailscale resolvers `resolveActor()` uses, so a test that
 * exercises the cached production entrypoint (e.g. `withActorEnv()`) is isolated
 * from whether the box running it is on the tailnet. `computeActor` already takes
 * injected resolvers for its unit tests; this extends the same seam to the cached
 * path. Pass `undefined` to restore the real tailscale resolvers. Resets the
 * cache so the next `resolveActor()` recomputes under the new resolvers.
 */
export function setActorResolvers(resolvers) {
    resolverOverride = resolvers;
    cached = undefined;
}
/** Clear the per-process cache. For tests, and for env changes within a run. */
export function resetActorCache() {
    cached = undefined;
}
/**
 * The env an actor propagates to child processes. Always carries the actor id +
 * kind (so children inherit and don't re-resolve). For a resolved human with a
 * real name and email, it also carries `GIT_AUTHOR_*` / `GIT_COMMITTER_*` so the
 * agent's own commits are credited to the person, not the shared account. An
 * unresolved actor sets no git identity -- git keeps its ambient config.
 */
export function actorEnv(actor) {
    const env = {
        AGENTS_ACTOR: actor.id,
        AGENTS_ACTOR_KIND: actor.kind,
    };
    if (actor.name)
        env.AGENTS_ACTOR_NAME = actor.name;
    if (actor.email)
        env.AGENTS_ACTOR_EMAIL = actor.email;
    if (actor.github)
        env.AGENTS_ACTOR_GITHUB = actor.github;
    if (actor.phoenixId)
        env.AGENTS_ACTOR_PHOENIX_ID = actor.phoenixId;
    if (actor.kind === 'human' && actor.name && actor.email) {
        env.GIT_AUTHOR_NAME = actor.name;
        env.GIT_AUTHOR_EMAIL = actor.email;
        env.GIT_COMMITTER_NAME = actor.name;
        env.GIT_COMMITTER_EMAIL = actor.email;
    }
    return env;
}
