"""ethtool backend — the RECOMMENDED physical path (in-tree lan78xx driver).

Read:  `ethtool -e <iface> raw on`            -> full image on stdout (bytes)
Write: `ethtool -E <iface> magic 0x78A5 offset <o> value <v>`  (one byte / call)

The driver issues EWEN internally on the -E path, so we never manage EWEN/EWDS
here. Per-byte writes are how the hardware works; the CLI batches a field's
bytes and calls write_bytes(), which loops one `ethtool -E` per byte.

This backend is `physical = True`; the CLI MUST escalate before any write.
Reads are free. We never call ethtool here from any read path other than
read_image(), and read_image() is non-destructive.
"""
from __future__ import annotations

import shutil
import subprocess

from .base import EepromBackend, BackendError
from ..eeprom import EEPROM_MAGIC, MAX_EEPROM_SIZE


class EthtoolBackend(EepromBackend):
    physical = True
    name = "ethtool"

    def __init__(self, iface: str, size: int = MAX_EEPROM_SIZE, sudo: bool = True):
        self.iface = iface
        self.size = size
        self.sudo = sudo
        if shutil.which("ethtool") is None:
            raise BackendError("ethtool not found on PATH; install ethtool")

    def _cmd(self, args: list[str]) -> list[str]:
        # sudo -n (non-interactive): if passwordless ethtool is configured it
        # works; otherwise it fails FAST instead of hanging on a password prompt
        # (there is no TTY). Callers treat that failure as "ethtool unavailable".
        base = ["sudo", "-n"] if self.sudo else []
        return base + ["ethtool"] + args

    def read_image(self) -> bytes:
        # `-e <iface> raw on` streams the raw EEPROM bytes to stdout.
        proc = subprocess.run(
            self._cmd(["-e", self.iface, "raw", "on"]),
            capture_output=True)
        if proc.returncode != 0:
            raise BackendError(self._clean_err(proc.stderr, proc.returncode))
        return proc.stdout

    @staticmethod
    def _clean_err(stderr: bytes, rc) -> str:
        """Map raw ethtool/sudo stderr to a clean, non-leaky message."""
        err = stderr.decode(errors="replace").strip()
        low = err.lower()
        if "enodata" in low or "no data" in low:
            return ("EEPROM reads as blank/unsigned (byte0 != 0xA5); "
                    "no valid image present")
        if "sudo" in low or "password" in low or "terminal is required" in low:
            return ("ethtool needs elevated access to read the EEPROM; "
                    "grant passwordless sudo for ethtool or run with privilege")
        if "no such device" in low or "no device" in low:
            return "no such network device (is the lan78xx netdev bound?)"
        if "operation not permitted" in low:
            return "operation not permitted; ethtool needs elevated access"
        return f"ethtool error (rc={rc})"

    def read_range(self, offset: int, length: int) -> bytes:
        proc = subprocess.run(
            self._cmd(["-e", self.iface, "raw", "on",
                       "offset", str(offset), "length", str(length)]),
            capture_output=True)
        if proc.returncode != 0:
            raise BackendError(self._clean_err(proc.stderr, proc.returncode))
        return proc.stdout

    def write_bytes(self, offset: int, values: bytes) -> None:
        for i, v in enumerate(values):
            o = offset + i
            proc = subprocess.run(
                self._cmd(["-E", self.iface,
                           "magic", hex(EEPROM_MAGIC),
                           "offset", str(o), "value", str(v)]),
                capture_output=True)
            if proc.returncode != 0:
                raise BackendError(
                    f"write failed at offset {o}: "
                    f"{self._clean_err(proc.stderr, proc.returncode)}")

    def reload(self) -> None:
        # No safe in-band reload; require a replug / usb reset. Surface clearly.
        raise BackendError(
            "reload the EEPROM by replugging the adapter or issuing a USB reset; "
            "the driver does not expose a safe RELOAD on this path")
