"""Claude Code file discovery and reading utilities.

Claude Code stores session files in ~/.claude/projects/[encoded-path]/.
Each session is a JSONL file with UUID filename.
Agent sessions are stored as agent-{agentId}.jsonl in the same directory.
"""

import json
from datetime import datetime
from logging import getLogger
from os import PathLike
from pathlib import Path
from typing import Any

logger = getLogger(__name__)

# Claude Code source type constant
CLAUDE_CODE_SOURCE_TYPE = "claude_code"

# Default Claude Code projects directory
DEFAULT_CLAUDE_DIR = Path.home() / ".claude" / "projects"


def decode_project_path(encoded: str, validate: bool = True) -> str:
    """Decode a Claude Code project path encoding.

    Claude Code encodes paths by replacing / with -.
    E.g., "-Users-jjallaire-dev-project" -> "/Users/jjallaire/dev/project"

    The encoding is lossy when directory names contain hyphens. When validate=True,
    this function tries different interpretations and returns the first one that
    exists on the filesystem. For example, "-Users-my-project-foo" could decode to:
    - /Users/my-project/foo (if /Users/my-project exists)
    - /Users/my/project-foo (if /Users/my/project-foo exists)

    Args:
        encoded: The encoded project path (directory name)
        validate: If True, validate against filesystem to handle hyphenated dirs.
            If False, use naive replacement (faster but may be wrong).

    Returns:
        The decoded file system path (best guess if validation fails)
    """
    if not encoded.startswith("-"):
        return encoded

    # Remove leading dash and split on remaining dashes
    parts = encoded[1:].split("-")

    if not validate:
        # Fast path: naive replacement
        return "/" + "/".join(parts)

    # Try to find a valid path by testing different split points
    decoded = _find_valid_path(parts)
    if decoded:
        return decoded

    # Fallback: naive replacement if no valid path found
    return "/" + "/".join(parts)


def _find_valid_path(parts: list[str]) -> str | None:
    """Find a valid filesystem path by testing different hyphen interpretations.

    First tries the simple interpretation (all hyphens are path separators).
    If that doesn't exist, uses DFS to try different combinations where some
    hyphens are literal characters in directory names.

    Args:
        parts: List of path components split on hyphens

    Returns:
        A valid path if found, None otherwise
    """
    if not parts:
        return None

    # Fast path: try naive interpretation first (most common case)
    naive_path = "/" + "/".join(parts)
    if Path(naive_path).exists():
        return naive_path

    # Slow path: DFS to find valid path when directories contain hyphens
    def search(index: int, current_path: Path, current_str: str) -> str | None:
        """Recursively search for valid paths."""
        if index >= len(parts):
            if current_path.exists():
                return current_str
            return None

        part = parts[index]

        # Option 1: Add as new path component (use /)
        new_path_slash = current_path / part
        new_str_slash = current_str + "/" + part
        # Continue if path exists OR we're not at the last segment
        # (we need to explore full paths before determining validity)
        if new_path_slash.exists() or index < len(parts) - 1:
            result = search(index + 1, new_path_slash, new_str_slash)
            if result:
                return result

        # Option 2: Merge with previous component using hyphen
        # Only if we have a previous component to merge with
        if current_str:
            parent_path = current_path.parent
            last_component = current_path.name
            merged_component = last_component + "-" + part
            new_path_hyphen = parent_path / merged_component
            # Reconstruct string: replace last component with merged one
            last_slash = current_str.rfind("/")
            new_str_hyphen = current_str[:last_slash] + "/" + merged_component

            if new_path_hyphen.exists() or index < len(parts) - 1:
                result = search(index + 1, new_path_hyphen, new_str_hyphen)
                if result:
                    return result

        return None

    # Start search from root
    return search(0, Path("/"), "")


def encode_project_path(path: str) -> str:
    """Encode a file system path for Claude Code directory naming.

    Args:
        path: The file system path

    Returns:
        The encoded directory name
    """
    return path.replace("/", "-")


