"""
Export serving tables from the pipeline DuckDB to the serving DuckDB (atomic swap).

Called by the supervisor after each SQLMesh transform run. Reads all tables in
the 'serving' schema from the pipeline DB (DUCKDB_PATH), writes them to a temp
file, then atomically renames it to the serving DB path (SERVING_DUCKDB_PATH).

The web app's analytics connection detects the inode change on the next query
and reopens the connection automatically — no restart or signal required.

Why two files?
  SQLMesh holds an exclusive write lock on DUCKDB_PATH during plan/run.
  The web app needs read-only access at all times. Two separate files allow
  both to operate concurrently: SQLMesh writes to the pipeline DB, the web
  app reads from the serving DB, and this script swaps them atomically.

The temp file is named _export.duckdb (not serving.duckdb.tmp) because DuckDB
names its catalog after the filename stem. A file named serving.* would create
a catalog named 'serving', which conflicts with the schema named 'serving'
inside the file, making all queries ambiguous.

Usage:
    DUCKDB_PATH=local.duckdb SERVING_DUCKDB_PATH=analytics.duckdb \\
        uv run python src/padelnomics/export_serving.py
"""

import json
import logging
import os
import re
from datetime import UTC, datetime
from pathlib import Path

import duckdb

logger = logging.getLogger(__name__)


def export_serving() -> None:
    """Copy all serving.* tables from the pipeline DB to the serving DB atomically."""
    pipeline_path = os.getenv("DUCKDB_PATH", "")
    serving_path = os.getenv("SERVING_DUCKDB_PATH", "")
    assert pipeline_path, "DUCKDB_PATH must be set"
    assert serving_path, "SERVING_DUCKDB_PATH must be set"
    assert os.path.exists(pipeline_path), f"Pipeline DB not found: {pipeline_path}"

    # Temp path in the same directory as the serving DB so rename() is atomic
    # (rename across filesystems is not atomic on Linux).
    tmp_path = os.path.join(os.path.dirname(os.path.abspath(serving_path)), "_export.duckdb")

    table_counts: dict[str, int] = {}

    src = duckdb.connect(pipeline_path, read_only=True)
    try:
        # SQLMesh creates serving views that reference "local".sqlmesh__serving.*
        # which fails when connecting directly.  Resolve the physical table each
        # view points to by parsing the view definition.
        view_rows = src.execute(
            "SELECT view_name, sql FROM duckdb_views()"
            " WHERE schema_name = 'serving' ORDER BY view_name"
        ).fetchall()
        assert view_rows, f"No views found in serving schema of {pipeline_path}"

        # Extract physical table reference from: CREATE VIEW ... AS SELECT * FROM "local".schema.table;
        physical_tables: list[tuple[str, str]] = []  # (logical_name, physical_ref)
        for view_name, view_sql in view_rows:
            # Pattern: ... FROM "local".sqlmesh__serving.serving__name__hash;
            # Strip the "local". prefix to get schema.table
            match = re.search(r'FROM\s+"local"\.(sqlmesh__serving\.\S+)', view_sql)
            assert match, f"Cannot parse view definition for {view_name}: {view_sql[:200]}"
            physical_tables.append((view_name, match.group(1)))

        logger.info(
            "Exporting %d serving tables: %s",
            len(physical_tables),
            [name for name, _ in physical_tables],
        )

        dst = duckdb.connect(tmp_path)
        try:
            dst.execute("CREATE SCHEMA IF NOT EXISTS serving")
            for logical_name, physical_ref in physical_tables:
                # Read via Arrow to avoid cross-connection catalog ambiguity.
                arrow_data = src.sql(f"SELECT * FROM {physical_ref}").arrow()
                dst.register("_src", arrow_data)
                dst.execute(f"CREATE OR REPLACE TABLE serving.{logical_name} AS SELECT * FROM _src")
                dst.unregister("_src")
                row_count = dst.sql(f"SELECT count(*) FROM serving.{logical_name}").fetchone()[0]
                table_counts[logical_name] = row_count
                logger.info(f"  serving.{logical_name}: {row_count:,} rows")
        finally:
            dst.close()
    finally:
        src.close()

    # Atomic rename — on Linux, rename() is atomic when src and dst are on the same filesystem.
    os.rename(tmp_path, serving_path)
    logger.info(f"Serving DB atomically updated: {serving_path}")

    # Write freshness metadata so the pSEO dashboard can show data age without
    # querying file mtimes (which are unreliable after rclone syncs).
    meta_path = Path(serving_path).parent / "_serving_meta.json"
    meta = {
        "exported_at_utc": datetime.now(tz=UTC).isoformat(),
        "tables": {name: {"row_count": count} for name, count in table_counts.items()},
    }
    meta_path.write_text(json.dumps(meta))
    logger.info("Wrote serving metadata: %s", meta_path)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
    export_serving()
