"""Per-run tool path anchors for loop guardrails and recovery."""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from typing import Any

from utils.path_normalize import normalize_path_bucket

_PROJECT_CREATED_RE = re.compile(
    r"Project\s+(?:'[^']+'\s+)?created\s+at\s+(\S+)",
    re.IGNORECASE,
)
_FILE_SAVED_RE = re.compile(
    r"File saved:\s*(.+?)(?:\s+\(\d+\s+bytes\))?\.?$",
    re.IGNORECASE,
)
_INDEX_HTML_RE = re.compile(
    r"index\.html:\s*\d+\s+bytes\s+\(([^)]+)\)",
    re.IGNORECASE,
)


def _parse_tool_args(raw: Any) -> dict[str, Any]:
    if isinstance(raw, dict):
        return dict(raw)
    if isinstance(raw, str) and raw.strip():
        try:
            parsed = json.loads(raw)
            if isinstance(parsed, dict):
                return parsed
        except Exception:
            pass
    return {}


def _path_from_args(tool_name: str, args: dict[str, Any]) -> str | None:
    if not args:
        return None
    try:
        from tool_arg_normalizer import normalize_tool_args, iter_scalar_top_level_path_fields

        work = normalize_tool_args(tool_name, dict(args))
        for field_name, meta in iter_scalar_top_level_path_fields(tool_name):
            if meta.get("intent") not in ("read", "write", "rw"):
                continue
            val = work.get(field_name)
            if isinstance(val, str) and val.strip():
                return val.strip()
    except Exception:
        pass
    for key in ("filepath", "dir_path", "file_path", "path", "project_name"):
        val = args.get(key)
        if isinstance(val, str) and val.strip():
            return val.strip()
    return None


