"""
Telephony devices
"""
import os
from collections.abc import Generator
from dataclasses import dataclass
from io import BufferedReader
from typing import Optional, Union, Any

from pydantic import TypeAdapter, Field, field_validator, field_serializer
from requests_toolbelt import MultipartEncoder

from .dynamic_settings import DevicesDynamicSettingsApi
from ..jobs import LineKeyTemplateAdvisoryTypes
from ...rest import RestSession
from ...api_child import ApiChild
from ...base import ApiModel, plus1, to_camel, enum_str
from ...base import SafeEnum as Enum
from ...common import PrimaryOrShared, UserType, ValidationStatus, DeviceCustomization, IdAndName, \
    ApplyLineKeyTemplateAction, UserLicenseType, DeviceType, UsageType

__all__ = ['DeviceManufacturer', 'DeviceManagedBy', 'OnboardingMethod', 'DeviceSettingsConfiguration',
           'SupportsLogCollection', 'SupportedDevice', 'SupportedDevices', 'MemberCommon', 'DeviceMember',
           'DeviceMembersResponse', 'AvailableMember', 'MACState',
           'MACStatus', 'MACValidationResponse', 'TelephonyDevicesApi', 'LineKeyType', 'ProgrammableLineKey',
           'LineKeyTemplate', 'TelephonyDeviceDetails', 'ActivationState', 'TelephonyDeviceOwner',
           'TelephonyDeviceProxy', 'LayoutMode', 'KemModuleType', 'KemKey', 'DeviceLayout', 'DeviceSettings',
           'BackgroundImage', 'BackgroundImages', 'DeleteImageRequestObject',
           'DeleteImageResponseSuccessObjectResult', 'DeleteImageResponseSuccessObject',
           'DeleteDeviceBackgroundImagesResponse', 'UserDeviceCount']


class DeviceManufacturer(str, Enum):
    cisco = 'CISCO'
    third_party = 'THIRD_PARTY'


class DeviceManagedBy(str, Enum):
    cisco = 'CISCO'
    customer = 'CUSTOMER'
    partner = 'PARTNER'


class OnboardingMethod(str, Enum):
    mac_address = 'MAC_ADDRESS'
    activation_code = 'ACTIVATION_CODE'
    no_method = 'NONE'


class DeviceSettingsConfiguration(str, Enum):
    #: Devices which supports Webex Calling Device Settings Configuration.
    webex_calling_device_configuration = 'WEBEX_CALLING_DEVICE_CONFIGURATION'
    #: Devices which supports Webex Device Settings Configuration.
    webex_device_configuration = 'WEBEX_DEVICE_CONFIGURATION'
    #: Devices which supports Webex Calling dynamic Settings Configuration.
    webex_calling_dynamic_device_configuration = 'WEBEX_CALLING_DYNAMIC_DEVICE_CONFIGURATION'
    #: Devices does not support any configuration.
    none_ = 'NONE'


class SupportsLogCollection(str, Enum):
    #: Devices which does not support log collection.
    none_ = 'NONE'
    #: Devices which supports Cisco PRT log collection.
    cisco_prt = 'CISCO_PRT'
    #: Devices which supports Cisco RoomOS log collection.
    cisco_roomos = 'CISCO_ROOMOS'


class SupportedDevice(ApiModel):
    #: Model name of the device.
    model: str
    #: Display name of the device.
    display_name: str
    #: The display name of the device family.
    family_display_name: Optional[str] = None
    #: Type of the device.
    device_type: DeviceType = Field(alias='type')
    #: Manufacturer of the device.
    manufacturer: DeviceManufacturer
    #: Users who manage the device.
    managed_by: DeviceManagedBy
    #: List of places the device is supported for.
    supported_for: list[UserType]
    #: Onboarding method.
    onboarding_method: list[OnboardingMethod]
    #: Enables / Disables layout configuration for devices.
    allow_configure_layout_enabled: bool
    #: Number of port lines.
    number_of_line_ports: int
    #: Indicates whether Kem support is enabled or not.
    kem_support_enabled: bool
    #: Module count.
    kem_module_count: Optional[int] = None
    #: Enables / disables Kem lines support.
    kem_lines_support_enabled: Optional[bool] = None
    #: Key expansion module type of the device.
    kem_module_type: Optional[list[str]] = None
    #: Enables / Disables the upgrade channel.
    upgrade_channel_enabled: Optional[bool] = None
    #: The default upgrade channel.
    default_upgrade_channel: Optional[str] = None
    #: Enables / disables the additional primary line appearances.
    additional_primary_line_appearances_enabled: Optional[bool] = None
    #: Enables / disables the additional shared line appearances.
    additional_secondary_line_appearances_enabled: Optional[bool] = None
    #: Enables / disables Basic emergency nomadic.
    basic_emergency_nomadic_enabled: Optional[bool] = None
    #: Enables / disables customized behavior support on devices
    customized_behaviors_enabled: Optional[bool] = None
    #: Enables / disables configuring port support on device.
    allow_configure_ports_enabled: Optional[bool] = None
    #: Enables / disables customizable line label.
    customizable_line_label_enabled: Optional[bool] = None
    #: Enables / disables support line port reordering.
    supports_line_port_reordering_enabled: Optional[bool] = None
    #: Enables / disables port number support.
    port_number_support_enabled: Optional[bool] = None
    #: Enables / disables T.38.
    t38_enabled: Optional[bool] = None
    #: Enables / disables call declined.
    call_declined_enabled: Optional[bool] = None
    #: Supports touch screen on device.
    touch_screen_phone: Optional[bool] = None
    #: Number of line key buttons for a device.
    number_of_line_key_buttons: Optional[int] = None
    #: Device settings configuration.
    device_settings_configuration: Optional[DeviceSettingsConfiguration] = None
    number_of_primary_display_configured_lines: Optional[int] = None
    #: Enables / disables hoteling host.
    allow_hoteling_host_enabled: Optional[bool] = None
    #: Device log collection configuration.
    supports_log_collection: Optional[SupportsLogCollection] = None
    #: Enables / disables apply changes.
    supports_apply_changes_enabled: Optional[bool] = None
    #: Enables / disables configure lines.
    allow_configure_lines_enabled: Optional[bool] = None
    #: Enables / disables configure phone settings.
    allow_configure_phone_settings_enabled: Optional[bool] = None
    #: Enables / disables hotline support.
    supports_hotline_enabled: Optional[bool] = None
    #: Supports hot desk only.
    supports_hot_desk_only: Optional[bool] = None
    max_number_of_line_appearances: Optional[int] = None


