#!/usr/bin/env python3
"""Project-first CARBON onboarding, durable workspace state, and next-action guidance."""

from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Any

from carbon_advisor import analyze
from carbon_schemas import INTENT_LIBRARY_SCHEMA, ensure_intent_library


SCHEMA = "carbon.workspace/v1"
PROFILE_SCHEMA = "carbon.workspace-profile/v1"
PREFERENCES_SCHEMA = "carbon.workspace-preferences/v1"
JOURNEY_SCHEMA = "carbon.workspace-journey/v1"
EVENT_SCHEMA = "carbon.workspace-event/v1"
PROACTIVITY = {"quiet", "guided", "proactive"}
STATE_DIRECTORIES = (
    "workspace",
    "framework",
    "runs",
    "learning",
    "recordings",
    "auto",
    "tests/bake-runs",
    "browser/sessions",
    "generated",
    "imports",
    "logs",
    "cache",
)
INITIAL_STATE_FILES = {
    "tests/intent-tests.json": {
        "schema": INTENT_LIBRARY_SCHEMA,
        "tests": [],
        "updatedAt": None,
    },
    "framework/coverage.json": {
        "schema": "carbon.coverage/v1",
        "claims": [],
        "surfaces": [],
        "gaps": [],
        "updatedAt": None,
    },
}
PROGRESS_PROTOCOL = [
    "Say what CARBON is inspecting before each phase.",
    "Separate observed project facts from inference and unknowns.",
    "Show the proposed focus and state location before creating project files.",
    "Offer ranked next actions; never run a suggested action merely because it was suggested.",
    "After each completed action, summarize evidence gained, remaining risk, and the next useful choices.",
]
DOCTRINE_PATH = Path(__file__).resolve().parents[1] / "references" / "testing-doctrine.json"


