"""Parse Codex JSON and JSONL conversation files."""

from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
import re
from typing import Any

try:
    import orjson as _json

    def _loads(line: str) -> Any:
        return _json.loads(line)


except Exception:  # pragma: no cover - fallback for environments without orjson
    import json as _json

    def _loads(line: str) -> Any:
        return _json.loads(line)


def _safe_json_dumps(value: Any) -> str:
    """Convert any value to a JSON-ish string for display."""
    if isinstance(value, str):
        return value
    try:
        import json

        return json.dumps(value, indent=2)
    except Exception:
        return str(value)


def _maybe_parse_json(value: Any) -> Any:
    """Parse JSON string inputs when possible."""
    if not isinstance(value, str):
        return value

    text = value.strip()
    if not text:
        return value

    if text[0] not in "[{":
        return value

    try:
        return _loads(text)
    except ValueError:
        return value


_ENV_CWD_PATTERNS = [
    re.compile(r"<cwd>(.*?)</cwd>", re.DOTALL),
    re.compile(r"Current working directory:\s*(.+)", re.IGNORECASE),
]


def extract_cwd_from_text(text: str) -> str | None:
    """Extract cwd from known environment-context text patterns."""
    if not text:
        return None

    for pattern in _ENV_CWD_PATTERNS:
        match = pattern.search(text)
        if match:
            cwd = match.group(1).strip()
            if cwd:
                return cwd

    return None


def _is_meta_user_text(text: str) -> bool:
    """Detect Codex bootstrap/system user messages that should be hidden by default."""
    stripped = text.strip()
    if not stripped:
        return True

    if stripped.startswith("<"):
        return True

    if stripped.startswith("# AGENTS.md instructions for "):
        return True

    if stripped.startswith("# AGENTS.md instructions"):
        return True

    if stripped.startswith("<environment_context>"):
        return True

    return False


@dataclass
class MessageBlock:
    """A block of content within a message."""

    type: str
    text: str | None = None
    tool_name: str | None = None
    tool_input: Any = None
    tool_id: str | None = None

    def summary(self, max_length: int = 100) -> str:
        """Get a short summary of this block."""
        if self.type == "text":
            text = self.text or ""
            if len(text) > max_length:
                return text[:max_length] + "..."
            return text
        elif self.type == "tool_use":
            return f"[Tool: {self.tool_name}]"
        elif self.type == "tool_result":
            text = self.text or ""
            if len(text) > max_length:
                return f"[Tool Result: {text[:max_length]}...]"
            return f"[Tool Result: {text}]"
        elif self.type == "thinking":
            text = self.text or ""
            if len(text) > max_length:
                return f"[Thinking: {text[:max_length]}...]"
            return f"[Thinking: {text}]"
        else:
            return f"[{self.type}]"


@dataclass
class Message:
    """A single message in a conversation."""

    uuid: str
    role: str  # "user", "assistant", "tool", etc.
    blocks: list[MessageBlock] = field(default_factory=list)
    timestamp: datetime | None = None
    model: str | None = None
    is_sidechain: bool = False
    is_meta: bool = False
    is_tool_result: bool = False

    @property
    def text(self) -> str:
        """Get all text content from this message."""
        texts: list[str] = []
        for block in self.blocks:
            if block.type == "text" and block.text:
                texts.append(block.text)
        return "\n".join(texts)

    @property
    def has_tool_use(self) -> bool:
        """Check if this message contains tool usage."""
        return any(b.type == "tool_use" for b in self.blocks)

    @property
    def tool_names(self) -> list[str]:
        """Get list of tools used in this message."""
        return [
            b.tool_name for b in self.blocks if b.type == "tool_use" and b.tool_name
        ]


