"""Minimal Linux GPIO v2 UAPI wrapper for output-only line control."""

from __future__ import annotations

import ctypes
import errno
import fcntl
import os
from typing import Sequence


GPIO_MAX_NAME_SIZE = 32
GPIO_V2_LINES_MAX = 64
GPIO_V2_LINE_NUM_ATTRS_MAX = 10
GPIO_V2_LINE_FLAG_OUTPUT = 1 << 3

_IOC_NRBITS = 8
_IOC_TYPEBITS = 8
_IOC_SIZEBITS = 14
_IOC_DIRBITS = 2

_IOC_NRSHIFT = 0
_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS

_IOC_WRITE = 1
_IOC_READ = 2


def _ioc(direction: int, ioc_type: int, number: int, size: int) -> int:
    """Build raw ioctl command number."""
    return (
        (direction << _IOC_DIRSHIFT)
        | (ioc_type << _IOC_TYPESHIFT)
        | (number << _IOC_NRSHIFT)
        | (size << _IOC_SIZESHIFT)
    )


def _iowr(ioc_type: int, number: int, struct_type: type[ctypes.Structure]) -> int:
    """Build read-write ioctl number for provided structure."""
    return _ioc(_IOC_READ | _IOC_WRITE, ioc_type, number, ctypes.sizeof(struct_type))


class GpioV2LineAttribute(ctypes.Structure):
    """ctypes mapping of `gpio_v2_line_attribute`."""

    _fields_ = [
        ("id", ctypes.c_uint32),
        ("padding", ctypes.c_uint32),
        ("value", ctypes.c_uint64),
    ]


class GpioV2LineConfigAttribute(ctypes.Structure):
    """ctypes mapping of `gpio_v2_line_config_attribute`."""

    _fields_ = [
        ("attr", GpioV2LineAttribute),
        ("mask", ctypes.c_uint64),
    ]


class GpioV2LineConfig(ctypes.Structure):
    """ctypes mapping of `gpio_v2_line_config`."""

    _fields_ = [
        ("flags", ctypes.c_uint64),
        ("num_attrs", ctypes.c_uint32),
        ("padding", ctypes.c_uint32 * 5),
        ("attrs", GpioV2LineConfigAttribute * GPIO_V2_LINE_NUM_ATTRS_MAX),
    ]


class GpioV2LineRequest(ctypes.Structure):
    """ctypes mapping of `gpio_v2_line_request`."""

    _fields_ = [
        ("offsets", ctypes.c_uint32 * GPIO_V2_LINES_MAX),
        ("consumer", ctypes.c_char * GPIO_MAX_NAME_SIZE),
        ("config", GpioV2LineConfig),
        ("num_lines", ctypes.c_uint32),
        ("event_buffer_size", ctypes.c_uint32),
        ("padding", ctypes.c_uint32 * 5),
        ("fd", ctypes.c_int32),
    ]


class GpioV2LineValues(ctypes.Structure):
    """ctypes mapping of `gpio_v2_line_values`."""

    _fields_ = [
        ("bits", ctypes.c_uint64),
        ("mask", ctypes.c_uint64),
    ]


GPIO_V2_GET_LINE_IOCTL = _iowr(0xB4, 0x07, GpioV2LineRequest)
GPIO_V2_LINE_SET_VALUES_IOCTL = _iowr(0xB4, 0x0F, GpioV2LineValues)


class GpioOutputLines:
    """Open and control one or more GPIO output lines as a single request."""

    def __init__(self, chip: str, offsets: Sequence[int], consumer: str) -> None:
        """Build GPIO line request descriptor."""
        if not chip:
            raise ValueError("gpio chip path must not be empty")
        if not offsets:
            raise ValueError("at least one GPIO line offset is required")
        if len(offsets) > GPIO_V2_LINES_MAX:
            raise ValueError(f"too many GPIO offsets requested: {len(offsets)}")

        normalized_offsets = [int(offset) for offset in offsets]
        if any(offset < 0 for offset in normalized_offsets):
            raise ValueError("GPIO offsets must be non-negative")
        if len(set(normalized_offsets)) != len(normalized_offsets):
            raise ValueError("GPIO offsets must be unique")

        self._chip = chip
        self._offsets = normalized_offsets
        self._consumer = (consumer or "radar_switch").encode("ascii", errors="ignore")[: GPIO_MAX_NAME_SIZE - 1]

        self._chip_fd = -1
        self._line_fd = -1

    def open(self) -> None:
        """Open GPIO chip and request configured output lines."""
        if self._line_fd >= 0:
            return

        try:
            self._chip_fd = os.open(self._chip, os.O_RDONLY | os.O_CLOEXEC)
        except OSError as exc:
            raise RuntimeError(f"Failed to open GPIO chip '{self._chip}': {exc}") from exc

        request = GpioV2LineRequest()
        for index, offset in enumerate(self._offsets):
            request.offsets[index] = ctypes.c_uint32(offset).value

        request.num_lines = ctypes.c_uint32(len(self._offsets)).value
        request.config.flags = ctypes.c_uint64(GPIO_V2_LINE_FLAG_OUTPUT).value
        request.consumer = self._consumer

        try:
            fcntl.ioctl(self._chip_fd, GPIO_V2_GET_LINE_IOCTL, request)
        except OSError as exc:
            self._close_chip_fd()
            raise RuntimeError(f"Failed to request GPIO lines on '{self._chip}': {exc}") from exc

        if request.fd < 0:
            self._close_chip_fd()
            raise RuntimeError(f"GPIO line request returned invalid fd for '{self._chip}'")

        self._line_fd = int(request.fd)

    def close(self) -> None:
        """Close line request and chip file descriptors."""
        self._close_line_fd()
        self._close_chip_fd()

    def set_values(self, values: Sequence[int]) -> None:
        """Apply output values for all requested lines."""
        if self._line_fd < 0:
            raise RuntimeError("GPIO line request is not open")
        if len(values) != len(self._offsets):
            raise ValueError(
                f"GPIO values length mismatch: expected {len(self._offsets)}, got {len(values)}"
            )

        bits = 0
        for index, value in enumerate(values):
            normalized = int(value)
            if normalized not in (0, 1):
                raise ValueError(f"GPIO output value must be 0 or 1, got {value}")
            if normalized == 1:
                bits |= (1 << index)

        mask = (1 << len(self._offsets)) - 1
        line_values = GpioV2LineValues(bits=ctypes.c_uint64(bits).value, mask=ctypes.c_uint64(mask).value)

        try:
            fcntl.ioctl(self._line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, line_values)
        except OSError as exc:
            if exc.errno == errno.ENODEV:
                raise RuntimeError("GPIO device disconnected") from exc
            raise RuntimeError(f"Failed to set GPIO output values: {exc}") from exc

    def _close_line_fd(self) -> None:
        """Close line file descriptor if currently open."""
        if self._line_fd >= 0:
            os.close(self._line_fd)
            self._line_fd = -1

    def _close_chip_fd(self) -> None:
        """Close chip file descriptor if currently open."""
        if self._chip_fd >= 0:
            os.close(self._chip_fd)
            self._chip_fd = -1
