"""
Parsing helpers — tool-call extraction, JSON normalisation, filler stripping.

Extracted from main.py to keep individual modules manageable.
"""

import json
import re


# ---------------------------------------------------------------------------
# Tool-call extraction
# ---------------------------------------------------------------------------

def _extract_tool_call(response_text: str):
    """Parse TOOL_CALL: JSON (or XML invoke) from LLM output.

    Returns (tool_dict | None, cleaned_text, had_tool_marker).
    """
    response_text = str(response_text or "")

    marker = "TOOL_CALL:"
    marker_idx = response_text.find(marker)
    if marker_idx == -1:
        xml_match = re.search(r'<invoke\s+name=["\']([\w_]+)["\']\s*>', response_text)
        if xml_match:
            tool_name = xml_match.group(1)
            args = {}
            for pm in re.finditer(r'<parameter\s+name=["\']([\w_]+)["\']>(.*?)</parameter>', response_text, re.DOTALL):
                args[pm.group(1)] = pm.group(2).strip()
            cleaned = re.sub(r'<function_calls>.*?</function_calls>', '', response_text, flags=re.DOTALL).strip()
            if not cleaned:
                cleaned = re.sub(r'<invoke.*?</invoke>', '', response_text, flags=re.DOTALL).strip()
            return {"tool": tool_name, "args": args}, _strip_tool_call_artifacts(cleaned), True
        return None, response_text, False

    brace_start = response_text.find("{", marker_idx)
    if brace_start == -1:
        cleaned = response_text.replace(marker, "").strip()
        cleaned = _strip_tool_call_artifacts(cleaned)
        return None, cleaned, True

    depth = 0
    in_string = False
    escaped = False
    brace_end = -1

    for pos, ch in enumerate(response_text[brace_start:], start=brace_start):
        if escaped:
            escaped = False
            continue
        if ch == "\\" and in_string:
            escaped = True
            continue
        if ch == '"':
            in_string = not in_string
            continue
        if in_string:
            continue
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                brace_end = pos
                break

    if brace_end == -1:
        cleaned = (response_text[:marker_idx]).strip()
        cleaned = _strip_tool_call_artifacts(cleaned)
        return None, cleaned, True

    json_payload = response_text[brace_start:brace_end + 1]
    cleaned = (response_text[:marker_idx] + response_text[brace_end + 1:]).strip()
    cleaned = _strip_tool_call_artifacts(cleaned)
    try:
        return json.loads(json_payload), cleaned, True
    except Exception:
        return None, cleaned, True


# ---------------------------------------------------------------------------
# Filler-phrase stripping  (soul.md §1)
# ---------------------------------------------------------------------------

_FILLER_PATTERNS = [
    re.compile(r"^(?:great\s+question|good\s+question)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:that'?s?\s+a\s+(?:great|good|interesting)\s+question)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:i'?d?\s+be\s+happy\s+to\s+help(?:\s+(?:you\s+)?with\s+that)?)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:sure[,!]?\s*(?:i\s+can\s+help(?:\s+with\s+that)?)?)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:of\s+course[,!]?\s*(?:i\s+can\s+help)?)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:absolutely[,!]?\s*(?:i\s+can\s+help)?)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:certainly[,!]?\s*(?:i\s+can\s+help)?)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:no\s+problem[,!]?\s*)", re.IGNORECASE),
    re.compile(r"^(?:let\s+me\s+(?:help\s+you\s+with\s+that|look\s+into\s+that\s+for\s+you|assist\s+you))[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:i'?ll\s+(?:help\s+you\s+with\s+that|look\s+into\s+that\s+for\s+you|take\s+care\s+of\s+that))[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:(?:graag|natuurlijk|zeker)[,!]?\s*(?:ik\s+(?:help|kan)\s+(?:je\s+)?(?:graag|daarmee|daarbij(?:\s+helpen)?)?[.,!]?\s*)?)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:(?:goede|mooie)\s+vraag)[.!]?\s*", re.IGNORECASE),
    re.compile(r"^(?:ik\s+help\s+je\s+(?:graag|daar\s+graag\s+(?:mee|bij)))[.!]?\s*", re.IGNORECASE),
]