@dataclass
class Conversation:
    """A parsed Codex conversation."""

    session_id: str
    messages: list[Message] = field(default_factory=list)
    cwd: str | None = None
    git_branch: str | None = None
    version: str | None = None
    summaries: list[str] = field(default_factory=list)
    file_path: Path | None = None

    @property
    def title(self) -> str:
        """Generate a title from the first user message."""
        for msg in self.messages:
            if msg.role != "user" or msg.is_meta:
                continue
            if not msg.text:
                continue

            text = msg.text.strip()
            if not text or text.startswith("<"):
                continue

            first_line = text.split("\n")[0]
            if len(first_line) > 80:
                return first_line[:77] + "..."
            return first_line
        return f"Session {self.session_id[:8]}"

    @property
    def start_time(self) -> datetime | None:
        """Get the timestamp of the first message."""
        for msg in self.messages:
            if msg.timestamp:
                return msg.timestamp
        return None

    @property
    def end_time(self) -> datetime | None:
        """Get the timestamp of the last message."""
        for msg in reversed(self.messages):
            if msg.timestamp:
                return msg.timestamp
        return None

    @property
    def user_message_count(self) -> int:
        """Count non-meta user messages."""
        return sum(1 for m in self.messages if m.role == "user" and not m.is_meta)

    @property
    def assistant_message_count(self) -> int:
        """Count assistant messages that include visible text content."""
        count = 0
        for message in self.messages:
            if message.role != "assistant":
                continue
            if any(b.type == "text" and (b.text or "").strip() for b in message.blocks):
                count += 1
        return count

    @property
    def tool_use_count(self) -> int:
        """Count total tool uses across all messages."""
        count = 0
        for msg in self.messages:
            count += sum(1 for b in msg.blocks if b.type == "tool_use")
        return count


def parse_content_blocks(content: Any) -> list[MessageBlock]:
    """Parse Codex message content into blocks."""
    blocks: list[MessageBlock] = []

    if isinstance(content, str):
        blocks.append(MessageBlock(type="text", text=content))
        return blocks

    if not isinstance(content, list):
        return blocks

    for item in content:
        if isinstance(item, str):
            blocks.append(MessageBlock(type="text", text=item))
            continue

        if not isinstance(item, dict):
            continue

        block_type = item.get("type", "unknown")
        text = item.get("text")

        if block_type in {
            "input_text",
            "output_text",
            "text",
            "summary_text",
        } and isinstance(text, str):
            blocks.append(MessageBlock(type="text", text=text))
            continue

        if block_type == "tool_use":
            blocks.append(
                MessageBlock(
                    type="tool_use",
                    tool_name=item.get("name"),
                    tool_input=item.get("input"),
                    tool_id=item.get("id"),
                )
            )
            continue

        if block_type == "tool_result":
            result_content = item.get("content", "")
            blocks.append(
                MessageBlock(type="tool_result", text=_safe_json_dumps(result_content))
            )
            continue

        if block_type == "thinking":
            blocks.append(MessageBlock(type="thinking", text=item.get("thinking", "")))
            continue

        if isinstance(text, str):
            blocks.append(MessageBlock(type="text", text=text))
            continue

        if block_type in {"input_image", "output_image", "image"}:
            blocks.append(MessageBlock(type="text", text="[image]"))
            continue

        if block_type in {"input_file", "output_file", "file"}:
            blocks.append(MessageBlock(type="text", text="[file]"))
            continue

    return blocks


def extract_text_and_flags(content: Any) -> tuple[str, bool, bool]:
    """Extract text and block flags from message content.

    Returns (text, has_blocks, has_tool_result).
    """
    blocks = parse_content_blocks(content)
    if not blocks:
        return "", False, False

    texts = [b.text for b in blocks if b.type == "text" and b.text]
    has_tool_result = any(b.type == "tool_result" for b in blocks)
    return "\n".join(texts), True, has_tool_result


def parse_timestamp(ts: str | None) -> datetime | None:
    """Parse an ISO timestamp string."""
    if not ts:
        return None
    try:
        if ts.endswith("Z"):
            ts = ts[:-1] + "+00:00"
        return datetime.fromisoformat(ts)
    except (ValueError, TypeError):
        return None


def _iter_raw_entries(file_path: Path):
    """Yield raw entries from either JSONL or legacy JSON files."""
    if file_path.suffix == ".json":
        try:
            data = _loads(file_path.read_text(encoding="utf-8"))
        except ValueError:
            return

        if not isinstance(data, dict):
            return

        session = data.get("session")
        if isinstance(session, dict):
            yield {"type": "legacy_session_meta", "session": session}

        items = data.get("items")
        if isinstance(items, list):
            for item in items:
                if isinstance(item, dict):
                    yield item
        return

    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue

            try:
                entry = _loads(line)
            except ValueError:
                continue

            if isinstance(entry, dict):
                yield entry


