"""GPU-resident decode adapter and RIFE-interpolated fast mode.

RM-71 PyNvVideoCodec -- replace `cv2.VideoCapture` with NVIDIA's
PyNvVideoCodec for NVIDIA users. Measured ~6x faster decode on
reference hardware AND decoded frames live on the GPU so zero CPU-GPU
copies are needed when the inpainter is also CUDA.

RM-72 RIFE-interpolated fast mode -- detect + inpaint every Nth frame
and synthesise intermediates with Practical-RIFE 4.x. Equivalent to
2-4x throughput on dialogue-heavy footage where the background is
smooth between cuts. We expose a thin wrapper around the
`practical-rife` Python package; users without RIFE installed fall
back to the standard frame-by-frame path.

Both adapters import lazily and return None on any failure so callers
transparently fall back to cv2.VideoCapture / nearest-keyframe reuse.
"""

from __future__ import annotations

import logging
import os
from typing import Mapping, Optional, Tuple

import cv2
import numpy as np

logger = logging.getLogger(__name__)

_TRUE_VALUES = {"1", "true", "yes", "on"}


def _env_truthy(name: str, default: bool = False) -> bool:
    raw = os.environ.get(name)
    if raw is None or str(raw).strip() == "":
        return default
    return str(raw).strip().lower() in _TRUE_VALUES


def _metadata_value(meta, *names, default=None):
    for name in names:
        if isinstance(meta, Mapping) and name in meta:
            return meta[name]
        if hasattr(meta, name):
            value = getattr(meta, name)
            return value() if callable(value) else value
        getter = "Get" + "".join(part.capitalize() for part in str(name).split("_"))
        if hasattr(meta, getter):
            try:
                return getattr(meta, getter)()
            except Exception:
                pass
    return default


def _to_numpy_frame(frame) -> Optional[np.ndarray]:
    if frame is None:
        return None
    if isinstance(frame, np.ndarray):
        return frame
    if hasattr(frame, "__dlpack__"):
        try:
            import torch  # type: ignore
            tensor = torch.from_dlpack(frame)
            if hasattr(tensor, "detach"):
                tensor = tensor.detach()
            if hasattr(tensor, "cpu"):
                tensor = tensor.cpu()
            return tensor.numpy()
        except Exception as exc:
            logger.debug(f"PyNvVideoCodec DLPack conversion failed: {exc}")
    for name in ("download", "cpu", "asnumpy", "numpy"):
        fn = getattr(frame, name, None)
        if fn is None:
            continue
        try:
            value = fn()
            if name == "cpu" and hasattr(value, "numpy"):
                value = value.numpy()
            return np.asarray(value)
        except Exception:
            continue
    try:
        return np.asarray(frame)
    except Exception:
        return None


def _frame_to_bgr(frame) -> Optional[np.ndarray]:
    arr = _to_numpy_frame(frame)
    if arr is None or arr.size == 0:
        return None
    if arr.ndim == 3 and arr.shape[0] in (3, 4) and arr.shape[2] not in (3, 4):
        arr = np.transpose(arr, (1, 2, 0))
    if arr.dtype != np.uint8:
        arr = np.clip(arr, 0, 255).astype(np.uint8)
    if arr.ndim == 2:
        return cv2.cvtColor(arr, cv2.COLOR_GRAY2BGR)
    if arr.ndim == 3 and arr.shape[2] == 3:
        return cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
    if arr.ndim == 3 and arr.shape[2] == 4:
        return cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR)
    if arr.ndim in (2, 3):
        try:
            return cv2.cvtColor(arr, cv2.COLOR_YUV2BGR_NV12)
        except Exception:
            return None
    return None