_FILLER_CLOSING_PATTERNS = [
    re.compile(r"\s*(?:(?:is\s+there\s+anything\s+else|anything\s+else)\s+(?:I\s+can\s+help\s+(?:you\s+)?with|you(?:'d|\s+would)\s+like\s+(?:to\s+know|me\s+to\s+do))\??)\s*$", re.IGNORECASE),
    re.compile(r"\s*(?:(?:let\s+me\s+know|feel\s+free\s+to\s+ask)\s+if\s+(?:you\s+(?:need|have|want)\s+(?:anything|something)\s+else|there'?s?\s+anything\s+else))[.!]?\s*$", re.IGNORECASE),
    re.compile(r"\s*(?:(?:laat\s+(?:het\s+)?(?:me\s+)?weten|vraag\s+gerust)\s+(?:als|of)\s+(?:je\s+(?:nog\s+)?(?:iets|wat)\s+(?:anders\s+)?(?:wilt|nodig\s+hebt)))[.!]?\s*$", re.IGNORECASE),
    re.compile(r"\s*(?:I\s+hope\s+(?:this|that)\s+helps)[.!]?\s*$", re.IGNORECASE),
    re.compile(r"\s*(?:(?:ik\s+)?hoop\s+dat\s+(?:dit|het)\s+helpt)[.!]?\s*$", re.IGNORECASE),
]


def _strip_filler_phrases(text: str) -> str:
    """Remove performative filler from the start/end of LLM responses (soul.md §1)."""
    if not text:
        return text
    value = text
    for pat in _FILLER_PATTERNS:
        value = pat.sub("", value, count=1)
    for pat in _FILLER_CLOSING_PATTERNS:
        value = pat.sub("", value, count=1)
    value = value.strip()
    return value if value else text


# ---------------------------------------------------------------------------
# Tool-call artifact cleanup
# ---------------------------------------------------------------------------

def _strip_tool_call_artifacts(text: str) -> str:
    value = str(text or "")
    value = re.sub(r"(?is)TOOL_CALL\s*:\s*\{.*?\}", "", value)
    value = re.sub(r"<\s*[\w.-]+:tool_call[^>]*>", "", value, flags=re.IGNORECASE)
    value = re.sub(r"</?\s*[\w.-]+:tool_call\s*>", "", value, flags=re.IGNORECASE)
    value = re.sub(r"<\s*tool_call[^>]*>", "", value, flags=re.IGNORECASE)
    value = re.sub(r"</?\s*tool_call\s*>", "", value, flags=re.IGNORECASE)
    value = re.sub(r"<function_calls>.*?</function_calls>", "", value, flags=re.IGNORECASE | re.DOTALL)
    return value.strip()


# ---------------------------------------------------------------------------
# JSON helpers
# ---------------------------------------------------------------------------

def _tool_json_message(payload: object) -> str:
    if not isinstance(payload, dict):
        return ""

    msg = str(payload.get("message") or "").strip()
    if msg:
        return msg

    data = payload.get("data")
    if isinstance(data, dict):
        title = str(data.get("title") or "").strip()
        content = str(data.get("content") or "").strip()
        if content:
            content = re.sub(r"\s+", " ", content).strip()
            snippets = re.split(r"(?<=[.!?])\s+", content)
            excerpt = " ".join(snippets[:2]).strip() or content[:420]
            excerpt = excerpt[:500].strip()
            if title:
                return f"{title}\n\n{excerpt}"
            return excerpt
        if title:
            return title
    return ""


