"""CopilotSource — SessionSource adapter for GitHub Copilot CLI sessions.

Copilot CLI stores sessions under ``~/.copilot/session-state/`` in two
formats:

1. **Session subdirectories (older):** ``<session-uuid>/workspace.yaml`` plus
   ``<session-uuid>/checkpoints/<session-uuid>.jsonl``.
2. **JSONL files (newer):** ``<session-uuid>.jsonl`` directly in
   session-state/.
"""

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

from loguru import logger

from copilot_session.config import CopilotConfig
from copilot_session.parser import (
    parse_datetime,
    parse_jsonl,
    parse_workspace_yaml,
)
from session_store import SessionExport, SessionInfo, SessionMessage
from session_store.errors import SessionLoadError, SessionNotFoundError


class CopilotSource:
    """SessionSource adapter for GitHub Copilot CLI."""

    def __init__(self, config: CopilotConfig | None = None) -> None:
        self.config = config or CopilotConfig()

    # ------------------------------------------------------------------
    # SessionSource protocol
    # ------------------------------------------------------------------

    def tool_name(self) -> str:
        """Return the tool identifier for Copilot CLI."""
        return "copilot"

    def list_sessions(self, limit: int = 20) -> list[SessionInfo]:
        """List recent Copilot sessions, sorted by last activity (newest first)."""
        session_dir = self.config.session_dir
        if not session_dir.exists():
            logger.warning("Copilot session dir does not exist: {}", session_dir)
            return []

        infos: list[SessionInfo] = []

        # Newer format: <session_id>.jsonl files directly in session_dir.
        for jsonl_path in session_dir.glob("*.jsonl"):
            session_id = jsonl_path.stem
            try:
                message_count = sum(
                    1
                    for line in jsonl_path.read_text(encoding="utf-8").splitlines()
                    if line.strip()
                )
            except OSError as exc:
                logger.warning("Failed to read {}: {}", jsonl_path, exc)
                continue
            try:
                mtime = datetime.fromtimestamp(jsonl_path.stat().st_mtime, UTC)
            except OSError as exc:
                logger.warning("Failed to stat {}: {}", jsonl_path, exc)
                continue
            infos.append(
                SessionInfo(
                    tool="copilot",
                    session_id=session_id,
                    title="Untitled",
                    message_count=message_count,
                    last_activity=mtime,
                    working_directory="",
                )
            )

        # Older format: <session_id>/ subdirectories containing workspace.yaml.
        for sub in session_dir.iterdir():
            if not sub.is_dir():
                continue
            session_id = sub.name
            ws_path = sub / "workspace.yaml"
            ws = parse_workspace_yaml(ws_path)
            title = "Untitled"
            working_directory = ""
            last_activity: datetime
            if ws is not None:
                title = str(ws.get("title") or "Untitled")
                working_directory = str(ws.get("working_directory") or "")
                last_activity = parse_datetime(ws.get("last_activity"))
            else:
                # Fall back to directory mtime.
                try:
                    last_activity = datetime.fromtimestamp(sub.stat().st_mtime, UTC)
                except OSError as exc:
                    logger.warning("Failed to stat dir {}: {}", sub, exc)
                    continue

            # Count messages from any .jsonl inside the subdirectory.
            message_count = self._count_messages_in_dir(sub)

            infos.append(
                SessionInfo(
                    tool="copilot",
                    session_id=session_id,
                    title=title,
                    message_count=message_count,
                    last_activity=last_activity,
                    working_directory=working_directory,
                )
            )

        infos.sort(key=lambda info: info.last_activity, reverse=True)
        return infos[:limit]

    def load_session(self, session_id: str) -> SessionExport:
        """Load a full session (all messages) by session ID."""
        session_dir = self.config.session_dir
        if not session_dir.exists():
            raise SessionLoadError(f"Copilot session dir does not exist: {session_dir}")

        jsonl_path = self._resolve_jsonl(session_id)
        if jsonl_path is None:
            raise SessionNotFoundError(f"No Copilot session found for session_id={session_id!r}")

        # Determine whether this session lives inside a subdirectory.
        sub_dir = jsonl_path.parent
        ws_path = sub_dir / "workspace.yaml"
        # Only treat workspace.yaml as a sibling if the jsonl is inside a
        # session subdirectory (not directly in session_dir).
        ws = None
        if sub_dir != session_dir and sub_dir.parent == session_dir:
            ws = parse_workspace_yaml(ws_path)
        elif sub_dir.name == "checkpoints" and sub_dir.parent.parent == session_dir:
            # checkpoints/<id>.jsonl → workspace.yaml is two levels up.
            ws = parse_workspace_yaml(sub_dir.parent / "workspace.yaml")

        title = "Untitled"
        working_directory = ""
        created_at_raw: str | None = None
        last_activity_raw: str | None = None
        session_metadata: dict[str, Any] = {}
        if ws is not None:
            title = str(ws.get("title") or "Untitled")
            working_directory = str(ws.get("working_directory") or ws.get("cwd") or "")
            created_at_raw = ws.get("created_at")
            last_activity_raw = ws.get("updated_at") or ws.get("last_activity")
            # Preserve tool-specific session fields in metadata.
            for key in ("id", "summary_count", "gitRoot", "branch"):
                if key in ws:
                    session_metadata[key] = ws[key]

        try:
            records = parse_jsonl(jsonl_path)
        except OSError as exc:
            logger.warning("Failed to read {}: {}", jsonl_path, exc)
            raise SessionLoadError(f"Failed to read Copilot session {session_id!r}: {exc}") from exc

        messages: list[SessionMessage] = []
        for index, rec in enumerate(records):
            # Collect tool-specific fields into metadata, excluding fields
            # that map directly to unified model fields.
            unified_keys = {"role", "content", "message_id", "created_at", "parent_node_id"}
            msg_metadata: dict[str, Any] = {k: v for k, v in rec.items() if k not in unified_keys}
            try:
                messages.append(
                    SessionMessage(
                        tool="copilot",
                        session_id=session_id,
                        node_id=str(rec.get("message_id") or f"node-{index}"),
                        parent_node_id=rec.get("parent_node_id"),
                        message_id=str(rec.get("message_id") or f"msg-{index}"),
                        role=str(rec.get("role") or "unknown"),
                        content=str(rec.get("content") or ""),
                        created_at=parse_datetime(rec.get("created_at")),
                        metadata=msg_metadata,
                    )
                )
            except (TypeError, ValueError) as exc:
                logger.warning("Skipping malformed message {} in {}: {}", index, jsonl_path, exc)
                continue

        # Derive created_at / last_activity_at from messages if not set from
        # workspace.yaml.
        if created_at_raw is None and messages:
            created_at_raw = messages[0].created_at.isoformat()
        if last_activity_raw is None and messages:
            last_activity_raw = messages[-1].created_at.isoformat()
        # Fall back to file mtime for last_activity_at.
        if last_activity_raw is None:
            with contextlib.suppress(OSError):
                last_activity_raw = datetime.fromtimestamp(
                    jsonl_path.stat().st_mtime, UTC
                ).isoformat()

        return SessionExport(
            tool="copilot",
            session_id=session_id,
            title=title,
            working_directory=working_directory,
            created_at=parse_datetime(created_at_raw) if created_at_raw else None,
            last_activity_at=parse_datetime(last_activity_raw) if last_activity_raw else None,
            model=None,
            messages=messages,
            metadata=session_metadata,
        )

    def detect_current_session(self) -> str | None:
        """Return the session ID of the most recently modified session.

        Returns None when the session directory is missing or empty. Never
        raises.
        """
        session_dir = self.config.session_dir
        if not session_dir.exists():
            logger.debug("Copilot session dir does not exist: {}", session_dir)
            return None

        best_id: str | None = None
        best_mtime: float = -1.0

        try:
            entries = list(session_dir.iterdir())
        except OSError as exc:
            logger.warning("Failed to list {}: {}", session_dir, exc)
            return None

        for entry in entries:
            try:
                if entry.is_file() and entry.suffix == ".jsonl":
                    mtime = entry.stat().st_mtime
                    candidate = entry.stem
                elif entry.is_dir():
                    ws = entry / "workspace.yaml"
                    mtime = ws.stat().st_mtime if ws.exists() else entry.stat().st_mtime
                    candidate = entry.name
                else:
                    continue
            except OSError as exc:
                logger.warning("Failed to stat {}: {}", entry, exc)
                continue

            if mtime > best_mtime:
                best_mtime = mtime
                best_id = candidate

        return best_id

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _resolve_jsonl(self, session_id: str) -> Path | None:
        """Locate the .jsonl file for a session ID.

        Checks, in order:
        1. ``<session_dir>/<session_id>.jsonl``
        2. ``<session_dir>/<session_id>/<session_id>.jsonl``
        3. ``<session_dir>/<session_id>/checkpoints/<session_id>.jsonl``
        4. Any ``*.jsonl`` directly inside ``<session_dir>/<session_id>/``
        """
        session_dir = self.config.session_dir

        direct = session_dir / f"{session_id}.jsonl"
        if direct.exists():
            return direct

        sub = session_dir / session_id
        if sub.is_dir():
            in_root = sub / f"{session_id}.jsonl"
            if in_root.exists():
                return in_root

            in_checkpoints = sub / "checkpoints" / f"{session_id}.jsonl"
            if in_checkpoints.exists():
                return in_checkpoints

            # Fall back to the first .jsonl found anywhere in the subdirectory.
            try:
                for candidate in sub.rglob("*.jsonl"):
                    if candidate.is_file():
                        return candidate
            except OSError as exc:
                logger.warning("Failed to search {}: {}", sub, exc)

        return None

    @staticmethod
    def _count_messages_in_dir(directory: Path) -> int:
        """Count non-empty lines across all .jsonl files in a directory tree."""
        total = 0
        try:
            for jsonl_path in directory.rglob("*.jsonl"):
                if not jsonl_path.is_file():
                    continue
                try:
                    total += sum(
                        1
                        for line in jsonl_path.read_text(encoding="utf-8").splitlines()
                        if line.strip()
                    )
                except OSError as exc:
                    logger.warning("Failed to read {}: {}", jsonl_path, exc)
                    continue
        except OSError as exc:
            logger.warning("Failed to scan {}: {}", directory, exc)
        return total
