from __future__ import annotations

import json
import os
import socket
import subprocess
import sys
import time
from pathlib import Path

import pytest

duckdb = pytest.importorskip("duckdb")


def _repo_root() -> Path:
    return Path(__file__).resolve().parents[4]


def _extend_sys_path() -> None:
    repo_root = _repo_root()
    src_paths = [
        repo_root / "sandcastle" / "cf_ontology" / "src",
        repo_root / "sandcastle" / "cf_opcua_server" / "src",
        repo_root / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine",
        repo_root / "sandcastle" / "cf_datahive" / "src",
    ]
    for src_path in src_paths:
        src_path_str = str(src_path)
        if src_path.exists() and src_path_str not in sys.path:
            sys.path.insert(0, src_path_str)


def _find_engine_exe() -> Path | None:
    root = _repo_root()
    candidates = [
        root / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine" / "build" / "Debug" / "cf_pipeline_v2.exe",
        root / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine" / "build" / "Release" / "cf_pipeline_v2.exe",
        root / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine" / "build" / "cf_pipeline_v2.exe",
        root / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine" / "build" / "cf_pipeline_v2",
    ]
    for candidate in candidates:
        if candidate.exists():
            return candidate
    return None


def _plugins_available() -> bool:
    root = _repo_root()
    required = [
        root / "sandcastle" / "cf_basic_steps" / "cf_basic_io" / "src" / "cf_basic_io" / "bin" / "cf_basic_io_v2.dll",
        root / "sandcastle" / "cf_basic_steps" / "cf_basic_signal" / "src" / "cf_basic_signal" / "bin" / "cf_basic_signal_v2.dll",
        root / "sandcastle" / "cf_basic_steps" / "cf_basic_sinks" / "src" / "cf_basic_sinks" / "bin" / "cf_basic_sinks_v2.dll",
    ]
    return all(path.exists() for path in required)


def _pythonpath_env(base_env: dict[str, str]) -> dict[str, str]:
    root = _repo_root()
    additions = [
        root / "sandcastle" / "cf_ontology" / "src",
        root / "sandcastle" / "cf_opcua_server" / "src",
        root / "sandcastle" / "cf_pipeline" / "cf_pipeline_engine",
        root / "sandcastle" / "cf_datahive" / "src",
    ]
    current = base_env.get("PYTHONPATH", "")
    parts = [str(path) for path in additions]
    if current:
        parts.append(current)
    base_env["PYTHONPATH"] = os.pathsep.join(parts)
    return base_env


def _run_cmd(args: list[str], *, env: dict[str, str], cwd: Path) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        args,
        cwd=str(cwd),
        env=env,
        capture_output=True,
        text=True,
        check=False,
    )


def _wait_for_port(host: str, port: int, timeout_seconds: float) -> bool:
    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        try:
            with socket.create_connection((host, port), timeout=1.0):
                return True
        except OSError:
            time.sleep(0.2)
    return False