def _extract_reasoning_text(payload: dict[str, Any]) -> str:
    """Extract a readable thinking summary from reasoning payloads."""
    summary = payload.get("summary")
    if isinstance(summary, list):
        parts: list[str] = []
        for item in summary:
            if not isinstance(item, dict):
                continue
            text = item.get("text")
            if isinstance(text, str) and text.strip():
                parts.append(text.strip())
        if parts:
            return "\n\n".join(parts)

    content = payload.get("content")
    if isinstance(content, str) and content.strip():
        return content.strip()

    return ""


def _message_signature(msg: Message) -> tuple:
    """Create a lightweight signature for adjacent duplicate detection."""
    block_sigs = []
    for block in msg.blocks:
        block_sigs.append(
            (
                block.type,
                block.text or "",
                block.tool_name or "",
                str(block.tool_input) if block.tool_input is not None else "",
                block.tool_id or "",
            )
        )

    return (msg.role, tuple(block_sigs))


def _append_message(messages: list[Message], msg: Message) -> None:
    """Append message while removing adjacent duplicates."""
    if messages:
        previous = messages[-1]
        if _message_signature(previous) == _message_signature(msg):
            return
    messages.append(msg)


def parse_conversation(file_path: Path) -> Conversation:
    """Parse a Codex conversation file."""
    session_id = file_path.stem
    cwd = None
    git_branch = None
    version = None
    summaries: list[str] = []
    current_model = None
    messages: list[Message] = []

    for entry in _iter_raw_entries(file_path):
        entry_type = entry.get("type")
        timestamp = parse_timestamp(entry.get("timestamp"))

        # Legacy session metadata from old JSON exports.
        if entry_type == "legacy_session_meta":
            session = entry.get("session")
            if isinstance(session, dict):
                if session.get("id"):
                    session_id = str(session["id"])
                if session.get("timestamp"):
                    timestamp = parse_timestamp(session.get("timestamp")) or timestamp
            continue

        # Modern metadata entry.
        if entry_type == "session_meta":
            payload = entry.get("payload", {})
            if isinstance(payload, dict):
                if payload.get("id"):
                    session_id = str(payload["id"])
                if payload.get("cwd"):
                    cwd = str(payload["cwd"])
                git = payload.get("git")
                if isinstance(git, dict) and git.get("branch"):
                    git_branch = str(git["branch"])
                if payload.get("cli_version"):
                    version = str(payload["cli_version"])
            continue

        # Early JSONL metadata line has no "type".
        if not entry_type and entry.get("id") and entry.get("timestamp"):
            session_id = str(entry.get("id"))
            git = entry.get("git")
            if isinstance(git, dict) and git.get("branch"):
                git_branch = str(git["branch"])
            continue

        if entry_type == "turn_context":
            payload = entry.get("payload")
            if isinstance(payload, dict):
                if payload.get("model"):
                    current_model = str(payload["model"])
                if not cwd and payload.get("cwd"):
                    cwd = str(payload["cwd"])
            continue

        if entry_type == "compacted":
            summaries.append("Context compacted during this session.")
            continue

        if entry.get("record_type") == "state":
            continue

        if entry_type == "event_msg":
            payload = entry.get("payload", {})
            if not isinstance(payload, dict):
                continue

            payload_type = payload.get("type")
            if payload_type == "context_compacted":
                summaries.append("Context compacted during this session.")
                continue

            if payload_type == "turn_aborted":
                summaries.append("Turn aborted.")
                continue

            if payload_type == "agent_reasoning" or payload_type == "token_count":
                continue

            if payload_type in {"user_message", "agent_message"}:
                text = payload.get("message")
                if not isinstance(text, str) or not text.strip():
                    continue

                role = "user" if payload_type == "user_message" else "assistant"
                msg = Message(
                    uuid="",
                    role=role,
                    blocks=[MessageBlock(type="text", text=text)],
                    timestamp=timestamp,
                    model=current_model if role == "assistant" else None,
                    is_meta=role == "user" and _is_meta_user_text(text),
                )

                if not cwd and role == "user":
                    parsed_cwd = extract_cwd_from_text(text)
                    if parsed_cwd:
                        cwd = parsed_cwd

                _append_message(messages, msg)
            continue

        if entry_type == "response_item":
            payload = entry.get("payload", {})
            if not isinstance(payload, dict):
                continue

            payload_type = payload.get("type")

            if payload_type == "message":
                role = str(payload.get("role", "assistant"))
                blocks = parse_content_blocks(payload.get("content", []))

                if not blocks:
                    text = payload.get("message")
                    if isinstance(text, str):
                        blocks = [MessageBlock(type="text", text=text)]

                if not blocks:
                    continue

                message_text = "\n".join(
                    b.text or "" for b in blocks if b.type == "text"
                ).strip()

                msg = Message(
                    uuid=str(payload.get("id") or ""),
                    role=role,
                    blocks=blocks,
                    timestamp=timestamp,
                    model=current_model if role == "assistant" else None,
                    is_meta=role in {"developer", "system"}
                    or (role == "user" and _is_meta_user_text(message_text)),
                )

                if not cwd and role == "user" and message_text:
                    parsed_cwd = extract_cwd_from_text(message_text)
                    if parsed_cwd:
                        cwd = parsed_cwd

                _append_message(messages, msg)
                continue

            if payload_type in {"function_call", "custom_tool_call"}:
                tool_name = payload.get("name")
                raw_input = (
                    payload.get("arguments")
                    if payload_type == "function_call"
                    else payload.get("input")
                )
                parsed_input = _maybe_parse_json(raw_input)
                msg = Message(
                    uuid=str(payload.get("id") or payload.get("call_id") or ""),
                    role="assistant",
                    blocks=[
                        MessageBlock(
                            type="tool_use",
                            tool_name=str(tool_name) if tool_name else None,
                            tool_input=parsed_input,
                            tool_id=str(payload.get("call_id") or ""),
                        )
                    ],
                    timestamp=timestamp,
                    model=current_model,
                )
                _append_message(messages, msg)
                continue

            if payload_type in {"function_call_output", "custom_tool_call_output"}:
                output = payload.get("output", "")
                msg = Message(
                    uuid=str(payload.get("id") or payload.get("call_id") or ""),
                    role="tool",
                    blocks=[
                        MessageBlock(
                            type="tool_result",
                            text=_safe_json_dumps(output),
                            tool_id=str(payload.get("call_id") or ""),
                        )
                    ],
                    timestamp=timestamp,
                    is_tool_result=True,
                )
                _append_message(messages, msg)
                continue

            if payload_type == "reasoning":
                reasoning_text = _extract_reasoning_text(payload)
                if reasoning_text:
                    msg = Message(
                        uuid=str(payload.get("id") or ""),
                        role="assistant",
                        blocks=[MessageBlock(type="thinking", text=reasoning_text)],
                        timestamp=timestamp,
                        model=current_model,
                        is_meta=True,
                    )
                    _append_message(messages, msg)
                continue

            continue

        # Legacy flat event format (older JSONL/JSON)
        if entry_type == "message":
            role = str(entry.get("role", "assistant"))
            blocks = parse_content_blocks(entry.get("content", []))
            if not blocks:
                continue

            message_text = "\n".join(
                b.text or "" for b in blocks if b.type == "text"
            ).strip()

            msg = Message(
                uuid=str(entry.get("id") or ""),
                role=role,
                blocks=blocks,
                timestamp=timestamp,
                model=current_model if role == "assistant" else None,
                is_meta=role in {"developer", "system"}
                or (role == "user" and _is_meta_user_text(message_text)),
            )

            if not cwd and role == "user" and message_text:
                parsed_cwd = extract_cwd_from_text(message_text)
                if parsed_cwd:
                    cwd = parsed_cwd

            _append_message(messages, msg)
            continue

        if entry_type in {"function_call", "custom_tool_call"}:
            tool_name = entry.get("name")
            raw_input = (
                entry.get("arguments")
                if entry_type == "function_call"
                else entry.get("input")
            )
            parsed_input = _maybe_parse_json(raw_input)
            msg = Message(
                uuid=str(entry.get("id") or entry.get("call_id") or ""),
                role="assistant",
                blocks=[
                    MessageBlock(
                        type="tool_use",
                        tool_name=str(tool_name) if tool_name else None,
                        tool_input=parsed_input,
                        tool_id=str(entry.get("call_id") or ""),
                    )
                ],
                timestamp=timestamp,
                model=current_model,
            )
            _append_message(messages, msg)
            continue

        if entry_type in {"function_call_output", "custom_tool_call_output"}:
            output = entry.get("output", "")
            msg = Message(
                uuid=str(entry.get("id") or entry.get("call_id") or ""),
                role="tool",
                blocks=[
                    MessageBlock(
                        type="tool_result",
                        text=_safe_json_dumps(output),
                        tool_id=str(entry.get("call_id") or ""),
                    )
                ],
                timestamp=timestamp,
                is_tool_result=True,
            )
            _append_message(messages, msg)
            continue

        if entry_type == "reasoning":
            reasoning_text = _extract_reasoning_text(entry)
            if reasoning_text:
                msg = Message(
                    uuid=str(entry.get("id") or ""),
                    role="assistant",
                    blocks=[MessageBlock(type="thinking", text=reasoning_text)],
                    timestamp=timestamp,
                    model=current_model,
                    is_meta=True,
                )
                _append_message(messages, msg)

    return Conversation(
        session_id=session_id,
        messages=messages,
        cwd=cwd,
        git_branch=git_branch,
        version=version,
        summaries=summaries,
        file_path=file_path,
    )


