from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from cybervisor.adapters.registry import get_adapter
from cybervisor.config import (
    HookConfig,
    StageContractConfig,
    contract_artifact_path,
    contract_prompt_path,
)

HOOK_CONFIG_NAME = "hook_config.json"
STOP_HOOK_VERIFIER_PROMPT_NAME = "stop-hook-verifier.md"
HOOK_SOCKET_NAME = "hook-events.sock"
SNAPSHOT_FILE_SUFFIX = ".snapshot.json"



def _load_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    content = path.read_text(encoding="utf-8").strip()
    if not content:
        return {}
    try:
        payload = json.loads(content)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid JSON in settings file {path}: {exc}") from exc
    if not isinstance(payload, dict):
        raise ValueError(f"Settings file must contain a JSON object: {path}")
    return payload


def _write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")


def _snapshot_path(runtime_dir: Path, agent_tool: str) -> Path:
    return runtime_dir / f"{agent_tool}{SNAPSHOT_FILE_SUFFIX}"


def _persist_settings_snapshot(runtime_dir: Path, agent_tool: str, settings_path: Path) -> None:
    snapshot_path = _snapshot_path(runtime_dir, agent_tool)
    if snapshot_path.exists():
        return

    snapshot_payload = {
        "settings_path": str(settings_path),
        "existed_before_run": settings_path.exists(),
        "original_content": settings_path.read_text(encoding="utf-8") if settings_path.exists() else None,
    }
    _write_json(snapshot_path, snapshot_payload)


def _restore_settings_snapshot(runtime_dir: Path, agent_tool: str) -> str:
    snapshot_path = _snapshot_path(runtime_dir, agent_tool)
    if not snapshot_path.exists():
        return "skipped"

    payload = _load_json(snapshot_path)
    settings_path = Path(str(payload["settings_path"]))
    existed_before_run = bool(payload.get("existed_before_run"))
    original_content = payload.get("original_content")
    if existed_before_run:
        if not isinstance(original_content, str):
            raise ValueError(f"Snapshot missing original_content for {settings_path}")
        settings_path.parent.mkdir(parents=True, exist_ok=True)
        settings_path.write_text(original_content, encoding="utf-8")
        snapshot_path.unlink(missing_ok=True)
        return "restored"

    if settings_path.exists():
        settings_path.unlink()
    if settings_path.parent.exists() and not any(settings_path.parent.iterdir()):
        settings_path.parent.rmdir()
    snapshot_path.unlink(missing_ok=True)
    return "deleted-created-file"


def _runtime_dir() -> Path:
    return Path(".cybervisor/hooks")


def _runtime_hook_config_path(runtime_dir: Path) -> Path:
    return runtime_dir / HOOK_CONFIG_NAME


def hook_socket_path() -> Path:
    return Path(".cybervisor") / HOOK_SOCKET_NAME


def _serialize_stage_contract_for_stage(stage_name: str, contract: StageContractConfig | None) -> dict[str, Any] | None:
    if contract is None:
        return None
    prompt_path = contract_prompt_path(stage_name)
    if not prompt_path.exists():
        raise FileNotFoundError(f"Contract prompt file not found: {prompt_path}")
    return {
        "artifact_path": str(contract_artifact_path(stage_name)),
        "prompt_path": str(prompt_path),
        "prompt_contents": prompt_path.read_text(encoding="utf-8"),
        "allowed_statuses": list(contract.allowed_statuses),
        "routes": {
            status: {
                "next_stage": route.next_stage,
                "injections": list(route.injections),
            }
            for status, route in contract.routes.items()
        },
    }


def _write_hook_runtime_config(
    runtime_dir: Path,
    agent_tool: str,
    *,
    active_stage_name: str | None = None,
    active_stage_prompt: str | None = None,
    active_stage_contract: StageContractConfig | None = None,
    active_stage_attempt: int | None = None,
    active_stage_max_retries: int | None = None,
) -> None:
    payload = {
        "agent_tool": agent_tool,
        "socket_path": str(hook_socket_path().resolve()),
        "active_stage_name": active_stage_name,
        "active_stage_prompt": active_stage_prompt,
        "active_stage_contract": (
            _serialize_stage_contract_for_stage(active_stage_name, active_stage_contract)
            if active_stage_name is not None
            else None
        ),
        "active_stage_attempt": active_stage_attempt,
        "active_stage_max_retries": active_stage_max_retries,
    }
    path = _runtime_hook_config_path(runtime_dir)
    path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")


def set_active_stage_contract(
    agent_tool: str,
    stage_name: str | None,
    stage_prompt: str | None,
    contract: StageContractConfig | None,
    *,
    attempt: int | None = None,
    max_retries: int | None = None,
) -> None:
    adapter = get_adapter(agent_tool)
    if not adapter.descriptor.capabilities.supports_hooks:
        return

    runtime_dir = _runtime_dir()
    config_path = _runtime_hook_config_path(runtime_dir)
    if not config_path.exists():
        return

    payload = _load_json(config_path)
    payload["active_stage_name"] = stage_name
    payload["active_stage_prompt"] = stage_prompt
    payload["active_stage_contract"] = (
        _serialize_stage_contract_for_stage(stage_name, contract)
        if stage_name is not None
        else None
    )
    payload["active_stage_attempt"] = attempt
    payload["active_stage_max_retries"] = max_retries
    _write_json(config_path, payload)


def _cleanup_runtime_files(runtime_dir: Path) -> None:

    config_path = _runtime_hook_config_path(runtime_dir)
    if config_path.exists():
        config_path.unlink()

    socket_path = hook_socket_path()
    if socket_path.exists():
        socket_path.unlink()

    if runtime_dir.exists() and not any(runtime_dir.iterdir()):
        runtime_dir.rmdir()

    cybervisor_dir = runtime_dir.parent
    if cybervisor_dir.exists() and not any(cybervisor_dir.iterdir()):
        cybervisor_dir.rmdir()


def inject_hooks(agent_tool: str, config: HookConfig) -> None:
    adapter = get_adapter(agent_tool)
    if not adapter.descriptor.capabilities.supports_hooks:
        return
    del config

    settings_path = adapter.settings_path()
    if settings_path is None:
        return
    _load_json(settings_path)

    runtime_dir = _runtime_dir()

    try:
        runtime_dir.mkdir(parents=True, exist_ok=True)
        _write_hook_runtime_config(runtime_dir, agent_tool)
        runtime_config_path = _runtime_hook_config_path(runtime_dir)
        _persist_settings_snapshot(runtime_dir, agent_tool, settings_path)
        payload = _load_json(settings_path)
        updated_payload = adapter.install_hook_settings(payload, runtime_config_path)
        _write_json(settings_path, updated_payload)
    except Exception:
        try:
            _restore_settings_snapshot(runtime_dir, agent_tool)
        except Exception:
            pass
        _cleanup_runtime_files(runtime_dir)
        raise


def remove_hooks(agent_tool: str) -> str:
    adapter = get_adapter(agent_tool)
    if not adapter.descriptor.capabilities.supports_hooks:
        return "skipped"

    runtime_dir = _runtime_dir()
    runtime_config_path = _runtime_hook_config_path(runtime_dir)
    settings_path = adapter.settings_path()
    if settings_path is not None and settings_path.exists():
        payload = _load_json(settings_path)
        updated_payload = adapter.remove_hook_settings(payload, runtime_config_path)
        _write_json(settings_path, updated_payload)
    restore_result = _restore_settings_snapshot(runtime_dir, agent_tool)
    _cleanup_runtime_files(runtime_dir)
    return restore_result
