from __future__ import annotations

from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from cf_ontology import pipeline_state_runtime as runtime
from cf_ontology.pipeline_states_store import PipelineStateStore


def _make_pipeline_doc(*, event_type: str) -> dict[str, Any]:
    return {
        "@context": {
            "cf": "https://cogniflow.org/ns#",
            "cfrun": "https://cogniflow.org/runtime#",
            "ex": "http://example.org/",
        },
        "@graph": [
            {
                "@id": "ex:pipeline",
                "@type": "cf:ProcessingPipeline",
                "cfrun:hasTrigger": {"@id": "ex:trigger"},
                "cfrun:minIntervalSeconds": 0,
            },
            {
                "@id": "ex:trigger",
                "@type": "cf:PipelineTrigger",
                "cfrun:eventType": event_type,
            },
            {
                "@id": "ex:endpointBinding",
                "@type": "cf:ParameterBinding",
                "cf:value": "opc.tcp://127.0.0.1:4840/VirtualPhServer",
            },
        ],
    }


def _setup_runtime(monkeypatch, tmp_path: Path, *, event_type: str, opcua_reachable: bool = True):
    semantics_dir = tmp_path / "semantics"
    semantics_dir.mkdir(parents=True, exist_ok=True)
    pipeline_doc = _make_pipeline_doc(event_type=event_type)
    run_calls: list[dict[str, Any]] = []

    class _FakeOntologyManager:
        def __init__(self, *, load_resources: bool = True) -> None:
            self._semantics_dir = semantics_dir
            self.pipeline_states_db_path = semantics_dir / "cf-pipeline-states.duckdb"
            self.pipelines_db_path = semantics_dir / "cf-pipelines.duckdb"

        def get_pipeline_revision(self, pipeline_id: str, *, flatten: bool = True) -> dict[str, Any]:
            return pipeline_doc

    def _fake_run_engine(*args, **kwargs) -> int:  # noqa: ANN001, ANN002
        run_calls.append(dict(kwargs))
        return 0

    monkeypatch.setattr(runtime, "OntologyManager", _FakeOntologyManager)
    monkeypatch.setattr(runtime, "_get_current_rev", lambda *args, **kwargs: 1)
    monkeypatch.setattr(runtime, "_get_graph_source_path", lambda *args, **kwargs: None)
    monkeypatch.setattr(runtime, "_run_engine", _fake_run_engine)
    monkeypatch.setattr(runtime, "_opcua_server_reachable", lambda endpoint, timeout=1.0: opcua_reachable)
    monkeypatch.setattr(runtime, "DEFAULT_EVENT_DEFER_SECONDS", 0.01)
    return semantics_dir, run_calls


def _assert_single_run_call_with_override(run_calls: list[dict[str, Any]]) -> None:
    assert len(run_calls) == 1
    overrides = run_calls[0].get("env_overrides")
    assert isinstance(overrides, dict)
    assert overrides.get("CF_ALLOW_DIRECT_ENGINE") == "1"


def _store_for(semantics_dir: Path) -> PipelineStateStore:
    store = PipelineStateStore(
        db_path=semantics_dir / "cf-pipeline-states.duckdb",
        semantics_dir=semantics_dir,
    )
    store.init_schema()
    return store


def test_run_loop_does_not_auto_enqueue_without_pending_event(monkeypatch, tmp_path: Path) -> None:
    semantics_dir, run_calls = _setup_runtime(monkeypatch, tmp_path, event_type="manual")
    runtime.activate_pipeline_state("pipe-a", desired_state="enabled")

    exit_code = runtime.run_event_loop("pipe-a", once=True, poll_interval=0.01)
    assert exit_code == 0
    assert run_calls == []

    store = _store_for(semantics_dir)
    con = store._connect(read_only=True)
    try:
        runs = con.execute("SELECT COUNT(*) FROM pipeline_runs").fetchone()
        events = con.execute("SELECT COUNT(*) FROM run_events").fetchone()
    finally:
        con.close()
    assert runs is not None and int(runs[0]) == 0
    assert events is not None and int(events[0]) == 0