class _PyNvVideoCapture:
    """`cv2.VideoCapture`-shaped wrapper around PyNvVideoCodec's
    SimpleDecoder. Frames are downloaded to CPU as BGR so the rest of
    the pipeline (which is cv2-based) does not need to change. The
    zero-copy GPU path is a larger refactor; we ship the throughput
    win without it for now.
    """

    def __init__(self, path: str):
        self._path = path
        self._decoder = None
        self._frame_count = 0
        self._fps = 0.0
        self._width = 0
        self._height = 0
        self._pos = 0
        self._opened = False
        self._mode = ""
        self._open()

    def _open(self) -> None:
        try:
            import PyNvVideoCodec as nvc  # type: ignore
        except ImportError:
            return
        if hasattr(nvc, "SimpleDecoder"):
            try:
                gpu_id = int(os.environ.get("VSR_PYNV_GPU_ID", "0") or "0")
                use_device_memory = _env_truthy("VSR_PYNV_DEVICE_MEMORY", True)
                output_color = None
                color_type = getattr(nvc, "OutputColorType", None)
                if color_type is not None:
                    output_color = (
                        getattr(color_type, "RGB", None)
                        or getattr(color_type, "rgb", None)
                    )
                kwargs = {
                    "gpu_id": gpu_id,
                    "use_device_memory": use_device_memory,
                }
                if output_color is not None:
                    kwargs["output_color_type"] = output_color
                try:
                    self._decoder = nvc.SimpleDecoder(self._path, **kwargs)
                except TypeError:
                    self._decoder = nvc.SimpleDecoder(self._path, gpu_id=gpu_id)
                meta_fn = getattr(self._decoder, "get_stream_metadata", None)
                meta = meta_fn() if callable(meta_fn) else self._decoder
                self._width = int(_metadata_value(meta, "width", default=0) or 0)
                self._height = int(_metadata_value(meta, "height", default=0) or 0)
                fps = (
                    _metadata_value(meta, "average_fps", "fps", "frame_rate", default=0.0)
                    or 0.0
                )
                self._fps = float(fps or 30.0)
                try:
                    self._frame_count = int(len(self._decoder))
                except Exception:
                    self._frame_count = int(
                        _metadata_value(meta, "num_frames", "frame_count", default=0)
                        or 0
                    )
                self._opened = self._width > 0 and self._height > 0
                self._mode = "simple"
                return
            except Exception as exc:
                logger.debug(f"PyNvVideoCodec SimpleDecoder open failed: {exc}")
        try:
            # The API surface varies across PyNvVideoCodec releases;
            # older releases exposed a CreateDecoder shape.
            self._decoder = nvc.CreateDecoder(self._path)
            self._width = int(self._decoder.GetWidth())
            self._height = int(self._decoder.GetHeight())
            self._fps = float(self._decoder.GetFrameRate() or 30.0)
            self._frame_count = int(self._decoder.GetFrameCount() or 0)
            self._opened = True
            self._mode = "legacy"
        except Exception as exc:
            logger.warning(f"PyNvVideoCodec open failed: {exc}")
            self._decoder = None
            self._opened = False

    def isOpened(self) -> bool:
        return self._opened

    def get(self, prop):
        if prop == cv2.CAP_PROP_FPS:
            return float(self._fps)
        if prop == cv2.CAP_PROP_FRAME_WIDTH:
            return float(self._width)
        if prop == cv2.CAP_PROP_FRAME_HEIGHT:
            return float(self._height)
        if prop == cv2.CAP_PROP_FRAME_COUNT:
            return float(self._frame_count)
        if prop == cv2.CAP_PROP_POS_FRAMES:
            return float(self._pos)
        return 0.0

    def set(self, prop, value) -> bool:
        if prop != cv2.CAP_PROP_POS_FRAMES:
            return False
        try:
            idx = max(0, min(self._frame_count, int(value)))
        except (TypeError, ValueError):
            return False
        if self._mode == "legacy":
            # RM-138: legacy mode reads sequentially with GetNextFrame(), so a
            # decoder without SeekFrame cannot reposition. Reporting success
            # here would satisfy the cv2.VideoCapture drop-in contract while
            # silently returning the wrong frame (resume / ROI re-scan
            # misalignment), so fail loudly and leave _pos untouched.
            seek = getattr(self._decoder, "SeekFrame", None)
            if not callable(seek):
                if idx != self._pos:
                    logger.warning(
                        "Hardware decoder cannot seek to frame %s (legacy "
                        "mode without SeekFrame); refusing the reposition.",
                        idx,
                    )
                    return False
                return True
            try:
                seek(idx)
            except Exception as exc:
                logger.warning(
                    "Hardware decoder seek to frame %s failed: %s", idx, exc)
                return False
        self._pos = idx
        return True

    def read(self) -> Tuple[bool, Optional[np.ndarray]]:
        if not self._opened:
            return False, None
        try:
            if self._mode == "simple":
                if self._frame_count and self._pos >= self._frame_count:
                    return False, None
                tensor = self._decoder[self._pos]
            else:
                tensor = self._decoder.GetNextFrame()
            bgr = _frame_to_bgr(tensor)
            if bgr is None:
                return False, None
            self._pos += 1
            return True, bgr
        except Exception as exc:
            logger.debug(f"PyNvVideoCapture.read failed: {exc}")
            return False, None

    def release(self) -> None:
        if self._decoder is not None:
            try:
                self._decoder = None
            except Exception:
                pass
        self._opened = False