class SupportedDevices(ApiModel):
    #: List of available upgrade channels.
    #: * `STABLE` - These are standard stable releases.
    #: * `STABLE_DELAY` - These are delayed stable releases.
    #: * `PREVIEW` - These are Preview/pre-release versions.
    #: * `BETA` - These are Beta testing versions.
    #: * `TESTING` - These are testing versions.
    upgrade_channel_list: Optional[list[str]] = None
    #: List of supported devices.
    devices: Optional[list[SupportedDevice]] = None


class ActivationState(str, Enum):
    #: Indicates a device is activating using an activation code.
    activating = 'activating'
    #: Indicates a device has been activated using an activation code.
    activated = 'activated'
    #: Indicates a device has not been activated using an activation code.
    deactivated = 'deactivated'


class TelephonyDeviceOwner(ApiModel):
    #: Identifies a device endpoint in standalone mode or a SIP URI public identity in IMS mode.
    line_port: Optional[str] = None
    #: SIP authentication user name for the owner of the device.
    sip_user_name: Optional[str] = None


class TelephonyDeviceProxy(ApiModel):
    #: Outgoing server which the phone should use for all SIP requests. Not set if the response has no body.
    outbound_proxy: Optional[str] = None


class TelephonyDeviceDetails(ApiModel):
    #: Manufacturer of the device.
    manufacturer: Optional[str] = None
    #: Device manager(s).
    managed_by: Optional[str] = None
    #: A unique identifier for the device.
    id: Optional[str] = None
    #: The current IP address of the device.
    ip: Optional[str] = None
    #: The unique address for the network adapter.
    mac: Optional[str] = None
    #: A model type of the device.
    model: Optional[str] = None
    #: Activation state of the device. This field is only populated for a device added by a unique activation code
    #: generated by Control Hub for use with Webex.
    activation_state: Optional[ActivationState] = None
    #: Comma-separated array of tags used to describe the device.
    description: Optional[list[str]] = None
    #: Enabled / disabled status of the upgrade channel.
    upgrade_channel_enabled: Optional[bool] = None
    owner: Optional[TelephonyDeviceOwner] = None
    proxy: Optional[TelephonyDeviceProxy] = None


class MemberCommon(ApiModel):
    #: Unique identifier for the member.
    member_id: str = Field(alias='id', default=None)
    member_type: UserType = Field(default=UserType.people)
    #: First name of a person or workspace.
    first_name: Optional[str] = None
    #: Last name of a person or workspace.
    last_name: Optional[str] = None
    #: Phone Number of a person or workspace. In some regions phone numbers are not returned in E.164 format. This
    #: will be supported in a future update.
    phone_number: Optional[str] = None
    #: Extension of a person or workspace.
    extension: Optional[str] = None
    #: Routing prefix of location.
    routingPrefix: Optional[str] = None
    #: Routing prefix + extension of a person or workspace.
    esn: Optional[str] = None
    #: T.38 Fax Compression setting and is available only for ATA Devices. Choose T.38 fax compression if the device
    #: requires this option. This will override user level compression options.
    t38_fax_compression_enabled: Optional[bool] = None
    #: Line type is used to differentiate Primary and SCA, at which endpoint it is assigned.
    line_type: PrimaryOrShared = Field(default=PrimaryOrShared.primary)
    #: Set how a person's device behaves when a call is declined. When set to true, a call decline request is extended
    #: to all the endpoints on the device. When set to false, a call decline request only declines the current endpoint.
    allow_call_decline_enabled: Optional[bool] = Field(default=True)
    location: Optional[IdAndName] = None
    license_type: Optional[UserLicenseType] = None

    @field_validator('phone_number', mode='before')
    def e164(cls, v):
        """
        :meta private:
        """
        return plus1(v)


class DeviceMember(MemberCommon):
    #: This field indicates whether the person or the workspace is the owner of the device, and points to a primary
    #: Line/Port of the device.
    primary_owner: bool = Field(default=False)
    #: Port number assigned to person or workspace.
    port: int = Field(default=1)
    #: Number of lines that have been configured for the person on the device. Can only be larger than one for primary
    #: owner
    line_weight: int = Field(default=1)
    #: Device line label.
    line_label: Optional[str] = None
    #: Registration Host IP address for the line port.
    host_ip: Optional[str] = Field(alias='hostIP', default=None)
    #: Registration Remote IP address for the line port.
    remote_ip: Optional[str] = Field(alias='remoteIP', default=None)
    #: Enable Hotline. Configure this line to automatically call a predefined number whenever taken off-hook. Once
    #: enabled, the line can only make calls to the predefined number set in hotlineDestination.
    hotline_enabled: bool = Field(default=False)
    #: The preconfigured number for Hotline. Required only if hotlineEnabled is set to true.
    hotline_destination: Optional[str] = None

    @staticmethod
    def from_available(available: 'AvailableMember') -> 'DeviceMember':
        data = available.model_dump(mode='json', by_alias=True, exclude_none=True)
        return DeviceMember.model_validate(data)


class DeviceMembersResponse(ApiModel):
    """
    Get Device Members response
    """
    model: str
    members: list[DeviceMember]
    max_line_count: int

    # assert that members are always sorted by port number
    @field_validator('members', mode='after')
    def sort_members(cls, v):
        """
        :meta private:
        """
        v.sort(key=lambda dm: dm.port)
        return v