def discover_session_files(
    path: str | PathLike[str] | None = None,
    session_id: str | None = None,
    from_time: datetime | None = None,
    to_time: datetime | None = None,
) -> list[Path]:
    """Discover Claude Code session files.

    Args:
        path: Path to search. Can be:
            - None: Scan all projects in ~/.claude/projects/
            - A project directory (e.g., ~/.claude/projects/-Users-foo-bar/)
            - A specific .jsonl file
        session_id: If provided, only return files matching this session UUID
        from_time: Only return files modified on or after this time
        to_time: Only return files modified before this time

    Returns:
        List of session file paths, sorted by modification time (newest first)
    """
    session_files: list[Path] = []

    if path is None:
        # Scan all projects
        if not DEFAULT_CLAUDE_DIR.exists():
            logger.warning(f"Claude Code directory not found: {DEFAULT_CLAUDE_DIR}")
            return []

        for project_dir in DEFAULT_CLAUDE_DIR.iterdir():
            if project_dir.is_dir():
                session_files.extend(
                    _find_sessions_in_directory(project_dir, session_id)
                )
    else:
        search_path = Path(path)

        if not search_path.exists():
            logger.warning(f"Path does not exist: {search_path}")
            return []

        if search_path.is_file():
            # Specific file
            if search_path.suffix == ".jsonl":
                session_files.append(search_path)
        elif search_path.is_dir():
            # Project directory or parent directory
            if any(search_path.glob("*.jsonl")):
                # It's a project directory with session files
                session_files.extend(
                    _find_sessions_in_directory(search_path, session_id)
                )
            else:
                # It might be a parent containing project directories
                for project_dir in search_path.iterdir():
                    if project_dir.is_dir():
                        session_files.extend(
                            _find_sessions_in_directory(project_dir, session_id)
                        )

    # Filter by session_id if specified and not already filtered
    if session_id:
        session_files = [f for f in session_files if f.stem == session_id]

    # Filter by time range
    if from_time or to_time:
        filtered = []
        for f in session_files:
            mtime = datetime.fromtimestamp(f.stat().st_mtime)
            if from_time and mtime < from_time:
                continue
            if to_time and mtime >= to_time:
                continue
            filtered.append(f)
        session_files = filtered

    # Sort by modification time (newest first)
    session_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)

    return session_files


def _find_sessions_in_directory(
    directory: Path, session_id: str | None = None
) -> list[Path]:
    """Find session files in a project directory.

    Excludes agent-*.jsonl files (those are loaded separately when needed).

    Args:
        directory: The project directory to search
        session_id: Optional specific session to find

    Returns:
        List of main session file paths
    """
    sessions = []

    for jsonl_file in directory.glob("*.jsonl"):
        # Skip agent files - they're loaded separately
        if jsonl_file.name.startswith("agent-"):
            continue

        # If looking for specific session, match by stem (UUID)
        if session_id:
            if jsonl_file.stem == session_id:
                sessions.append(jsonl_file)
        else:
            sessions.append(jsonl_file)

    return sessions


def find_agent_file(
    project_dir: Path,
    agent_id: str,
    session_file: Path | None = None,
) -> Path | None:
    """Find an agent session file by ID.

    Searches for agent files in the session's subagents directory,
    falling back to legacy flat layout.

    Args:
        project_dir: The project directory containing session files
        agent_id: The agent ID (e.g., "a038f97")
        session_file: Path to the parent session JSONL file. When provided,
            searches {session_file.stem}/subagents/ for the agent file.

    Returns:
        Path to agent file, or None if not found
    """
    # Check session-scoped subagents directory
    if session_file is not None:
        agent_file = (
            session_file.parent
            / session_file.stem
            / "subagents"
            / f"agent-{agent_id}.jsonl"
        )
        if agent_file.exists():
            return agent_file

    # Fall back to legacy location (direct in project dir)
    agent_file = project_dir / f"agent-{agent_id}.jsonl"
    if agent_file.exists():
        return agent_file

    return None