def try_open_pynv(path: str):
    """Return a _PyNvVideoCapture instance when the open succeeded,
    or None when PyNvVideoCodec is unavailable / the file cannot be
    opened. The caller falls back to cv2.VideoCapture."""
    cap = _PyNvVideoCapture(path)
    if cap.isOpened():
        logger.info("PyNvVideoCodec decode pathway active")
        return cap
    return None


def pynv_decode_status(env: Optional[Mapping[str, str]] = None) -> dict:
    source = os.environ if env is None else env
    version = None
    try:
        import importlib.metadata
        import importlib.util
        spec = importlib.util.find_spec("PyNvVideoCodec")
        if spec is not None:
            for package in ("PyNvVideoCodec", "nvidia-pynvvideoCodec"):
                try:
                    version = importlib.metadata.version(package)
                    break
                except importlib.metadata.PackageNotFoundError:
                    continue
    except Exception:
        spec = None
    enabled = (
        str(source.get("VSR_PYNVVIDEOCODEC", "") or "").strip().lower()
        in _TRUE_VALUES
    )
    return {
        "available": spec is not None,
        "version": version,
        "enabled": enabled,
        "device_memory": (
            str(source.get("VSR_PYNV_DEVICE_MEMORY", "1") or "1").strip().lower()
            in _TRUE_VALUES
        ),
        "status": "available" if spec is not None else "not_installed",
        "next_action": (
            "" if spec is not None
            else (
                "Install NVIDIA PyNvVideoCodec and use --decode-accel pynv "
                "or VSR_PYNVVIDEOCODEC=1 on supported NVIDIA systems."
            )
        ),
    }


_RIFE_STATE: dict = {"probed": False, "model": None}


def maybe_interpolate_pair(prev_frame: np.ndarray,
                            next_frame: np.ndarray,
                            t: float = 0.5) -> Optional[np.ndarray]:
    """RM-72: synthesise an intermediate frame between `prev_frame` and
    `next_frame` at time `t in [0, 1]` using Practical-RIFE.

    Returns the synthesised frame, or None when RIFE is unavailable.
    Callers fall back to a duplicate of the nearest cleaned keyframe.
    """
    if _RIFE_STATE["probed"] and _RIFE_STATE["model"] is None:
        return None
    if not _RIFE_STATE["probed"]:
        _RIFE_STATE["probed"] = True
        try:
            from practical_rife import RIFE  # type: ignore
            _RIFE_STATE["model"] = RIFE()
        except Exception:
            logger.info(
                "practical-rife not available; RIFE interpolation off."
            )
            return None
    try:
        rife = _RIFE_STATE["model"]
        return rife.interpolate(prev_frame, next_frame, timestep=t)
    except Exception as exc:
        logger.debug(f"RIFE interpolation failed: {exc}")
        return None


def is_rife_available() -> bool:
    try:
        import practical_rife  # type: ignore  # noqa: F401
        return True
    except ImportError:
        return False
