"""Persistent DF CPU-affinity disable (run/cpu_affinity.disabled).

Default policy pins DyzineFrame to cores 0-2 so EtherGold can own core 3.
That starved MP4 (2-core exclusive) and still hurts fireworks on 3 cores.
Operator disable restores all CPUs — same as before the pin — without a
kernel rebuild. HDMI Supervisor and set_cpu_affinity.sh write the flag and
live-taskset running pids (children inherit the supervisor mask).
"""

from __future__ import annotations

import os
import shutil
import subprocess
import sys
from pathlib import Path

APP_ROOT = Path(__file__).resolve().parent
FLAG_PATH = APP_ROOT / "run" / "cpu_affinity.disabled"
LOCK_PATH = APP_ROOT / "run" / "dyzineframe.lock"
DF_PIN_DEFAULT = os.environ.get("OHD_CPU_DF_SET", "0-2")


def flag_path() -> Path:
    return FLAG_PATH


def is_disabled() -> bool:
    return FLAG_PATH.is_file()


def all_cpu_list() -> str:
    n = os.cpu_count() or 1
    if n <= 1:
        return "0"
    return f"0-{n - 1}"


def set_disabled(want: bool) -> None:
    FLAG_PATH.parent.mkdir(parents=True, exist_ok=True)
    if want:
        FLAG_PATH.write_text(
            "CPU affinity off by DyzineFrame Supervisor — DF uses all cores\n",
            encoding="utf-8",
        )
    else:
        try:
            FLAG_PATH.unlink(missing_ok=True)
        except OSError:
            pass


def _lock_pid() -> int | None:
    if not LOCK_PATH.is_file():
        return None
    try:
        text = LOCK_PATH.read_text(encoding="utf-8").strip()
        pid = int(text) if text.isdigit() else 0
    except (OSError, ValueError):
        return None
    return pid if pid > 0 else None


def _pgrep_python(script_tail: str) -> list[int]:
    if sys.platform == "win32":
        return []
    try:
        out = subprocess.run(
            ["pgrep", "-f", f"python3? .*{script_tail}"],
            capture_output=True,
            text=True,
            check=False,
        )
    except OSError:
        return []
    pids: list[int] = []
    for line in out.stdout.splitlines():
        line = line.strip()
        if line.isdigit():
            pids.append(int(line))
    return pids


def target_pids() -> list[int]:
    """Supervisor + DyzineFrame pids to rebind (own tree only)."""
    found: set[int] = set()
    lock = _lock_pid()
    if lock:
        found.add(lock)
    found.update(_pgrep_python("dyzineframe_supervisor\\.py"))
    found.update(_pgrep_python("DyzineFrame\\.py"))
    me = os.getpid()
    found.add(me)
    ppid = os.getppid()
    if ppid > 1:
        found.add(ppid)
    return sorted(p for p in found if p > 1)


def apply_live(enabled: bool) -> list[str]:
    """taskset running DF/supervisor. No-op on Windows or without taskset."""
    if sys.platform == "win32":
        return ["cpu-affinity: skip live pin (Windows)"]
    mask = DF_PIN_DEFAULT if enabled else all_cpu_list()
    lines: list[str] = []
    if not shutil.which("taskset"):
        return ["cpu-affinity: taskset not on PATH"]
    for pid in target_pids():
        if not Path(f"/proc/{pid}").is_dir():
            continue
        proc = subprocess.run(
            ["taskset", "-pc", mask, str(pid)],
            capture_output=True,
            text=True,
            check=False,
        )
        if proc.returncode == 0:
            lines.append(f"cpu-affinity: pid={pid} → {mask}")
        else:
            err = (proc.stderr or proc.stdout or "").strip().splitlines()
            tail = err[-1] if err else f"rc={proc.returncode}"
            lines.append(f"cpu-affinity: WARN pid={pid} {tail}")
    if not lines:
        lines.append(f"cpu-affinity: no pids (wanted {mask})")
    return lines
