"""Claude Code Integration API.

Provides three integration surfaces:

1. Session Tracking  — GET /claude-code/sessions, /sessions/{id}, /projects
   Reads ~/.claude/projects/**/*.jsonl directly. Extracts token usage, model,
   cost estimates, and active status without requiring a gateway.

2. Config Scanner    — GET /claude-code/config
   Reads ~/.claude/settings.json, ~/.codex/config.toml, and ~/.codex/rules/.

3. Direct CLI info   — GET /claude-code/cli
   Reports which CLI tools are detected on the host and their versions.
   (Actual CLI execution goes through the gateway; this endpoint surfaces
   discovery metadata for the dashboard.)
"""

from __future__ import annotations

import asyncio
import shutil
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Query, status

from app.api.deps import require_org_member
from app.core.logging import get_logger
from app.schemas.claude_code import (
    ClaudeConfigRead,
    ClaudeProjectRead,
    ClaudeSessionListResponse,
    ClaudeSessionRead,
    ClaudeSessionStatsRead,
    CommandEntry,
    FileEntry,
    SessionMessagesResponse,
    SessionMessage,
    SessionTokensRead as ClaudeSessionTokensRead,
    SessionTokenUsageRead,
    TextBlock,
    ThinkingBlock,
    ToolAnalyticsResponse,
    ToolUseBlock,
)
from app.services import claude_code_reader as reader
from app.services.organizations import OrganizationContext

logger = get_logger(__name__)

router = APIRouter(prefix="/claude-code", tags=["claude-code"])
ORG_MEMBER_DEP = Depends(require_org_member)


def _session_to_read(s: reader.ClaudeSession) -> ClaudeSessionRead:
    return ClaudeSessionRead(
        session_id=s.session_id,
        project_dir=s.project_dir,
        cwd=s.cwd,
        title=s.title,
        models=s.models,
        tokens=ClaudeSessionTokensRead(
            input=s.tokens.input,
            output=s.tokens.output,
            cache_read=s.tokens.cache_read,
            cache_write=s.tokens.cache_write,
            total=s.tokens.total,
        ),
        cost_usd=s.cost_usd,
        message_count=s.message_count,
        first_message_at=s.first_message_at,
        last_message_at=s.last_message_at,
        is_active=s.is_active,
        entrypoints=s.entrypoints,
        git_branch=s.git_branch,
        version=s.version,
    )


# ── Session Tracking ──────────────────────────────────────────────────────────