def load_testing_doctrine() -> dict[str, Any]:
    try:
        doctrine = json.loads(DOCTRINE_PATH.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        raise RuntimeError(f"CARBON testing doctrine is unavailable: {error}") from error
    if doctrine.get("schema") != "carbon.testing-doctrine/v1":
        raise RuntimeError("CARBON testing doctrine has an unsupported schema.")
    return doctrine


TESTING_DOCTRINE = load_testing_doctrine()
PROFESSIONAL_JUDGMENT = {
    "schema": TESTING_DOCTRINE["schema"],
    "version": TESTING_DOCTRINE["version"],
    "voice": "Direct, practical, curious, and evidence-led. Prefer specific project language over testing ceremony.",
    "northStar": TESTING_DOCTRINE["northStar"],
    "biases": [principle["stance"] for principle in TESTING_DOCTRINE["principles"]],
    "practiceBalances": TESTING_DOCTRINE["spices"],
    "decisionRules": TESTING_DOCTRINE["decisionRules"],
    "antiPatterns": TESTING_DOCTRINE["antiPatterns"],
    "identityBoundary": TESTING_DOCTRINE["identityBoundary"],
}


def now() -> str:
    return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def load_json(path: Path, fallback: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return fallback


def atomic_json(path: Path, value: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
        json.dump(value, handle, indent=2, ensure_ascii=False)
        handle.write("\n")
        temporary = Path(handle.name)
    os.replace(temporary, path)


def append_event(state_dir: Path, action: str, details: dict[str, Any] | None = None) -> None:
    event = {
        "schema": EVENT_SCHEMA,
        "at": now(),
        "action": action,
        "details": details or {},
    }
    path = state_dir / "workspace" / "events.jsonl"
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(event, ensure_ascii=False) + "\n")


def state_dir_for(root: Path, state_root: str = "") -> Path:
    if state_root:
        candidate = Path(state_root).expanduser().resolve()
        return candidate if candidate.name == ".carbon" else candidate / ".carbon"
    project_state = root / ".carbon"
    return project_state.resolve() if project_state.is_symlink() else project_state


def state_files(state_dir: Path) -> dict[str, Path]:
    workspace = state_dir / "workspace"
    return {
        "profile": workspace / "profile.json",
        "preferences": workspace / "preferences.json",
        "journey": workspace / "journey.json",
        "events": workspace / "events.jsonl",
        "readme": state_dir / "README.md",
    }


def project_advice(root: Path, scope: str, max_files: int) -> dict[str, Any]:
    return analyze(root, scope, max(100, min(50_000, max_files)))


def focus_options(advice: dict[str, Any]) -> list[dict[str, Any]]:
    observed = advice["observed"]
    options: list[dict[str, Any]] = []

    def add(identifier: str, label: str, why: str, command: str, recommended: bool = False) -> None:
        options.append({
            "id": identifier,
            "label": label,
            "why": why,
            "firstCommand": command,
            "recommended": recommended,
        })

    changes = observed.get("changes") or []
    surfaces = observed.get("surfaces") or {}
    tests = observed.get("tests") or {}
    risks = observed.get("riskAreas") or []
    if changes:
        add(
            "current-change",
            "The current change",
            f"{len(changes)} changed paths were detected; validate the smallest risk-relevant slice first.",
            "$carbon test the current change",
            recommended=True,
        )
    if surfaces.get("webUi"):
        add(
            "critical-user-journeys",
            "Critical user journeys and personas",
            "User-interface signals were found; exercise behavior, accessibility, recovery, and persona friction.",
            "$carbon-browser map and test the critical user journeys",
            recommended=not changes,
        )
    if surfaces.get("api"):
        add(
            "api-boundaries",
            "API contracts and failure boundaries",
            "API routes or schema signals were found; connect positive, negative, auth, idempotency, and resilience cases to product flows.",
            "$carbon-generate API contract and boundary tests",
        )
    if surfaces.get("ai"):
        add(
            "ai-confidence",
            "AI behavior, evaluation, and confidence",
            "AI-system signals were found; deterministic checks alone cannot establish decision-relevant confidence.",
            "$carbon-confidence-plan map AI risks, claims, evals, and release evidence",
        )
    if tests.get("files"):
        add(
            "coverage-efficiency",
            "Existing coverage and suite efficiency",
            f"{tests['files']} likely test files were detected; identify high-value reuse, gaps, duplication, flakiness, and slow feedback.",
            "$ai-upgrade assess the existing test suite for AI readiness",
        )
    if risks:
        add(
            "highest-risk-areas",
            "The highest-risk product areas",
            "Project markers suggest consequential boundaries that deserve depth before broad test-count growth.",
            "$carbon test the highest-risk product areas",
        )
    add(
        "whole-project",
        "The whole project",
        "Build a balanced baseline across product behavior, APIs, data, accessibility, performance, security, operations, and existing tests.",
        "$carbon test this whole project --size medium --strategy balanced",
        recommended=not options,
    )
    add(
        "custom",
        "A custom concern or outcome",
        "Describe the release, flow, failure, persona, risk, or quality question that matters most right now.",
        "$carbon <describe the focus in plain language>",
    )
    return options


def load_journey(files: dict[str, Path]) -> dict[str, Any]:
    journey = load_json(files["journey"], {})
    if not isinstance(journey, dict) or journey.get("schema") != JOURNEY_SCHEMA:
        journey = {"schema": JOURNEY_SCHEMA, "commands": {}, "completedMilestones": [], "updatedAt": now()}
    journey.setdefault("commands", {})
    journey.setdefault("completedMilestones", [])
    return journey


def decorated_actions(advice: dict[str, Any], journey: dict[str, Any]) -> list[dict[str, Any]]:
    commands = journey.get("commands") or {}
    actions = []
    for step in advice.get("nextSteps") or []:
        command = str(step.get("command") or "")
        command_key = command.split()[0].lstrip("$/")
        history = commands.get(command_key) or commands.get(command) or {}
        actions.append({
            **step,
            "hasRunInWorkspace": bool(history.get("count")),
            "runCount": int(history.get("count") or 0),
            "lastRunAt": history.get("lastRunAt"),
        })
    actions.sort(key=lambda action: (action["hasRunInWorkspace"], action.get("priority", 99)))
    return actions


def status_payload(root: Path, state_dir: Path, scope: str, max_files: int) -> dict[str, Any]:
    files = state_files(state_dir)
    profile = load_json(files["profile"], {})
    preferences = load_json(files["preferences"], {})
    journey = load_journey(files)
    advice = project_advice(root, scope, max_files)
    initialized = bool(profile.get("schema") == PROFILE_SCHEMA and preferences.get("onboardingCompleteAt"))
    return {
        "schema": SCHEMA,
        "framework": {"name": "CARBON", "releaseStage": "beta"},
        "action": "inspect",
        "projectRoot": str(root),
        "stateDirectory": str(state_dir),
        "defaultStateDirectory": str(root / ".carbon"),
        "stateIsProjectLocal": state_dir == root / ".carbon" and not (root / ".carbon").is_symlink(),
        "stateIsProjectDiscoverable": state_dir == root / ".carbon" or (root / ".carbon").is_symlink(),
        "firstRun": not initialized,
        "onboardingComplete": initialized,
        "needsFocusChoice": not initialized,
        "choicePrompt": "What should CARBON focus on first? Choose one option, say 'all', or describe a custom focus in your own words.",
        "focusOptions": focus_options(advice),
        "currentPreferences": preferences,
        "observed": advice["observed"],
        "gaps": advice["gaps"],
        "recommendedStart": advice["recommendedStart"],
        "nextActions": decorated_actions(advice, journey),
        "commandsNotRunYet": [
            action["command"] for action in decorated_actions(advice, journey)
            if not action["hasRunInWorkspace"]
        ],
        "progress": [
            "Resolved the project root and CARBON state location.",
            "Inspected languages, frameworks, current changes, tests, UI, API, AI, automation, and risk signals.",
            "Checked prior CARBON onboarding, command history, and durable evidence state.",
            "Ranked focus choices and next actions by observed project value and untried workflows.",
        ],
        "progressProtocol": PROGRESS_PROTOCOL,
        "professionalJudgment": PROFESSIONAL_JUDGMENT,
        "authority": "Suggestions are invitations, not execution. CARBON waits for the user's focus choice and preserves approval gates for mutations, paid/public actions, schedules, regression promotion, and release decisions.",
    }


def write_readme(path: Path, root: Path, state_dir: Path) -> None:
    content = f"""# CARBON project state

This directory is CARBON's local, inspectable state for `{root.name}`.

- `workspace/` — project profile, focus, command journey, and activity events
- `framework/` — product model, coverage, plans, analysis, and confidence reports
- `runs/` — observed test runs and evidence
- `learning/` — unfamiliar-surface observations and supported candidates
- `recordings/` — redacted interaction recordings and playback outcomes
- `tests/` — AI-intent tests and evidence-gated import bakes
- `browser/` — negotiated local browser-session ledgers
- `auto/` — bounded automatic validation-loop state

Default location: `{root / '.carbon'}`
Current state directory: `{state_dir}`

Move this state only through CARBON's relocation action so project discovery remains intact. Project code, tests, external services, releases, and paid/public actions are never changed merely because this state exists.
"""
    path.write_text(content, encoding="utf-8")


def initialize(root: Path, state_dir: Path, focus: str, all_areas: bool, proactivity: str,
               scope: str, max_files: int) -> dict[str, Any]:
    if not focus.strip() and not all_areas:
        raise ValueError("Choose a focus, describe a custom focus, or pass --all before initializing CARBON in this workspace.")
    if proactivity not in PROACTIVITY:
        raise ValueError(f"proactivity must be one of: {', '.join(sorted(PROACTIVITY))}")
    advice = project_advice(root, scope, max_files)
    project_state = root / ".carbon"
    if state_dir != project_state:
        if project_state.exists() or project_state.is_symlink():
            raise ValueError("Project-local .carbon already exists. Use the confirmed relocate action instead of initializing a second state directory.")
        state_dir.mkdir(parents=True, exist_ok=True)
        project_state.symlink_to(state_dir, target_is_directory=True)
    for relative in STATE_DIRECTORIES:
        (state_dir / relative).mkdir(parents=True, exist_ok=True)
    for relative, template in INITIAL_STATE_FILES.items():
        target = state_dir / relative
        if not target.exists():
            atomic_json(target, {**template, "updatedAt": now()})
    ensure_intent_library(state_dir / "tests" / "intent-tests.json", root)
    files = state_files(state_dir)
    completed_at = now()
    profile = {
        "schema": PROFILE_SCHEMA,
        "projectRoot": str(root),
        "project": advice["observed"]["project"],
        "surfaces": advice["observed"]["surfaces"],
        "tests": advice["observed"]["tests"],
        "automation": advice["observed"]["automation"],
        "riskAreas": advice["observed"]["riskAreas"],
        "inventoryGaps": advice["gaps"],
        "createdAt": completed_at,
        "updatedAt": completed_at,
    }
    existing_preferences = load_json(files["preferences"], {})
    preferences = {
        "schema": PREFERENCES_SCHEMA,
        "projectRoot": str(root),
        "stateDirectory": str(state_dir),
        "focus": "all relevant project areas" if all_areas else focus.strip(),
        "focusAll": bool(all_areas),
        "proactivity": proactivity,
        "voiceProfile": "carbon-professional/v1",
        "onboardingCompleteAt": existing_preferences.get("onboardingCompleteAt") or completed_at,
        "updatedAt": completed_at,
    }
    journey = load_journey(files)
    journey["updatedAt"] = completed_at
    atomic_json(files["profile"], profile)
    atomic_json(files["preferences"], preferences)
    atomic_json(files["journey"], journey)
    write_readme(files["readme"], root, state_dir)
    append_event(state_dir, "workspace-initialized", {
        "focus": preferences["focus"],
        "focusAll": preferences["focusAll"],
        "proactivity": proactivity,
    })
    result = status_payload(root, state_dir, scope, max_files)
    result.update({
        "action": "initialize",
        "firstRun": False,
        "onboardingComplete": True,
        "needsFocusChoice": False,
        "created": [str(state_dir / relative) for relative in STATE_DIRECTORIES]
        + [str(state_dir / relative) for relative in INITIAL_STATE_FILES],
        "progress": [
            "Confirmed the user's project focus and guidance preference.",
            "Created the project-local CARBON state structure without changing application code or external systems.",
            "Saved the observed project profile, focus, command journey, and inspectable activity log.",
            "Ranked the next useful choices, prioritizing workflows not yet used in this workspace.",
        ],
    })
    return result


def set_focus(root: Path, state_dir: Path, focus: str, all_areas: bool, proactivity: str | None,
              scope: str, max_files: int) -> dict[str, Any]:
    files = state_files(state_dir)
    preferences = load_json(files["preferences"], {})
    if not preferences:
        raise ValueError("CARBON workspace onboarding is not initialized. Inspect first, then initialize with a focus choice.")
    if not focus.strip() and not all_areas and proactivity is None:
        raise ValueError("Provide a focus choice, --all, or a proactivity setting.")
    if proactivity is not None and proactivity not in PROACTIVITY:
        raise ValueError(f"proactivity must be one of: {', '.join(sorted(PROACTIVITY))}")
    if focus.strip() or all_areas:
        preferences["focus"] = "all relevant project areas" if all_areas else focus.strip()
        preferences["focusAll"] = bool(all_areas)
    if proactivity is not None:
        preferences["proactivity"] = proactivity
    preferences["updatedAt"] = now()
    atomic_json(files["preferences"], preferences)
    append_event(state_dir, "workspace-preferences-updated", {
        "focus": preferences.get("focus"),
        "focusAll": preferences.get("focusAll"),
        "proactivity": preferences.get("proactivity"),
    })
    result = status_payload(root, state_dir, scope, max_files)
    result["action"] = "set-focus"
    result["progress"] = [
        "Read the current workspace focus and guidance preference.",
        "Updated only CARBON's local workspace preferences.",
        "Re-ranked next actions against the project and command journey.",
    ]
    return result


def record(root: Path, state_dir: Path, command: str) -> dict[str, Any]:
    files = state_files(state_dir)
    if not files["preferences"].is_file():
        return {"schema": SCHEMA, "action": "record", "recorded": False, "reason": "workspace-not-initialized"}
    key = command.strip().lstrip("$/").split()[0]
    if not key:
        raise ValueError("command is required.")
    journey = load_journey(files)
    entry = journey["commands"].get(key) or {"count": 0, "firstRunAt": now()}
    entry["count"] = int(entry.get("count") or 0) + 1
    entry["lastRunAt"] = now()
    journey["commands"][key] = entry
    journey["updatedAt"] = now()
    atomic_json(files["journey"], journey)
    append_event(state_dir, "command-invoked", {"command": key})
    return {"schema": SCHEMA, "action": "record", "recorded": True, "command": key, "history": entry}


def relocate(root: Path, destination: str, confirm: bool, scope: str, max_files: int) -> dict[str, Any]:
    if not confirm:
        raise ValueError("Relocating CARBON state requires explicit confirm=true after the user approves the exact destination.")
    project_state = root / ".carbon"
    if not project_state.exists() and not project_state.is_symlink():
        raise ValueError("No project CARBON state exists to relocate.")
    current_state = project_state.resolve()
    destination_dir = state_dir_for(root, destination)
    if destination_dir == current_state:
        return status_payload(root, destination_dir, "", max_files)
    if current_state in destination_dir.parents:
        raise ValueError("Destination cannot be inside the current CARBON state directory.")
    returning_to_project = project_state.is_symlink() and destination_dir == project_state
    if not returning_to_project and destination_dir.exists() and any(destination_dir.iterdir()):
        raise ValueError(f"Destination CARBON state is not empty: {destination_dir}")
    destination_dir.parent.mkdir(parents=True, exist_ok=True)
    if project_state.is_symlink():
        project_state.unlink()
    shutil.move(str(current_state), str(destination_dir))
    if returning_to_project:
        pass
    elif project_state.exists():
        if project_state.is_dir() and not any(project_state.iterdir()):
            project_state.rmdir()
        else:
            raise ValueError("Project .carbon could not be replaced with a discovery symlink after relocation.")
    if not returning_to_project:
        project_state.symlink_to(destination_dir, target_is_directory=True)
    files = state_files(destination_dir)
    preferences = load_json(files["preferences"], {})
    preferences["stateDirectory"] = str(destination_dir)
    preferences["updatedAt"] = now()
    atomic_json(files["preferences"], preferences)
    append_event(destination_dir, "workspace-relocated", {"from": str(current_state), "to": str(destination_dir)})
    result = status_payload(root, destination_dir, scope, max_files)
    result.update({
        "action": "relocate",
        "relocated": True,
        "from": str(current_state),
        "to": str(destination_dir),
        "projectDiscoveryLink": str(project_state),
        "progress": [
            "Confirmed the exact relocation destination and explicit approval.",
            "Moved the CARBON state directory without changing project code or external systems.",
            "Created a project-local discovery link so existing CARBON commands continue to find the state.",
        ],
    })
    return result


def main() -> int:
    parser = argparse.ArgumentParser(description="CARBON project-first onboarding and durable workspace state.")
    commands = parser.add_subparsers(dest="action", required=True)

    def common(command: argparse.ArgumentParser, *, state: bool = True) -> None:
        command.add_argument("--root", default=".")
        if state:
            command.add_argument("--state-root", default="")
        command.add_argument("--scope", default="")
        command.add_argument("--max-files", type=int, default=20_000)

    common(commands.add_parser("inspect"))
    common(commands.add_parser("status"))
    common(commands.add_parser("suggest"))
    initialize_parser = commands.add_parser("init")
    common(initialize_parser)
    initialize_parser.add_argument("--focus", default="")
    initialize_parser.add_argument("--all", action="store_true")
    initialize_parser.add_argument("--proactivity", choices=sorted(PROACTIVITY), default="guided")
    focus_parser = commands.add_parser("focus")
    common(focus_parser)
    focus_parser.add_argument("--focus", default="")
    focus_parser.add_argument("--all", action="store_true")
    focus_parser.add_argument("--proactivity", choices=sorted(PROACTIVITY))
    record_parser = commands.add_parser("record")
    common(record_parser)
    record_parser.add_argument("--command", required=True)
    relocate_parser = commands.add_parser("relocate")
    common(relocate_parser, state=False)
    relocate_parser.add_argument("--destination", required=True)
    relocate_parser.add_argument("--confirm", action="store_true")
    args = parser.parse_args()
    root = Path(args.root).expanduser().resolve()
    if not root.is_dir():
        print(json.dumps({"error": f"Project root is not a directory: {root}"}), file=sys.stderr)
        return 2
    max_files = max(100, min(50_000, args.max_files))
    try:
        if args.action == "relocate":
            result = relocate(root, args.destination, args.confirm, args.scope, max_files)
        else:
            state_dir = state_dir_for(root, args.state_root)
            if args.action in {"inspect", "status", "suggest"}:
                result = status_payload(root, state_dir, args.scope, max_files)
                result["action"] = args.action
            elif args.action == "init":
                result = initialize(root, state_dir, args.focus, args.all, args.proactivity, args.scope, max_files)
            elif args.action == "focus":
                result = set_focus(root, state_dir, args.focus, args.all, args.proactivity, args.scope, max_files)
            elif args.action == "record":
                result = record(root, state_dir, args.command)
            else:
                raise ValueError(f"Unsupported action: {args.action}")
    except (OSError, ValueError) as error:
        print(json.dumps({"error": str(error)}), file=sys.stderr)
        return 1
    print(json.dumps(result, indent=2, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