def test_run_loop_consumes_pending_event_and_marks_consumed(monkeypatch, tmp_path: Path) -> None:
    semantics_dir, run_calls = _setup_runtime(monkeypatch, tmp_path, event_type="manual")
    runtime.activate_pipeline_state("pipe-a", desired_state="enabled")

    store = _store_for(semantics_dir)
    event_id, created = store.insert_event(
        pipeline_id="pipe-a",
        event_type="manual",
        source="test",
        payload={"case": "consume"},
        dedupe_key="consume-1",
    )
    assert created is True

    exit_code = runtime.run_event_loop("pipe-a", once=True, poll_interval=0.01)
    assert exit_code == 0
    _assert_single_run_call_with_override(run_calls)

    con = store._connect(read_only=True)
    try:
        event_row = con.execute(
            "SELECT run_id, status FROM run_events WHERE event_id = ?",
            [event_id],
        ).fetchone()
        run_row = con.execute(
            "SELECT run_id, status, triggered_by_event_id FROM pipeline_runs ORDER BY created_at DESC LIMIT 1",
        ).fetchone()
    finally:
        con.close()

    assert event_row is not None
    assert event_row[0] is not None
    assert str(event_row[1]) == "consumed"
    assert run_row is not None
    assert str(run_row[0]) == str(event_row[0])
    assert str(run_row[1]) == "succeeded"
    assert str(run_row[2]) == str(event_id)


def test_paused_pipeline_defers_pending_event_until_enabled(monkeypatch, tmp_path: Path) -> None:
    semantics_dir, run_calls = _setup_runtime(monkeypatch, tmp_path, event_type="manual")
    runtime.activate_pipeline_state("pipe-a", desired_state="paused")

    store = _store_for(semantics_dir)
    event_id, _ = store.insert_event(
        pipeline_id="pipe-a",
        event_type="manual",
        source="test",
        payload={"case": "defer"},
        dedupe_key="defer-1",
    )

    first_exit = runtime.run_event_loop("pipe-a", once=True, poll_interval=0.01)
    assert first_exit == 0
    assert run_calls == []

    con = store._connect(read_only=False)
    try:
        deferred = con.execute(
            "SELECT run_id, defer_count FROM run_events WHERE event_id = ?",
            [event_id],
        ).fetchone()
    finally:
        con.close()
    assert deferred is not None
    assert deferred[0] is None
    assert int(deferred[1] or 0) >= 1

    store.defer_event(
        event_id=event_id,
        reason="test-fast-forward",
        next_attempt_at=datetime.now(timezone.utc) - timedelta(seconds=1),
    )
    runtime.set_pipeline_state("pipe-a", "enabled")
    second_exit = runtime.run_event_loop("pipe-a", once=True, poll_interval=0.01)
    assert second_exit == 0
    _assert_single_run_call_with_override(run_calls)


def test_consume_event_to_run_is_single_winner(monkeypatch, tmp_path: Path) -> None:
    semantics_dir, _ = _setup_runtime(monkeypatch, tmp_path, event_type="manual")
    store = _store_for(semantics_dir)
    event_id, _ = store.insert_event(
        pipeline_id="pipe-a",
        event_type="manual",
        source="test",
        payload={"case": "atomic"},
        dedupe_key="atomic-1",
    )

    con = store._connect(read_only=False)
    try:
        win = store.consume_event_to_run(
            event_id=event_id,
            pipeline_rev=1,
            priority=0,
            now=datetime.now(timezone.utc),
            con=con,
        )
        lose = store.consume_event_to_run(
            event_id=event_id,
            pipeline_rev=1,
            priority=0,
            now=datetime.now(timezone.utc),
            con=con,
        )
    finally:
        con.close()

    assert win is not None
    assert lose is None


def test_opcua_unreachable_event_is_deferred_and_returns_exit_20(monkeypatch, tmp_path: Path) -> None:
    semantics_dir, run_calls = _setup_runtime(
        monkeypatch,
        tmp_path,
        event_type="opcua_signal",
        opcua_reachable=False,
    )
    runtime.activate_pipeline_state("pipe-a", desired_state="enabled")
    store = _store_for(semantics_dir)
    event_id, _ = store.insert_event(
        pipeline_id="pipe-a",
        event_type="opcua_signal",
        source="test",
        payload={"case": "opcua-unreachable"},
        dedupe_key="opcua-unreachable-1",
    )

    exit_code = runtime.run_event_loop("pipe-a", once=True, poll_interval=0.01)
    assert exit_code == runtime.OPCUA_ENDPOINT_UNREACHABLE_EXIT_CODE
    assert run_calls == []

    con = store._connect(read_only=True)
    try:
        row = con.execute(
            "SELECT run_id, defer_count, defer_reason FROM run_events WHERE event_id = ?",
            [event_id],
        ).fetchone()
    finally:
        con.close()
    assert row is not None
    assert row[0] is None
    assert int(row[1] or 0) >= 1
    assert str(row[2]) == "opcua_unreachable"