def find_team_agent_file(session_file: Path, prompt: str) -> Path | None:
    """Find a team agent's session file by matching the spawn prompt.

    Team agents use human-readable IDs (e.g., ``ux-researcher@todo-cli-design``)
    but their session files use hex hash names (e.g., ``agent-a8e6899e9870c1c5f.jsonl``).
    This function scans subagent files and matches by comparing the prompt text
    against the first user message content.

    Args:
        session_file: Path to the parent session JSONL file.
        prompt: The prompt text from the Agent tool call.

    Returns:
        Path to the matching agent file, or None if not found.
    """
    subagents_dir = session_file.parent / session_file.stem / "subagents"
    if not subagents_dir.is_dir():
        return None

    # Use a prefix of the prompt for matching (handles minor formatting diffs)
    match_text = prompt[:200].strip()
    if not match_text:
        return None

    for agent_file in sorted(subagents_dir.glob("agent-*.jsonl")):
        first_content = _peek_first_user_content(agent_file)
        if first_content and match_text in first_content:
            return agent_file

    logger.debug(f"No team agent file matched prompt prefix in {subagents_dir}")
    return None


def _peek_first_user_content(agent_file: Path, max_lines: int = 5) -> str | None:
    """Read the content of the first user message in an agent file.

    Args:
        agent_file: Path to an agent JSONL file.
        max_lines: Maximum lines to scan.

    Returns:
        The message content string, or None.
    """
    try:
        with open(agent_file, encoding="utf-8") as f:
            for _, line in zip(range(max_lines), f, strict=False):
                line = line.strip()
                if not line:
                    continue
                try:
                    event = json.loads(line)
                    if event.get("type") == "user":
                        content = event.get("message", {}).get("content", "")
                        if isinstance(content, str):
                            return content
                        # Content is a list of blocks
                        if isinstance(content, list):
                            texts = []
                            for block in content:
                                if isinstance(block, dict):
                                    texts.append(str(block.get("text", "")))
                            return " ".join(texts)
                except json.JSONDecodeError:
                    continue
    except OSError:
        raise
    return None


def _peek_field(session_file: Path, field: str, max_lines: int = 10) -> str | None:
    """Read a field value from the first few lines of a JSONL session file.

    Streams the file and stops as soon as the field is found, avoiding
    reading the entire file.

    Args:
        session_file: Path to a JSONL session file
        field: JSON field name to extract
        max_lines: Maximum number of lines to scan

    Returns:
        The field value as a string, or None if not found
    """
    try:
        with open(session_file, encoding="utf-8") as f:
            for _, line in zip(range(max_lines), f, strict=False):
                line = line.strip()
                if not line:
                    continue
                try:
                    event = json.loads(line)
                    value = event.get(field)
                    if value:
                        return str(value)
                except json.JSONDecodeError:
                    continue
    except OSError:
        logger.warning(f"Could not read session file: {session_file}")
    return None


def peek_slug(session_file: Path, max_lines: int = 10) -> str | None:
    """Read the slug from the first few lines of a session file.

    Args:
        session_file: Path to a JSONL session file
        max_lines: Maximum number of lines to scan

    Returns:
        The slug string, or None if not found
    """
    return _peek_field(session_file, "slug", max_lines)


def peek_first_timestamp(session_file: Path, max_lines: int = 10) -> str | None:
    """Read the first timestamp from a session file without reading the whole file.

    Args:
        session_file: Path to a JSONL session file
        max_lines: Maximum number of lines to scan

    Returns:
        ISO timestamp string, or None if not found
    """
    return _peek_field(session_file, "timestamp", max_lines)


def read_jsonl_events(path: Path) -> list[dict[str, Any]]:
    """Read all events from a JSONL file.

    Args:
        path: Path to the JSONL file

    Returns:
        List of parsed JSON events
    """
    events = []
    with open(path, encoding="utf-8") as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            try:
                events.append(json.loads(line))
            except json.JSONDecodeError as e:
                logger.warning(f"Invalid JSON at {path}:{line_num}: {e}")
                continue
    return events


def get_project_path_from_file(session_file: Path) -> str:
    """Extract the project path from a session file location.

    Args:
        session_file: Path to a session file

    Returns:
        The decoded project path
    """
    # The parent directory name is the encoded project path
    encoded = session_file.parent.name
    return decode_project_path(encoded)


def get_source_uri(session_file: Path, session_id: str | None = None) -> str:
    """Generate a source URI for a session file.

    Args:
        session_file: Path to the session file
        session_id: Optional specific session within the file (for split sessions)

    Returns:
        A file:// URI pointing to the session
    """
    uri = f"file://{session_file}"
    if session_id:
        uri += f"#{session_id}"
    return uri
