"""Parse Claude Code .jsonl transcripts into LogEntry objects."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from blackbox.models import LogEntry, SessionInfo

_fromisoformat = datetime.fromisoformat


def ts_to_epoch(ts: str | None) -> float:
    if not ts:
        return 0.0
    try:
        dt = _fromisoformat(ts)
        if dt.tzinfo is None:
            return dt.replace(tzinfo=UTC).timestamp()
        return dt.timestamp()
    except (ValueError, AttributeError):
        return 0.0


def extract_text_content(content: Any) -> str:
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        return "\n".join(
            block.get("text", "") for block in content if isinstance(block, dict) and block.get("type") == "text"
        )
    return ""


def extract_tool_uses(content: Any) -> list[dict[str, Any]]:
    if not isinstance(content, list):
        return []
    return [block for block in content if isinstance(block, dict) and block.get("type") == "tool_use"]


def extract_tool_results(content: Any) -> list[dict[str, Any]]:
    if not isinstance(content, list):
        return []
    return [block for block in content if isinstance(block, dict) and block.get("type") == "tool_result"]


def parse_transcript(path: Path) -> list[LogEntry]:
    """Parse a Claude Code .jsonl transcript into a list of LogEntry objects."""
    entries: list[LogEntry] = []
    _extend = entries.extend
    _loads = json.loads
    for line in path.read_bytes().decode("utf-8").splitlines():
        if not line or line.isspace():
            continue
        try:
            raw = _loads(line)
        except json.JSONDecodeError:
            continue
        _extend(parse_entry(raw))
    return entries


def parse_transcript_tail(path: Path, offset: int) -> tuple[list[LogEntry], int]:
    """Parse only new bytes appended after *offset*. Returns (entries, new_offset)."""
    with path.open("rb") as f:
        f.seek(offset)
        tail = f.read()
        new_offset = offset + len(tail)
    entries: list[LogEntry] = []
    for line in tail.decode("utf-8", errors="replace").splitlines():
        if not line.strip():
            continue
        try:
            raw = json.loads(line)
        except json.JSONDecodeError:
            continue
        entries.extend(parse_entry(raw))
    return entries, new_offset


def parse_entry(raw: dict[str, Any]) -> list[LogEntry]:
    entry_type = raw.get("type", "")
    ts = ts_to_epoch(raw.get("timestamp"))
    message = raw.get("message", {})

    if entry_type == "user":
        return parse_user_entry(ts, message, raw)
    if entry_type == "assistant":
        return parse_assistant_entry(ts, message)
    if entry_type == "system":
        text = extract_text_content(message.get("content", "")) if isinstance(message, dict) else str(message)
        if text:
            return [LogEntry(timestamp=ts, source="system", level="info", message=text)]
    return []


def parse_user_entry(ts: float, message: Any, raw: dict[str, Any]) -> list[LogEntry]:  # noqa: C901
    if not isinstance(message, dict):
        return []
    content = message.get("content", "")
    entries: list[LogEntry] = []

    if isinstance(content, list):
        has_tool_result = False
        for block in content:
            if isinstance(block, dict) and block.get("type") == "tool_result":
                has_tool_result = True
                result_text = block.get("content", "")
                if isinstance(result_text, list):
                    result_text = " ".join(b.get("text", "") for b in result_text if isinstance(b, dict))
                is_error = block.get("is_error", False)
                tool_use_result = raw.get("toolUseResult", {})
                if not isinstance(tool_use_result, dict):
                    tool_use_result = {}
                stdout = tool_use_result.get("stdout", "")
                stderr = tool_use_result.get("stderr", "")
                display = stdout or result_text or ""
                if is_error and stderr:
                    display = stderr
                level = "error" if is_error else "tool_result"
                entries.append(LogEntry(timestamp=ts, source="claude", level=level, message=display[:2000]))
        if has_tool_result:
            return entries
        text = "\n".join(
            block.get("text", "") for block in content if isinstance(block, dict) and block.get("type") == "text"
        )
    elif isinstance(content, str):
        text = content
    else:
        text = ""

    if text:
        entries.append(LogEntry(timestamp=ts, source="user", level="info", message=text))
    return entries


def parse_assistant_entry(ts: float, message: Any) -> list[LogEntry]:
    if not isinstance(message, dict):
        return []
    content = message.get("content", [])
    if not isinstance(content, list):
        return [LogEntry(timestamp=ts, source="claude", level="assistant", message=str(content))] if content else []

    entries: list[LogEntry] = []
    for block in content:
        if not isinstance(block, dict):
            continue
        btype = block.get("type", "")
        if btype == "text":
            text = block.get("text", "")
            if text:
                entries.append(LogEntry(timestamp=ts, source="claude", level="assistant", message=text))
        elif btype == "tool_use":
            tool_name = block.get("name", "tool")
            tool_input = block.get("input", {})
            preview = tool_input_preview(tool_name, tool_input)
            entries.append(
                LogEntry(
                    timestamp=ts,
                    source="claude",
                    level="tool_call",
                    message=f"{tool_name}: {preview}",
                    data={"tool": tool_name, "input_preview": preview},
                )
            )
        elif btype == "thinking":
            entries.append(LogEntry(timestamp=ts, source="claude", level="assistant", message="(thinking)"))
    return entries


def tool_input_preview(tool_name: str, tool_input: dict[str, Any]) -> str:
    if tool_name == "Bash":
        return str(tool_input.get("command", ""))
    if tool_name in ("Read", "Write"):
        return str(tool_input.get("file_path", ""))
    if tool_name == "Edit":
        fp = tool_input.get("file_path", "")
        old = str(tool_input.get("old_string", ""))[:80]
        return f"{fp}\n{old}..."
    if tool_name == "Agent":
        return str(tool_input.get("description", tool_input.get("prompt", "")))[:200]
    if tool_name == "Skill":
        return str(tool_input.get("skill", ""))
    return json.dumps(tool_input, default=str)[:200]


def scan_sessions(projects_dir: Path) -> list[SessionInfo]:
    """Scan ~/.claude/projects/ for session transcripts."""
    sessions: list[SessionInfo] = []
    if not projects_dir.is_dir():
        return sessions

    for project_dir in sorted(projects_dir.iterdir()):
        if not project_dir.is_dir():
            continue
        project_name = decode_project_name(project_dir.name)
        for jsonl in sorted(project_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True):
            session_id = jsonl.stem
            info = quick_session_info(jsonl, session_id, project_dir.name, project_name)
            if info:
                sessions.append(info)

    sessions.sort(key=lambda s: s.started_at, reverse=True)
    return sessions


_DECODE_SKIP = frozenset({"Users", "private", "tmp", ""})


def decode_project_name(encoded: str) -> str:
    parts = encoded.split("-")
    if len(parts) >= 2 and parts[0] == "":
        meaningful = [p for p in parts if p not in _DECODE_SKIP]
        if meaningful:
            return "/".join(meaningful[-2:]) if len(meaningful) >= 2 else meaningful[-1]
    return encoded


def quick_session_info(  # noqa: C901, PLR0912
    path: Path,
    session_id: str,
    encoded_project: str,
    project_name: str,
) -> SessionInfo | None:
    """Read just enough of the transcript to build sidebar metadata."""
    first_prompt = ""
    started_at = 0.0
    finished_at = 0.0
    message_count = 0
    cwd = ""

    _loads = json.loads
    try:
        with path.open() as f:
            for i, line in enumerate(f):
                if not line or line.isspace():
                    continue
                try:
                    raw = _loads(line)
                except json.JSONDecodeError:
                    continue
                ts = ts_to_epoch(raw.get("timestamp"))
                if ts and (started_at == 0.0 or ts < started_at):
                    started_at = ts
                if ts and ts > finished_at:
                    finished_at = ts

                if raw.get("type") == "user":
                    message_count += 1
                    msg = raw.get("message", {})
                    if isinstance(msg, dict) and not first_prompt:
                        content = msg.get("content", "")
                        text = extract_text_content(content)
                        if text and not any(
                            isinstance(b, dict) and b.get("type") == "tool_result"
                            for b in (content if isinstance(content, list) else [])
                        ):
                            first_prompt = text[:120]
                    if not cwd:
                        cwd = raw.get("cwd", "")

                if i > 500:
                    break
    except OSError:
        return None

    if started_at == 0.0:
        return None

    # Use file mtime for finished_at — always accurate even for resumed
    # sessions, and avoids reading the entire file for long transcripts.
    try:
        mtime = path.stat().st_mtime
        if mtime > started_at:
            finished_at = mtime
    except OSError:
        pass

    display_name = project_name
    if cwd:
        parts = Path(cwd).parts
        if len(parts) >= 2:
            display_name = "/".join(parts[-2:])

    return SessionInfo(
        session_id=session_id,
        project_path=encoded_project,
        project_name=display_name,
        transcript_path=str(path),
        started_at=started_at,
        finished_at=finished_at if finished_at > started_at else None,
        first_prompt=first_prompt,
        message_count=message_count,
    )
