"""CLI for observability context operations."""

from __future__ import annotations

import json
from pathlib import Path

import typer
import uvicorn

from common.server import create_app
from otelctx.api.routes import build_router, ingest_payload
from otelctx.archive import ArchiveTier
from otelctx.cache import DeadLetterQueue, DlqReplayWorker, _DlqBase
from otelctx.config import OtelSettings
from otelctx.db.repository import OtelRepository
from otelctx.graph import ServiceGraph
from otelctx.ingest import normalize_log_payload, normalize_metric_payload, normalize_trace_payload
from otelctx.otlp_grpc import start_otlp_grpc_server
from otelctx.realtime import RealtimeHub, build_ws_router
from otelctx.sampler import AdaptiveSampler
from otelctx.telemetry import telemetry
from otelctx.tsdb import TsdbRingBuffer

app = typer.Typer(help="Otel context commands.", no_args_is_help=True)


def build_app(settings: OtelSettings | None = None):
    """Create the otelctx ASGI app."""
    resolved_settings = settings or OtelSettings()
    repository = OtelRepository(resolved_settings.db_path)
    repository.create_schema()
    _DlqBase.metadata.create_all(repository.engine)

    tsdb = TsdbRingBuffer(per_series_capacity=resolved_settings.tsdb_capacity)
    archive = ArchiveTier(resolved_settings.archive_dir, hot_days=resolved_settings.hot_days)
    dlq = DeadLetterQueue(repository.session_factory)
    sampler = AdaptiveSampler(
        rate=resolved_settings.sampling_rate,
        always_errors=resolved_settings.sampling_always_errors,
        latency_threshold_ms=resolved_settings.sampling_latency_threshold_ms,
    )
    graph = ServiceGraph()
    hub = RealtimeHub(
        buffer_size=resolved_settings.ws_buffer_size,
        flush_interval_ms=resolved_settings.ws_flush_interval_ms,
    )

    dlq_worker = DlqReplayWorker(
        dlq,
        lambda payload_str: ingest_payload(repository, json.loads(payload_str)),
        interval_seconds=resolved_settings.dlq_replay_interval_seconds,
        max_retries=resolved_settings.dlq_max_retries,
    )
    dlq_worker.start()
    graph.start_auto_rebuild(repository, interval_seconds=30.0)

    app_instance = create_app("otelctx", "0.1.0")
    app_instance.include_router(
        build_router(repository, graph=graph, tsdb=tsdb, archive=archive, dlq=dlq, sampler=sampler)
    )
    app_instance.include_router(build_ws_router(hub))
    if resolved_settings.prometheus_enabled:
        app_instance.include_router(telemetry.build_router())

    @app_instance.post("/v1/traces")
    async def ingest_otlp(payload: dict[str, object]) -> dict[str, int]:
        return ingest_payload(repository, payload)

    @app_instance.post("/v1/logs")
    async def ingest_otlp_logs(payload: dict[str, object]) -> dict[str, int]:
        return ingest_payload(repository, payload)

    @app_instance.post("/v1/metrics")
    async def ingest_otlp_metrics(payload: dict[str, object]) -> dict[str, int]:
        return ingest_payload(repository, payload)

    return app_instance


@app.command("ingest")
def ingest(
    path: str = typer.Argument(..., help="Path to a JSON OTLP or custom payload file."),
    db_path: str | None = typer.Option(None, "--db", help="Path to the otelctx SQLite database."),
) -> None:
    """Ingest traces, logs, or metrics from a JSON payload file."""
    settings = OtelSettings()
    repository = OtelRepository(db_path or settings.db_path)
    repository.create_schema()
    payload = json.loads(Path(path).read_text(encoding="utf-8"))
    traces = normalize_trace_payload(payload)
    logs = normalize_log_payload(payload)
    metrics = normalize_metric_payload(payload)
    if not traces and not logs and not metrics:
        typer.echo("No traces, logs, or metrics found in payload")
        raise typer.Exit(code=1)

    summary = {"traces": 0, "spans": 0, "logs": 0, "metrics": 0}
    if traces:
        trace_summary = repository.ingest_traces([trace for trace in traces if trace.trace_id])
        summary["traces"] = trace_summary["traces"]
        summary["spans"] = trace_summary["spans"]
    if logs:
        summary["logs"] = repository.ingest_logs(logs)["logs"]
    if metrics:
        summary["metrics"] = repository.ingest_metrics(metrics)["metrics"]

    typer.echo(f"Ingested traces: {summary['traces']}")
    typer.echo(f"Ingested spans: {summary['spans']}")
    typer.echo(f"Ingested logs: {summary['logs']}")
    typer.echo(f"Ingested metrics: {summary['metrics']}")


