#!/usr/bin/env python3
"""Durable, redacted browser-flow recording and playback state for CARBON."""

from __future__ import annotations

import argparse
import json
import os
import re
import secrets
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


SCHEMA = "carbon.recording/v1"
SECRET_PATTERNS = (
    re.compile(r"(?i)\b(bearer|token|api[_ -]?key|password|passwd|secret)\b\s*[:=]\s*\S+"),
    re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"),
    re.compile(r"\b[A-Za-z0-9+/]{32,}={0,2}\b"),
)
SENSITIVE_FIELD = re.compile(
    r"(?i)\b(password|passwd|passcode|pin|token|secret|api[_ -]?key|card|cvv|cvc|cookie|authorization)\b"
)


def now() -> str:
    return datetime.now(timezone.utc).isoformat()


def safe_text(value: Any, limit: int = 4000) -> str:
    text = str(value or "").replace("\x00", "").strip()[:limit]
    for pattern in SECRET_PATTERNS:
        text = pattern.sub("[REDACTED]", text)
    return text


def write_json(path: Path, value: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(value, handle, indent=2, ensure_ascii=False)
            handle.write("\n")
        os.replace(tmp_name, path)
    finally:
        if os.path.exists(tmp_name):
            os.unlink(tmp_name)


def read_recording(directory: Path) -> dict[str, Any]:
    path = directory / "recording.json"
    if not path.is_file():
        raise SystemExit(f"Recording not found: {path}")
    data = json.loads(path.read_text(encoding="utf-8"))
    if data.get("schema") != SCHEMA:
        raise SystemExit(f"Unsupported recording schema: {data.get('schema')}")
    return data


def save(directory: Path, data: dict[str, Any]) -> None:
    data["updatedAt"] = now()
    write_json(directory / "recording.json", data)


def recording_dir(value: str) -> Path:
    return Path(value).expanduser().resolve()


def command_init(args: argparse.Namespace) -> None:
    root = Path(args.root).expanduser().resolve()
    record_id = args.id or f"rec-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{secrets.token_hex(3)}"
    directory = root / ".carbon" / "recordings" / record_id
    if directory.exists():
        raise SystemExit(f"Recording already exists: {directory}")
    created = now()
    data = {
        "schema": SCHEMA,
        "id": record_id,
        "title": safe_text(args.title, 200),
        "target": safe_text(args.target, 2000),
        "status": "draft",
        "createdAt": created,
        "updatedAt": created,
        "steps": [],
        "assertions": [],
        "playbacks": [],
        "safety": {
            "secretsStored": False,
            "destructiveActionsAuthorized": False,
            "note": "The browser acts; this artifact records redacted evidence.",
        },
    }
    save(directory, data)
    print(directory)


def command_start(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    if data["status"] not in {"draft", "paused"}:
        raise SystemExit(f"Cannot start recording from status {data['status']}")
    data["status"] = "recording"
    data.setdefault("startedAt", now())
    save(directory, data)
    print(directory / "recording.json")


def command_step(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    if data["status"] != "recording":
        raise SystemExit("Start the recording before adding steps.")
    if args.destructive and not args.confirm_destructive:
        raise SystemExit("Destructive step not recorded. Add --confirm-destructive only after scoped user approval.")
    step_id = f"S{len(data['steps']) + 1:03d}"
    sensitive = bool(args.sensitive or SENSITIVE_FIELD.search(f"{args.action} {args.target}"))
    step = {
        "id": step_id,
        "action": safe_text(args.action, 80),
        "target": safe_text(args.target, 500),
        "value": "[REDACTED]" if sensitive and args.value else safe_text(args.value, 1000),
        "url": safe_text(args.url, 2000),
        "actual": safe_text(args.actual, 2000),
        "evidence": safe_text(args.evidence, 2000),
        "sensitive": sensitive,
        "destructive": bool(args.destructive),
        "recordedAt": now(),
    }
    data["steps"].append(step)
    if args.destructive:
        data["safety"]["destructiveActionsAuthorized"] = True
    save(directory, data)
    print(step_id)


def command_assertion(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    if data["status"] != "recording":
        raise SystemExit("Start the recording before adding assertions.")
    assertion_id = f"A{len(data['assertions']) + 1:03d}"
    data["assertions"].append({
        "id": assertion_id,
        "expected": safe_text(args.expected, 2000),
        "target": safe_text(args.target, 500),
        "evidence": safe_text(args.evidence, 2000),
        "recordedAt": now(),
    })
    save(directory, data)
    print(assertion_id)


def candidate_test(data: dict[str, Any]) -> dict[str, Any]:
    return {
        "schema": "carbon.candidate-test/v1",
        "status": "candidate",
        "source": "carbon-recording",
        "sourceRef": data["id"],
        "kind": "flow",
        "title": data["title"],
        "target": data["target"],
        "steps": [
            {
                "id": step["id"],
                "action": step["action"],
                "target": step["target"],
                **({"value": step["value"]} if step.get("value") else {}),
            }
            for step in data["steps"]
        ],
        "assertions": data["assertions"],
        "evidenceLevel": "candidate-until-playback",
    }


def command_complete(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    if data["status"] not in {"recording", "paused"}:
        raise SystemExit(f"Cannot complete recording from status {data['status']}")
    if not data["steps"]:
        raise SystemExit("A recording needs at least one step.")
    data["status"] = "recorded"
    data["completedAt"] = now()
    save(directory, data)
    write_json(directory / "candidate-test.json", candidate_test(data))
    print(directory / "candidate-test.json")


def command_playback_start(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    if data["status"] not in {"recorded", "validated"}:
        raise SystemExit("Complete the recording before playback.")
    playback_id = f"P{len(data['playbacks']) + 1:03d}"
    data["playbacks"].append({
        "id": playback_id,
        "status": "running",
        "startedAt": now(),
        "steps": [
            {"stepId": step["id"], "status": "pending", "actual": "", "evidence": ""}
            for step in data["steps"]
        ],
    })
    save(directory, data)
    print(playback_id)


def active_playback(data: dict[str, Any]) -> dict[str, Any]:
    for playback in reversed(data["playbacks"]):
        if playback["status"] == "running":
            return playback
    raise SystemExit("No playback is running.")


def command_playback_update(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    playback = active_playback(data)
    step = next((item for item in playback["steps"] if item["stepId"] == args.step_id), None)
    if not step:
        raise SystemExit(f"Unknown step ID: {args.step_id}")
    step.update({
        "status": args.status,
        "actual": safe_text(args.actual, 2000),
        "evidence": safe_text(args.evidence, 2000),
        "updatedAt": now(),
    })
    save(directory, data)
    print(args.step_id)


def command_playback_complete(args: argparse.Namespace) -> None:
    directory = recording_dir(args.recording_dir)
    data = read_recording(directory)
    playback = active_playback(data)
    pending = [step["stepId"] for step in playback["steps"] if step["status"] in {"pending", "running"}]
    if pending:
        raise SystemExit(f"Playback still has pending steps: {', '.join(pending)}")
    playback["status"] = "failed" if any(step["status"] == "failed" for step in playback["steps"]) else "passed"
    playback["completedAt"] = now()
    if playback["status"] == "passed":
        data["status"] = "validated"
        validated = candidate_test(data)
        validated["status"] = "validated-candidate"
        validated["evidenceLevel"] = "playback-observed"
        validated["playbackId"] = playback["id"]
        write_json(directory / "candidate-test.json", validated)
    save(directory, data)
    print(playback["status"])


def command_show(args: argparse.Namespace) -> None:
    data = read_recording(recording_dir(args.recording_dir))
    print(json.dumps(data, indent=2, ensure_ascii=False))


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description=__doc__)
    sub = root.add_subparsers(dest="command", required=True)
    init = sub.add_parser("init")
    init.add_argument("--root", required=True)
    init.add_argument("--target", required=True)
    init.add_argument("--title", required=True)
    init.add_argument("--id")
    init.set_defaults(func=command_init)

    start = sub.add_parser("start")
    start.add_argument("--recording-dir", required=True)
    start.set_defaults(func=command_start)

    step = sub.add_parser("step")
    step.add_argument("--recording-dir", required=True)
    step.add_argument("--action", required=True)
    step.add_argument("--target", required=True)
    step.add_argument("--value", default="")
    step.add_argument("--url", default="")
    step.add_argument("--actual", default="")
    step.add_argument("--evidence", default="")
    step.add_argument("--sensitive", action="store_true")
    step.add_argument("--destructive", action="store_true")
    step.add_argument("--confirm-destructive", action="store_true")
    step.set_defaults(func=command_step)

    assertion = sub.add_parser("assertion")
    assertion.add_argument("--recording-dir", required=True)
    assertion.add_argument("--expected", required=True)
    assertion.add_argument("--target", default="")
    assertion.add_argument("--evidence", default="")
    assertion.set_defaults(func=command_assertion)

    complete = sub.add_parser("complete")
    complete.add_argument("--recording-dir", required=True)
    complete.set_defaults(func=command_complete)

    playback_start = sub.add_parser("playback-start")
    playback_start.add_argument("--recording-dir", required=True)
    playback_start.set_defaults(func=command_playback_start)

    playback_update = sub.add_parser("playback-update")
    playback_update.add_argument("--recording-dir", required=True)
    playback_update.add_argument("--step-id", required=True)
    playback_update.add_argument("--status", required=True, choices=("passed", "failed", "blocked", "skipped"))
    playback_update.add_argument("--actual", default="")
    playback_update.add_argument("--evidence", default="")
    playback_update.set_defaults(func=command_playback_update)

    playback_complete = sub.add_parser("playback-complete")
    playback_complete.add_argument("--recording-dir", required=True)
    playback_complete.set_defaults(func=command_playback_complete)

    show = sub.add_parser("show")
    show.add_argument("--recording-dir", required=True)
    show.set_defaults(func=command_show)
    return root


if __name__ == "__main__":
    arguments = parser().parse_args()
    arguments.func(arguments)