@dataclass
class RunToolAnchorState:
    anchors: dict[str, str] = field(default_factory=dict)
    recent_path_attempts: list[str] = field(default_factory=list)
    last_research_topic: str = ""
    _workspace: str = ""

    def configure_workspace(self, workspace: str) -> None:
        self._workspace = (workspace or "").strip()

    def _remember_path(self, path: str) -> None:
        norm = normalize_path_bucket(path, workspace=self._workspace)
        if not norm:
            return
        if norm in self.recent_path_attempts:
            self.recent_path_attempts.remove(norm)
        self.recent_path_attempts.append(norm)
        if len(self.recent_path_attempts) > 8:
            self.recent_path_attempts = self.recent_path_attempts[-8:]

    def record_tool_call(
        self,
        name: str,
        args: dict[str, Any] | str,
        result_text: str,
        *,
        success: bool = True,
    ) -> None:
        tool = (name or "").strip()
        if not tool:
            return
        parsed_args = _parse_tool_args(args)
        path_val = _path_from_args(tool, parsed_args)
        if path_val:
            self._remember_path(path_val)
            if tool == "read_file":
                self.anchors["last_read_path"] = path_val
            elif tool in ("write_text_file", "save_to_file", "append_to_text_file"):
                self.anchors["last_write_path"] = path_val
            elif tool == "list_directory":
                self.anchors["last_list_dir"] = path_val

        if tool == "hyperframes_create_video":
            slug = parsed_args.get("project_name")
            if isinstance(slug, str) and slug.strip():
                self.anchors["hyperframes.project_slug"] = slug.strip()
            try:
                from utils.hyperframes_manager import resolve_project_dir

                if isinstance(slug, str) and slug.strip():
                    proj_dir, _err = resolve_project_dir(slug.strip())
                    if proj_dir:
                        self.anchors["hyperframes.project_dir"] = proj_dir
                        self._remember_path(proj_dir)
            except Exception:
                pass

        if success and tool in ("deep_web_search", "search_web", "plan_research_subquestions"):
            q = parsed_args.get("query") or parsed_args.get("topic")
            if isinstance(q, str) and q.strip():
                self.last_research_topic = q.strip()[:500]

        self._absorb_result_patterns(result_text)

    def _absorb_result_patterns(self, result_text: str) -> None:
        """Extract known path anchors from tool result prose (tool name optional)."""
        text = (result_text or "").strip()
        if not text:
            return
        m = _PROJECT_CREATED_RE.search(text)
        if m:
            self.anchors["last_project_dir"] = m.group(1)
            self._remember_path(m.group(1))
        m = _FILE_SAVED_RE.search(text)
        if m:
            self.anchors["last_write_path"] = m.group(1)
            self._remember_path(m.group(1))
        m = _INDEX_HTML_RE.search(text)
        if m:
            self.anchors["last_index_html"] = m.group(1)
            self._remember_path(m.group(1))

    def format_summary(self, *, max_chars: int = 400) -> str:
        if not self.anchors:
            return ""
        lines: list[str] = []
        for key, val in self.anchors.items():
            lines.append(f"- {key}: {val}")
        body = "\n".join(lines)
        if len(body) > max_chars:
            body = body[: max_chars - 3].rstrip() + "…"
        return body

    def format_recent_paths(self) -> str:
        if not self.recent_path_attempts:
            return ""
        return ", ".join(self.recent_path_attempts[-5:])

    def _tool_result_success(self, content: str) -> bool:
        lower = content.strip().lower()
        return not lower.startswith(
            ("path does not exist", "error", "failed", "[policy blocked]")
        )

    def _record_from_assistant_tool_call(self, tc: dict[str, Any], result_text: str) -> None:
        if not isinstance(tc, dict):
            return
        fn = tc.get("function") if isinstance(tc.get("function"), dict) else tc
        if not isinstance(fn, dict):
            return
        tool_name = str(fn.get("name") or tc.get("name") or "").strip()
        if not tool_name:
            return
        raw_args = fn.get("arguments") or tc.get("arguments") or "{}"
        tool_args = _parse_tool_args(raw_args)
        self.record_tool_call(
            tool_name,
            tool_args,
            result_text,
            success=self._tool_result_success(result_text),
        )

    def _find_assistant_tool_call_by_id(
        self,
        messages: list[dict[str, Any]],
        start_index: int,
        tool_call_id: str,
    ) -> dict[str, Any] | None:
        if not tool_call_id:
            return None
        for j in range(start_index, max(-1, start_index - 6), -1):
            prev = messages[j]
            if not isinstance(prev, dict) or prev.get("role") != "assistant":
                continue
            for tc in prev.get("tool_calls") or []:
                if not isinstance(tc, dict):
                    continue
                fn = tc.get("function") if isinstance(tc.get("function"), dict) else tc
                cid = str(tc.get("id") or (fn.get("id") if isinstance(fn, dict) else "") or "")
                if cid == tool_call_id:
                    return tc
        return None

    def hydrate_from_messages(self, messages: list[dict[str, Any]], *, max_tool_turns: int = 24) -> None:
        """Rebuild anchors from recent tool-call / tool-result pairs in conversation history."""
        if not isinstance(messages, list):
            return
        pending: list[tuple[dict[str, Any] | None, str]] = []
        seen = 0
        i = len(messages) - 1
        while i >= 0 and seen < max_tool_turns:
            msg = messages[i]
            if not isinstance(msg, dict):
                i -= 1
                continue
            role = msg.get("role")
            if role == "tool":
                content = msg.get("content")
                if not isinstance(content, str):
                    i -= 1
                    continue
                tc_id = str(msg.get("tool_call_id") or "").strip()
                matched = self._find_assistant_tool_call_by_id(messages, i - 1, tc_id)
                if matched is None and not tc_id:
                    for j in range(i - 1, max(-1, i - 6), -1):
                        prev = messages[j]
                        if not isinstance(prev, dict) or prev.get("role") != "assistant":
                            continue
                        tcs = [
                            tc for tc in (prev.get("tool_calls") or [])
                            if isinstance(tc, dict)
                        ]
                        if len(tcs) == 1:
                            matched = tcs[0]
                        break
                if matched is not None:
                    pending.append((matched, content))
                    seen += 1
            elif role == "user":
                from utils.transcript_roles import is_tool_result_carrier_message

                if is_tool_result_carrier_message(msg):
                    content = msg.get("content")
                    if not isinstance(content, str):
                        i -= 1
                        continue
                    from utils.transcript_roles import _TOOL_RESPONSE_BLOCK_RE

                    blocks = [
                        m.group(1).strip()
                        for m in _TOOL_RESPONSE_BLOCK_RE.finditer(content)
                        if m.group(1).strip()
                    ]
                    xml_calls: list[dict[str, Any]] = []
                    for j in range(i - 1, max(-1, i - 6), -1):
                        prev = messages[j]
                        if not isinstance(prev, dict) or prev.get("role") != "assistant":
                            continue
                        if prev.get("tool_calls"):
                            for tc in prev.get("tool_calls") or []:
                                if isinstance(tc, dict):
                                    xml_calls.append(tc)
                            break
                        prev_content = prev.get("content")
                        if isinstance(prev_content, str) and "<tool_call" in prev_content.lower():
                            try:
                                from agent_loop_tool_parse import _parse_xml_tool_calls

                                xml_calls = _parse_xml_tool_calls(prev_content)
                            except Exception:
                                xml_calls = []
                            break
                    for idx, block_text in enumerate(blocks):
                        tc = xml_calls[idx] if idx < len(xml_calls) else None
                        pending.append((tc, block_text))
                    if blocks:
                        seen += len(blocks)
            i -= 1
        pending.reverse()
        for tc, content in pending:
            if tc is not None:
                self._record_from_assistant_tool_call(tc, content)
            else:
                self._absorb_result_patterns(content)