class AvailableMember(MemberCommon):
    ...


class MACState(str, Enum):
    """
    State of the MAC address.
    """
    #: The requested MAC address is available.
    available = 'AVAILABLE'
    #: The requested MAC address is unavailable.
    unavailable = 'UNAVAILABLE'
    #: The requested MAC address is duplicated.
    duplicate_in_list = 'DUPLICATE_IN_LIST'
    #: The requested MAC address is invalid.
    invalid = 'INVALID'


class MACStatus(ApiModel):
    #: MAC address.
    mac: str
    #: State of the MAC address.
    state: MACState
    #: MAC address validation error code.
    error_code: Optional[int] = None
    #: Provides a status message about the MAC address.
    message: Optional[str] = None


class MACValidationResponse(ApiModel):
    #: Status of MAC address.
    status: ValidationStatus
    #: Contains an array of all the MAC address provided and their statuses.
    mac_status: Optional[list[MACStatus]] = None


class LineKeyType(str, Enum):
    #: PRIMARY_LINE is the user's primary extension. This is the default assignment for Line Key Index 1 and cannot be
    #: modified.
    primary_line = 'PRIMARY_LINE'
    #: Shows the appearance of other users on the owner's phone.
    shared_line = 'SHARED_LINE'
    #: Enables User and Call Park monitoring.
    monitor = 'MONITOR'
    #: Enables the configure layout feature in Control Hub to set call park extension implicitly.
    call_park_extension = 'CALL_PARK_EXTENSION'
    #: Allows users to reach a telephone number, extension or a SIP URI.
    speed_dial = 'SPEED_DIAL'
    #: An open key will automatically take the configuration of a monitor button starting with the first open key.
    #: These buttons are also usable by the user to configure speed dial numbers on these keys.
    open = 'OPEN'
    #: Button not usable but reserved for future features.
    closed = 'CLOSED'
    #: Allows users to manage call forwarding for features via schedule-based routing.
    mode_management = 'MODE_MANAGEMENT'


class ProgrammableLineKey(ApiModel):
    #: An index representing a Line Key. Index starts from 1 representing the first key on the left side of the phone.
    line_key_index: Optional[int] = None
    #: The action that would be performed when the Line Key is pressed.
    line_key_type: Optional[LineKeyType] = None
    #: This is applicable only when the lineKeyType is `SPEED_DIAL`.
    line_key_label: Optional[str] = None
    #: Applicable only when the `lineKeyType` is `SPEED_DIAL`. Value must be a valid telephone number, ext, or SIP URI
    #: (format: `user@host` using A-Z,a-z,0-9,-_ .+ for `user` and `host`).
    line_key_value: Optional[str] = None
    #: Shared line index is the line label number of the shared or virtual line assigned in the configured lines. Since
    #: you can add multiple appearances of the same shared or virtual line on a phone, entering the index number
    #: assigns the respective line to a line key. This is applicable only when the `lineKeyType` is SHARED_LINE, If
    #: multiple programmable line keys are configured as shared lines, and If the `sharedLineIndex` is sent for any of
    #: the shared line, then the `sharedLineIndex` should be sent for all other shared lines. When `lineKeyType` is
    #: SHARED_LINE and `sharedLineIndex` is not assigned to any of the configured lines, then `sharedLineIndex` is
    #: assigned by default in the order the shared line appears in the request.
    shared_line_index: Optional[int] = None

    @classmethod
    def standard_plk_list(cls, lines: int = 10) -> list['ProgrammableLineKey']:
        """
        get a standard list of programmable line keys of given length.
        1st line key is primary line and all other are "open"

        :param lines: number of programmable line keys
        :return: list of programmable line keys
        """
        r = [ProgrammableLineKey(line_key_index=i, line_key_type=LineKeyType.open) for i in range(1, lines + 1)]
        r[0].line_key_type = LineKeyType.primary_line
        return r


class LineKeyTemplate(ApiModel):
    #: Unique identifier for the Line Key Template
    id: Optional[str] = None
    #: Name of the Line Key Template
    template_name: Optional[str] = None
    #: The Device Model for which the Line Key Template is applicable
    device_model: Optional[str] = None
    #: The friendly display name used to represent the device model in Control Hub
    display_name: Optional[str] = Field(alias='modelDisplayName', default=None)
    #: Indicates whether user can reorder the line keys.
    user_reorder_enabled: Optional[bool] = None
    #: Contains a mapping of Line Keys and their corresponding actions.
    line_keys: Optional[list[ProgrammableLineKey]] = None

    def create_or_update(self) -> dict[str, Any]:
        """
        dict for create or update

        :meta private:
        """
        return self.model_dump(mode='json', exclude_none=True, by_alias=True, exclude={'id', 'display_name'})


class LayoutMode(str, Enum):
    #: Default layout mode when a new device is added.
    default = 'DEFAULT'
    #: Enables a device to have its custom layout.
    custom = 'CUSTOM'


class KemModuleType(str, Enum):
    #: Extension module has 14 line keys that can be configured.
    kem_14_keys = 'KEM_14_KEYS'
    #: Extension module has 18 line keys that can be configured.
    kem_18_keys = 'KEM_18_KEYS'
    #: Extension module has 20 line keys that can be configured.
    kem_20_keys = 'KEM_20_KEYS'


