"""data_factory_record_run — wraps `fn run <function_id>` and persists metrics in data_factory.db."""

import os
import re
import sqlite3
import subprocess
import uuid
from datetime import datetime, timezone
from pathlib import Path


def _now_iso8601() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"


def _elapsed_ms(start: float) -> int:
    import time
    return int((time.monotonic() - start) * 1000)


def _parse_rows_out(stdout: str) -> int:
    """Parse first line matching rows/extracted/written/count[:= ]+N (case insensitive)."""
    pattern = re.compile(r'^(?:rows|extracted|written|count)[:=\s]+(\d+)', re.IGNORECASE | re.MULTILINE)
    m = pattern.search(stdout)
    return int(m.group(1)) if m else 0


def _kb_out(stdout: str) -> int:
    return round(len(stdout.encode("utf-8")) / 1024)


def data_factory_record_run(
    node_id: str,
    function_id: str,
    args: list | None = None,
    db_path: str | None = None,
    trigger: str = "manual",
) -> dict:
    """Wrap `fn run <function_id>` and record execution metrics in data_factory.db.

    Args:
        node_id: ID of the node in data_factory.db.nodes that owns this run.
        function_id: Registry function ID to execute (passed to `fn run`).
        args: Extra CLI args forwarded to `fn run` after function_id.
        db_path: Absolute path to data_factory.db. Defaults to
            ${FN_REGISTRY_ROOT}/apps/data_factory/data_factory.db.
        trigger: Origin of the run — 'manual'|'cron'|'dag'|'api'.

    Returns:
        dict with keys: run_id, status, rows_out, kb_out, duration_ms, stdout, stderr.
    """
    import time

    # --- resolve FN_REGISTRY_ROOT ---
    registry_root = os.environ.get("FN_REGISTRY_ROOT", "").strip()
    if not registry_root:
        raise RuntimeError(
            "FN_REGISTRY_ROOT env var is not set. "
            "Export it before calling data_factory_record_run."
        )
    registry_root = Path(registry_root)

    # --- resolve db_path ---
    if db_path is None:
        db_path = registry_root / "apps" / "data_factory" / "data_factory.db"
    db_path = Path(db_path)
    if not db_path.exists():
        raise FileNotFoundError(f"data_factory.db not found at {db_path}")

    # --- resolve fn binary ---
    fn_bin = registry_root / "fn"
    if not fn_bin.exists():
        raise FileNotFoundError(
            f"fn binary not found at {fn_bin}. "
            "Run `CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/` in FN_REGISTRY_ROOT."
        )

    # --- generate run_id ---
    run_id = uuid.uuid4().hex[:16]

    # --- INSERT running record ---
    started_at = _now_iso8601()
    try:
        conn = sqlite3.connect(str(db_path))
        conn.execute("PRAGMA foreign_keys = ON")
        try:
            conn.execute(
                "INSERT INTO runs(id, node_id, started_at, status, trigger) VALUES (?,?,?,?,?)",
                (run_id, node_id, started_at, "running", trigger),
            )
            conn.commit()
        except sqlite3.IntegrityError as e:
            conn.close()
            raise RuntimeError(
                f"FK violation — node_id '{node_id}' does not exist in nodes table. "
                f"Insert the node first. SQLite error: {e}"
            )
    except sqlite3.Error as e:
        raise RuntimeError(f"Failed to open/write data_factory.db at {db_path}: {e}")

    # --- run fn ---
    cmd = [str(fn_bin), "run", function_id] + (args or [])
    t0 = time.monotonic()
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(registry_root))
    except Exception as e:
        duration_ms = _elapsed_ms(t0)
        finished_at = _now_iso8601()
        conn.execute(
            "UPDATE runs SET finished_at=?, status=?, duration_ms=?, error=? WHERE id=?",
            (finished_at, "failed", duration_ms, str(e)[:2000], run_id),
        )
        conn.commit()
        conn.close()
        return {
            "run_id": run_id,
            "status": "failed",
            "rows_out": 0,
            "kb_out": 0,
            "duration_ms": duration_ms,
            "stdout": "",
            "stderr": str(e),
        }

    duration_ms = _elapsed_ms(t0)
    finished_at = _now_iso8601()
    stdout = result.stdout or ""
    stderr = result.stderr or ""
    status = "success" if result.returncode == 0 else "failed"
    rows_out = _parse_rows_out(stdout)
    kb = _kb_out(stdout)
    error_text = stderr[:2000] if status == "failed" else ""

    conn.execute(
        """UPDATE runs
           SET finished_at=?, status=?, rows_out=?, kb_out=?,
               duration_ms=?, error=?
           WHERE id=?""",
        (finished_at, status, rows_out, kb, duration_ms, error_text, run_id),
    )
    conn.commit()
    conn.close()

    return {
        "run_id": run_id,
        "status": status,
        "rows_out": rows_out,
        "kb_out": kb,
        "duration_ms": duration_ms,
        "stdout": stdout,
        "stderr": stderr,
    }