def test_opcua_fifo_avg_demo_persists_datahive_parquet(tmp_path: Path) -> None:
    _extend_sys_path()

    engine_exe = _find_engine_exe()
    if engine_exe is None:
        pytest.skip("cf_pipeline_v2 executable not found; build cf_pipeline_engine first")
    if not _plugins_available():
        pytest.skip("required step plugins not found; build cf_basic_io/cf_basic_signal/cf_basic_sinks first")

    from cf_datahive import DataHiveClient

    repo_root = _repo_root()
    pipeline_path = (
        repo_root
        / "sandcastle"
        / "cf_pipeline"
        / "cf_pipeline_engine"
        / "examples"
        / "opcua_fifo_avg_to_duckdb_parquet_triggered.jsonld"
    )
    semantics_dir = tmp_path / "semantics"
    semantics_dir.mkdir()
    workspace_root = tmp_path / "workspace"
    workspace_root.mkdir()

    env = _pythonpath_env(dict(os.environ))
    env["CF_PIPELINE_V2_EXE"] = str(engine_exe)

    server_proc: subprocess.Popen[str] | None = None

    try:
        cmd_base = [
            sys.executable,
            "-m",
            "cf_ontology",
        ]

        init_proc = _run_cmd(
            cmd_base + [
                "init",
                "--semantics-dir",
                str(semantics_dir),
                "--rebuild",
            ],
            env=env,
            cwd=tmp_path,
        )
        assert init_proc.returncode == 0, init_proc.stderr

        id_proc = _run_cmd(
            cmd_base + [
                "pipeline",
                "id",
                "--jsonld",
                str(pipeline_path),
            ],
            env=env,
            cwd=tmp_path,
        )
        assert id_proc.returncode == 0, id_proc.stderr
        pipeline_state_id = id_proc.stdout.strip().splitlines()[-1]
        assert pipeline_state_id

        commit_proc = _run_cmd(
            cmd_base + [
                "pipeline",
                "commit",
                "--semantics-dir",
                str(semantics_dir),
                "--pipeline-id",
                pipeline_state_id,
                "--jsonld",
                str(pipeline_path),
                "--user",
                "test",
                "--message",
                "integration",
            ],
            env=env,
            cwd=tmp_path,
        )
        assert commit_proc.returncode == 0, commit_proc.stderr

        activate_proc = _run_cmd(
            cmd_base + [
                "state",
                "activate",
                "--semantics-dir",
                str(semantics_dir),
                "--pipeline-id",
                pipeline_state_id,
                "--desired-state",
                "enabled",
            ],
            env=env,
            cwd=tmp_path,
        )
        assert activate_proc.returncode == 0, activate_proc.stderr

        server_proc = subprocess.Popen(
            [
                sys.executable,
                "-m",
                "cf_opcua_server.cli",
                "start",
                "--interval",
                "0.5",
                "--duration",
                "60",
            ],
            cwd=str(tmp_path),
            env=env,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            text=True,
        )
        assert _wait_for_port("127.0.0.1", 4840, timeout_seconds=15.0), "OPC UA server did not start"

        emit_event_proc = _run_cmd(
            cmd_base
            + [
                "state",
                "emit-event",
                "--semantics-dir",
                str(semantics_dir),
                "--pipeline-id",
                pipeline_state_id,
                "--event-type",
                "opcua_signal",
                "--source",
                "integration-test",
                "--dedupe-key",
                "integration-opcua-001",
            ],
            cwd=tmp_path,
            env=env,
        )
        assert emit_event_proc.returncode == 0, emit_event_proc.stderr

        run_loop_proc = _run_cmd(
            cmd_base
            + [
                "state",
                "run-loop",
                "--semantics-dir",
                str(semantics_dir),
                "--pipeline-id",
                pipeline_state_id,
                "--poll-interval",
                "0.2",
                "--once",
            ],
            cwd=tmp_path,
            env=env,
        )
        if run_loop_proc.returncode != 0 and "Failed to load plugin:" in run_loop_proc.stderr:
            pytest.skip("pipeline plugin load failed in this environment; rebuild plugins with matching toolchain")
        assert run_loop_proc.returncode == 0, run_loop_proc.stderr

        state_db = duckdb.connect(str(semantics_dir / "cf-pipeline-states.duckdb"), read_only=True)
        try:
            linkage_row = state_db.execute(
                """
                SELECT
                  re.event_id,
                  re.run_id,
                  re.status,
                  pr.triggered_by_event_id,
                  pr.status
                FROM run_events AS re
                JOIN pipeline_runs AS pr
                  ON pr.run_id = re.run_id
                WHERE re.pipeline_id = ? AND re.dedupe_key = ?
                ORDER BY re.created_at DESC
                LIMIT 1
                """,
                [pipeline_state_id, "integration-opcua-001"],
            ).fetchone()
        finally:
            state_db.close()
        assert linkage_row is not None
        assert str(linkage_row[0]) == str(linkage_row[3])
        assert linkage_row[1] is not None
        assert str(linkage_row[2]) == "consumed"
        assert str(linkage_row[4]) == "succeeded"

        client = DataHiveClient(str(workspace_root))
        runs = client.list_runs("opcua_fifo_avg")
        assert runs, "no datahive runs written for opcua_fifo_avg"

        run_id = runs[0].run_id
        run = client.open_run("opcua_fifo_avg", run_id)
        manifest_path = run.run_path / "manifest.json"
        assert manifest_path.exists()

        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
        assert manifest["status"] == "committed"
        assert isinstance(manifest.get("session_id"), str) and manifest["session_id"]
        assert isinstance(manifest.get("session_run_index"), int)
        assert f"run_id={run_id}" in str(manifest["tables"]["measurements"]["path"])
        assert run.run_path.parent.name == "runs"
        assert run.run_path.parent.parent.name == manifest["session_id"]

        table = client.read_table("opcua_fifo_avg", run_id, "measurements")
        payload = table.to_pydict()
        assert table.num_rows == 20
        assert payload["cycle_id"] == list(range(20))

        archive_candidates = list(workspace_root.rglob("archive.jsonl"))
        assert not archive_candidates
    finally:
        if server_proc is not None:
            server_proc.terminate()
            try:
                server_proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                server_proc.kill()
                server_proc.wait(timeout=5)