def find_conversations(directory: Path) -> list[Path]:
    """Find all conversation files in a Codex sessions directory."""
    files: list[Path] = []

    for path in directory.rglob("*"):
        if not path.is_file():
            continue
        if path.suffix not in {".jsonl", ".json"}:
            continue
        files.append(path)

    return sorted(files, key=lambda p: p.stat().st_mtime, reverse=True)


@dataclass
class ConversationSummary:
    """Lightweight summary of a conversation file."""

    session_id: str
    title: str
    start_time: datetime | None
    user_message_count: int
    assistant_message_count: int
    cwd: str | None = None


@dataclass
class SearchMatch:
    """Search match within a conversation."""

    session_id: str
    title: str
    timestamp: datetime | None
    snippet: str


def _truncate_title(text: str, max_len: int = 80) -> str:
    line = text.split("\n")[0].strip()
    if len(line) > max_len:
        return line[: max_len - 3] + "..."
    return line


def summarize_conversation(file_path: Path) -> ConversationSummary:
    """Parse just enough to summarize a conversation for listings."""
    session_id = file_path.stem
    cwd = None
    title = None
    start_time = None
    user_count = 0
    assistant_count = 0
    last_signature: tuple[str, str] | None = None

    for entry in _iter_raw_entries(file_path):
        entry_type = entry.get("type")
        timestamp = parse_timestamp(entry.get("timestamp"))

        if entry_type == "legacy_session_meta":
            session = entry.get("session")
            if isinstance(session, dict) and session.get("id"):
                session_id = str(session["id"])
            continue

        if entry_type == "session_meta":
            payload = entry.get("payload", {})
            if isinstance(payload, dict):
                if payload.get("id"):
                    session_id = str(payload["id"])
                if payload.get("cwd"):
                    cwd = str(payload["cwd"])
            continue

        if not entry_type and entry.get("id") and entry.get("timestamp"):
            session_id = str(entry.get("id"))
            continue

        if entry_type == "turn_context":
            payload = entry.get("payload")
            if isinstance(payload, dict):
                if not cwd and payload.get("cwd"):
                    cwd = str(payload["cwd"])
            continue

        if entry.get("record_type") == "state":
            continue

        role = None
        text = ""

        if entry_type == "response_item":
            payload = entry.get("payload", {})
            if not isinstance(payload, dict) or payload.get("type") != "message":
                continue

            role = payload.get("role")
            blocks = parse_content_blocks(payload.get("content", []))
            text = "\n".join(b.text or "" for b in blocks if b.type == "text").strip()
            if not text and isinstance(payload.get("message"), str):
                text = payload["message"].strip()

        elif entry_type == "event_msg":
            payload = entry.get("payload", {})
            if not isinstance(payload, dict):
                continue

            payload_type = payload.get("type")
            if payload_type == "user_message":
                role = "user"
                text = str(payload.get("message") or "").strip()
            elif payload_type == "agent_message":
                role = "assistant"
                text = str(payload.get("message") or "").strip()
            else:
                continue

        elif entry_type == "message":
            role = entry.get("role")
            blocks = parse_content_blocks(entry.get("content", []))
            text = "\n".join(b.text or "" for b in blocks if b.type == "text").strip()

        if role not in {"user", "assistant"} or not text:
            continue

        signature = (role, text)
        if last_signature == signature:
            continue
        last_signature = signature

        if not cwd and role == "user":
            parsed_cwd = extract_cwd_from_text(text)
            if parsed_cwd:
                cwd = parsed_cwd

        if timestamp and (start_time is None or timestamp < start_time):
            start_time = timestamp

        if role == "user":
            if _is_meta_user_text(text):
                continue
            user_count += 1
            if title is None:
                title = _truncate_title(text)

        elif role == "assistant":
            assistant_count += 1

    if not title:
        title = f"Session {session_id[:8]}"

    return ConversationSummary(
        session_id=session_id,
        title=title,
        start_time=start_time,
        user_message_count=user_count,
        assistant_message_count=assistant_count,
        cwd=cwd,
    )


