from __future__ import annotations

import json
import socket
from datetime import timedelta
from pathlib import Path

import pytest

from recoil.execution.execution_store import ExecutionStore
from recoil.execution.pass_store import PassStore
from recoil.execution.state_lease import (
    LEASE_TTL_S,
    OVERRIDE_ENV,
    StateLeaseHeldError,
    _now,
)


def _write_json(path: Path, data: dict) -> bytes:
    path.parent.mkdir(parents=True, exist_ok=True)
    raw = json.dumps(data, indent=2).encode("utf-8")
    path.write_bytes(raw)
    return raw


def _lease_path(project_root: Path) -> Path:
    return project_root / "_pipeline" / "state" / ".write_lease.json"


def _write_lease(
    project_root: Path,
    *,
    hostname: str,
    acquired_at,
    ttl_s: int = LEASE_TTL_S,
) -> Path:
    path = _lease_path(project_root)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(
            {
                "hostname": hostname,
                "pid": 123,
                "acquired_at": acquired_at.isoformat(),
                "ttl_s": ttl_s,
            },
            indent=2,
        ),
        encoding="utf-8",
    )
    return path


@pytest.fixture()
def project(monkeypatch, tmp_path):
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").write_text("recoil-data-root\n", encoding="utf-8")
    project_root = root / "leaseproj"
    project_root.mkdir()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    monkeypatch.delenv(OVERRIDE_ENV, raising=False)
    monkeypatch.setattr(socket, "gethostname", lambda: "this-host")
    return "leaseproj", project_root


def _shot_data(shot_id: str = "EP001_SH001", status: str = "previs_pending") -> dict:
    return {
        "schema_version": 1,
        "shot_id": shot_id,
        "episode_id": "EP001",
        "status": status,
        "takes": [],
        "gate_results": {},
        "cost_incurred": 0.0,
    }


def test_pass_store_create_pass_foreign_lease_leaves_state_unmodified(project):
    project_name, project_root = project
    _write_lease(
        project_root,
        hostname="other-host",
        acquired_at=_now() - timedelta(seconds=60),
    )
    passes_file = (
        project_root
        / "_pipeline"
        / "state"
        / "visual"
        / "passes"
        / "ep_001_pass_state.json"
    )

    with pytest.raises(StateLeaseHeldError):
        PassStore(project_name).create_pass("EP001_PASS_001_TEST", ["EP001_SH001"])

    assert not passes_file.exists()


@pytest.mark.parametrize(
    ("operation", "shot_id", "expected_present"),
    [
        ("update_shot", "EP001_SH001", True),
        ("insert_shot", "EP001_SH002", False),
        ("append_take", "EP001_SH001", True),
        ("delete_shot", "EP001_SH001", True),
        ("atomic_transition", "EP001_SH001", True),
    ],
)
def test_execution_store_mutators_foreign_lease_leave_state_unmodified(
    project,
    operation,
    shot_id,
    expected_present,
):
    project_name, project_root = project
    shots_dir = project_root / "_pipeline" / "state" / "visual" / "shots"
    existing_path = shots_dir / "EP001_SH001.json"
    original = _write_json(existing_path, _shot_data())
    _write_lease(
        project_root,
        hostname="other-host",
        acquired_at=_now() - timedelta(seconds=60),
    )

    store = ExecutionStore(project=project_name, migrate=False)

    with pytest.raises(StateLeaseHeldError):
        if operation == "update_shot":
            store.update_shot(shot_id, status="previs_generating")
        elif operation == "insert_shot":
            store.insert_shot(_shot_data(shot_id))
        elif operation == "append_take":
            store.append_take(shot_id, {"take": 1})
        elif operation == "delete_shot":
            store.delete_shot(shot_id)
        elif operation == "atomic_transition":
            store.atomic_transition(shot_id, {"previs_pending"}, "previs_generating")

    target_path = shots_dir / f"{shot_id}.json"
    assert target_path.exists() is expected_present
    if expected_present:
        assert target_path.read_bytes() == original


def test_own_lease_allows_pass_store_write_and_renews(project):
    project_name, project_root = project
    old = _now() - timedelta(seconds=60)
    lease_path = _write_lease(project_root, hostname="this-host", acquired_at=old)

    PassStore(project_name).create_pass("EP001_PASS_001_TEST", ["EP001_SH001"])

    lease = json.loads(lease_path.read_text(encoding="utf-8"))
    assert lease["hostname"] == "this-host"
    assert lease["acquired_at"] != old.isoformat()


def test_expired_foreign_lease_allows_execution_store_write_and_renews(project):
    project_name, project_root = project
    old = _now() - timedelta(seconds=LEASE_TTL_S + 1)
    lease_path = _write_lease(project_root, hostname="other-host", acquired_at=old)

    ExecutionStore(project=project_name, migrate=False).insert_shot(_shot_data())

    lease = json.loads(lease_path.read_text(encoding="utf-8"))
    assert lease["hostname"] == "this-host"
    assert lease["acquired_at"] != old.isoformat()


def test_external_db_path_does_not_interact_with_project_lease(project, tmp_path):
    project_name, project_root = project
    lease_path = _write_lease(
        project_root,
        hostname="other-host",
        acquired_at=_now() - timedelta(seconds=60),
    )
    before = lease_path.read_bytes()

    store = ExecutionStore(project=project_name, db_path=tmp_path / "external-shots")
    store.insert_shot(_shot_data())

    assert store.get_shot("EP001_SH001") is not None
    assert lease_path.read_bytes() == before
