"""Per-workspace Claude Code config isolation (OBS-10).

A bridge claude_code run launched with the host's environment inherits the
user's personal ~/.claude — global CLAUDE.md, auto-memory, settings — so the
engine read the host's dev-assistant context instead of behaving as an
Atashi teammate (observed live 2026-06-11: a run quoted the host's memory
files and named itself after the underlying model).

The fix mirrors codex's throwaway CODEX_HOME, but persistent and inside the
consent boundary: every granted workspace gets an Atashi-owned config home
at <workspace>/.atashi/claude, and runs launch with CLAUDE_CONFIG_DIR
pointing at it. The .atashi folder is where all Atashi state for that
workspace lives; the seeded CLAUDE.md is the small "Atashi setup" memory
that leads the engine there. Subscription auth keeps working because the
host's .credentials.json is symlinked in (CLAUDE_CONFIG_DIR relocates
credential lookup too — a fresh empty home would be "Not logged in").
"""

from __future__ import annotations

import json
import logging
import os
from pathlib import Path
from typing import Optional

from atashi_bridge.identity import render_identity_block, upsert_identity_section

logger = logging.getLogger(__name__)

_ATASHI_SETUP_MEMORY = """\
# Atashi setup

This Claude Code run is operating as an Atashi teammate inside a workspace
the user granted to Atashi. Your persona and instructions arrive in the run
prompt from the Atashi platform — follow them.

- Everything Atashi related for this workspace lives in the `.atashi/`
  folder at the workspace root. Treat it as Atashi's home; keep any working
  notes or state you need under it, and leave the user's own files to the
  task at hand.
- This run is deliberately isolated from the host machine's personal Claude
  configuration and memory. Do not seek out or rely on personal context
  outside the granted workspace.
"""


def _host_config_home() -> Path:
    """The host user's real Claude config home (where their login lives)."""
    explicit = os.environ.get("CLAUDE_CONFIG_DIR", "").strip()
    if explicit:
        return Path(explicit)
    return Path.home() / ".claude"


def write_agent_identity(
    config_home: str,
    agent_identity: Optional[dict],
    filename: str = "CLAUDE.md",
) -> bool:
    """L7 — write the agent's identity into an engine's OWN native file.

    Deliberately SEPARATE from ensure_claude_config_home rather than an extra
    argument to it. Two reasons, and the first is the load-bearing one:

    1. `ensure_claude_config_home(workspace)` is an established contract with
       existing callers and test doubles. Widening its arity to carry identity
       broke a real test double that stubs it with one parameter — and the
       fallback path only catches OSError, so the TypeError would have escaped
       as a run failure. Adding a capability must not change an existing
       signature out from under its callers.
    2. Provisioning the config home and naming the agent are different jobs
       with different failure consequences. A failed identity write should not
       cost the user their isolated config home.

    `filename` is the engine's own native identity file, relative to that
    engine's config home: CLAUDE.md for claude_code (the default, so existing
    callers are untouched), AGENTS.md for codex. Measured 2026-08-13 with a
    codeword probe AND a negative control — codex reads AGENTS.md out of
    CODEX_HOME, which runner.py already provisions.

    **antigravity/agy is absent because it is UNRESOLVED, not because it is
    settled.** ⚠ Scope of what was actually measured: GEMINI.md and AGENTS.md
    were tested **in the working directory only**, both negative. That does NOT
    establish that agy reads no identity file — claude_code and codex both read
    theirs from a CONFIG HOME, not from cwd, and a global
    `~/.gemini/GEMINI.md` exists on this box (16 KB, an identity/context
    document) that was never part of that test. The read-probe could not be
    completed on 2026-08-14 because agy was signed out (OAuth prompt, timed
    out). So agy stays on the per-turn system prompt for now, and this comment
    must NOT be read as "agy cannot have an identity file".

    I first wrote this paragraph as "it reads NO identity file at all". That was
    an overclaim from a cwd-scoped test, corrected here before merge. It is the
    same shape as the false `gemini -> GEMINI.md` claim this branch stack
    removes: a conclusion wider than its measurement.

    agent_identity is {"name": ..., "role": ...}, where `role` is the agent's
    own PERSONA (the user-set `custom_role`, e.g. "Engineering lead") and is
    OPTIONAL — many agents have none.

    ⚠ DO NOT confuse this with the PROJECT role. They are different facts with
    different scopes and both are correct where they are:
      * persona (here, agent-level)  -> "You are the user's Engineering lead."
      * project role bundle (cloud)  -> promptAssembler._build_project_role_section,
        "For this project you hold the role of X", resolved per project.
    I briefly concluded these were two owners of one fact and nearly stripped
    the persona out of this file to "fix" a contradiction that does not exist.
    Recorded so the next reader does not repeat it.

    Returns True when a block was
    written. The span is REPLACED, never appended, and everything outside the
    markers is left exactly as the user wrote it — which is what keeps
    ensure_claude_config_home's "a customised CLAUDE.md is never overwritten"
    promise true.
    """
    if not agent_identity:
        return False
    try:
        block = render_identity_block(
            name=agent_identity.get("name"),
            role=agent_identity.get("role"),
        )
        upsert_identity_section(Path(config_home) / filename, block)
        return True
    except (ValueError, OSError) as exc:
        # A nameless, oversized or unwritable identity is not worth failing a
        # run over — the system prompt still carries the persona. Log loudly so
        # a dead identity channel cannot pass for a working one.
        logger.warning("isolation: skipping identity block: %s", exc)
        return False


def ensure_claude_config_home(workspace_dir: str) -> str:
    """Provision <workspace>/.atashi/claude and return its path.

    Idempotent. Existing files are respected: a customised CLAUDE.md is never
    overwritten, an existing .claude.json only gains the trust entry for this
    workspace, and a real (non-symlink) credentials file is left alone.
    """
    workspace = Path(workspace_dir)
    home = workspace / ".atashi" / "claude"
    home.mkdir(parents=True, exist_ok=True)

    claude_md = home / "CLAUDE.md"
    if not claude_md.exists():
        claude_md.write_text(_ATASHI_SETUP_MEMORY)

    # Pre-seat onboarding + the trust dialog for this workspace so headless
    # -p runs never stall on interactive first-run state.
    state_file = home / ".claude.json"
    state: dict = {}
    if state_file.exists():
        try:
            state = json.loads(state_file.read_text())
        except (json.JSONDecodeError, OSError):
            logger.warning("isolation: unreadable %s, reseeding", state_file)
            state = {}
    state.setdefault("hasCompletedOnboarding", True)
    projects = state.setdefault("projects", {})
    projects.setdefault(str(workspace), {})["hasTrustDialogAccepted"] = True
    state_file.write_text(json.dumps(state, indent=2))

    # Subscription auth: link the host's credentials into the isolated home.
    # A symlink (not a copy) so host-side token refreshes stay visible. If
    # the CLI ever atomically rewrites through the link the file forks into
    # a real one here — which the guard below then respects.
    creds = home / ".credentials.json"
    host_creds = _host_config_home() / ".credentials.json"
    if not creds.exists() and not creds.is_symlink() and host_creds.exists():
        try:
            creds.symlink_to(host_creds)
        except OSError as exc:
            logger.warning("isolation: could not link credentials: %s", exc)

    return str(home)