def _extract_search_entry(
    entry: dict[str, Any],
) -> tuple[str, str, datetime | None] | None:
    """Extract (role, text, timestamp) from a Codex entry for search."""
    entry_type = entry.get("type")
    timestamp = parse_timestamp(entry.get("timestamp"))
    role = None
    text = ""

    if entry_type == "response_item":
        payload = entry.get("payload", {})
        if not isinstance(payload, dict) or payload.get("type") != "message":
            return None
        role = payload.get("role")
        blocks = parse_content_blocks(payload.get("content", []))
        text = "\n".join(b.text or "" for b in blocks if b.type == "text").strip()
        if not text and isinstance(payload.get("message"), str):
            text = payload["message"].strip()

    elif entry_type == "event_msg":
        payload = entry.get("payload", {})
        if not isinstance(payload, dict):
            return None
        payload_type = payload.get("type")
        if payload_type == "user_message":
            role = "user"
            text = str(payload.get("message") or "").strip()
        elif payload_type == "agent_message":
            role = "assistant"
            text = str(payload.get("message") or "").strip()
        else:
            return None

    elif entry_type == "message":
        role = entry.get("role")
        blocks = parse_content_blocks(entry.get("content", []))
        text = "\n".join(b.text or "" for b in blocks if b.type == "text").strip()

    else:
        return None

    if role not in {"user", "assistant"} or not text:
        return None
    if role == "user" and _is_meta_user_text(text):
        return None

    return role, text, timestamp


