"""
Pure parsing logic for LLM CLI outputs.

This module contains pure functions (no I/O) for extracting text from
various LLM CLI JSON/JSONL outputs. All functions have @pre/@post contracts
and doctests.

Shell layer functions in lattice.shell.llm wrap these with Result types
and handle JSON decode errors.

Reference: RFC-002 §4.3 Project Evolution
"""

from __future__ import annotations

import json
from typing import Any

import deal
from invar_runtime import skip_property_test


@deal.pre(lambda data: isinstance(data, dict))
@deal.post(lambda result: result is None or isinstance(result, str))
def extract_claude_text(data: dict[str, Any]) -> str | None:
    """Extract text from Claude CLI JSON output.

    Claude responses have format:
    - {"content": [{"type": "text", "text": "..."}]} (blocks)
    - {"content": "direct string"} (string)
    - {"result": "fallback"} (fallback field)

    Args:
        data: Parsed JSON dict from Claude CLI.

    Returns:
        Extracted text string, or None if no recognized format.

    >>> extract_claude_text({"content": [{"type": "text", "text": "Hello"}]})
    'Hello'
    >>> extract_claude_text({"content": [{"type": "text", "text": "Hi"}, {"type": "text", "text": " there"}]})
    'Hi there'
    >>> extract_claude_text({"content": "Direct string"})
    'Direct string'
    >>> extract_claude_text({"result": "Fallback result"})
    'Fallback result'
    >>> extract_claude_text({"other": "data"})
    >>> extract_claude_text({})
    >>> extract_claude_text({"content": []})
    >>> extract_claude_text({"content": [{"type": "image", "data": "..."}]})
    """
    # Priority 1: content field with text blocks
    if "content" in data:
        content = data["content"]

        # List of content blocks
        if isinstance(content, list):
            text_parts: list[str] = []
            for block in content:
                if isinstance(block, dict) and block.get("type") == "text":
                    text = block.get("text")
                    if isinstance(text, str):
                        text_parts.append(text)
            if text_parts:
                return "".join(text_parts)
            return None

        # Direct string content
        if isinstance(content, str):
            return content

        # Other content types (number, bool, etc.)
        return str(content) if content else None

    # Priority 2: result field fallback
    if "result" in data:
        result = data["result"]
        if isinstance(result, str):
            return result
        return str(result) if result else None

    return None


@deal.pre(lambda data: isinstance(data, dict))
@deal.post(lambda result: result is None or isinstance(result, str))
def extract_opencode_text(data: dict[str, Any]) -> str | None:
    """Extract text from a single OpenCode JSONL line.

    OpenCode can output in several formats:
    - {"type": "text", "part": {"text": "..."}} (primary)
    - {"content": "..."} (fallback)
    - {"response": "..."} (fallback)

    Args:
        data: Parsed JSON dict from a single JSONL line.

    Returns:
        Extracted text string, or None if no recognized format.

    >>> extract_opencode_text({"type": "text", "part": {"text": "Hello"}})
    'Hello'
    >>> extract_opencode_text({"content": "Direct content"})
    'Direct content'
    >>> extract_opencode_text({"response": "Response text"})
    'Response text'
    >>> extract_opencode_text({"type": "other"})
    >>> extract_opencode_text({})
    """
    # Priority 1: text type with part.text
    if data.get("type") == "text":
        part = data.get("part", {})
        if isinstance(part, dict) and "text" in part:
            text = part["text"]
            if isinstance(text, str):
                return text

    # Priority 2: content field
    if "content" in data:
        content = data["content"]
        if isinstance(content, str):
            return content
        return str(content) if content else None

    # Priority 3: response field
    if "response" in data:
        response = data["response"]
        if isinstance(response, str):
            return response
        return str(response) if response else None

    return None


# json.loads triggers CrossHairInternal in symbolic execution (regex length tracking bug in crosshair/libimpl/relib.py:649)
# crosshair: off
@skip_property_test(
    "crosshair_incompatible: json.loads triggers CrossHairInternal in symbolic execution (regex length tracking bug)"
)
@deal.post(lambda result: result is None or isinstance(result, str))
def extract_text_from_jsonl(text: str) -> str | None:
    """Find and extract text from JSONL output.

    Iterates through JSONL lines and returns the first text found
    using extract_opencode_text.

    Args:
        text: Raw JSONL output (newline-separated JSON objects).

    Returns:
        Extracted text string, or None if no text found in any line.

    >>> extract_text_from_jsonl('{"type": "text", "part": {"text": "Hello"}}')
    'Hello'
    >>> extract_text_from_jsonl('{"other": true}\\n{"type": "text", "part": {"text": "Found"}}')
    'Found'
    >>> extract_text_from_jsonl('{"other": true}\\n\\n{"more": false}')
    >>> extract_text_from_jsonl('')
    >>> extract_text_from_jsonl('not valid json\\n{"type": "text", "part": {"text": "OK"}}')
    'OK'
    """
    lines = text.strip().split("\n")

    for line in lines:
        line = line.strip()
        if not line:
            continue
        try:
            data = json.loads(line)
            if isinstance(data, dict):
                result = extract_opencode_text(data)
                if result is not None:
                    return result
        except json.JSONDecodeError:
            continue

    return None