def _normalize_tool_json_reply(text: str, fallback_tool_result: str = "") -> str:
    value = _strip_tool_call_artifacts(text)

    json_blocks = re.findall(r"```(?:json)?\s*([\s\S]*?)```", value, flags=re.IGNORECASE)
    for block in json_blocks:
        candidate = block.strip()
        if not candidate:
            continue
        try:
            parsed = json.loads(candidate)
        except Exception:
            continue
        message = _tool_json_message(parsed)
        if message:
            return message

    if re.search(r'"status"\s*:\s*"(?:ok|error)"', value, re.IGNORECASE):
        fragment = _extract_balanced_json_fragment(value, "{")
        if fragment:
            try:
                parsed = json.loads(fragment)
            except Exception:
                parsed = None
            message = _tool_json_message(parsed)
            if message:
                return message

    if json_blocks:
        value = re.sub(r"```(?:json)?[\s\S]*?```", "", value, flags=re.IGNORECASE)

    value = re.sub(r"(?m)^\s*}\s*$", "", value)
    value = re.sub(r"\n{3,}", "\n\n", value)
    value = value.strip()

    if len(re.sub(r"[{}\s]+", "", value)) >= 24:
        return value

    fallback = str(fallback_tool_result or "").strip()
    if not fallback:
        return value

    try:
        fallback_parsed = json.loads(fallback)
    except Exception:
        fallback_parsed = None

    fallback_msg = _tool_json_message(fallback_parsed)
    if fallback_msg:
        return fallback_msg

    compact = re.sub(r"\s+", " ", fallback).strip()
    if not compact:
        return value
    snippets = re.split(r"(?<=[.!?])\s+", compact)
    excerpt = " ".join(snippets[:2]).strip() or compact[:420]
    return excerpt[:500].strip()


def _extract_balanced_json_fragment(text: str, opening: str) -> str:
    closing = "]" if opening == "[" else "}"
    start = text.find(opening)
    if start == -1:
        return ""

    depth = 0
    in_string = False
    escaped = False
    for pos, ch in enumerate(text[start:], start=start):
        if escaped:
            escaped = False
            continue
        if ch == "\\" and in_string:
            escaped = True
            continue
        if ch == '"':
            in_string = not in_string
            continue
        if in_string:
            continue
        if ch == opening:
            depth += 1
        elif ch == closing:
            depth -= 1
            if depth == 0:
                return text[start:pos + 1]
    return ""


def _parse_model_json(raw_text: str, *, expect: str = "any"):
    value = str(raw_text or "").strip()
    if not value:
        raise ValueError("Empty model output")

    cleaned = re.sub(r"<think>[\s\S]*?</think>", "", value, flags=re.IGNORECASE)
    cleaned = re.sub(r"(?im)^\s*</?think>\s*$", "", cleaned)
    cleaned = re.sub(r"(?im)^\s*`{2,}\s*[a-z]*\s*$", "", cleaned)
    cleaned = re.sub(r"(?im)^\s*```\s*[a-z]*\s*$", "", cleaned)
    cleaned = cleaned.strip()

    candidates: list[str] = []
    if cleaned:
        candidates.append(cleaned)

    openings: list[str]
    if expect == "array":
        openings = ["["]
    elif expect == "object":
        openings = ["{"]
    else:
        openings = ["[", "{"]

    for opening in openings:
        fragment = _extract_balanced_json_fragment(cleaned, opening)
        if fragment:
            candidates.append(fragment)

    last_error = None
    for candidate in candidates:
        try:
            parsed = json.loads(candidate)
        except Exception as exc:
            last_error = exc
            continue
        if expect == "array" and not isinstance(parsed, list):
            last_error = ValueError("Expected JSON array")
            continue
        if expect == "object" and not isinstance(parsed, dict):
            last_error = ValueError("Expected JSON object")
            continue
        return parsed

    if last_error:
        raise last_error
    raise ValueError("Could not parse model JSON")


def _parse_planner_output(raw_text: str) -> dict:
    parsed = _parse_model_json(raw_text, expect="any")
    routine_id = ""

    if isinstance(parsed, list):
        raw_steps = parsed
    elif isinstance(parsed, dict):
        routine_id = str(parsed.get("routine_id") or "").strip().lower()
        steps_value = parsed.get("steps")
        if isinstance(steps_value, list):
            raw_steps = steps_value
        elif routine_id:
            raw_steps = []
        else:
            raise ValueError("Planner object must include 'steps' or 'routine_id'")
    else:
        raise ValueError("Planner output must be a JSON array or object")

    steps: list[dict] = []
    for item in raw_steps:
        if isinstance(item, str):
            steps.append({"step": item, "needs_search": False, "search_hint": ""})
        elif isinstance(item, dict) and "step" in item:
            steps.append({
                "step": str(item.get("step") or "").strip(),
                "needs_search": bool(item.get("needs_search", False)),
                "search_hint": str(item.get("search_hint") or "").strip(),
            })
        else:
            raise ValueError(f"Unexpected plan item: {item}")

    return {"routine_id": routine_id, "steps": steps}