@router.get(
    "/sessions",
    response_model=ClaudeSessionListResponse,
    summary="List local Claude Code sessions",
    description=(
        "Auto-discovers sessions from `~/.claude/projects/**/*.jsonl`. "
        "Returns token usage, model info, cost estimates, and active status "
        "for each session without requiring a gateway connection."
    ),
)
async def list_sessions(
    project: str | None = Query(None, description="Filter by project directory name substring"),
    active_only: bool = Query(False, description="Return only currently active sessions"),
    limit: int = Query(100, ge=1, le=500, description="Maximum sessions to return"),
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> ClaudeSessionListResponse:
    """List Claude Code sessions from local JSONL files."""
    sessions = await asyncio.to_thread(
        reader.list_sessions,
        project_filter=project,
        active_only=active_only,
        limit=limit,
    )
    stats = reader.session_stats(sessions)
    return ClaudeSessionListResponse(
        sessions=[_session_to_read(s) for s in sessions],
        total=len(sessions),
        stats=ClaudeSessionStatsRead(
            session_count=stats["session_count"],
            active_sessions=stats["active_sessions"],
            total_tokens=stats["total_tokens"],
            total_cost_usd=stats["total_cost_usd"],
            models=stats["models"],
        ),
    )


@router.get(
    "/sessions/{session_id}",
    response_model=ClaudeSessionRead,
    summary="Get a single Claude Code session",
)
async def get_session(
    session_id: str,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> ClaudeSessionRead:
    """Return a single parsed session by its UUID."""
    session = await asyncio.to_thread(reader.get_session, session_id)
    if session is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
    return _session_to_read(session)


@router.get(
    "/sessions/{session_id}/messages",
    response_model=SessionMessagesResponse,
    summary="Get conversation messages for a Claude Code session",
    description=(
        "Parses the raw JSONL file for a session and returns the full conversation "
        "thread: user turns, assistant text/thinking blocks, and tool calls with "
        "embedded results. Tool-result-only user turns are suppressed. "
        "Duplicate assistant message records (streaming artefacts) are merged."
    ),
)
async def get_session_messages(
    session_id: str,
    limit: int = Query(200, ge=1, le=500, description="Max messages to return"),
    offset: int = Query(0, ge=0, description="Number of messages to skip"),
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> SessionMessagesResponse:
    """Return the conversation thread for a single Claude Code session."""
    result = await asyncio.to_thread(reader.get_session_messages, session_id, limit, offset)
    if result is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")

    messages, total = result

    def _to_schema(m: reader.ParsedMessage) -> SessionMessage:
        return SessionMessage(
            uuid=m.uuid,
            role=m.role,
            timestamp=m.timestamp,
            text_blocks=[TextBlock(text=b.text, truncated=b.truncated) for b in m.text_blocks],
            thinking_blocks=[ThinkingBlock(text=b.text, truncated=b.truncated) for b in m.thinking_blocks],
            tool_uses=[
                ToolUseBlock(
                    tool_use_id=t.tool_use_id,
                    tool_name=t.tool_name,
                    input=t.input,
                    input_truncated=t.input_truncated,
                    result=t.result,
                    result_truncated=t.result_truncated,
                    is_error=t.is_error,
                )
                for t in m.tool_uses
            ],
            model=m.model,
            tokens=SessionTokenUsageRead(
                input=m.tokens.input,
                output=m.tokens.output,
                cache_read=m.tokens.cache_read,
                cache_write=m.tokens.cache_write,
            ) if m.tokens else None,
        )

    return SessionMessagesResponse(
        session_id=session_id,
        messages=[_to_schema(m) for m in messages],
        total=total,
        has_more=(offset + limit) < total,
    )


@router.get(
    "/projects",
    response_model=list[ClaudeProjectRead],
    summary="List discovered local Claude Code projects",
    description="Returns one entry per project directory with aggregated token and cost stats.",
)
async def list_projects(
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> list[ClaudeProjectRead]:
    """Aggregate session stats grouped by project directory."""
    projects = await asyncio.to_thread(reader.list_projects)
    return [
        ClaudeProjectRead(
            project_dir=p["project_dir"],
            cwd=p.get("cwd"),
            session_count=p["session_count"],
            total_tokens=p["total_tokens"],
            total_cost_usd=p["total_cost_usd"],
            last_active_at=p.get("last_active_at"),
            is_active=p["is_active"],
        )
        for p in projects
    ]


# ── Tool Analytics ───────────────────────────────────────────────────────────

@router.get(
    "/analytics/tools",
    response_model=ToolAnalyticsResponse,
    summary="Aggregate tool-use statistics across Claude Code sessions",
    description=(
        "Scans local JSONL session files and returns counts of each tool used, "
        "top files read and written, and top Bash commands by binary name. "
        "Duplicate streaming records are deduplicated by block id. "
        "Use `days` to scope the analysis window and `project` to narrow by project."
    ),
)
async def get_tool_analytics(
    project: str | None = Query(None, description="Filter by project directory name substring"),
    days: int = Query(30, ge=1, le=365, description="Number of days to look back (uses file mtime)"),
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> ToolAnalyticsResponse:
    """Return aggregated tool-use analytics from local Claude Code sessions."""
    data = await asyncio.to_thread(reader.get_tool_analytics, project, days)
    return ToolAnalyticsResponse(
        tool_counts=data["tool_counts"],
        top_files_read=[FileEntry(path=e["path"], count=e["count"]) for e in data["top_files_read"]],
        top_files_written=[FileEntry(path=e["path"], count=e["count"]) for e in data["top_files_written"]],
        top_commands=[CommandEntry(command=e["command"], count=e["count"]) for e in data["top_commands"]],
        session_count=data["session_count"],
        date_range_days=data["date_range_days"],
    )


# ── Config Scanner ────────────────────────────────────────────────────────────

@router.get(
    "/config",
    response_model=ClaudeConfigRead,
    summary="Read local Claude Code and Codex CLI configuration",
    description=(
        "Reads `~/.claude/settings.json`, `~/.codex/config.toml`, and "
        "`~/.codex/rules/*.rules`. Never returns secrets — only settings "
        "and trust/rule data."
    ),
)
async def get_config(
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> ClaudeConfigRead:
    """Return parsed local CLI configuration."""
    cfg = await asyncio.to_thread(reader.read_config)
    return ClaudeConfigRead(
        claude_settings=cfg.claude_settings,
        codex_config=cfg.codex_config,
        codex_rules=cfg.codex_rules,
        claude_credentials_configured=cfg.claude_credentials_path is not None,
        codex_credentials_configured=cfg.codex_credentials_path is not None,
    )


# ── Direct CLI Discovery ──────────────────────────────────────────────────────

async def _cli_version(cmd: list[str]) -> str | None:
    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
        line = stdout.decode(errors="replace").strip().splitlines()[0] if stdout else ""
        return line or None
    except Exception:
        return None


@router.get(
    "/cli",
    summary="Detect installed CLI tools",
    description=(
        "Checks which of Claude Code, Codex CLI, and Ollama are installed "
        "on the host and returns their versions. No gateway required."
    ),
)
async def detect_cli(
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> dict[str, Any]:
    """Return discovery metadata for locally installed CLI tools."""
    claude_path = shutil.which("claude")
    codex_path  = shutil.which("codex")
    ollama_path = shutil.which("ollama")

    claude_version = await _cli_version(["claude", "--version"]) if claude_path else None
    codex_version  = await _cli_version(["codex",  "--version"]) if codex_path  else None
    ollama_version = await _cli_version(["ollama",  "--version"]) if ollama_path else None

    return {
        "claude": {
            "installed": claude_path is not None,
            "path": claude_path,
            "version": claude_version,
            "projects_dir": str(reader._projects_dir()),
            "projects_dir_exists": reader._projects_dir().exists(),
        },
        "codex": {
            "installed": codex_path is not None,
            "path": codex_path,
            "version": codex_version,
        },
        "ollama": {
            "installed": ollama_path is not None,
            "path": ollama_path,
            "version": ollama_version,
        },
    }