@app.command("stats")
def stats(db_path: str | None = typer.Option(None, "--db", help="Path to the otelctx SQLite database.")) -> None:
    """Show OTEL statistics."""
    settings = OtelSettings()
    repository = OtelRepository(db_path or settings.db_path)
    repository.create_schema()
    snapshot = repository.get_stats()
    typer.echo(f"Traces: {snapshot.traces}")
    typer.echo(f"Spans: {snapshot.spans}")
    typer.echo(f"Logs: {snapshot.logs}")
    typer.echo(f"Metrics: {snapshot.metrics}")
    typer.echo(f"Services: {snapshot.services}")
    typer.echo(f"Error spans: {snapshot.error_spans}")


@app.command("serve")
def serve(
    host: str | None = typer.Option(None, "--host", help="Bind host."),
    http_port: int | None = typer.Option(None, "--http-port", help="HTTP API port."),
    grpc_port: int | None = typer.Option(None, "--grpc-port", help="OTLP gRPC receiver port."),
    db_path: str | None = typer.Option(None, "--db", help="Path to the otelctx SQLite database."),
) -> None:
    """Run the OTEL API server."""
    settings = OtelSettings()
    updates: dict[str, object] = {}
    if db_path is not None:
        updates["db_path"] = Path(db_path)
    if grpc_port is not None:
        updates["grpc_port"] = grpc_port
    if updates:
        settings = settings.model_copy(update=updates)

    repository = OtelRepository(settings.db_path)
    repository.create_schema()
    grpc_server = None

    if settings.grpc_port > 0:
        try:
            grpc_server = start_otlp_grpc_server(
                settings.grpc_port,
                lambda payload: ingest_payload(repository, payload),
            )
            typer.echo(f"OTLP gRPC receiver listening on 0.0.0.0:{settings.grpc_port}")
        except RuntimeError as exc:
            typer.echo(f"OTLP gRPC disabled: {exc}")

    try:
        uvicorn.run(build_app(settings), host=host or settings.host, port=http_port or settings.port)
    finally:
        if grpc_server is not None:
            grpc_server.stop(grace=1)


def _parse_addr(addr: str) -> tuple[str, int]:
    host, separator, port_text = addr.rpartition(":")
    if not separator or not host or not port_text:
        raise ValueError(f"Address must be in host:port form, got: {addr}")
    return host, int(port_text)


def _write_mcp_config(output_path: Path, server_name: str, entry: dict) -> Path:
    import json as _json
    if output_path.exists():
        try:
            config = _json.loads(output_path.read_text(encoding="utf-8"))
        except _json.JSONDecodeError:
            config = {}
    else:
        config = {}
    if not isinstance(config, dict):
        config = {}
    servers = config.get("servers")
    if not isinstance(servers, dict):
        servers = {}
    inputs = config.get("inputs")
    if not isinstance(inputs, list):
        inputs = []
    servers[server_name] = entry
    config["servers"] = servers
    config["inputs"] = inputs
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(_json.dumps(config, indent=2) + "\n", encoding="utf-8")
    return output_path


@app.command("mcp")
def mcp_cmd(
    transport: str = typer.Option("stdio", "--transport", help="Transport: stdio, sse, or streamable-http."),
    addr: str = typer.Option("127.0.0.1:8082", "--addr", help="Bind address for HTTP MCP transports."),
    db_path: str | None = typer.Option(None, "--db", help="Path to the otelctx SQLite database."),
) -> None:
    """Run the otelctx MCP server."""
    from otelctx.mcp import build_mcp_server
    settings = OtelSettings()
    if db_path is not None:
        settings = settings.model_copy(update={"db_path": Path(db_path)})
    host, port = _parse_addr(addr)
    server = build_mcp_server(settings, host=host, port=port)
    server.run(transport=transport)


@app.command("setup")
def setup(
    binary: str = typer.Option("ossCtx", "--binary", help="Command used to launch the unified CLI."),
    transport: str = typer.Option("stdio", "--transport", help="Transport: stdio, sse, or streamable-http."),
    addr: str = typer.Option("127.0.0.1:8082", "--addr", help="Address for HTTP MCP transports."),
    db_path: str | None = typer.Option(None, "--db", help="Optional otelctx database path to bake into the config."),
    output: str = typer.Option(".vscode/mcp.json", "--output", help="Path to the VS Code MCP config file to write."),
    server_name: str = typer.Option("ossctx-otelctx", "--server-name", help="Server name in the MCP config."),
) -> None:
    """Write or update a VS Code MCP config file for otelctx."""
    from otelctx.mcp import build_mcp_server_entry
    entry = build_mcp_server_entry(binary=binary, transport=transport, addr=addr, db_path=db_path)
    target = _write_mcp_config(Path(output), server_name=server_name, entry=entry)
    typer.echo(f"Wrote MCP config: {target}")


def compat_main() -> None:
    """Compatibility entrypoint for legacy `otelcontext` command."""
    app()
