from __future__ import annotations

import fcntl
import json
import subprocess
import tempfile
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import TextIO

POSTGRES_DATA_PATH = "/var/lib/postgresql/data"
POSTGRES_TMPFS_SPEC = f"{POSTGRES_DATA_PATH}:rw,nosuid,nodev,noexec,size=384m"
VOLUME_GUARD_LOCK = Path(tempfile.gettempdir()) / "livingip-teleo-docker-volume-lifecycle.lock"


@dataclass(frozen=True)
class DockerVolumeSetGuard:
    before: frozenset[str]
    context: str
    owner_thread_id: int


_PROCESS_GUARD = threading.RLock()
_LOCK_FILE: TextIO | None = None
_GUARD_STACK: list[DockerVolumeSetGuard] = []


def _docker(*arguments: str, check: bool = True) -> subprocess.CompletedProcess[str]:
    completed = subprocess.run(
        ["docker", *arguments],
        text=True,
        capture_output=True,
        check=False,
        timeout=120,
    )
    if check and completed.returncode != 0:
        raise AssertionError(completed.stderr or completed.stdout)
    return completed


def docker_volume_names() -> frozenset[str]:
    completed = _docker("volume", "ls", "--quiet")
    return frozenset(name for name in completed.stdout.splitlines() if name)


def assert_docker_volume_set_unchanged(before: frozenset[str], *, context: str) -> None:
    after = docker_volume_names()
    added = sorted(after - before)
    removed = sorted(before - after)
    assert after == before, (
        f"daemon-wide Docker volume drift while running {context}; another Docker job may be responsible; "
        f"added={added!r}; removed={removed!r}; before_count={len(before)}; after_count={len(after)}"
    )


def acquire_docker_volume_set_guard(*, context: str) -> DockerVolumeSetGuard:
    global _LOCK_FILE

    _PROCESS_GUARD.acquire()
    opened_here = False
    try:
        if _LOCK_FILE is None:
            lock_file = VOLUME_GUARD_LOCK.open("a+", encoding="utf-8")
            try:
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
            except BaseException:
                lock_file.close()
                raise
            _LOCK_FILE = lock_file
            opened_here = True
        guard = DockerVolumeSetGuard(
            before=docker_volume_names(),
            context=context,
            owner_thread_id=threading.get_ident(),
        )
        _GUARD_STACK.append(guard)
        return guard
    except BaseException:
        if opened_here and _LOCK_FILE is not None:
            fcntl.flock(_LOCK_FILE.fileno(), fcntl.LOCK_UN)
            _LOCK_FILE.close()
            _LOCK_FILE = None
        _PROCESS_GUARD.release()
        raise


def release_docker_volume_set_guard(guard: DockerVolumeSetGuard) -> None:
    global _LOCK_FILE

    assert guard.owner_thread_id == threading.get_ident(), "Docker volume guard released from a different thread"
    assert _GUARD_STACK and _GUARD_STACK[-1] is guard, "Docker volume guards must be released in LIFO order"
    try:
        assert_docker_volume_set_unchanged(guard.before, context=guard.context)
    finally:
        _GUARD_STACK.pop()
        if not _GUARD_STACK:
            assert _LOCK_FILE is not None
            fcntl.flock(_LOCK_FILE.fileno(), fcntl.LOCK_UN)
            _LOCK_FILE.close()
            _LOCK_FILE = None
        _PROCESS_GUARD.release()


def inspect_postgres_tmpfs_container(name: str) -> dict:
    inspected = json.loads(_docker("inspect", name).stdout)[0]
    assert inspected["Mounts"] == [], f"{name} unexpectedly uses Docker volumes or bind mounts"
    tmpfs = inspected["HostConfig"]["Tmpfs"]
    assert POSTGRES_DATA_PATH in tmpfs, f"{name} does not mount PostgreSQL data on tmpfs"
    return inspected


def remove_container_and_volumes(name: str) -> None:
    removed = _docker("rm", "--force", "--volumes", name, check=False)
    if removed.returncode != 0 and "no such container" not in removed.stderr.lower():
        raise AssertionError(removed.stderr or removed.stdout)

    assert _docker("container", "inspect", name, check=False).returncode != 0
    names = _docker(
        "container",
        "ls",
        "--all",
        "--filter",
        f"name=^/{name}$",
        "--format",
        "{{.Names}}",
    )
    assert names.stdout.strip() == "", f"container still exists after cleanup: {name}"
