"""Command log API endpoints."""

import csv
import io
from datetime import datetime
from typing import Literal

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from shuttle.db.models import CommandLog, Node
from shuttle.web.deps import get_db_session
from shuttle.web.schemas import LogListResponse

router = APIRouter(tags=["logs"])


async def _batch_node_names(db: AsyncSession, node_ids: set[str]) -> dict[str, str]:
    """Load node names for a set of node IDs."""
    if not node_ids:
        return {}
    result = await db.execute(select(Node.id, Node.name).where(Node.id.in_(node_ids)))
    return {row.id: row.name for row in result.all()}


def _log_to_response(log: CommandLog, node_names: dict[str, str]) -> dict:
    return {
        "id": log.id,
        "session_id": log.session_id,
        "node_id": log.node_id,
        "node_name": node_names.get(log.node_id),
        "command": log.command,
        "exit_code": log.exit_code,
        "stdout": log.stdout,
        "stderr": log.stderr,
        "security_level": log.security_level,
        "bypassed": log.bypassed,
        "duration_ms": log.duration_ms,
        "executed_at": log.executed_at,
    }


def _build_filter_stmt(
    stmt,
    node_id: str | None,
    session_id: str | None,
    since: datetime | None = None,
    until: datetime | None = None,
):
    """Apply optional filters to a select statement."""
    if node_id is not None:
        stmt = stmt.where(CommandLog.node_id == node_id)
    if session_id is not None:
        stmt = stmt.where(CommandLog.session_id == session_id)
    if since is not None:
        stmt = stmt.where(CommandLog.executed_at >= since)
    if until is not None:
        stmt = stmt.where(CommandLog.executed_at <= until)
    return stmt


@router.get("/logs", response_model=LogListResponse)
async def list_logs(
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=200),
    node_id: str | None = None,
    session_id: str | None = None,
    since: str | None = None,
    until: str | None = None,
    db: AsyncSession = Depends(get_db_session),
):
    """Paginated list of command logs with optional filters."""
    try:
        since_dt = datetime.fromisoformat(since) if since else None
    except ValueError as err:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid 'since' datetime format: {since!r}. Use ISO 8601 format (e.g. 2025-01-01T00:00:00).",
        ) from err
    try:
        until_dt = datetime.fromisoformat(until) if until else None
    except ValueError as err:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid 'until' datetime format: {until!r}. Use ISO 8601 format (e.g. 2025-01-01T00:00:00).",
        ) from err

    # Total count
    count_stmt = select(func.count(CommandLog.id))
    count_stmt = _build_filter_stmt(count_stmt, node_id, session_id, since_dt, until_dt)
    total = (await db.execute(count_stmt)).scalar_one()

    # Paginated items
    items_stmt = (
        select(CommandLog)
        .order_by(CommandLog.executed_at.desc())
        .offset((page - 1) * page_size)
        .limit(page_size)
    )
    items_stmt = _build_filter_stmt(items_stmt, node_id, session_id, since_dt, until_dt)
    result = await db.execute(items_stmt)
    logs = list(result.scalars().all())

    node_ids = {log.node_id for log in logs}
    node_names = await _batch_node_names(db, node_ids)

    return LogListResponse(
        items=[_log_to_response(log, node_names) for log in logs],
        total=total,
        page=page,
        page_size=page_size,
    )


@router.get("/logs/export")
async def export_logs(
    format: Literal["json", "csv"] = "json",
    node_id: str | None = None,
    session_id: str | None = None,
    db: AsyncSession = Depends(get_db_session),
):
    """Export all matching logs as JSON or CSV (max 50,000 rows)."""
    MAX_EXPORT_ROWS = 50_000
    stmt = (
        select(CommandLog)
        .order_by(CommandLog.executed_at.desc())
        .limit(MAX_EXPORT_ROWS + 1)
    )
    stmt = _build_filter_stmt(stmt, node_id, session_id)
    result = await db.execute(stmt)
    logs = list(result.scalars().all())

    truncated = len(logs) > MAX_EXPORT_ROWS
    if truncated:
        logs = logs[:MAX_EXPORT_ROWS]

    node_ids = {log.node_id for log in logs}
    node_names = await _batch_node_names(db, node_ids)

    items = [_log_to_response(log, node_names) for log in logs]

    if format == "csv":
        output = io.StringIO()
        if items:
            writer = csv.DictWriter(output, fieldnames=list(items[0].keys()))
            writer.writeheader()
            for item in items:
                # Convert datetime objects to ISO strings for CSV
                row = {
                    k: v.isoformat() if hasattr(v, "isoformat") else v
                    for k, v in item.items()
                }
                writer.writerow(row)
        if truncated:
            output.write(
                f"\n# NOTE: Export truncated to {MAX_EXPORT_ROWS} rows. Apply filters to narrow results.\n"
            )
        content = output.getvalue()
        return StreamingResponse(
            iter([content]),
            media_type="text/csv",
            headers={"Content-Disposition": "attachment; filename=logs.csv"},
        )

    # JSON format
    # Convert datetimes to ISO strings for JSON serialization
    json_items = []
    for item in items:
        json_item = {
            k: v.isoformat() if hasattr(v, "isoformat") else v for k, v in item.items()
        }
        json_items.append(json_item)

    if truncated:
        return {
            "items": json_items,
            "truncated": True,
            "note": f"Export truncated to {MAX_EXPORT_ROWS} rows. Apply filters to narrow results.",
        }
    return json_items
