"""OpenClaw-assisted evaluation for Hikaflow run bundles."""

from __future__ import annotations

import json
import subprocess
from pathlib import Path
from typing import Any, Dict, Optional, Tuple


def _read_run_context(run_path: Path) -> Dict[str, Any]:
    """Read a compact context snapshot from a run directory."""
    manifest_path = run_path / "manifest.json"
    if not manifest_path.exists():
        raise ValueError(f"Run manifest not found: {manifest_path}")

    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    context: Dict[str, Any] = {
        "run_id": manifest.get("run_id"),
        "entry_cmd": manifest.get("entry_cmd", {}).get("argv", []),
        "duration": manifest.get("duration"),
        "exit_code": manifest.get("exit_code"),
    }

    result_path = run_path / "result.json"
    if result_path.exists():
        try:
            result = json.loads(result_path.read_text(encoding="utf-8"))
            context["result"] = {
                "exit_code": result.get("exit_code"),
                "exception": result.get("exception"),
                "divergence": result.get("divergence"),
            }
        except json.JSONDecodeError:
            context["result_raw"] = result_path.read_text(encoding="utf-8", errors="replace")[:2000]

    return context


def build_openclaw_prompt(run_context: Dict[str, Any]) -> str:
    """Build a deterministic prompt that asks OpenClaw for structured feedback."""
    context_json = json.dumps(run_context, indent=2, sort_keys=True)
    return (
        "You are evaluating Hikaflow (deterministic replay/debugging tooling).\n"
        "Given this captured run context, provide a strict JSON response only.\n"
        "Focus on how to test and improve the tool.\n\n"
        "Required JSON schema:\n"
        "{\n"
        '  "summary": "short diagnosis",\n'
        '  "gaps": ["key testing or reliability gaps"],\n'
        '  "improvements": ["concrete product/code improvements"],\n'
        '  "next_tests": ["specific tests to add or run"],\n'
        '  "priority": "high|medium|low"\n'
        "}\n\n"
        "Run context:\n"
        f"{context_json}\n"
    )


def build_openclaw_command(
    message: str,
    *,
    model: Optional[str] = None,
    timeout_seconds: int = 120,
    local: bool = True,
) -> list[str]:
    """Build the OpenClaw CLI invocation."""
    cmd = ["openclaw", "agent", "--message", message, "--json", "--timeout", str(timeout_seconds)]
    if local:
        cmd.append("--local")
    if model:
        cmd.extend(["--model", model])
    return cmd


def _extract_first_json(text: str) -> Dict[str, Any]:
    """Extract first JSON object from plain text or fenced blocks."""
    text = text.strip()
    if not text:
        raise ValueError("Empty output")

    # First try direct parse.
    try:
        parsed = json.loads(text)
        if isinstance(parsed, dict):
            return parsed
    except json.JSONDecodeError:
        pass

    # Then try fenced block parsing.
    start = text.find("{")
    end = text.rfind("}")
    if start == -1 or end == -1 or end <= start:
        raise ValueError("No JSON object found in output")
    snippet = text[start : end + 1]
    parsed = json.loads(snippet)
    if not isinstance(parsed, dict):
        raise ValueError("Parsed output is not a JSON object")
    return parsed


def parse_openclaw_output(stdout: str) -> Tuple[Dict[str, Any], Dict[str, Any]]:
    """Parse OpenClaw CLI JSON envelope and the model's structured payload."""
    envelope = _extract_first_json(stdout)

    # OpenClaw `--json` may return nested payloads; normalize to text.
    candidate_text = ""
    if isinstance(envelope.get("text"), str):
        candidate_text = envelope["text"]
    elif isinstance(envelope.get("message"), dict):
        maybe_text = envelope["message"].get("text")
        if isinstance(maybe_text, str):
            candidate_text = maybe_text
    elif isinstance(envelope.get("reply"), dict):
        maybe_text = envelope["reply"].get("text")
        if isinstance(maybe_text, str):
            candidate_text = maybe_text

    if not candidate_text:
        # If envelope itself already looks like target payload, accept it.
        if {"summary", "gaps", "improvements", "next_tests", "priority"}.issubset(set(envelope)):
            return envelope, envelope
        raise ValueError("OpenClaw output did not include a textual reply")

    payload = _extract_first_json(candidate_text)
    return envelope, payload


def run_openclaw_eval(
    run_path: Path,
    *,
    model: Optional[str] = None,
    timeout_seconds: int = 120,
    local: bool = True,
) -> Dict[str, Any]:
    """Execute OpenClaw evaluation for a run bundle."""
    context = _read_run_context(run_path)
    prompt = build_openclaw_prompt(context)
    cmd = build_openclaw_command(prompt, model=model, timeout_seconds=timeout_seconds, local=local)

    proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
    if proc.returncode != 0:
        stderr = (proc.stderr or "").strip()
        raise RuntimeError(f"OpenClaw command failed (exit {proc.returncode}): {stderr[:800]}")

    envelope, payload = parse_openclaw_output(proc.stdout)
    return {
        "command": cmd,
        "run_path": str(run_path),
        "openclaw": envelope,
        "evaluation": payload,
    }
