"""Prompt-facing summaries for worker history and tool observations."""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

from nipux_cli.metric_format import format_metric_value
from nipux_cli.source_quality import anti_bot_reason
from nipux_cli.worker_policy import BROWSER_REF_IGNORE_NAMES


def compact(value: Any, limit: int = 500) -> str:
    text = json.dumps(value, ensure_ascii=False, sort_keys=True) if not isinstance(value, str) else value
    text = " ".join(text.split())
    return text if len(text) <= limit else text[:limit] + "..."


def clip_text(value: Any, limit: int) -> str:
    text = " ".join(str(value or "").split())
    if len(text) <= limit:
        return text
    return text[: max(0, limit - 3)].rstrip() + "..."


def format_step_for_prompt(step: dict[str, Any]) -> str:
    tool = f" tool={step['tool_name']}" if step.get("tool_name") else ""
    summary = step.get("summary") or step.get("error") or ""
    pieces = [f"- #{step['step_no']} {step['kind']} {step['status']}{tool}: {summary}"]
    input_data = step.get("input") or {}
    args = input_data.get("arguments") if isinstance(input_data, dict) else None
    if args:
        pieces.append(f"  args: {compact(args, 320)}")
    output = step.get("output") or {}
    observation = observation_for_prompt(step.get("tool_name"), output)
    if observation:
        pieces.append(f"  observed: {observation}")
    return "\n".join(pieces)


def observation_for_prompt(tool_name: str | None, output: dict[str, Any]) -> str:
    if not output:
        return ""
    if output.get("error"):
        if tool_name in {"browser_click", "browser_type"}:
            recovery = output.get("recovery_snapshot") if isinstance(output.get("recovery_snapshot"), dict) else {}
            candidates = browser_candidates_for_prompt(recovery)
            suffix = f"; recovery_candidates={candidates}" if candidates else ""
            return clip_text(f"error={output.get('error')}; guidance={output.get('recovery_guidance', '')}{suffix}", 700)
        evidence_grounding = output.get("evidence_grounding") if isinstance(output.get("evidence_grounding"), dict) else {}
        if evidence_grounding:
            missing_paths = evidence_grounding.get("missing_candidate_paths")
            if isinstance(missing_paths, list) and missing_paths:
                missing_paths = [path for path in (clean_prompt_candidate_path(item) for item in missing_paths) if path]
                return clip_text(
                    "error=evidence grounding required; missing_exact_paths="
                    + ", ".join(str(path) for path in missing_paths[:8])
                    + "; rewrite the durable record with exact observed paths or state why they are irrelevant",
                    900,
                )
            unsupported = evidence_grounding.get("unsupported_tokens")
            if isinstance(unsupported, list) and unsupported:
                return clip_text(
                    "error=evidence grounding required; unsupported="
                    + ", ".join(str(token) for token in unsupported[:10])
                    + "; use only tokens present in recent observed evidence or record uncertainty",
                    700,
                )
        recent_artifacts = output.get("recent_artifacts") if isinstance(output.get("recent_artifacts"), list) else []
        if tool_name == "read_artifact" and recent_artifacts:
            refs = []
            for artifact in recent_artifacts[:6]:
                if not isinstance(artifact, dict):
                    continue
                number = str(artifact.get("number") or "").strip()
                artifact_id = str(artifact.get("id") or "").strip()
                title = str(artifact.get("title") or "").strip()
                label = artifact_id or number
                if label:
                    refs.append(f"{label}={title}" if title else label)
            suffix = f"; valid_recent_artifacts={'; '.join(refs)}" if refs else ""
            return clip_text(f"error={output.get('error')}; guidance={output.get('guidance') or ''}{suffix}", 900)
        return clip_text(f"error={output.get('error')}; guidance={output.get('guidance') or ''}", 700)
    if tool_name == "web_search":
        results = output.get("results") if isinstance(output.get("results"), list) else []
        titles = []
        for result in results[:5]:
            title = result.get("title") or "untitled"
            url = result.get("url") or ""
            titles.append(f"{title} <{url}>")
        return clip_text(f"query={output.get('query')!r}; results={'; '.join(titles)}", 650)
    if tool_name == "web_extract":
        pages = output.get("pages") if isinstance(output.get("pages"), list) else []
        parts = []
        for page in pages[:3]:
            if page.get("error"):
                parts.append(f"{page.get('url')}: ERROR {page.get('error')}")
            else:
                text = str(page.get("text") or "")
                parts.append(f"{page.get('url')}: {clip_text(text, 160)}")
        return clip_text("; ".join(parts), 650)
    if tool_name == "shell_exec":
        stdout = str(output.get("stdout") or "")
        stderr = str(output.get("stderr") or "")
        excerpt = stdout.strip() or stderr.strip()
        return (
            f"command={output.get('command')!r}; rc={output.get('returncode')}; "
            f"duration={output.get('duration_seconds')}s; output={clip_text(excerpt, 360)}"
        )[:650]
    if tool_name == "write_artifact":
        return f"saved artifact={output.get('artifact_id')} path={output.get('path')}"
    if tool_name == "report_update":
        update = output.get("update") if isinstance(output.get("update"), dict) else {}
        return clip_text(f"agent_update={update.get('message') or ''}", 420)
    if tool_name == "record_lesson":
        lesson = output.get("lesson") if isinstance(output.get("lesson"), dict) else {}
        return clip_text(f"lesson={lesson.get('category') or 'memory'}: {lesson.get('lesson') or ''}", 420)
    if tool_name == "record_memory_graph":
        return (
            f"memory_graph added_nodes={output.get('added_nodes')} "
            f"updated_nodes={output.get('updated_nodes')} added_edges={output.get('added_edges')}"
        )[:520]
    if tool_name == "search_memory_graph":
        nodes = output.get("nodes") if isinstance(output.get("nodes"), list) else []
        titles = [str(node.get("title") or node.get("key") or "memory") for node in nodes[:5] if isinstance(node, dict)]
        return clip_text(f"memory_query={output.get('query')!r}; nodes={'; '.join(titles)}", 520)
    if tool_name == "record_source":
        source = output.get("source") if isinstance(output.get("source"), dict) else {}
        return (
            f"source={source.get('source')} score={source.get('usefulness_score')} "
            f"findings={source.get('yield_count')} fails={source.get('fail_count')} outcome={source.get('last_outcome')}"
        )[:420]
    if tool_name == "record_findings":
        return f"finding ledger updated added={output.get('added')} updated={output.get('updated')}"[:700]
    if tool_name == "record_experiment":
        experiment = output.get("experiment") if isinstance(output.get("experiment"), dict) else {}
        metric = ""
        if experiment.get("metric_value") is not None:
            metric = format_metric_value(
                experiment.get("metric_name") or "metric",
                experiment.get("metric_value"),
                experiment.get("metric_unit") or "",
            )
        delta = f" delta={experiment.get('delta_from_previous_best')}" if experiment.get("delta_from_previous_best") is not None else ""
        best = " best_observed" if experiment.get("best_observed") else ""
        return clip_text(f"experiment={experiment.get('title')} status={experiment.get('status')} {metric}{delta}{best}", 520)
    if tool_name == "acknowledge_operator_context":
        return f"operator_context {output.get('status')} count={output.get('count')}"[:700]
    if tool_name == "browser_navigate":
        data = output.get("data") if isinstance(output.get("data"), dict) else {}
        title = data.get("title") or ""
        url = data.get("url") or ""
        snapshot = str(output.get("snapshot") or "")
        warning = anti_bot_reason(title, url, snapshot)
        suffix = f"; source_warning={warning}" if warning else ""
        candidates = browser_candidates_for_prompt(output)
        candidate_suffix = f"; candidates={candidates}" if candidates else ""
        return clip_text(f"opened {title} <{url}>; snapshot_chars={len(snapshot)}{suffix}{candidate_suffix}", 700)
    if tool_name == "browser_snapshot":
        data = output.get("data") if isinstance(output.get("data"), dict) else {}
        snapshot = str(output.get("snapshot") or data.get("snapshot") or output.get("data") or "")
        warning = anti_bot_reason(snapshot)
        suffix = f"; source_warning={warning}" if warning else ""
        candidates = browser_candidates_for_prompt(output)
        candidate_suffix = f"; candidates={candidates}" if candidates else ""
        return clip_text(f"snapshot_chars={len(snapshot)}{suffix}{candidate_suffix}", 700)
    return compact(output, 700)


