"""
Claude Code transcript parser.

Parses JSONL transcript files generated by Claude Code CLI.
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from typing import Any

from gobby.sessions.transcripts.base import ParsedMessage, TokenUsage

logger = logging.getLogger(__name__)


class ClaudeTranscriptParser:
    """
    Parses JSONL transcript files from Claude Code.

    Implements the TranscriptParser protocol for Claude Code's specific
    transcript format. Session boundaries are marked by /clear commands.

    This is a stateless utility class that provides methods for reading,
    parsing, and analyzing transcript files. It does not maintain any
    session state and can be shared across multiple sessions.

    Thread-safe: All methods are stateless and can be called concurrently.
    """

    def __init__(self, logger_instance: logging.Logger | None = None):
        """
        Initialize ClaudeTranscriptParser.

        Args:
            logger_instance: Optional logger instance to use. If not provided,
                           uses the module-level logger.
        """
        self.logger = logger_instance or logger

    def extract_last_messages(
        self, turns: list[dict[str, Any]], num_pairs: int = 2
    ) -> list[dict[str, Any]]:
        """
        Extract last N user<>agent message pairs from transcript.

        Args:
            turns: List of transcript turns
            num_pairs: Number of user/agent message pairs to extract (default: 2)

        Returns:
            List of message dicts with "role" and "content" fields, ordered chronologically

        Example:
            >>> parser = ClaudeTranscriptParser()
            >>> messages = parser.read_jsonl("/path/to/transcript.jsonl")
            >>> last_msgs = parser.extract_last_messages(messages, num_pairs=3)
            >>> len(last_msgs)
            6  # 3 pairs = 6 messages
        """
        messages: list[dict[str, str]] = []
        for turn in reversed(turns):
            # Claude Code transcript structure has message nested
            message = turn.get("message", {})
            role = message.get("role")
            if role in ["user", "assistant"]:
                content = message.get("content", "")

                # Assistant messages have content as array of blocks
                if isinstance(content, list):
                    text_parts = []
                    for block in content:
                        if isinstance(block, dict) and block.get("type") == "text":
                            text_parts.append(block.get("text", ""))
                    content = " ".join(text_parts)

                messages.insert(0, {"role": role, "content": str(content)})
                if len(messages) >= num_pairs * 2:
                    break
        return messages

    def extract_turns_since_clear(
        self, turns: list[dict[str, Any]], max_turns: int = 50
    ) -> list[dict[str, Any]]:
        """
        Extract turns since the most recent /clear, up to max_turns.

        Logic:
        1. Find most recent /clear in the transcript (handling consecutive /clears as one boundary)
        2. Start from the turn AFTER the last /clear
        3. Go back up to max_turns but stop if we hit another /clear
        4. Consecutive /clear commands are treated as a single conversation boundary

        Args:
            turns: List of all transcript turns
            max_turns: Maximum number of turns to extract (default: 50)

        Returns:
            List of turns representing the current conversation segment

        Example:
            Turn 100: user message
            Turn 101: /clear
            Turn 102: /clear (consecutive)
            Turn 103: user message
            Turn 104: agent message

            -> Returns turns 103-104 (after the /clear cluster)
        """
        if not turns:
            return []

        # Find the most recent /clear by scanning backwards
        # We want the LAST /clear in any consecutive cluster
        most_recent_clear_idx = None
        for i in range(len(turns) - 1, -1, -1):
            if self.is_session_boundary(turns[i]):
                # Found a /clear - this is the most recent one
                most_recent_clear_idx = i
                break

        # If no /clear found, just take the last max_turns
        if most_recent_clear_idx is None:
            result = turns[-max_turns:] if len(turns) > max_turns else turns
            result, removed = self._validate_tool_pairing(result)
            if removed:
                self.logger.debug(f"Removed {len(removed)} orphaned tool_results: {removed}")
            return result

        # Start after this /clear (which is the last in any cluster since we scanned backwards)
        start_idx = most_recent_clear_idx + 1
        end_idx = len(turns)

        # Now go backwards from the /clear we found to check for:
        # 1. Another /clear (conversation boundary)
        # 2. Max turns limit
        # We want at most max_turns AFTER the most recent /clear
        # So if we have 150 total turns and most_recent_clear is at 100,
        # we want to limit to turns 101-150 (50 turns) if end is at 150

        # If the segment after most_recent_clear is already <= max_turns, we're done
        segment_size = end_idx - start_idx
        if segment_size <= max_turns:
            # Check if there's a previous /clear we should respect
            search_idx = most_recent_clear_idx - 1

            # Skip consecutive /clears going backwards
            while search_idx >= 0 and self.is_session_boundary(turns[search_idx]):
                search_idx -= 1

            # Search for previous /clear boundary (no max_turns limit since current segment is small)
            for i in range(search_idx, -1, -1):
                if self.is_session_boundary(turns[i]):
                    # Found previous /clear - make sure we start after it
                    boundary_idx = i
                    # Skip forward over consecutive /clears
                    while boundary_idx < most_recent_clear_idx and self.is_session_boundary(
                        turns[boundary_idx + 1]
                    ):
                        boundary_idx += 1
                    start_idx = max(start_idx, boundary_idx + 1)
                    break

            result = turns[start_idx:end_idx]
            result, removed = self._validate_tool_pairing(result)
            if removed:
                self.logger.debug(f"Removed {len(removed)} orphaned tool_results: {removed}")
            return result

        # Segment is > max_turns, so we need to limit it
        # Take the last max_turns from the segment
        start_idx = end_idx - max_turns

        # But make sure we don't cross a /clear boundary
        search_idx = most_recent_clear_idx - 1

        # Skip consecutive /clears going backwards
        while search_idx >= 0 and self.is_session_boundary(turns[search_idx]):
            search_idx -= 1

        # Check if there's a /clear between start_idx and most_recent_clear_idx
        for i in range(most_recent_clear_idx - 1, start_idx - 1, -1):
            if self.is_session_boundary(turns[i]):
                # Found a /clear in our window - start after it
                boundary_idx = i
                while boundary_idx < most_recent_clear_idx and self.is_session_boundary(
                    turns[boundary_idx + 1]
                ):
                    boundary_idx += 1
                start_idx = boundary_idx + 1
                break

        result = turns[start_idx:end_idx]
        result, removed = self._validate_tool_pairing(result)
        if removed:
            self.logger.debug(f"Removed {len(removed)} orphaned tool_results: {removed}")
        return result

    def is_session_boundary(self, turn: dict[str, Any]) -> bool:
        """
        Check if a turn is a session boundary (/clear command).

        Args:
            turn: Transcript turn dict

        Returns:
            True if turn contains a /clear command marker

        Example:
            >>> parser = ClaudeTranscriptParser()
            >>> turn = {"type": "user", "message": {"content": "<command-name>/clear</command-name>"}}
            >>> parser.is_session_boundary(turn)
            True
        """
        if turn.get("type") != "user":
            return False

        message = turn.get("message", {})
        content = message.get("content", "")

        # Check for /clear command marker
        # Check for /clear command marker
        return "<command-name>/clear</command-name>" in str(content)

    def _validate_tool_pairing(
        self, turns: list[dict[str, Any]]
    ) -> tuple[list[dict[str, Any]], list[str]]:
        """Remove orphaned tool_results that reference missing tool_use blocks.

        This prevents Claude API validation errors when truncation cuts between
        a tool_use and its corresponding tool_result.

        Args:
            turns: List of transcript turns to validate

        Returns:
            Tuple of (cleaned turns, list of removed tool_use_ids)
        """
        # Collect valid tool_use_ids from assistant messages
        valid_ids: set[str] = set()
        for turn in turns:
            content = turn.get("message", {}).get("content", [])
            if isinstance(content, list):
                for block in content:
                    if isinstance(block, dict) and block.get("type") == "tool_use":
                        if tid := block.get("id"):
                            valid_ids.add(tid)

        # Filter orphaned tool_results from user messages
        cleaned: list[dict[str, Any]] = []
        removed: list[str] = []
        for turn in turns:
            msg = turn.get("message", {})
            content = msg.get("content", [])
            if isinstance(content, list):
                new_content: list[Any] = []
                for block in content:
                    if isinstance(block, dict) and block.get("type") == "tool_result":
                        tid = block.get("tool_use_id")
                        if tid and tid not in valid_ids:
                            removed.append(tid)
                            continue
                    new_content.append(block)
                if new_content != content:
                    turn = {**turn, "message": {**msg, "content": new_content}}
            cleaned.append(turn)

        return cleaned, removed

    def parse_line(self, line: str, index: int) -> ParsedMessage | None:
        """
        Parse a single line from the transcript JSONL.

        Args:
            line: Raw JSON line string
            index: Line index (0-based)

        Returns:
            ParsedMessage object or None if line should be skipped
        """
        if not line.strip():
            return None

        try:
            data = json.loads(line)
        except json.JSONDecodeError:
            self.logger.warning(f"Invalid JSON at line {index}")
            return None

        # Ensure data is a dict (JSON could be a string, number, etc.)
        if not isinstance(data, dict):
            self.logger.debug(f"Skipping non-object JSON at line {index}")
            return None

        # Extract basic fields
        msg_type = data.get("type", "unknown")
        timestamp_str = data.get("timestamp") or datetime.now(UTC).isoformat()
        try:
            timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
        except ValueError:
            timestamp = datetime.now(UTC)

        # Claude Code format handling
        role = "unknown"
        content = ""
        content_type = "text"
        tool_name = None
        tool_input = None
        tool_result = None
        tool_use_id = None

        if msg_type == "user":
            role = "user"
            msg_data = data.get("message", {})
            content = str(msg_data.get("content", ""))

        elif msg_type in ("agent", "assistant"):
            role = "assistant"
            msg_data = data.get("message", {})
            content_blocks = msg_data.get("content", [])

            # Handle list of blocks (Claude 3 format)
            if isinstance(content_blocks, list):
                text_parts = []
                for block in content_blocks:
                    if not isinstance(block, dict):
                        continue

                    block_type = block.get("type")

                    if block_type == "text":
                        text_parts.append(block.get("text", ""))

                    elif block_type == "tool_use":
                        content_type = "tool_use"
                        tool_name = block.get("name")
                        tool_input = block.get("input")
                        tool_use_id = block.get("id")

                    elif block_type == "tool_result":
                        content_type = "tool_result"
                        # Tool results usually come in a separate message or block
                        # For now we map strictly to transcript lines

                content = " ".join(text_parts)
            else:
                content = str(content_blocks)

        elif msg_type == "tool_result":
            role = "tool"
            content_type = "tool_result"
            tool_name = data.get("tool_name")
            tool_result = data.get("result")
            tool_use_id = data.get("tool_use_id")
            content = str(tool_result)

        else:
            # Skip unknown message types (e.g., 'progress', 'error' internal events)
            return None

        usage, model = self._extract_usage(data)

        return ParsedMessage(
            index=index,
            role=role,
            content=content,
            content_type=content_type,
            tool_name=tool_name,
            tool_input=tool_input,
            tool_result=tool_result,
            timestamp=timestamp,
            raw_json=data,
            usage=usage,
            tool_use_id=tool_use_id,
            model=model,
        )

    def _extract_usage(self, data: dict[str, Any]) -> tuple[TokenUsage | None, str | None]:
        """Extract token usage and model from message data.

        Returns:
            Tuple of (TokenUsage | None, model string | None)
        """
        # Extract model from message object
        model = data.get("message", {}).get("model")

        # Check for top-level usage field (some formats)
        usage_data = data.get("usage")

        # Check inside message object (standard Claude API format)
        if not usage_data:
            usage_data = data.get("message", {}).get("usage")

        if not usage_data:
            return None, model

        # Use explicit presence checks to handle 0 correctly
        input_tokens = (
            usage_data["input_tokens"]
            if "input_tokens" in usage_data
            else usage_data.get("inputTokens", 0)
        )
        output_tokens = (
            usage_data["output_tokens"]
            if "output_tokens" in usage_data
            else usage_data.get("outputTokens", 0)
        )
        cache_creation_tokens = (
            usage_data["cache_creation_input_tokens"]
            if "cache_creation_input_tokens" in usage_data
            else usage_data.get("cacheCreationInputTokens", 0)
        )
        cache_read_tokens = (
            usage_data["cache_read_input_tokens"]
            if "cache_read_input_tokens" in usage_data
            else usage_data.get("cacheReadInputTokens", 0)
        )
        # Cost might be calculated or provided
        total_cost_usd = (
            usage_data["cost"] if "cost" in usage_data else usage_data.get("total_cost")
        )

        return TokenUsage(
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            cache_creation_tokens=cache_creation_tokens,
            cache_read_tokens=cache_read_tokens,
            total_cost_usd=total_cost_usd,
        ), model

    def parse_lines(self, lines: list[str], start_index: int = 0) -> list[ParsedMessage]:
        """
        Parse a list of transcript lines.

        Args:
            lines: List of JSON line strings
            start_index: Starting index for messages

        Returns:
            List of parsed ParsedMessage objects
        """
        parsed_messages = []
        current_index = start_index

        for line in lines:
            message = self.parse_line(line, current_index)
            if message:
                parsed_messages.append(message)
                current_index += 1

        return parsed_messages

    # Backward-compatible alias
    is_clear_command = is_session_boundary


# Backward-compatible alias for existing code
TranscriptProcessor = ClaudeTranscriptParser
