"""Extract and analyze user messages from Claude Code logs.

Provides functionality to extract user messages from Claude Code session
logs stored in ~/.claude/projects/.

Usage as CLI:
    ll-messages                    # Last 100 messages to file
    ll-messages -n 50              # Last 50 messages
    ll-messages --since 2026-01-01 # Since date
    ll-messages -o output.jsonl    # Custom output path
    ll-messages --stdout           # Print to terminal instead of file

Usage as library:
    from little_loops.user_messages import extract_user_messages, get_project_folder

    project_folder = get_project_folder()
    messages = extract_user_messages(project_folder, limit=50)
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

__all__ = [
    "UserMessage",
    "ResponseMetadata",
    "CommandRecord",
    "ExampleRecord",
    "get_project_folder",
    "extract_user_messages",
    "extract_commands",
    "build_examples",
    "save_messages",
]


@dataclass
class UserMessage:
    """Extracted user message with metadata.

    Attributes:
        content: The text content of the user message
        timestamp: When the message was sent
        session_id: Claude Code session identifier
        uuid: Unique message identifier
        cwd: Working directory when message was sent
        git_branch: Git branch active when message was sent
        is_sidechain: Whether this was a sidechain message
    """

    content: str
    timestamp: datetime
    session_id: str
    uuid: str
    cwd: str | None = None
    git_branch: str | None = None
    is_sidechain: bool = False

    response_metadata: ResponseMetadata | None = None

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        result: dict[str, object] = {
            "content": self.content,
            "timestamp": self.timestamp.isoformat(),
            "session_id": self.session_id,
            "uuid": self.uuid,
            "cwd": self.cwd,
            "git_branch": self.git_branch,
            "is_sidechain": self.is_sidechain,
        }
        if self.response_metadata is not None:
            result["response_metadata"] = self.response_metadata.to_dict()
        return result


@dataclass
class ResponseMetadata:
    """Metadata extracted from assistant response.

    Attributes:
        tools_used: List of tools and their usage counts
        files_read: Files accessed via Read tool
        files_modified: Files changed via Edit/Write tools
        completion_status: "success", "failure", or "partial"
        error_message: Error text if failure detected
    """

    tools_used: list[dict[str, str | int]]
    files_read: list[str]
    files_modified: list[str]
    completion_status: str
    error_message: str | None = None

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        return {
            "tools_used": self.tools_used,
            "files_read": self.files_read,
            "files_modified": self.files_modified,
            "completion_status": self.completion_status,
            "error_message": self.error_message,
        }


@dataclass
class CommandRecord:
    """Extracted CLI command from assistant tool_use.

    Attributes:
        content: The command string that was executed
        timestamp: When the command was issued
        session_id: Claude Code session identifier
        uuid: Unique record identifier
        tool: Tool name (e.g., "Bash")
        cwd: Working directory when command was issued
        git_branch: Git branch active when command was issued
    """

    content: str
    timestamp: datetime
    session_id: str
    uuid: str
    tool: str
    cwd: str | None = None
    git_branch: str | None = None

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        return {
            "type": "command",
            "content": self.content,
            "timestamp": self.timestamp.isoformat(),
            "session_id": self.session_id,
            "uuid": self.uuid,
            "tool": self.tool,
            "cwd": self.cwd,
            "git_branch": self.git_branch,
        }


@dataclass
class ExampleRecord:
    """Training example pair extracted from a skill invocation session.

    Attributes:
        skill: The skill name (e.g., "capture-issue")
        input: Concatenated preceding user messages as context
        output: JSON-serialized ResponseMetadata summary (tools_used, files_modified,
            completion_status); free-text assistant response capture is deferred
        session_id: Claude Code session identifier
        timestamp: When the skill was invoked
        context_window: Number of preceding messages used as context
    """

    skill: str
    input: str
    output: str
    session_id: str
    timestamp: datetime
    context_window: int

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        return {
            "type": "example",
            "skill": self.skill,
            "input": self.input,
            "output": self.output,
            "session_id": self.session_id,
            "timestamp": self.timestamp.isoformat(),
            "context_window": self.context_window,
        }


def _extract_response_metadata(response_record: dict) -> ResponseMetadata | None:
    """Extract metadata from an assistant response record.

    Args:
        response_record: The assistant record from JSONL

    Returns:
        ResponseMetadata if parseable, None otherwise
    """
    message_data = response_record.get("message", {})
    content = message_data.get("content", [])

    if not isinstance(content, list):
        return None

    tools_used: dict[str, int] = {}
    files_read: list[str] = []
    files_modified: list[str] = []

    for block in content:
        if not isinstance(block, dict):
            continue
        if block.get("type") != "tool_use":
            continue

        tool_name = block.get("name", "")
        tools_used[tool_name] = tools_used.get(tool_name, 0) + 1

        tool_input = block.get("input", {})
        if tool_name == "Read":
            file_path = tool_input.get("file_path")
            if file_path:
                files_read.append(file_path)
        elif tool_name in ("Edit", "Write"):
            file_path = tool_input.get("file_path")
            if file_path:
                files_modified.append(file_path)

    # Detect completion status from text content
    completion_status = _detect_completion_status(content)
    error_message = _detect_error_message(content) if completion_status == "failure" else None

    # Convert tools_used dict to list format
    tools_list: list[dict[str, str | int]] = [
        {"tool": name, "count": count} for name, count in tools_used.items()
    ]

    return ResponseMetadata(
        tools_used=tools_list,
        files_read=files_read,
        files_modified=files_modified,
        completion_status=completion_status,
        error_message=error_message,
    )


def _aggregate_response_metadata(responses: list[dict]) -> ResponseMetadata | None:
    """Aggregate metadata from multiple assistant response records.

    Combines tool counts, file lists, and uses completion status from final response.

    Args:
        responses: List of assistant records from JSONL

    Returns:
        Aggregated ResponseMetadata, or None if no valid responses
    """
    if not responses:
        return None

    tools_used: dict[str, int] = {}
    files_read: set[str] = set()
    files_modified: set[str] = set()
    completion_status = "success"
    error_message: str | None = None

    for response_record in responses:
        message_data = response_record.get("message", {})
        content = message_data.get("content", [])

        if not isinstance(content, list):
            continue

        for block in content:
            if not isinstance(block, dict):
                continue
            if block.get("type") != "tool_use":
                continue

            tool_name = block.get("name", "")
            tools_used[tool_name] = tools_used.get(tool_name, 0) + 1

            tool_input = block.get("input", {})
            if tool_name == "Read":
                file_path = tool_input.get("file_path")
                if file_path:
                    files_read.add(file_path)
            elif tool_name in ("Edit", "Write"):
                file_path = tool_input.get("file_path")
                if file_path:
                    files_modified.add(file_path)

    # Use completion status from the final response
    final_content = responses[-1].get("message", {}).get("content", [])
    if isinstance(final_content, list):
        completion_status = _detect_completion_status(final_content)
        if completion_status == "failure":
            error_message = _detect_error_message(final_content)

    # Convert to output format
    tools_list: list[dict[str, str | int]] = [
        {"tool": name, "count": count} for name, count in tools_used.items()
    ]

    return ResponseMetadata(
        tools_used=tools_list,
        files_read=sorted(files_read),
        files_modified=sorted(files_modified),
        completion_status=completion_status,
        error_message=error_message,
    )


def _detect_completion_status(content: list) -> str:
    """Detect completion status from response content.

    Args:
        content: List of content blocks from assistant response

    Returns:
        "success", "failure", or "partial"
    """
    text_parts = []
    for block in content:
        if isinstance(block, dict) and block.get("type") == "text":
            text_parts.append(block.get("text", ""))

    text = " ".join(text_parts).lower()

    # Check for error indicators
    error_patterns = ["error", "failed", "couldn't", "unable to", "cannot"]
    if any(pattern in text for pattern in error_patterns):
        return "failure"

    # Check for partial completion
    partial_patterns = ["partially", "some of", "not all", "incomplete"]
    if any(pattern in text for pattern in partial_patterns):
        return "partial"

    return "success"


def _detect_error_message(content: list) -> str | None:
    """Extract error message from response content.

    Args:
        content: List of content blocks from assistant response

    Returns:
        Error message if found, None otherwise
    """
    for block in content:
        if isinstance(block, dict) and block.get("type") == "text":
            text = block.get("text", "")
            # Look for common error message patterns
            lower_text = text.lower()
            if "error:" in lower_text or "failed:" in lower_text:
                # Extract the line containing the error
                for line in text.split("\n"):
                    if "error" in line.lower() or "failed" in line.lower():
                        result = line.strip()[:200]  # Limit length
                        return result if isinstance(result, str) else None
    return None


def get_project_folder(cwd: Path | None = None) -> Path | None:
    """Map current directory to Claude Code project folder.

    Converts: /home/user/foo/bar -> ~/.claude/projects/-home-user-foo-bar

    Args:
        cwd: Working directory to map. If None, uses current directory.

    Returns:
        Path to Claude project folder, or None if it doesn't exist.
    """
    if cwd is None:
        cwd = Path.cwd()

    # Convert path to dash-separated format
    # /home/user/foo/bar -> -home-user-foo-bar
    path_str = str(cwd.resolve())
    encoded_path = path_str.replace("/", "-")

    # Build project folder path
    claude_projects = Path.home() / ".claude" / "projects"
    project_folder = claude_projects / encoded_path

    if project_folder.exists():
        return project_folder

    return None


def extract_user_messages(
    project_folder: Path,
    limit: int | None = None,
    since: datetime | None = None,
    include_agent_sessions: bool = True,
    include_response_context: bool = False,
) -> list[UserMessage]:
    """Extract user messages from all JSONL session files.

    Filters:
    - type == "user"
    - message.content is string (real user input)
    - message.content is array but [0].type != "tool_result"

    Args:
        project_folder: Path to Claude project folder
        limit: Maximum number of messages to return
        since: Only include messages after this datetime
        include_agent_sessions: Whether to include agent-*.jsonl files
        include_response_context: Whether to include metadata from assistant responses

    Returns:
        Messages sorted by timestamp, most recent first.
    """
    messages: list[UserMessage] = []

    # Find all JSONL files
    pattern = "*.jsonl"
    jsonl_files = list(project_folder.glob(pattern))

    for jsonl_file in jsonl_files:
        # Skip agent sessions if requested
        if not include_agent_sessions and jsonl_file.name.startswith("agent-"):
            continue

        try:
            # If we need response context, read all records first to pair user/assistant
            if include_response_context:
                all_records: list[dict] = []
                with open(jsonl_file, encoding="utf-8") as f:
                    for line in f:
                        line = line.strip()
                        if not line:
                            continue
                        try:
                            record = json.loads(line)
                            all_records.append(record)
                        except json.JSONDecodeError:
                            continue

                # Process records, pairing user messages with their responses
                messages.extend(_extract_messages_with_context(all_records, jsonl_file, since))
            else:
                # Original behavior: stream through file
                with open(jsonl_file, encoding="utf-8") as f:
                    for line in f:
                        line = line.strip()
                        if not line:
                            continue

                        try:
                            record = json.loads(line)
                        except json.JSONDecodeError:
                            continue

                        msg = _parse_user_record(record, jsonl_file, since)
                        if msg is not None:
                            messages.append(msg)

        except OSError:
            # Skip files that can't be read
            continue

    # Sort by timestamp, most recent first
    messages.sort(key=lambda m: m.timestamp, reverse=True)

    # Apply limit
    if limit is not None:
        messages = messages[:limit]

    return messages


def extract_commands(
    project_folder: Path,
    limit: int | None = None,
    since: datetime | None = None,
    include_agent_sessions: bool = True,
    tools: list[str] | None = None,
) -> list[CommandRecord]:
    """Extract CLI commands from assistant tool_use messages.

    Parses assistant messages for tool_use blocks and extracts command strings.

    Args:
        project_folder: Path to Claude project folder
        limit: Maximum number of commands to return
        since: Only include commands after this datetime
        include_agent_sessions: Whether to include agent-*.jsonl files
        tools: Filter to specific tools (default: ["Bash"])

    Returns:
        Commands sorted by timestamp, most recent first.
    """
    if tools is None:
        tools = ["Bash"]

    commands: list[CommandRecord] = []

    # Find all JSONL files
    pattern = "*.jsonl"
    jsonl_files = list(project_folder.glob(pattern))

    for jsonl_file in jsonl_files:
        # Skip agent sessions if requested
        if not include_agent_sessions and jsonl_file.name.startswith("agent-"):
            continue

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

                    try:
                        record = json.loads(line)
                    except json.JSONDecodeError:
                        continue

                    cmds = _parse_command_record(record, jsonl_file, since, tools)
                    commands.extend(cmds)

        except OSError:
            # Skip files that can't be read
            continue

    # Sort by timestamp, most recent first
    commands.sort(key=lambda c: c.timestamp, reverse=True)

    # Apply limit
    if limit is not None:
        commands = commands[:limit]

    return commands


def _parse_command_record(
    record: dict,
    jsonl_file: Path,
    since: datetime | None,
    tools: list[str],
) -> list[CommandRecord]:
    """Parse CLI commands from an assistant record.

    Args:
        record: The JSON record from JSONL
        jsonl_file: Source file (for fallback timestamp)
        since: Filter for commands after this datetime
        tools: Tool names to extract (e.g., ["Bash"])

    Returns:
        List of CommandRecord for each matching tool_use block
    """
    # Filter for assistant messages only
    if record.get("type") != "assistant":
        return []

    message_data = record.get("message", {})
    content = message_data.get("content", [])

    if not isinstance(content, list):
        return []

    # Parse timestamp
    timestamp_str = record.get("timestamp", "")
    try:
        timestamp_str = timestamp_str.replace("Z", "+00:00")
        timestamp = datetime.fromisoformat(timestamp_str)
        if timestamp.tzinfo is not None:
            timestamp = timestamp.replace(tzinfo=None)
    except (ValueError, AttributeError):
        timestamp = datetime.fromtimestamp(jsonl_file.stat().st_mtime)

    # Apply since filter
    if since and timestamp < since:
        return []

    commands: list[CommandRecord] = []

    for block in content:
        if not isinstance(block, dict):
            continue
        if block.get("type") != "tool_use":
            continue

        tool_name = block.get("name", "")
        if tool_name not in tools:
            continue

        tool_input = block.get("input", {})
        command_str = tool_input.get("command", "")
        if not command_str:
            continue

        commands.append(
            CommandRecord(
                content=command_str,
                timestamp=timestamp,
                session_id=record.get("sessionId", ""),
                uuid=record.get("uuid", ""),
                tool=tool_name,
                cwd=record.get("cwd"),
                git_branch=record.get("gitBranch"),
            )
        )

    return commands


def _parse_user_record(
    record: dict,
    jsonl_file: Path,
    since: datetime | None,
) -> UserMessage | None:
    """Parse a single user record into a UserMessage.

    Args:
        record: The JSON record from JSONL
        jsonl_file: Source file (for fallback timestamp)
        since: Filter for messages after this datetime

    Returns:
        UserMessage if valid user message, None otherwise
    """
    # Filter for user messages only
    if record.get("type") != "user":
        return None

    message_data = record.get("message", {})
    content = message_data.get("content")

    # Skip if no content
    if content is None:
        return None

    # Check if this is a real user message or tool_result
    if isinstance(content, str):
        # String content = real user message
        message_content = content
    elif isinstance(content, list):
        # Array content - check first element
        if len(content) > 0 and content[0].get("type") == "tool_result":
            # This is a tool result, skip it
            return None
        # Extract text from array (could be text blocks)
        text_parts = []
        for block in content:
            if isinstance(block, dict):
                if block.get("type") == "text":
                    text_parts.append(block.get("text", ""))
                elif "content" in block:
                    text_parts.append(str(block.get("content", "")))
        message_content = "\n".join(text_parts) if text_parts else str(content)
    else:
        return None

    # Parse timestamp
    timestamp_str = record.get("timestamp", "")
    try:
        # Handle ISO 8601 format with Z suffix
        timestamp_str = timestamp_str.replace("Z", "+00:00")
        timestamp = datetime.fromisoformat(timestamp_str)
        # Convert to naive datetime for consistent comparison
        if timestamp.tzinfo is not None:
            timestamp = timestamp.replace(tzinfo=None)
    except (ValueError, AttributeError):
        # Use file modification time as fallback
        timestamp = datetime.fromtimestamp(jsonl_file.stat().st_mtime)

    # Apply since filter
    if since and timestamp < since:
        return None

    # Create message object
    return UserMessage(
        content=message_content,
        timestamp=timestamp,
        session_id=record.get("sessionId", ""),
        uuid=record.get("uuid", ""),
        cwd=record.get("cwd"),
        git_branch=record.get("gitBranch"),
        is_sidechain=record.get("isSidechain", False),
    )


def _extract_messages_with_context(
    records: list[dict],
    jsonl_file: Path,
    since: datetime | None,
) -> list[UserMessage]:
    """Extract user messages with response context from a list of records.

    Pairs each user message with ALL following assistant responses until the
    next user message, aggregating tool usage and file changes.

    Args:
        records: List of all records from a JSONL file
        jsonl_file: Source file (for fallback timestamp)
        since: Filter for messages after this datetime

    Returns:
        List of UserMessages with response_metadata populated
    """
    messages: list[UserMessage] = []

    current_msg: UserMessage | None = None
    current_responses: list[dict] = []

    for record in records:
        if record.get("type") == "user":
            if current_msg is not None:
                current_msg.response_metadata = _aggregate_response_metadata(current_responses)
                messages.append(current_msg)
            current_msg = _parse_user_record(record, jsonl_file, since)
            current_responses = []
        elif record.get("type") == "assistant" and current_msg is not None:
            current_responses.append(record)

    # Emit the final group
    if current_msg is not None:
        current_msg.response_metadata = _aggregate_response_metadata(current_responses)
        messages.append(current_msg)

    return messages


def save_messages(
    messages: list[UserMessage],
    output_path: Path | None = None,
) -> Path:
    """Save messages to timestamped JSONL file.

    Args:
        messages: List of UserMessage objects to save
        output_path: Output file path. If None, uses default location.

    Returns:
        Path to the saved file.
    """
    if output_path is None:
        # Default: ./.ll/user-messages-{timestamp}.jsonl
        timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        output_dir = Path.cwd() / ".ll"
        output_dir.mkdir(parents=True, exist_ok=True)
        output_path = output_dir / f"user-messages-{timestamp}.jsonl"

    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    with open(output_path, "w", encoding="utf-8") as f:
        for msg in messages:
            f.write(json.dumps(msg.to_dict()) + "\n")

    return output_path


def build_examples(
    messages: list[UserMessage],
    skill: str,
    context_window: int = 3,
) -> list[ExampleRecord]:
    """Build training example pairs from skill invocation sessions.

    Groups messages by session, identifies skill trigger records (the user-side record
    whose content contains ``<command-name>/ll:SKILL_NAME</command-name>``), and pairs
    each trigger with the N preceding messages as input context.

    Args:
        messages: UserMessage list (already filtered to skill-matching sessions)
        skill: The skill name to build examples for (e.g. "capture-issue")
        context_window: Number of preceding messages to include as context (default 3)

    Returns:
        List of ExampleRecord objects, one per skill trigger record found.
    """
    import re

    skill_pattern = re.compile(rf"<command-name>/ll:{re.escape(skill)}</command-name>")

    # Group by session_id, sorted ascending by timestamp
    sessions: dict[str, list[UserMessage]] = {}
    for msg in messages:
        sessions.setdefault(msg.session_id, []).append(msg)
    for session_msgs in sessions.values():
        session_msgs.sort(key=lambda m: m.timestamp)

    examples: list[ExampleRecord] = []
    for session_id, session_msgs in sessions.items():
        for idx, msg in enumerate(session_msgs):
            if not skill_pattern.search(msg.content):
                continue

            # Collect N preceding messages as context
            preceding = session_msgs[max(0, idx - context_window) : idx]
            input_text = "\n\n".join(m.content for m in preceding)

            # Serialize response_metadata as output
            if msg.response_metadata is not None:
                output_str = json.dumps(msg.response_metadata.to_dict())
            else:
                output_str = "{}"

            examples.append(
                ExampleRecord(
                    skill=skill,
                    input=input_text,
                    output=output_str,
                    session_id=session_id,
                    timestamp=msg.timestamp,
                    context_window=context_window,
                )
            )

    return examples


def print_messages_to_stdout(messages: list[UserMessage]) -> None:
    """Print messages to stdout in JSONL format.

    Args:
        messages: List of UserMessage objects to print
    """
    import sys

    for msg in messages:
        print(json.dumps(msg.to_dict()), file=sys.stdout)
