#!/usr/bin/env python3
"""HTTP trigger for DyzineFrame TARGAFILE screen grab (same as S key).

EtherGold HTTP must not open the P3 serial port. DF owns it. This module
writes ~/DyzineFrame/run/p3_grab.request and reads p3_grab.status.json.
PNG files are served only from ~/p3-grabs (allowlisted).
"""
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any, Dict, Optional

from df_deploy_control import DF_ROOT, TOPBOX_USER, _chown_topbox, _python_has_cmd

HINT = (
    "TARGAFILE takes ~30–90 s and freezes the T340+ bank display. "
    "Same grab as the DyzineFrame S key. Save the PNG from this browser "
    "(phone/laptop Downloads)."
)
FLAG_NAME = "p3_grab.request"
STATUS_NAME = "p3_grab.status.json"


def grabs_root() -> Path:
    env = (os.environ.get("P3_GRABS_DIR") or "").strip()
    if env:
        return Path(env)
    if os.name == "nt":
        return Path.home() / "p3-grabs"
    return Path("/home") / TOPBOX_USER / "p3-grabs"


def status_path() -> Path:
    return DF_ROOT / "run" / STATUS_NAME


def flag_path() -> Path:
    return DF_ROOT / "run" / FLAG_NAME


def df_running() -> bool:
    return _python_has_cmd("DyzineFrame.py")


def read_status() -> Dict[str, Any]:
    path = status_path()
    if not path.is_file():
        return {}
    try:
        loaded = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError, json.JSONDecodeError):
        return {}
    return loaded if isinstance(loaded, dict) else {}


def resolve_allowed_png(raw: str | None) -> Optional[Path]:
    """Return an existing .png under grabs_root(), or None (path escape rejected)."""
    if not raw or not str(raw).strip():
        return None
    candidate = Path(str(raw).strip())
    if not candidate.is_absolute():
        candidate = grabs_root() / candidate
    try:
        resolved = candidate.resolve()
        root = grabs_root().resolve()
        resolved.relative_to(root)
    except (OSError, ValueError, RuntimeError):
        return None
    if resolved.suffix.lower() != ".png" or not resolved.is_file():
        return None
    return resolved


def public_status(*, require_df: bool | None = None) -> Dict[str, Any]:
    raw = read_status()
    png = resolve_allowed_png(raw.get("png") if isinstance(raw.get("png"), str) else None)
    check_df = os.name != "nt" if require_df is None else require_df
    running = df_running() if check_df else True
    return {
        "ok": True,
        "state": raw.get("state") or "idle",
        "utc": raw.get("utc"),
        "filename": raw.get("filename"),
        "error": raw.get("error"),
        "has_image": png is not None,
        "df_running": running if check_df else None,
        "hint": HINT,
    }


def _write_status(payload: Dict[str, Any]) -> None:
    path = status_path()
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        tmp = path.with_suffix(".json.tmp")
        tmp.write_text(json.dumps(payload), encoding="utf-8")
        tmp.replace(path)
        _chown_topbox(path)
    except OSError:
        pass


def request_capture(*, require_df: bool | None = None) -> Dict[str, Any]:
    """Write the DF flag. Does not open P3. TARGAFILE is DF's job."""
    check_df = os.name != "nt" if require_df is None else require_df
    raw = read_status()
    if raw.get("state") == "busy":
        out = public_status(require_df=require_df)
        out["ok"] = False
        out["error"] = "A capture is already in progress"
        return out
    if check_df and not df_running():
        out = public_status(require_df=require_df)
        out["ok"] = False
        out["error"] = (
            "DyzineFrame is not running — TARGAFILE uses the P3 port DF owns. "
            "Restart DyzineFrame from Config, then Capture."
        )
        return out
    run_dir = DF_ROOT / "run"
    try:
        run_dir.mkdir(parents=True, exist_ok=True)
        path = flag_path()
        path.write_text("1\n", encoding="utf-8")
        _chown_topbox(path)
    except OSError as exc:
        return {
            "ok": False,
            "error": f"cannot write {flag_path()}: {exc}",
            "state": "error",
            "hint": HINT,
        }
    _write_status(
        {
            "state": "queued",
            "utc": None,
            "error": None,
            "png": None,
            "tga": None,
            "filename": None,
        }
    )
    out = public_status(require_df=require_df)
    out["ok"] = True
    out["state"] = "queued"
    return out


def image_bytes() -> tuple[Optional[bytes], Optional[str], Optional[str]]:
    """Return (png_bytes, filename, error)."""
    raw = read_status()
    png_path = resolve_allowed_png(raw.get("png") if isinstance(raw.get("png"), str) else None)
    if png_path is None:
        return None, None, "no PNG yet"
    try:
        return png_path.read_bytes(), png_path.name, None
    except OSError as exc:
        return None, None, str(exc)