def _title_fn(file_path: Path) -> tuple[str, str]:
    """Return (session_id, title) for a Codex conversation file."""
    summary = summarize_conversation(file_path)
    return summary.session_id, summary.title


def search_files(files: list[Path], query: str, limit: int) -> list[SearchMatch]:
    """Search a selected set of conversation files."""
    from claude_history._search import search_files as _search_files

    results = _search_files(files, query, limit, _extract_search_entry, _title_fn)
    return [
        SearchMatch(
            session_id=r.session_id,
            title=r.title,
            timestamp=r.timestamp,
            snippet=r.snippet,
        )
        for r in results
    ]


def search_conversations(
    sessions_dir: Path, query: str, limit: int
) -> list[SearchMatch]:
    """Search across all Codex conversations."""
    return search_files(find_conversations(sessions_dir), query, limit)


def search_project(
    project_files: list[Path], query: str, limit: int
) -> list[SearchMatch]:
    """Search conversations within a specific project file list."""
    return search_files(project_files, query, limit)


def get_file_size_human(path: Path) -> str:
    """Get human-readable file size."""
    size = path.stat().st_size
    for unit in ["B", "KB", "MB", "GB"]:
        if size < 1024:
            return f"{size:.1f}{unit}"
        size /= 1024
    return f"{size:.1f}TB"