class KemKey(ApiModel):
    #: An index representing a KEM Module. The Index starts from 1 representing the first KEM Module.
    kem_module_index: Optional[int] = None
    #: An index representing a KEM Key. The Index starts from 1 representing the first key on the left side of the
    #: phone.
    kem_key_index: Optional[int] = None
    #: The action that would be performed when the KEM Key is pressed.
    kem_key_type: Optional[LineKeyType] = None
    #: Applicable only when the kemKeyType is `SPEED_DIAL`.
    kem_key_label: Optional[str] = None
    #: Applicable only when the kemKeyType is `SPEED_DIAL`. Value must be a valid Telephone Number, Ext, or SIP URI
    #: (format: `user@host` limited to `A-Z,a-z,0-9,-_ .+` for user and host).
    kem_key_value: Optional[str] = None
    #: Shared line index is the line label number of the shared or virtual line assigned in the configured lines. Since
    #: you can add multiple appearances of the same shared or virtual line on a phone, entering the index number
    #: assigns the respective line to a line key. This is applicable only when the `lineKeyType` is SHARED_LINE, If
    #: multiple programmable line keys are configured as shared lines, and If the `sharedLineIndex` is sent for any of
    #: the shared line, then the `sharedLineIndex` should be sent for all other shared lines. When `lineKeyType` is
    #: SHARED_LINE and `sharedLineIndex` is not assigned to any of the configured lines, then `sharedLineIndex` is
    #: assigned by default in the order the shared line appears in the request.
    shared_line_index: Optional[int] = None


class DeviceLayout(ApiModel):
    #: Defines the layout mode of the device, i.e. DEFAULT or CUSTOM.
    layout_mode: Optional[LayoutMode] = None
    #: If `true`, user customization is enabled..
    user_reorder_enabled: Optional[bool] = None
    #: Contains a mapping of Line Keys and their corresponding actions.
    line_keys: Optional[list[ProgrammableLineKey]] = None
    #: Type of KEM module.
    kem_module_type: Optional[KemModuleType] = None
    #: Contains a mapping of KEM Keys and their corresponding actions.
    kem_keys: Optional[list[KemKey]] = None

    def update(self) -> dict:
        """
        get data for update
        :meta private:
        """
        return self.model_dump(mode='json', exclude_none=True, by_alias=True)


class DeviceSettings(ApiModel):
    #: True -> Minimize data use during compression.
    #:
    #: False -> Ignore data use during compression.
    compression: Optional[bool] = None

    @field_serializer('compression')
    def ser_compression(self, v: bool) -> str:
        """

        :meta private:
        """
        return 'ON' if v else 'OFF'


class BackgroundImage(ApiModel):
    #: The URL of the image file.
    background_image_url: Optional[str] = None
    #: The name of the image file.
    file_name: Optional[str] = None
    #: The total number of images in the org after uploading.
    count: Optional[int] = None


class BackgroundImages(ApiModel):
    #: Array of background images.
    background_images: Optional[list[BackgroundImage]] = None
    #: The total number of images in the org.
    count: Optional[int] = None


class DeleteImageRequestObject(ApiModel):
    #: The name of the image file to be deleted.
    file_name: Optional[str] = None
    #: Flag to force delete the image. When `forceDelete` = true, if any device, location, or org level custom
    #: background URL is configured with the `backgroundImageURL` containing the filename being deleted, the
    #: background image is set to `None`.
    force_delete: Optional[bool] = None


class DeleteImageResponseSuccessObjectResult(ApiModel):
    #: The status of the deletion.
    status: Optional[int] = None


class DeleteImageResponseSuccessObject(ApiModel):
    #: The name of the image file.
    file_name: Optional[str] = None
    #: The result of the deletion.
    result: Optional[DeleteImageResponseSuccessObjectResult] = None


class DeleteDeviceBackgroundImagesResponse(ApiModel):
    #: Array of deleted images.
    items: Optional[list[DeleteImageResponseSuccessObject]] = None
    #: The total number of images in the org after deletion.
    count: Optional[int] = None


class UserDeviceCount(ApiModel):
    #: The total count of devices associated with the user as a sum of:
    #:
    #: - Count of total primary physical devices.
    #: - Count of Webex-Team system device endpoints.
    #: - Count of 1 for any or all applications present.
    total_device_count: Optional[int] = None
    #: The total count of applications associated with the user.
    applications_count: Optional[int] = None