def clean_prompt_candidate_path(value: Any) -> str:
    raw = str(value or "").strip().rstrip(".,:;)")
    if not raw or "://" in raw or raw.startswith("//") or "..." in raw or "…" in raw or "*" in raw:
        return ""
    name = Path(raw).name
    suffix = Path(name).suffix
    if not name or name.startswith(".") or not suffix:
        return ""
    if not re.match(r"^\.[A-Za-z0-9][A-Za-z0-9_]{1,12}$", suffix) or not any(ch.isalpha() for ch in suffix):
        return ""
    return raw


def browser_candidates_for_prompt(output: dict[str, Any], *, limit: int = 18) -> str:
    refs = output.get("refs") if isinstance(output.get("refs"), dict) else None
    if refs is None:
        data = output.get("data") if isinstance(output.get("data"), dict) else {}
        refs = data.get("refs") if isinstance(data.get("refs"), dict) else {}
    candidates = []
    seen = set()
    for ref, item in refs.items():
        if not isinstance(item, dict):
            continue
        role = str(item.get("role") or "")
        if role not in {"link", "heading", "cell"}:
            continue
        name = " ".join(str(item.get("name") or "").split())
        key = name.lower().strip()
        if not name or key in BROWSER_REF_IGNORE_NAMES:
            continue
        if len(name) < 3 or len(name) > 90 or key in seen:
            continue
        if role == "cell" and (_looks_like_metric_cell(name) or _looks_like_service_description(name)):
            continue
        seen.add(key)
        candidates.append(f"{name} (@{ref})")
        if len(candidates) >= limit:
            break
    return "; ".join(candidates)


def _looks_like_metric_cell(name: str) -> bool:
    text = name.strip()
    return bool(re.fullmatch(r"(?:n/?a|na|[-+]?\d+(?:\.\d+)?(?:/5)?|[$€£]?\d[\d,]*(?:\.\d+)?%?)", text, re.I))


def _looks_like_service_description(name: str) -> bool:
    text = name.lower()
    if "," in text and len(text.split()) >= 6:
        return True
    service_terms = ("custom ecommerce", "ux/ui", "payment integration", "mobile responsiveness", "headless commerce")
    return any(term in text for term in service_terms) and len(text.split()) >= 5
