"""sync command — sync local sessions to the central database."""

from __future__ import annotations

import asyncio
import time

import typer
from rich.console import Console

from cli_framework import get_context
from session_hub_cli.cli import app
from session_hub_cli.commands._helpers import get_engine_config
from session_hub_cli.context import SessionHubContext
from session_hub_core import (
    ConnectionError,
    SyncEngine,
    SyncResult,
    resolve_hostname,
    save_host_state,
)
from session_store import SessionInfo, SessionStore


@app.command("sync")
def sync(
    ctx: typer.Context,
    tool: str | None = typer.Option(
        None, "--tool", "-t", help="Sync only this tool (devin, opencode, copilot)"
    ),
    host: str | None = typer.Option(None, "--host", help="Override the hostname tag"),
    full: bool = typer.Option(
        False, "--full", help="Full sync (ignore sync_log watermark, re-sync everything)"
    ),
    dry_run: bool = typer.Option(
        False, "--dry-run", help="Show what would be synced without writing"
    ),
    quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress output except errors"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Show per-session progress"),
    batch_size: int = typer.Option(100, "--batch-size", help="Message batch size for upserts"),
) -> None:
    """Sync local sessions to the central database."""
    app_ctx = get_context(ctx)
    assert isinstance(app_ctx, SessionHubContext)
    console = app_ctx.console

    config = get_engine_config(console)
    config.batch_size = batch_size

    try:
        resolved_host = resolve_hostname(override=host)
    except ValueError as exc:
        console.print(f"[red]Error:[/red] Invalid hostname: {exc}")
        raise typer.Exit(1) from None

    store = SessionStore()
    engine = SyncEngine(config, resolved_host)
    try:
        results = asyncio.run(_sync(engine, store, tool, full, dry_run, quiet, verbose, console))
    except ConnectionError as exc:
        console.print(f"[red]Error:[/red] Cannot connect to PostgreSQL: {exc}")
        raise typer.Exit(1) from None
    except KeyboardInterrupt:
        console.print("[dim]Interrupted.[/dim]")
        raise typer.Exit(130) from None

    for result in results:
        for err in result.errors:
            console.print(f"[red]Error:[/red] {err}")
    code = _exit_code(results)
    if code:
        raise typer.Exit(code)


async def _sync(
    engine: SyncEngine,
    store: SessionStore,
    tool: str | None,
    full: bool,
    dry_run: bool,
    quiet: bool,
    verbose: bool,
    console: Console,
) -> list[SyncResult]:
    started = time.monotonic()
    tools = [tool] if tool else sorted({s.tool for s in store.list_sessions(limit=10000)})
    results: list[SyncResult] = []
    try:
        for name in tools:
            sessions = store.list_sessions(tool=name, limit=10000)
            if verbose and not dry_run and not quiet and sessions:
                for info in sessions:
                    console.print(
                        f'[{name}] Syncing {info.session_id} "{info.title}" '
                        f"({info.message_count} messages)... ",
                        end="",
                    )
            if full:
                result = (
                    _dry_full_result(engine.host, sessions)
                    if dry_run
                    else await _full_sync(engine, store, name, sessions)
                )
            else:
                result = await engine.sync_tool(name, store=store, dry_run=dry_run)
            if verbose and not dry_run and not quiet and sessions:
                console.print("[red]Error[/red]" if result.errors else "OK")
            results.append(result)
            if not quiet:
                _print_tool_result(console, name, result, dry_run)
    finally:
        await engine.close()
    if not quiet:
        _print_total(console, results, engine.host, dry_run, started)
    return results


async def _full_sync(
    engine: SyncEngine,
    store: SessionStore,
    tool: str,
    sessions: list[SessionInfo],
) -> SyncResult:
    result = SyncResult(host=engine.host)
    for info in sessions:
        try:
            export = store.get_session(info.session_id, tool=tool)
            session_result = await engine.sync_session_export(export)
            result.sessions_upserted += session_result.sessions_upserted
            result.messages_upserted += session_result.messages_upserted
            result.messages_deleted += session_result.messages_deleted
        except Exception as exc:
            result.errors.append(f"{tool}/{info.session_id}: {exc}")
    save_host_state(engine.host)
    return result


def _dry_full_result(host: str, sessions: list[SessionInfo]) -> SyncResult:
    result = SyncResult(host=host)
    result.sessions_upserted = len(sessions)
    result.messages_upserted = sum(s.message_count for s in sessions)
    return result


def _print_tool_result(console: Console, tool: str, result: SyncResult, dry_run: bool) -> None:
    if dry_run:
        n = result.sessions_upserted
        label = "session" if n == 1 else "sessions"
        if result.messages_upserted > 0:
            console.print(
                f"Would sync {n} {label} ({result.messages_upserted} messages) from {tool}"
            )
        else:
            console.print(f"Would sync {n} {label} from {tool}")


def _print_total(
    console: Console,
    results: list[SyncResult],
    host: str,
    dry_run: bool,
    started: float,
) -> None:
    sessions = sum(r.sessions_upserted for r in results)
    messages = sum(r.messages_upserted for r in results)
    if dry_run:
        console.print(f"Total: {sessions} sessions, {messages} messages")
    else:
        console.print(
            f"Synced {sessions} sessions ({messages} messages) from {host} "
            f"in {time.monotonic() - started:.1f}s"
        )


def _exit_code(results: list[SyncResult]) -> int:
    if not results:
        return 0
    failed = [r for r in results if r.errors]
    if len(failed) == len(results):
        return 3
    if failed:
        return 2
    return 0
