"""Parse Claude Code JSONL conversation files."""

from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
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)


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

    type: str
    text: str | None = None
    tool_name: str | None = None
    tool_input: dict[str, Any] | None = 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", or "tool"
    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  # True if this is a tool result message

    @property
    def text(self) -> str:
        """Get all text content from this message."""
        texts = []
        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 Claude Code 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" and not msg.is_meta and msg.text:
                text = msg.text.strip()
                # Skip command messages
                if text.startswith("<"):
                    continue
                # Truncate long titles
                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."""
        return sum(1 for m in self.messages if m.role == "assistant")

    @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 message content into blocks."""
    blocks = []

    if isinstance(content, str):
        blocks.append(MessageBlock(type="text", text=content))
    elif isinstance(content, list):
        for item in content:
            if isinstance(item, dict):
                block_type = item.get("type", "unknown")
                if block_type == "text":
                    blocks.append(MessageBlock(type="text", text=item.get("text", "")))
                elif 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"),
                        )
                    )
                elif block_type == "tool_result":
                    result_content = item.get("content", "")
                    if isinstance(result_content, list):
                        # Extract text from result content blocks
                        texts = []
                        for rc in result_content:
                            if isinstance(rc, dict) and rc.get("type") == "text":
                                texts.append(rc.get("text", ""))
                        result_content = "\n".join(texts)
                    blocks.append(
                        MessageBlock(type="tool_result", text=str(result_content))
                    )
                elif block_type == "thinking":
                    blocks.append(
                        MessageBlock(type="thinking", text=item.get("thinking", ""))
                    )
                else:
                    blocks.append(MessageBlock(type=block_type, text=str(item)))
            elif isinstance(item, str):
                blocks.append(MessageBlock(type="text", text=item))

    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).
    """
    if isinstance(content, str):
        return content, True, False

    if isinstance(content, list):
        texts: list[str] = []
        has_blocks = False
        has_tool_result = False

        for item in content:
            if isinstance(item, dict):
                has_blocks = True
                block_type = item.get("type", "unknown")
                if block_type == "text":
                    texts.append(item.get("text", ""))
                elif block_type == "tool_result":
                    has_tool_result = True
                # Other block types don't contribute to text.
            elif isinstance(item, str):
                has_blocks = True
                texts.append(item)

        return "\n".join(texts), has_blocks, has_tool_result

    return "", False, False


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


def parse_conversation(file_path: Path) -> Conversation:
    """Parse a Claude Code JSONL conversation file."""
    messages_by_uuid: dict[str, Message] = {}
    session_id = file_path.stem
    cwd = None
    git_branch = None
    version = None
    summaries = []

    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

            entry_type = entry.get("type")

            # Skip non-message entries
            if entry_type == "file-history-snapshot":
                continue

            # Extract summaries
            if entry_type == "summary":
                if summary_text := entry.get("summary"):
                    summaries.append(summary_text)
                continue

            # Extract metadata
            if entry.get("sessionId"):
                session_id = entry["sessionId"]
            if entry.get("cwd"):
                cwd = entry["cwd"]
            if entry.get("gitBranch"):
                git_branch = entry["gitBranch"]
            if entry.get("version"):
                version = entry["version"]

            # Parse message entries
            if entry_type in ("user", "assistant") and "message" in entry:
                msg_data = entry["message"]
                uuid = entry.get("uuid", "")
                msg_id = msg_data.get("id", uuid)

                # Use message ID for deduplication (streaming updates share same ID)
                dedup_key = msg_id or uuid

                content = msg_data.get("content", [])
                blocks = parse_content_blocks(content)

                # Skip empty messages
                if not blocks:
                    continue

                timestamp = parse_timestamp(entry.get("timestamp"))
                model = msg_data.get("model")
                is_sidechain = entry.get("isSidechain", False)
                is_meta = entry.get("isMeta", False)

                # Check if this is a tool result (user message containing tool_result blocks)
                is_tool_result = entry_type == "user" and any(
                    b.type == "tool_result" for b in blocks
                )
                role = "tool" if is_tool_result else entry_type

                # Update or create message
                if dedup_key in messages_by_uuid:
                    # Merge blocks (streaming updates)
                    existing = messages_by_uuid[dedup_key]
                    existing.blocks = blocks  # Replace with latest
                    if timestamp:
                        existing.timestamp = timestamp
                else:
                    messages_by_uuid[dedup_key] = Message(
                        uuid=uuid,
                        role=role,
                        blocks=blocks,
                        timestamp=timestamp,
                        model=model,
                        is_sidechain=is_sidechain,
                        is_meta=is_meta,
                        is_tool_result=is_tool_result,
                    )

    # Sort messages by timestamp
    messages = list(messages_by_uuid.values())
    messages.sort(key=lambda m: m.timestamp or datetime.min)

    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 JSONL conversation files in a directory."""
    return sorted(
        directory.glob("*.jsonl"), 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


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

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


def _title_from_user_messages(
    session_id: str, messages: dict[str, dict[str, Any]]
) -> str:
    best_text = None
    best_ts = None

    for data in messages.values():
        if data.get("role", "user") != "user":
            continue
        if data.get("is_meta"):
            continue
        text = (data.get("text") or "").strip()
        if not text or text.startswith("<"):
            continue
        ts = data.get("timestamp") or datetime.min
        if best_ts is None or ts < best_ts:
            best_ts = ts
            best_text = text

    if not best_text:
        return f"Session {session_id[:8]}"

    first_line = best_text.split("\n")[0]
    if len(first_line) > 80:
        return first_line[:77] + "..."
    return first_line


def summarize_conversation(file_path: Path) -> ConversationSummary:
    """Parse just enough to summarize a conversation for listings."""
    messages: dict[str, dict[str, Any]] = {}
    session_id = file_path.stem

    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

            entry_type = entry.get("type")
            if entry_type == "file-history-snapshot":
                continue
            if entry_type == "summary":
                continue

            if entry.get("sessionId"):
                session_id = entry["sessionId"]

            if entry_type in ("user", "assistant") and "message" in entry:
                msg_data = entry["message"]
                uuid = entry.get("uuid", "")
                msg_id = msg_data.get("id", uuid)
                dedup_key = msg_id or uuid

                content = msg_data.get("content", [])
                text, has_blocks, has_tool_result = extract_text_and_flags(content)
                if not has_blocks:
                    continue

                role = (
                    "tool" if entry_type == "user" and has_tool_result else entry_type
                )
                timestamp = parse_timestamp(entry.get("timestamp"))
                is_meta = entry.get("isMeta", False)

                messages[dedup_key] = {
                    "role": role,
                    "timestamp": timestamp,
                    "text": text,
                    "is_meta": is_meta,
                }

    user_count = sum(
        1 for m in messages.values() if m["role"] == "user" and not m["is_meta"]
    )
    assistant_count = sum(1 for m in messages.values() if m["role"] == "assistant")
    start_time = min(
        (m["timestamp"] for m in messages.values() if m["timestamp"]),
        default=None,
    )
    title = _title_from_user_messages(session_id, messages)

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


def _extract_search_entry(
    entry: dict[str, Any],
) -> tuple[str, str, datetime | None] | None:
    """Extract (role, text, timestamp) from a Claude JSONL entry for search."""
    entry_type = entry.get("type")
    if entry_type not in ("user", "assistant"):
        return None
    if "message" not in entry:
        return None

    msg_data = entry["message"]
    content = msg_data.get("content", [])
    text, has_blocks, has_tool_result = extract_text_and_flags(content)
    if not has_blocks or not text:
        return None

    if entry_type == "user" and has_tool_result:
        return None
    if entry.get("isMeta", False):
        return None

    timestamp = parse_timestamp(entry.get("timestamp"))
    return entry_type, text, timestamp


def _fast_title_from_file(file_path: Path) -> tuple[str, str]:
    """Return (session_id, title) for a conversation file."""
    session_id = file_path.stem
    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 entry.get("sessionId"):
                session_id = entry["sessionId"]

            entry_type = entry.get("type")
            if entry_type != "user" or "message" not in entry:
                continue

            msg_data = entry["message"]
            content = msg_data.get("content", [])
            text, has_blocks, has_tool_result = extract_text_and_flags(content)
            if not has_blocks:
                continue

            if entry.get("isMeta", False):
                continue

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

            first_line = text.split("\n")[0]
            if len(first_line) > 80:
                return session_id, first_line[:77] + "..."
            return session_id, first_line

    return session_id, f"Session {session_id[:8]}"


def search_conversations(
    projects_dir: Path, query: str, limit: int
) -> list[SearchMatch]:
    """Search across all conversations, using ripgrep if available."""
    from ._search import search_dirs

    results = search_dirs(
        [projects_dir],
        query,
        limit,
        _extract_search_entry,
        _fast_title_from_file,
        glob_pattern="*.jsonl",
    )
    return [
        SearchMatch(
            session_id=r.session_id,
            title=r.title,
            timestamp=r.timestamp,
            snippet=r.snippet,
        )
        for r in results
    ]


def search_project(project_path: Path, query: str, limit: int) -> list[SearchMatch]:
    """Search conversations within a specific project directory."""
    from ._search import search_files

    files = find_conversations(project_path)
    results = search_files(
        files,
        query,
        limit,
        _extract_search_entry,
        _fast_title_from_file,
    )
    return [
        SearchMatch(
            session_id=r.session_id,
            title=r.title,
            timestamp=r.timestamp,
            snippet=r.snippet,
        )
        for r in results
    ]


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"
