"""Define /device endpoints.

This module contains the Device class for interacting with
Kwikset smart lock device endpoints.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

from .access_code import (
    AccessCodeResult,
    AccessCodeSchedule,
    build_access_code_message,
    build_delete_message,
)
from .const import (
    ACCESS_CODE_SYNC_URL,
    ACCESS_CODE_URL,
    ACCESSORY_COMMAND_URL,
    AUTOLOCK_COMMAND_URL,
    GET_DEVICE_HISTORY_URL,
    GET_DEVICE_URL,
    GET_HOME_DEVICES_URL,
    LOCK_COMMAND_URL,
    LOGGER,
    MIN_ACCESS_CODES,
)
from .errors import (
    InvalidActionError,
    InvalidDeviceError,
    InvalidStatusError,
    InvalidUserError,
    MinimumAccessCodeError,
)
from .types import RequestProtocol


class LockAction(str, Enum):
    """Define valid lock actions."""

    LOCK = "lock"
    UNLOCK = "unlock"


class DeviceStatus(str, Enum):
    """Define valid device status values."""

    ENABLED = "true"
    DISABLED = "false"


class AccessoryAction(str, Enum):
    """Define valid accessory actions."""

    LED_STATUS = "ledstatus"
    AUDIO_STATUS = "audiostatus"
    SECURE_SCREEN_STATUS = "securescreenstatus"


@dataclass
class ValidationResult:
    """Result of a validation operation.

    :param is_valid: Whether the validation passed
    :type is_valid: ``bool``
    :param errors: List of validation error messages
    :type errors: ``List[str]``
    :param warnings: List of validation warning messages
    :type warnings: ``List[str]``
    """

    is_valid: bool = True
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def add_error(self, message: str) -> None:
        """Add an error message and mark as invalid.

        :param message: The error message to add
        :type message: ``str``
        """
        self.is_valid = False
        self.errors.append(message)

    def add_warning(self, message: str) -> None:
        """Add a warning message.

        :param message: The warning message to add
        :type message: ``str``
        """
        self.warnings.append(message)

    def merge(self, other: ValidationResult) -> None:
        """Merge another validation result into this one.

        :param other: The other validation result to merge
        :type other: ``ValidationResult``
        """
        if not other.is_valid:
            self.is_valid = False
        self.errors.extend(other.errors)
        self.warnings.extend(other.warnings)


@dataclass
class CommandResult:
    """Result of a device command operation.

    :param success: Whether the command succeeded
    :type success: ``bool``
    :param data: Response data from the command
    :type data: ``Optional[Dict[str, Any]]``
    :param device_id: The device identifier
    :type device_id: ``Optional[str]``
    :param action: The action performed
    :type action: ``Optional[str]``
    :param validation: Validation result for the command
    :type validation: ``Optional[ValidationResult]``
    :param error_message: Error message if command failed
    :type error_message: ``Optional[str]``
    """

    success: bool = True
    data: dict[str, Any] | None = None
    device_id: str | None = None
    action: str | None = None
    validation: ValidationResult | None = None
    error_message: str | None = None


class Device:
    """Define an object to handle device-related API endpoints.

    :param request: An async callable conforming to RequestProtocol
    :type request: ``RequestProtocol``
    """

    __slots__ = ("_request",)

    # Required fields for device validation
    REQUIRED_DEVICE_FIELDS: tuple[str, ...] = ("serialnumber",)
    # Required fields for user validation (at minimum, one name field)
    REQUIRED_USER_FIELDS: tuple[str, ...] = ()
    # Maximum length for user/source names in lock commands
    MAX_NAME_LENGTH: int = 7

    def __init__(self, request: RequestProtocol) -> None:
        """Initialize the Device endpoint handler."""
        self._request: RequestProtocol = request

    @staticmethod
    def _validate_device(device: dict[str, Any] | None) -> ValidationResult:
        """Validate device data contains required fields.

        :param device: Device information dictionary
        :type device: ``Optional[Dict[str, Any]]``
        :rtype: ``ValidationResult``
        """
        result = ValidationResult()

        if device is None:
            result.add_error("Device cannot be None")
            return result

        if not isinstance(device, dict):
            result.add_error(f"Device must be a dictionary, got {type(device).__name__}")
            return result

        missing_fields: list[str] = []
        for field_name in Device.REQUIRED_DEVICE_FIELDS:
            if field_name not in device or not device[field_name]:
                missing_fields.append(field_name)

        if missing_fields:
            result.add_error(f"Device missing required fields: {', '.join(missing_fields)}")

        return result

    @staticmethod
    def _validate_user(user: dict[str, Any] | None) -> ValidationResult:
        """Validate user data contains expected fields.

        :param user: User information dictionary
        :type user: ``Optional[Dict[str, Any]]``
        :rtype: ``ValidationResult``
        """
        result = ValidationResult()

        if user is None:
            result.add_error("User cannot be None")
            return result

        if not isinstance(user, dict):
            result.add_error(f"User must be a dictionary, got {type(user).__name__}")
            return result

        # Check for at least one name field
        has_firstname = "firstname" in user and user["firstname"]
        has_lastname = "lastname" in user and user["lastname"]

        if not has_firstname and not has_lastname:
            result.add_warning("User has no firstname or lastname; command will use empty name")

        return result

    @staticmethod
    def _validate_lock_action(action: str) -> ValidationResult:
        """Validate lock action is valid.

        :param action: The action to validate
        :type action: ``str``
        :rtype: ``ValidationResult``
        """
        result = ValidationResult()

        valid_actions = [a.value for a in LockAction]

        if action not in valid_actions:
            result.add_error(
                f"Invalid lock action '{action}'. Valid actions: {', '.join(valid_actions)}"
            )

        return result

    @staticmethod
    def _validate_status(status: str) -> ValidationResult:
        """Validate status value is valid.

        :param status: The status value to validate
        :type status: ``str``
        :rtype: ``ValidationResult``
        """
        result = ValidationResult()

        valid_statuses = [s.value for s in DeviceStatus]

        if status not in valid_statuses:
            result.add_error(
                f"Invalid status '{status}'. Valid statuses: {', '.join(valid_statuses)}"
            )

        return result

    @staticmethod
    def _validate_accessory_action(action: str) -> ValidationResult:
        """Validate accessory action is valid.

        :param action: The accessory action to validate
        :type action: ``str``
        :rtype: ``ValidationResult``
        """
        result = ValidationResult()

        valid_actions = [a.value for a in AccessoryAction]

        if action not in valid_actions:
            result.add_error(
                f"Invalid accessory action '{action}'. Valid actions: {', '.join(valid_actions)}"
            )

        return result

    async def get_devices(self, home_id: str) -> list[dict[str, Any]]:
        """Return all devices for a specific home.

        :param home_id: Unique identifier for the home
        :type home_id: ``str``
        :raises ValueError: If home_id is empty or None
        :rtype: ``List[Dict[str, Any]]``
        """
        if not home_id:
            raise ValueError("home_id cannot be empty or None")

        devices: dict[str, Any] = await self._request(
            "get",
            GET_HOME_DEVICES_URL % home_id,
        )
        result: list[dict[str, Any]] = devices["data"]
        return result

    async def get_device_info(self, device_id: str) -> dict[str, Any] | None:
        """Return detailed information for a specific device.

        :param device_id: Unique identifier for the device
        :type device_id: ``str``
        :raises ValueError: If device_id is empty or None
        :rtype: ``Optional[Dict[str, Any]]``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        device_info: dict[str, Any] = await self._request(
            "get",
            GET_DEVICE_URL % device_id,
        )
        data = device_info.get("data", [])
        return data[0] if data else None

    async def get_device_history(self, device_id: str, top: int = 200) -> dict[str, Any]:
        """Return event history for a specific device.

        :param device_id: Unique identifier for the device (serialnumber)
        :type device_id: ``str``
        :param top: Maximum number of history events to return (default 200)
        :type top: ``int``
        :raises ValueError: If device_id is empty or None
        :raises ValueError: If top is not a positive integer
        :rtype: ``Dict[str, Any]``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        if top < 1:
            raise ValueError("top must be a positive integer")

        url = f"{GET_DEVICE_HISTORY_URL % device_id}?top={top}"
        response: dict[str, Any] = await self._request("get", url)
        return response

    async def _lock_action(
        self,
        device: dict[str, Any],
        user: dict[str, Any],
        action: str,
    ) -> CommandResult:
        """Perform a lock or unlock action on a device.

        :param device: Device information dictionary containing 'serialnumber'
        :type device: ``Dict[str, Any]``
        :param user: User information dictionary containing 'firstname' and 'lastname'
        :type user: ``Dict[str, Any]``
        :param action: The action to perform ('lock' or 'unlock')
        :type action: ``str``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidUserError: If user data is invalid
        :raises InvalidActionError: If action is not valid
        :rtype: ``CommandResult``
        """
        # Comprehensive validation
        validation = ValidationResult()
        validation.merge(self._validate_device(device))
        validation.merge(self._validate_user(user))
        validation.merge(self._validate_lock_action(action))

        device_id = device.get("serialnumber") if isinstance(device, dict) else None

        if not validation.is_valid:
            # Log validation errors
            for error in validation.errors:
                LOGGER.error("Lock action validation failed: %s", error)

            # Raise appropriate exception based on first error
            if any("Device" in e for e in validation.errors):
                raise InvalidDeviceError(
                    message="; ".join(e for e in validation.errors if "Device" in e),
                    missing_fields=[
                        f
                        for f in self.REQUIRED_DEVICE_FIELDS
                        if device is None or f not in device or not device.get(f)
                    ]
                    if isinstance(device, dict) or device is None
                    else None,
                )
            if any("User" in e for e in validation.errors):
                raise InvalidUserError(
                    message="; ".join(e for e in validation.errors if "User" in e),
                )
            if any("action" in e.lower() for e in validation.errors):
                raise InvalidActionError(
                    message="; ".join(e for e in validation.errors if "action" in e.lower()),
                    action=action,
                    valid_actions=[a.value for a in LockAction],
                )

        # Log warnings
        for warning in validation.warnings:
            LOGGER.warning("Lock action warning: %s", warning)

        # Build user name (truncated to MAX_NAME_LENGTH)
        user_name = f"{user.get('firstname', '')}{user.get('lastname', '')}"[: self.MAX_NAME_LENGTH]
        source_name = "apikwikset"[: self.MAX_NAME_LENGTH]

        payload = {
            "action": action,
            "source": json.dumps({"name": user_name, "device": source_name}),
        }

        LOGGER.debug(
            "Executing %s action on device %s",
            action,
            device_id,
        )

        try:
            response: dict[str, Any] = await self._request(
                "patch",
                LOCK_COMMAND_URL % device["serialnumber"],
                data=json.dumps(payload),
            )

            LOGGER.info(
                "Successfully executed %s action on device %s",
                action,
                device_id,
            )

            return CommandResult(
                success=True,
                data=response["data"],
                device_id=device_id,
                action=action,
                validation=validation,
            )
        except Exception as err:
            LOGGER.error(
                "Failed to execute %s action on device %s: %s",
                action,
                device_id,
                str(err),
            )
            return CommandResult(
                success=False,
                device_id=device_id,
                action=action,
                validation=validation,
                error_message=str(err),
            )

    async def _set_action(
        self,
        device: dict[str, Any],
        action: str,
        status: str,
    ) -> CommandResult:
        """Set an accessory action status on a device.

        :param device: Device information dictionary containing 'serialnumber'
        :type device: ``Dict[str, Any]``
        :param action: The accessory action type
        :type action: ``str``
        :param status: The status value to set ('true' or 'false')
        :type status: ``str``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidActionError: If action is not valid
        :raises InvalidStatusError: If status is not valid
        :rtype: ``CommandResult``
        """
        # Comprehensive validation
        validation = ValidationResult()
        validation.merge(self._validate_device(device))
        validation.merge(self._validate_accessory_action(action))
        validation.merge(self._validate_status(status))

        device_id = device.get("serialnumber") if isinstance(device, dict) else None

        if not validation.is_valid:
            # Log validation errors
            for error in validation.errors:
                LOGGER.error("Set action validation failed: %s", error)

            # Raise appropriate exception based on first error
            if any("Device" in e for e in validation.errors):
                raise InvalidDeviceError(
                    message="; ".join(e for e in validation.errors if "Device" in e),
                    missing_fields=[
                        f
                        for f in self.REQUIRED_DEVICE_FIELDS
                        if device is None or f not in device or not device.get(f)
                    ]
                    if isinstance(device, dict) or device is None
                    else None,
                )
            if any("action" in e.lower() for e in validation.errors):
                raise InvalidActionError(
                    message="; ".join(e for e in validation.errors if "action" in e.lower()),
                    action=action,
                    valid_actions=[a.value for a in AccessoryAction],
                )
            if any("status" in e.lower() for e in validation.errors):
                raise InvalidStatusError(
                    message="; ".join(e for e in validation.errors if "status" in e.lower()),
                    status=status,
                    valid_statuses=[s.value for s in DeviceStatus],
                )

        payload = {action: status}

        LOGGER.debug(
            "Setting %s to %s on device %s",
            action,
            status,
            device_id,
        )

        try:
            response: dict[str, Any] = await self._request(
                "patch",
                ACCESSORY_COMMAND_URL % (device["serialnumber"], action),
                data=json.dumps(payload),
            )

            LOGGER.info(
                "Successfully set %s to %s on device %s",
                action,
                status,
                device_id,
            )

            return CommandResult(
                success=True,
                data=response["data"],
                device_id=device_id,
                action=f"{action}={status}",
                validation=validation,
            )
        except Exception as err:
            LOGGER.error(
                "Failed to set %s to %s on device %s: %s",
                action,
                status,
                device_id,
                str(err),
            )
            return CommandResult(
                success=False,
                device_id=device_id,
                action=f"{action}={status}",
                validation=validation,
                error_message=str(err),
            )

    async def lock_device(
        self,
        device: dict[str, Any],
        user: dict[str, Any],
    ) -> CommandResult:
        """Lock a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param user: User information dictionary
        :type user: ``Dict[str, Any]``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidUserError: If user data is invalid
        :rtype: ``CommandResult``
        """
        return await self._lock_action(device, user, LockAction.LOCK.value)

    async def unlock_device(
        self,
        device: dict[str, Any],
        user: dict[str, Any],
    ) -> CommandResult:
        """Unlock a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param user: User information dictionary
        :type user: ``Dict[str, Any]``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidUserError: If user data is invalid
        :rtype: ``CommandResult``
        """
        return await self._lock_action(device, user, LockAction.UNLOCK.value)

    async def set_ledstatus(
        self,
        device: dict[str, Any],
        status: str,
    ) -> CommandResult:
        """Set the LED status on a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param status: LED status value ('true' or 'false')
        :type status: ``str``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidStatusError: If status is not 'true' or 'false'
        :rtype: ``CommandResult``
        """
        return await self._set_action(device, AccessoryAction.LED_STATUS.value, status)

    async def set_audiostatus(
        self,
        device: dict[str, Any],
        status: str,
    ) -> CommandResult:
        """Set the audio status on a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param status: Audio status value ('true' or 'false')
        :type status: ``str``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidStatusError: If status is not 'true' or 'false'
        :rtype: ``CommandResult``
        """
        return await self._set_action(device, AccessoryAction.AUDIO_STATUS.value, status)

    async def set_securescreenstatus(
        self,
        device: dict[str, Any],
        status: str,
    ) -> CommandResult:
        """Set the secure screen status on a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param status: Secure screen status value ('true' or 'false')
        :type status: ``str``
        :raises InvalidDeviceError: If device data is invalid
        :raises InvalidStatusError: If status is not 'true' or 'false'
        :rtype: ``CommandResult``
        """
        return await self._set_action(device, AccessoryAction.SECURE_SCREEN_STATUS.value, status)

    # Convenience methods with boolean parameters

    async def set_led_enabled(
        self,
        device: dict[str, Any],
        enabled: bool,
    ) -> CommandResult:
        """Enable or disable the LED on a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param enabled: True to enable LED, False to disable
        :type enabled: ``bool``
        :raises InvalidDeviceError: If device data is invalid
        :rtype: ``CommandResult``
        """
        status = DeviceStatus.ENABLED.value if enabled else DeviceStatus.DISABLED.value
        return await self.set_ledstatus(device, status)

    async def set_audio_enabled(
        self,
        device: dict[str, Any],
        enabled: bool,
    ) -> CommandResult:
        """Enable or disable the audio on a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param enabled: True to enable audio, False to disable
        :type enabled: ``bool``
        :raises InvalidDeviceError: If device data is invalid
        :rtype: ``CommandResult``
        """
        status = DeviceStatus.ENABLED.value if enabled else DeviceStatus.DISABLED.value
        return await self.set_audiostatus(device, status)

    async def set_secure_screen_enabled(
        self,
        device: dict[str, Any],
        enabled: bool,
    ) -> CommandResult:
        """Enable or disable the secure screen on a device.

        :param device: Device information dictionary
        :type device: ``Dict[str, Any]``
        :param enabled: True to enable secure screen, False to disable
        :type enabled: ``bool``
        :raises InvalidDeviceError: If device data is invalid
        :rtype: ``CommandResult``
        """
        status = DeviceStatus.ENABLED.value if enabled else DeviceStatus.DISABLED.value
        return await self.set_securescreenstatus(device, status)

    async def set_autolock_enabled(
        self,
        device: dict[str, Any],
        enabled: bool,
    ) -> CommandResult:
        """Enable or disable auto-lock on a device.

        Auto-lock uses a dedicated endpoint (``/devices/{id}/autolock``) with a
        camelCase JSON-boolean payload (``{"autoLockEnabled": true}``), which is
        distinct from the generic accessory-status endpoint used by LED, audio,
        and secure-screen settings.

        :param device: Device information dictionary containing 'serialnumber'
        :type device: ``Dict[str, Any]``
        :param enabled: True to enable auto-lock, False to disable
        :type enabled: ``bool``
        :raises InvalidDeviceError: If device data is invalid
        :rtype: ``CommandResult``
        """
        validation = ValidationResult()
        validation.merge(self._validate_device(device))

        device_id = device.get("serialnumber") if isinstance(device, dict) else None

        if not validation.is_valid:
            for error in validation.errors:
                LOGGER.error("Set auto-lock validation failed: %s", error)
            raise InvalidDeviceError(
                message="; ".join(validation.errors),
                missing_fields=[
                    f
                    for f in self.REQUIRED_DEVICE_FIELDS
                    if device is None or f not in device or not device.get(f)
                ]
                if isinstance(device, dict) or device is None
                else None,
            )

        payload = {"autoLockEnabled": bool(enabled)}

        LOGGER.debug(
            "Setting auto-lock to %s on device %s",
            enabled,
            device_id,
        )

        try:
            response: dict[str, Any] = await self._request(
                "patch",
                AUTOLOCK_COMMAND_URL % device["serialnumber"],
                data=json.dumps(payload),
            )

            LOGGER.info(
                "Successfully set auto-lock to %s on device %s",
                enabled,
                device_id,
            )

            return CommandResult(
                success=True,
                data=response.get("data"),
                device_id=device_id,
                action="autolock",
                validation=validation,
            )
        except Exception as err:
            LOGGER.error(
                "Failed to set auto-lock to %s on device %s: %s",
                enabled,
                device_id,
                str(err),
            )
            return CommandResult(
                success=False,
                device_id=device_id,
                action="autolock",
                validation=validation,
                error_message=str(err),
            )

    # Access code management methods

    async def get_access_codes(self, device_id: str) -> list[dict[str, Any]]:
        """Return all access codes for a device.

        :param device_id: Device serial number
        :type device_id: ``str``
        :raises ValueError: If device_id is empty or None
        :rtype: ``List[Dict[str, Any]]``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        response: dict[str, Any] = await self._request(
            "get",
            ACCESS_CODE_URL % device_id,
        )
        result: list[dict[str, Any]] = response.get("data", [])
        return result

    async def create_access_code(
        self,
        device_id: str,
        code: str,
        name: str,
        schedule: AccessCodeSchedule,
        slot: int = 0,
        enabled: bool = True,
    ) -> AccessCodeResult:
        """Create an access code on a device.

        :param device_id: Device serial number
        :type device_id: ``str``
        :param code: Numeric access code (4-8 digits)
        :type code: ``str``
        :param name: Friendly name for the access code
        :type name: ``str``
        :param schedule: Schedule configuration
        :type schedule: ``AccessCodeSchedule``
        :param slot: Slot index on the lock (0-255)
        :type slot: ``int``
        :param enabled: Whether the code is enabled
        :type enabled: ``bool``
        :raises ValueError: If parameters are invalid
        :rtype: ``AccessCodeResult``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        message = build_access_code_message(slot, code, name, schedule, enabled)
        payload = json.dumps({"message": message})

        response: dict[str, Any] = await self._request(
            "post",
            ACCESS_CODE_URL % device_id,
            data=payload,
        )

        data = response.get("data", [{}])
        first = data[0] if data else {}
        return AccessCodeResult(
            token=first.get("token", ""),
            last_update_status=first.get("lastupdatestatus", 0),
        )

    async def edit_access_code(
        self,
        device_id: str,
        code: str,
        name: str,
        schedule: AccessCodeSchedule,
        slot: int,
        enabled: bool = True,
    ) -> AccessCodeResult:
        """Edit an existing access code on a device.

        :param device_id: Device serial number
        :type device_id: ``str``
        :param code: Numeric access code (4-8 digits)
        :type code: ``str``
        :param name: Friendly name for the access code
        :type name: ``str``
        :param schedule: Schedule configuration
        :type schedule: ``AccessCodeSchedule``
        :param slot: Slot index on the lock (0-255)
        :type slot: ``int``
        :param enabled: Whether the code is enabled
        :type enabled: ``bool``
        :raises ValueError: If parameters are invalid
        :rtype: ``AccessCodeResult``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        message = build_access_code_message(slot, code, name, schedule, enabled)
        payload = json.dumps({"message": message})

        response: dict[str, Any] = await self._request(
            "patch",
            ACCESS_CODE_URL % device_id,
            data=payload,
        )

        data = response.get("data", [{}])
        first = data[0] if data else {}
        return AccessCodeResult(
            token=first.get("token", ""),
            last_update_status=first.get("lastupdatestatus", 0),
        )

    async def delete_access_code(
        self,
        device_id: str,
        slot: int,
    ) -> dict[str, Any]:
        """Delete a single access code from a device.

        Validates that at least one access code will remain after deletion.
        Every Kwikset lock requires a minimum of one access code at all times.

        :param device_id: Device serial number
        :type device_id: ``str``
        :param slot: Slot index of the access code to delete (0-255)
        :type slot: ``int``
        :raises ValueError: If parameters are invalid
        :raises MinimumAccessCodeError: If only one access code remains
        :rtype: ``Dict[str, Any]``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        # Client-side validation: ensure at least one code remains
        current_codes = await self.get_access_codes(device_id)
        if len(current_codes) <= MIN_ACCESS_CODES:
            raise MinimumAccessCodeError(
                device_id=device_id,
                current_count=len(current_codes),
            )

        message = build_delete_message(slot)
        payload = json.dumps({"message": message})

        response: dict[str, Any] = await self._request(
            "delete",
            ACCESS_CODE_URL % device_id,
            data=payload,
        )
        return response

    async def delete_all_access_codes(
        self,
        device_id: str,
    ) -> dict[str, Any]:
        """Delete all access codes from a device.

        .. warning::
            This operation always raises MinimumAccessCodeError because
            Kwikset locks require at least one access code at all times.
            Deleting all codes would leave zero, violating this constraint.

        :param device_id: Device serial number
        :type device_id: ``str``
        :raises ValueError: If device_id is empty
        :raises MinimumAccessCodeError: Always raised — cannot delete all codes
        :rtype: ``Dict[str, Any]``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        raise MinimumAccessCodeError(
            message="Cannot delete all access codes. At least one access code is required.",
            device_id=device_id,
            current_count=None,
        )

    async def disable_access_code(
        self,
        device_id: str,
        code: str,
        name: str,
        schedule: AccessCodeSchedule,
        slot: int,
    ) -> AccessCodeResult:
        """Disable an access code on a device.

        Sends a PATCH request with the full access code data and enabled=False.
        The slot must be the lock-assigned slot index.

        :param device_id: Device serial number
        :type device_id: ``str``
        :param code: Numeric access code (4-8 digits)
        :type code: ``str``
        :param name: Friendly name for the access code
        :type name: ``str``
        :param schedule: Schedule configuration
        :type schedule: ``AccessCodeSchedule``
        :param slot: Lock-assigned slot index (0-255)
        :type slot: ``int``
        :raises ValueError: If parameters are invalid
        :rtype: ``AccessCodeResult``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        message = build_access_code_message(slot, code, name, schedule, enabled=False)
        payload = json.dumps({"message": message})

        response: dict[str, Any] = await self._request(
            "patch",
            ACCESS_CODE_URL % device_id,
            data=payload,
        )

        data = response.get("data", [{}])
        first = data[0] if data else {}
        return AccessCodeResult(
            token=first.get("token", ""),
            last_update_status=first.get("lastupdatestatus", 0),
        )

    async def enable_access_code(
        self,
        device_id: str,
        code: str,
        name: str,
        schedule: AccessCodeSchedule,
        slot: int,
    ) -> AccessCodeResult:
        """Enable a previously disabled access code on a device.

        Sends a PATCH request with the full access code data and enabled=True.
        The slot must be the lock-assigned slot index.

        :param device_id: Device serial number
        :type device_id: ``str``
        :param code: Numeric access code (4-8 digits)
        :type code: ``str``
        :param name: Friendly name for the access code
        :type name: ``str``
        :param schedule: Schedule configuration
        :type schedule: ``AccessCodeSchedule``
        :param slot: Lock-assigned slot index (0-255)
        :type slot: ``int``
        :raises ValueError: If parameters are invalid
        :rtype: ``AccessCodeResult``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")

        message = build_access_code_message(slot, code, name, schedule, enabled=True)
        payload = json.dumps({"message": message})

        response: dict[str, Any] = await self._request(
            "patch",
            ACCESS_CODE_URL % device_id,
            data=payload,
        )

        data = response.get("data", [{}])
        first = data[0] if data else {}
        return AccessCodeResult(
            token=first.get("token", ""),
            last_update_status=first.get("lastupdatestatus", 0),
        )

    async def get_access_code_status(
        self,
        device_id: str,
        token: str,
    ) -> dict[str, Any]:
        """Get the sync status of an access code operation.

        :param device_id: Device serial number
        :type device_id: ``str``
        :param token: Token from create/edit response
        :type token: ``str``
        :raises ValueError: If parameters are invalid
        :rtype: ``Dict[str, Any]``
        """
        if not device_id:
            raise ValueError("device_id cannot be empty or None")
        if not token:
            raise ValueError("token cannot be empty or None")

        response: dict[str, Any] = await self._request(
            "get",
            ACCESS_CODE_SYNC_URL % (device_id, token),
        )
        return response