@dataclass(init=False, repr=False)
class TelephonyDevicesApi(ApiChild, base='telephony/config'):
    """
    Telephony devices API
    """
    dynamic_settings: DevicesDynamicSettingsApi

    def __init__(self, session: RestSession):
        super().__init__(session=session)
        self.dynamic_settings = DevicesDynamicSettingsApi(session=session)

    def supported_devices(self, allow_configure_layout_enabled: bool = None, type_: str = None,
                          org_id: str = None) -> SupportedDevices:
        """
        Read the List of Supported Devices

        Gets the list of supported devices for an organization.

        Retrieving this list requires a full or read-only administrator auth token with a scope of
        `spark-admin:telephony_config_read`.

        :param allow_configure_layout_enabled: List supported devices that allow the user to configure the layout.
        :type allow_configure_layout_enabled: bool
        :param type_: List supported devices of a specific type. To excluded device types from a request or query, add
            `type=not:DEVICE_TYPE`. For example, `type=not:MPP`.
        :type type_: str
        :param org_id: List supported devices for an organization.
        :type org_id: str
        :rtype: SupportedDevices
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        if allow_configure_layout_enabled is not None:
            params['allowConfigureLayoutEnabled'] = str(allow_configure_layout_enabled).lower()
        if type is not None:
            params['type'] = type_
        url = self.ep('supportedDevices')
        data = self.get(url=url, params=params)
        return SupportedDevices.model_validate(data)

    def details(self, device_id: str, org_id: str = None) -> TelephonyDeviceDetails:
        """
        Get Webex Calling Device Details

        Not supported for Webex for Government (FedRAMP)

        Retrieves Webex Calling device details that include information needed for third-party device management.

        Webex calling devices are associated with a specific user Workspace or Virtual Line. Webex Calling devices
        share the location with the entity that owns them.

        Person or workspace to which the device is assigned. Its fields point to a primary line/port of the device.

        Requires a full, location, user, or read-only admin auth token with the scope of
        `spark-admin:telephony_config_read`.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param org_id: ID of the organization in which the device resides.
        :type org_id: str
        :rtype: :class:`TelephonyDeviceDetails`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'devices/{device_id}')
        data = super().get(url, params=params)
        r = TelephonyDeviceDetails.model_validate(data)
        return r

    def update_third_party_device(self, device_id: str, sip_password: str, org_id: str = None):
        """
        Update Third Party Device

        Not supported for Webex for Government (FedRAMP)

        Modify a device's `sipPassword`.

        Updating `sipPassword` on the device requires a full or user administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param sip_password: Password to be updated.
        :type sip_password: str
        :param org_id: ID of the organization in which the device resides.
        :type org_id: str
        :rtype: None
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = dict()
        body['sipPassword'] = sip_password
        url = self.ep(f'devices/{device_id}')
        super().put(url, params=params, json=body)

    def members(self, device_id: str, org_id: str = None) -> DeviceMembersResponse:
        """
        Get Device Members

        Get the list of all the members of the device including primary and secondary users.

        A device member can be either a person or a workspace. An admin can access the list of member details, modify
        member details and search for available members on a device.

        Retrieving this list requires a full or read-only administrator auth token with a scope
        of spark-admin:telephony_config_read.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param org_id: Retrieves the list of all members of the device in this Organization.
        :type org_id: str
        :return: Device model, line count, and members
        :rtype: DeviceMembersResponse
        """
        params = org_id and {'orgId': org_id} or None
        url = self.ep(f'devices/{device_id}/members')
        data = self.get(url=url, params=params)
        return DeviceMembersResponse.model_validate(data)

    def update_members(self, device_id: str, members: list[Union[DeviceMember, AvailableMember]] = None,
                       org_id: str = None):
        """
        Modify member details on the device.

        A device member can be either a person or a workspace. An admin can access the list of member details,
        modify member details and search for available members on a device.

        Modifying members on the device requires a full administrator auth token with a scope
        of spark-admin:telephony_config_write.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param members: New member details for the device. If the member's list is missing then all the users are
            removed except the primary user.
        :type members: list[Union[DeviceMember, AvailableMember]
        :param org_id: Modify members on the device in this organization.
        :type org_id: str
        """
        members_for_update = []
        for member in members or []:
            if isinstance(member, AvailableMember):
                member = DeviceMember.from_available(member)
            else:
                member = member.model_copy(deep=True)
            members_for_update.append(member)

        if members_for_update:
            # now assign port indices
            port = 1
            for member in members_for_update:
                member.port = port
                port += member.line_weight

        # create body
        if members_for_update:
            members = ','.join(m.model_dump_json(include={'member_id', 'port', 't38_fax_compression_enabled',
                                                          'primary_owner', 'line_type', 'line_weight', 'line_label',
                                                          'hotline_enabled', 'hotline_destination',
                                                          'allow_call_decline_enabled'})
                               for m in members_for_update)
            body = f'{{"members": [{members}]}}'
        else:
            body = None

        url = self.ep(f'devices/{device_id}/members')
        params = org_id and {'orgId': org_id} or None
        self.put(url=url, data=body, params=params)

    def available_members(self, device_id: str, location_id: str = None, member_name: str = None,
                          phone_number: str = None, extension: str = None, org_id: str = None,
                          usage_type: UsageType = None,
                          order: str = None,
                          **params) -> Generator[AvailableMember, None, None]:
        """
        Search members that can be assigned to the device.

        A device member can be either a person or a workspace. A admin can access the list of member details,
        modify member details and search for available members on a device.

        This requires a full or read-only administrator auth token with a scope of spark-admin:telephony_config_read.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param location_id: Search (Contains) based on number.
        :type location_id: str
        :param member_name: Search (Contains) numbers based on member name.
        :type member_name: str
        :param phone_number: Search (Contains) based on number.
        :type phone_number: str
        :param extension: Search (Contains) based on extension.
        :type extension: str
        :param usage_type: Search for members eligible to become the owner of the device, or share line on the device.
        :type usage_type: UsageType
        :param order: Sort the list of available members on the device in ascending order by name, use either last name
            `lname` or first name `fname`. Default: last name in ascending order.
        :type order: str
        :param org_id: Retrieves the list of available members on the device in this Organization.
        :type org_id: str
        :return: list of available members
        """
        params.update((to_camel(p), v) for p, v in locals().items()
                      if p not in {'self', 'params', 'device_id'} and v is not None)
        url = self.ep(f'devices/{device_id}/availableMembers')
        # noinspection PyTypeChecker
        return self.session.follow_pagination(url=url, model=AvailableMember, params=params, item_key='members')

    def apply_changes(self, device_id: str, org_id: str = None):
        """
        Apply Changes for a specific device

        Issues request to the device to download and apply changes to the configuration.

        Applying changes for a specific device requires a full administrator auth token with a scope
        of spark-admin:telephony_config_write.
        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param org_id: Apply changes for a device in this Organization.
        :type org_id: str
        """
        params = org_id and {'orgId': org_id} or None
        url = self.ep(f'devices/{device_id}/actions/applyChanges/invoke')
        self.post(url=url, params=params)

    def device_settings(self, device_id: str, device_model: str = None, org_id: str = None) -> DeviceCustomization:
        """
        Get override settings for a device.

        Device settings lists all the applicable settings for an MPP and an ATA devices at the device level. An admin
        can also modify the settings. DECT devices do not support settings at the device level.

        This requires a full or read-only administrator auth token with a scope of spark-admin:telephony_config_read.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param device_model: The model type of the device. The corresponding device model display name sometimes called
            the product name, can also be used to specify the model.
        :type device_model: str
        :param org_id: Settings on the device in this organization.
        :type org_id: str
        :return: Device settings
        :rtype: DeviceCustomization
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        if device_model is not None:
            params['deviceModel'] = device_model
        url = self.ep(f'devices/{device_id}/settings')
        data = self.get(url=url, params=params)
        return DeviceCustomization.model_validate(data)

    def update_device_settings(self, device_id: str, device_model: str, customization: DeviceCustomization,
                               org_id: str = None):
        """
        Modify override settings for a device.

        Device settings list all the applicable settings for an MPP and an ATA devices at the device level. Admins
        can also modify the settings. NOTE: DECT devices do not support settings at the device level.

        Updating settings on the device requires a full administrator auth token with a scope
        of spark-admin:telephony_config_write.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param device_model: Device model name.
        :type device_model: str
        :param customization: Indicates the customization object of the device settings.
        :type customization: DeviceCustomization
        :param org_id: Organization in which the device resides..
        :type org_id: str

        Example :

            .. code-block:: python

                # target_device is a TelephonyDevice object
                target_device: TelephonyDevice

                # get device level settings
                settings = api.telephony.devices.device_settings(device_id=target_device.device_id,
                                                                 device_model=target_device.model)

                # update settings (display name format) and enable device level customization
                settings.customizations.mpp.display_name_format = DisplayNameSelection.person_last_then_first_name
                settings.custom_enabled = True

                # update the device level settings
                api.telephony.devices.update_device_settings(device_id=target_device.device_id,
                                                             device_model=target_device.model,
                                                             customization=settings)

                # apply changes to device
                api.telephony.devices.apply_changes(device_id=target_device.device_id)

        """
        params = {'model': device_model}
        if org_id:
            params['orgId'] = org_id
        url = self.ep(f'devices/{device_id}/settings')
        body = customization.model_dump_json(include={'customizations', 'custom_enabled'})
        self.put(url=url, params=params, data=body)

    def validate_macs(self, macs: list[str], org_id: str = None) -> MACValidationResponse:
        """
        Validate a list of MAC addresses.

        Validating this list requires a full or read-only administrator auth token with a scope
        of spark-admin:telephony_config_write.

        :param macs: MAC addresses to be validated.
        :type macs: list[str]
        :param org_id: Validate the mac address(es) for this organization.
        :type org_id: str
        :return: validation response
        :rtype: :class:`MACValidationResponse`
        """
        params = org_id and {'orgId': org_id} or None
        url = self.ep('devices/actions/validateMacs/invoke')
        data = self.post(url=url, params=params, json={'macs': macs})
        return MACValidationResponse.model_validate(data)

    def create_line_key_template(self, template: LineKeyTemplate,
                                 org_id: str = None) -> str:
        """
        Create a Line Key Template

        Create a Line Key Template in this organization.

        Line Keys, also known as Programmable Line Keys (PLK) are the keys found on either sides of a typical desk phone
        display.
        A Line Key Template is a definition of actions that will be performed by each of the Line Keys for a particular
        device model.
        This API allows customers to create a Line Key Template for a device model.

        Creating a Line Key Template requires a full administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param template: Line key template to create
        :type template: LineKeyTemplate
        :param org_id: id of organization to create the line key template in
        :type org_id: str
        :return: id of new line key template
        :rtype: str
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = template.create_or_update()
        url = self.ep('devices/lineKeyTemplates')
        data = super().post(url, params=params, json=body)
        r = data['id']
        return r

    def list_line_key_templates(self, org_id: str = None) -> list[LineKeyTemplate]:
        """
        Read the list of Line Key Templates

        List all Line Key Templates available for this organization.

        Line Keys, also known as Programmable Line Keys (PLK) are the keys found on either sides of a typical desk phone
        display.
        A Line Key Template is a definition of actions that will be performed by each of the Line Keys for a particular
        device model.
        This API allows users to retrieve the list of Line Key Templates that are available for the organization.

        Retrieving this list requires a full, user or read-only administrator or location administrator auth token with
        a scope of `spark-admin:telephony_config_read`.

        :param org_id: List line key templates for this organization.
        :type org_id: str
        :rtype: list[LineKeyTemplate]
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep('devices/lineKeyTemplates')
        data = super().get(url, params=params)
        r = TypeAdapter(list[LineKeyTemplate]).validate_python(data['lineKeyTemplates'])
        return r

    def line_key_template_details(self, template_id: str, org_id: str = None) -> LineKeyTemplate:
        """
        Get details of a Line Key Template

        Get detailed information about a Line Key Template by template ID in an organization.

        Line Keys, also known as Programmable Line Keys (PLK) are the keys found on either sides of a typical desk phone
        display.
        A Line Key Template is a definition of actions that will be performed by each of the Line Keys for a particular
        device model.
        This API allows users to retrieve a line key template by its ID in an organization.

        Retrieving a line key template requires a full, user or read-only administrator auth token with a scope of
        `spark-admin:telephony_config_read`.

        :param template_id: Get line key template for this template ID.
        :type template_id: str
        :param org_id: Retrieve a line key template for this organization.
        :type org_id: str
        :rtype: :class:`GetLineKeyTemplateResponse`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'devices/lineKeyTemplates/{template_id}')
        data = super().get(url, params=params)
        r = LineKeyTemplate.model_validate(data)
        return r

    def modify_line_key_template(self, template: LineKeyTemplate, org_id: str = None):
        """
        Modify a Line Key Template

        Modify a line key template by its template ID in an organization.

        Line Keys, also known as Programmable Line Keys (PLK) are the keys found on either sides of a typical desk phone
        display.
        A Line Key Template is a definition of actions that will be performed by each of the Line Keys for a particular
        device model.
        This API allows users to modify an existing Line Key Template by its ID in an organization.

        Modifying an existing line key template requires a full administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param template: new line key template settings
        :type template: LineKeyTemplate
        :param org_id: Modify a line key template for this organization.
        :type org_id: str
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'devices/lineKeyTemplates/{template.id}')
        super().put(url, params=params, json=template.create_or_update())

    def delete_line_key_template(self, template_id: str, org_id: str = None):
        """
        Delete a Line Key Template

        Delete a Line Key Template by its template ID in an organization.

        Line Keys, also known as Programmable Line Keys (PLK) are the keys found on either sides of a typical desk phone
        display.
        A Line Key Template is a definition of actions that will be performed by each of the Line Keys for a particular
        device model.
        This API allows users to delete an existing Line Key Templates by its ID in an organization.

        Deleting an existing line key template requires a full administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param template_id: Delete line key template with this template ID.
        :type template_id: str
        :param org_id: Delete a line key template for this organization.
        :type org_id: str
        :rtype: None
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'devices/lineKeyTemplates/{template_id}')
        super().delete(url, params=params)

    def preview_apply_line_key_template(self, action: ApplyLineKeyTemplateAction, template_id: str = None,
                                        location_ids: list[str] = None,
                                        exclude_devices_with_custom_layout: bool = None,
                                        include_device_tags: list[str] = None, exclude_device_tags: list[str] = None,
                                        advisory_types: LineKeyTemplateAdvisoryTypes = None,
                                        org_id: str = None) -> int:
        """
        Preview Apply Line Key Template

        Preview the number of devices that will be affected by the application of a Line Key Template or when resetting
        devices to their factory Line Key settings.

        Line Keys, also known as Programmable Line Keys (PLK) are the keys found on either sides of a typical desk phone
        display.
        A Line Key Template is a definition of actions that will be performed by each of the Line Keys for a particular
        device model.
        This API allows users to preview the number of devices that will be affected if a customer were to apply a Line
        Key Template or apply factory default Line Key settings to devices.

        Retrieving the number of devices affected requires a full administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param action: Line key Template action to perform.
        :type action: PostApplyLineKeyTemplateRequestAction
        :param template_id: `templateId` is required for `APPLY_TEMPLATE` action.
        :type template_id: str
        :param location_ids: Used to search for devices only in the given locations.
        :type location_ids: list[str]
        :param exclude_devices_with_custom_layout: Indicates whether to exclude devices with custom layout.
        :type exclude_devices_with_custom_layout: bool
        :param include_device_tags: Include devices only with these tags.
        :type include_device_tags: list[str]
        :param exclude_device_tags: Exclude devices with these tags.
        :type exclude_device_tags: list[str]
        :param advisory_types: Refine search with advisories.
        :type advisory_types: LineKeyTemplateAdvisoryTypes
        :param org_id: Preview Line Key Template for this organization.
        :type org_id: str
        :rtype: int
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = dict()
        body['action'] = enum_str(action)
        if template_id is not None:
            body['templateId'] = template_id
        if location_ids is not None:
            body['locationIds'] = location_ids
        if exclude_devices_with_custom_layout is not None:
            body['excludeDevicesWithCustomLayout'] = exclude_devices_with_custom_layout
        if include_device_tags is not None:
            body['includeDeviceTags'] = include_device_tags
        if exclude_device_tags is not None:
            body['excludeDeviceTags'] = exclude_device_tags
        if advisory_types is not None:
            body['advisoryTypes'] = advisory_types.model_dump(mode='json', by_alias=True, exclude_none=True)
        url = self.ep('devices/actions/previewApplyLineKeyTemplate/invoke')
        data = super().post(url, params=params, json=body)
        r = data['deviceCount']
        return r

    def get_device_layout(self, device_id: str, org_id: str = None) -> DeviceLayout:
        """
        Get Device Layout by Device ID

        Get layout information of a device by device ID in an organization.

        Device layout customizes a user’s programmable line keys (PLK) on the phone and any attached Key Expansion
        Modules (KEM) with the existing configured line members and the user’s monitoring list.

        This API requires a full or location administrator auth token with a scope
        of `spark-admin:telephony_config_read`.

        :param device_id: Get device layout for this device ID.
        :type device_id: str
        :param org_id: Retrieve a device layout for the device in this organization.
        :type org_id: str
        :rtype: :class:`DeviceLayout`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'devices/{device_id}/layout')
        data = super().get(url, params=params)
        r = DeviceLayout.model_validate(data)
        return r

    def modify_device_layout(self, device_id: str, layout: DeviceLayout,
                             org_id: str = None):
        """
        Modify Device Layout by Device ID

        Modify the layout of a device by device ID in an organization.

        Device layout customizes a user’s programmable line keys (PLK) on the phone and any attached Key Expansion
        Modules (KEM) with the existing configured line members and the user’s monitoring list.

        This API requires a full or location administrator auth token with a scope
        of `spark-admin:telephony_config_write`.

        :param device_id: Modify device layout for this device ID.
        :type device_id: str
        :param layout: New layout
        :param org_id: Modify a device layout for the device in this organization.
        :type org_id: str
        :rtype: None
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = layout.update()
        url = self.ep(f'devices/{device_id}/layout')
        super().put(url, params=params, json=body)

    def get_person_device_settings(self, person_id: str, org_id: str = None) -> DeviceSettings:
        """
        Get Device Settings for a Person

        Device settings list the compression settings for a person.

        Device settings customize a device's behavior and performance. The compression field optimizes call quality for
        inbound and outbound calls.

        This API requires a full, location, user, or read-only administrator auth token with a scope of
        `spark-admin:telephony_config_read`.

        :param person_id: ID of the person to retrieve device settings.
        :type person_id: str
        :param org_id: Retrieves the device settings for a person in this organization.
        :type org_id: str
        :rtype: Compression
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'people/{person_id}/devices/settings')
        data = super().get(url, params=params)
        r = DeviceSettings.model_validate(data)
        return r

    def update_person_device_settings(self, person_id: str, settings: DeviceSettings, org_id: str = None):
        """
        Update Device Settings for a Person

        Update device settings modifies the compression settings for a person.

        Device settings customize a device's behavior and performance. The compression field optimizes call quality for
        inbound and outbound calls.

        This API requires a full, location, or user administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param person_id: ID of the person to update device settings.
        :type person_id: str
        :param settings: New device settings
        :type settings: DeviceSettings
        :param org_id: Modify device settings for a person in this organization.
        :type org_id: str
        :rtype: None
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = settings.model_dump(mode='json', exclude_none=True, by_alias=True)
        url = self.ep(f'people/{person_id}/devices/settings')
        super().put(url, params=params, json=body)

    def get_workspace_device_settings(self, workspace_id: str, org_id: str = None) -> DeviceSettings:
        """
        Get Device Settings for a Workspace

        Device settings list the compression settings for a workspace.

        Device settings customize a device's behavior and performance. The compression field optimizes call quality for
        inbound and outbound calls.

        This API requires a full, location, user, or read-only administrator auth token with a scope of
        `spark-admin:telephony_config_read`.

        :param workspace_id: ID of the workspace for which to retrieve device settings.
        :type workspace_id: str
        :param org_id: Retrieves the device settings for a workspace in this organization.
        :type org_id: str
        :rtype: Compression
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'workspaces/{workspace_id}/devices/settings')
        data = super().get(url, params=params)
        r = DeviceSettings.model_validate(data)
        return r

    def update_workspace_device_settings(self, workspace_id: str, settings: DeviceSettings, org_id: str = None):
        """
        Update Device Settings for a Workspace

        Update device settings modifies the compression settings for a workspace.

        Device settings customize a device's behavior and performance. The compression field optimizes call quality for
        inbound and outbound calls.

        This API requires a full, location, or user administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param workspace_id: ID of the workspace for which to update device settings.
        :type workspace_id: str
        :param settings: New device settings
        :type settings: DeviceSettings
        :param org_id: Modify the device settings for a workspace in this organization.
        :type org_id: str
        :rtype: None
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = settings.model_dump(mode='json', exclude_none=True, by_alias=True)
        url = self.ep(f'workspaces/{workspace_id}/devices/settings')
        super().put(url, params=params, json=body)

    def list_background_images(self, org_id: str = None) -> BackgroundImages:
        """
        Read the List of Background Images

        Gets the list of device background images for an organization.

        Webex Calling supports the upload of up to 100 background image files for each org. These image files can then
        be referenced by MPP phones in that org for use as their background image.

        Retrieving this list requires a full, device, or read-only administrator auth token with a scope of
        `spark-admin:telephony_config_read`.

        :param org_id: Retrieves the list of images in this organization.
        :type org_id: str
        :rtype: :class:`BackgroundImages`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep('devices/backgroundImages')
        data = super().get(url, params=params)
        r = BackgroundImages.model_validate(data)
        return r

    def upload_background_image(self, device_id: str, file: Union[BufferedReader, str], file_name: str = None,
                                org_id: str = None) -> BackgroundImage:
        """
        Upload a Device Background Image

        Configure a device's background image by uploading an image with file format, `.jpeg` or `.png`, encoded image
        file. Maximum image file size allowed to upload is 625 KB.

        The request must be a multipart/form-data request rather than JSON, using the image/jpeg or image/png
        content-type.

        Webex Calling supports the upload of up to 100 background image files for each org. These image files can then
        be referenced by MPP phones in that org for use as their background image.

        Uploading a device background image requires a full or device administrator auth token with a scope
        of `spark-admin:telephony_config_write`.

        :param device_id: Unique identifier for the device.
        :type device_id: str
        :param file: the file to be uploaded, can be a path to a file or a buffered reader (opened file); if a
            reader referring to an open file is passed then make sure to open the file as binary b/c otherwise the
            content length might be calculated wrong
        :type file: Union[BufferedReader, str]
        :param file_name: filename for the content. Only required if content is a reader
        :type file_name: str
        :param org_id: Uploads the image in this organization.
        :type org_id: str
        :rtype: :class:`BackgroundImage`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'devices/{device_id}/actions/backgroundImageUpload/invoke')
        if isinstance(file, str):
            file_name = file_name or os.path.basename(file)
            file = open(file, mode='rb')
            must_close = True
        else:
            must_close = False
            # an existing reader
            if not file_name:
                raise ValueError('file_name is required')
        encoder = MultipartEncoder({'fileName': file_name,
                                    'file': (file_name, file, f'image/{file_name.split(".")[-1].lower()}')})
        try:
            data = super().post(url, data=encoder, headers={'Content-Type': encoder.content_type},
                                params=params)
        finally:
            if must_close:
                file.close()
        r = BackgroundImage.model_validate(data)
        return r

    def delete_background_images(self, background_images: list[DeleteImageRequestObject],
                                 org_id: str = None) -> DeleteDeviceBackgroundImagesResponse:
        """
        Delete Device Background Images

        Delete the list of designated device background images for an organization. Maximum is 10 images per request.

        Deleting a device background image requires a full or device administrator auth token with a scope of
        `spark-admin:telephony_config_write`.

        :param background_images: Array of images to be deleted.
        :type background_images: list[DeleteImageRequestObject]
        :param org_id: Deletes the list of images in this organization.
        :type org_id: str
        :rtype: :class:`DeleteDeviceBackgroundImagesResponse`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        body = dict()
        body['backgroundImages'] = TypeAdapter(list[DeleteImageRequestObject]).dump_python(background_images,
                                                                                           mode='json', by_alias=True,
                                                                                           exclude_none=True)
        url = self.ep('devices/backgroundImages')
        data = super().delete(url, params=params, json=body)
        r = DeleteDeviceBackgroundImagesResponse.model_validate(data)
        return r

    def user_devices_count(self, person_id: str, org_id: str = None) -> UserDeviceCount:
        """
        Get User Devices Count

        Get the total device and application count for a person.

        The device count can be used to determine if more devices can be added for users with a device count limit. For
        example, users with standard calling licenses can only have one physical device.

        This requires a full or read-only administrator or location administrator auth token with a scope of
        `spark-admin:telephony_config_read`.

        :param person_id: Person for whom to retrieve the device count.
        :type person_id: str
        :param org_id: Organization to which the person belongs.
        :type org_id: str
        :rtype: :class:`UserDeviceCount`
        """
        params = {}
        if org_id is not None:
            params['orgId'] = org_id
        url = self.ep(f'people/{person_id}/devices/count')
        data = super().get(url, params=params)
        r = UserDeviceCount.model_validate(data)
        return r
