"""Config flow for Midea Air Appliance (Local) integration."""
from __future__ import annotations

from ipaddress import IPv4Address, IPv4Network
import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import (
    ATTR_ID,
    ATTR_NAME,
    CONF_API_VERSION,
    CONF_BROADCAST_ADDRESS,
    CONF_DEVICES,
    CONF_DISCOVERY,
    CONF_ID,
    CONF_INCLUDE,
    CONF_IP_ADDRESS,
    CONF_NAME,
    CONF_PASSWORD,
    CONF_SCAN_INTERVAL,
    CONF_TOKEN,
    CONF_TTL,
    CONF_TYPE,
    CONF_UNIQUE_ID,
    CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowHandler, FlowResult
from homeassistant.helpers import config_validation as cv
import voluptuous as vol

from midea_beautiful.cloud import MideaCloud
from midea_beautiful.exceptions import (
    AuthenticationError,
    CloudAuthenticationError,
    CloudError,
    MideaError,
    MideaNetworkError,
    ProtocolError,
    RetryLaterError,
)
from midea_beautiful.lan import LanDevice
from midea_beautiful.midea import (
    APPLIANCE_TYPE_DEHUMIDIFIER,
    SUPPORTED_APPS,
)

from custom_components.midea_dehumidifier_lan import Hub
from custom_components.midea_dehumidifier_lan.const import (
    NAME,
    CURRENT_CONFIG_VERSION,
    SUPPORTED_APPLIANCES,
    CONF_ADVANCED_SETTINGS,
    CONF_DEBUG,
    CONF_MOBILE_APP,
    CONF_TOKEN_KEY,
    DEFAULT_APP,
    DEFAULT_DISCOVERY_MODE,
    DEFAULT_PASSWORD,
    DEFAULT_SCAN_INTERVAL,
    DEFAULT_TTL,
    DEFAULT_USERNAME,
    DISCOVERY_CLOUD,
    DISCOVERY_IGNORE,
    DISCOVERY_LAN,
    DISCOVERY_MODE_LABELS,
    DISCOVERY_WAIT,
    DOMAIN,
    LOCAL_BROADCAST,
    UNKNOWN_IP,
)
from custom_components.midea_dehumidifier_lan.util import (
    MideaClient,
    RedactedConf,
    address_ok,
    supported_appliance,
)

_LOGGER = logging.getLogger(__name__)


def _appliance_schema(  # pylint: disable=too-many-arguments
    name: str,
    address: str = UNKNOWN_IP,
    ttl: int = DEFAULT_TTL,
    token: str = "",
    token_key: str = "",
    discovery_mode=DISCOVERY_WAIT,
) -> vol.Schema:
    return vol.Schema(
        {
            vol.Optional(CONF_DISCOVERY, default=str(discovery_mode)): vol.In(
                DISCOVERY_MODE_LABELS
            ),
            vol.Optional(
                CONF_IP_ADDRESS,
                default=address or UNKNOWN_IP,
            ): cv.string,
            vol.Required(CONF_NAME, default=name): cv.string,
            vol.Required(
                CONF_TTL,
                msg="Test",
                default=ttl,
                description={"suffix": "minutes"},
            ): cv.positive_int,
            vol.Optional(CONF_TOKEN, default=token or ""): cv.string,
            vol.Optional(CONF_TOKEN_KEY, default=token_key or ""): cv.string,
        }
    )


# pylint: disable=too-many-arguments
def _advanced_settings_schema(
    username: str = "",
    password: str = "",
    app: str = DEFAULT_APP,
    broadcast_address: str = "",
    appliances: list[str] = None,
    debug: bool = False,
) -> vol.Schema:
    appliances = appliances or [APPLIANCE_TYPE_DEHUMIDIFIER]
    return vol.Schema(
        {
            vol.Required(CONF_USERNAME, default=username): cv.string,
            vol.Required(CONF_PASSWORD, default=password): cv.string,
            vol.Optional(CONF_MOBILE_APP, default=app): vol.In(SUPPORTED_APPS.keys()),
            vol.Optional(CONF_BROADCAST_ADDRESS, default=broadcast_address): cv.string,
            vol.Required(
                CONF_SCAN_INTERVAL,
                msg="Test",
                default=DEFAULT_SCAN_INTERVAL,
                description={"suffix": "minutes"},
            ): cv.positive_int,
            vol.Required(CONF_INCLUDE, default=appliances): vol.All(
                cv.multi_select(SUPPORTED_APPLIANCES),
                vol.Length(min=1, msg="Must select at least one appliance category"),
            ),
            vol.Required(CONF_DEBUG, default=debug): bool,
        }
    )


def _reauth_schema(
    username: str,
    password: str,
) -> vol.Schema:
    return vol.Schema(
        {
            vol.Required(CONF_USERNAME, default=username): cv.string,
            vol.Required(CONF_PASSWORD, default=password): cv.string,
        }
    )


def _user_schema(username: str, password: str, app: str) -> vol.Schema:

    return vol.Schema(
        {
            vol.Required(CONF_USERNAME, default=username): cv.string,
            vol.Required(CONF_PASSWORD, default=password): cv.string,
            vol.Optional(CONF_MOBILE_APP, default=app): vol.In(SUPPORTED_APPS.keys()),
            vol.Required(CONF_ADVANCED_SETTINGS, default=False): bool,
        }
    )


# pylint: disable=too-many-instance-attributes
class _MideaFlow(FlowHandler):
    """Base class for Midea data flows"""

    def __init__(self) -> None:
        super().__init__()
        self.appliance_idx = -1
        self.appliances: list[LanDevice] = []
        self._client: MideaClient | None = None
        self.cloud: MideaCloud | None = None  # type: ignore
        self.conf = {}
        self.config_entry: ConfigEntry | None = None
        self.devices_conf: list[dict[str, Any]] = []
        self.discovered_appliances: list[LanDevice | None] = []
        self.error_cause: str = ""
        self.errors: dict[str, Any] = {}
        self.indexes_to_process = []

    @property
    def client(self) -> MideaClient:
        """Returns instance of MideaClient."""
        if not self._client:
            self._client = MideaClient(self.hass)
        return self._client

    def _process_exception(self: _MideaFlow, ex: Exception) -> None:
        if isinstance(ex, _FlowException):
            _LOGGER.warning(
                "Caught flow exception during appliance step %s", ex, exc_info=True
            )
            self.error_cause = str(ex.cause)
            self.errors["base"] = ex.message
        elif isinstance(ex, CloudAuthenticationError):
            self.error_cause = f"{ex.error_code} - {ex.message}"
            self.errors["base"] = "invalid_auth"
        elif isinstance(ex, CloudError):
            self.error_cause = f"{ex.error_code} - {ex.message}"
            self.errors["base"] = "midea_client"
        elif isinstance(ex, RetryLaterError):
            self.error_cause = f"{ex.error_code} - {ex.message}"
            self.errors["base"] = "retry_later"
        elif isinstance(ex, MideaError):
            self.error_cause = f"{ex.message}"
            self.errors["base"] = "midea_client"
        else:
            raise ex

    def _connect_to_cloud(self: _MideaFlow, extra_conf: dict[str, Any] = None) -> None:
        """Validates that cloud credentials are valid"""
        cfg = self.conf | (extra_conf or {})
        try:
            self.cloud = self.client.connect_to_cloud(cfg)
        except MideaError as ex:
            raise _FlowException("no_cloud", str(ex)) from ex

    def _validate_appliance(
        self: _MideaFlow, appliance: LanDevice, device_conf: dict
    ) -> LanDevice | None:
        """
        Validates that appliance configuration is correct and matches physical
        device
        """
        discovery_mode = device_conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE)
        if discovery_mode == DISCOVERY_IGNORE:
            _LOGGER.debug("Ignoring appliance %s", appliance)
            return None
        if discovery_mode == DISCOVERY_WAIT:
            _LOGGER.debug(
                "Attempt to discover appliance %s will be made later",
                appliance,
            )
            return None
        try:
            if discovery_mode == DISCOVERY_CLOUD:
                discovered = self.client.appliance_state(
                    appliance_id=appliance.appliance_id,
                    cloud=self.cloud,
                    use_cloud=True,
                )
            else:  # DISCOVERY_LAN
                ip_address = appliance.address
                if not address_ok(ip_address):
                    raise _FlowException("invalid_ip_address", ip_address)
                try:
                    IPv4Address(ip_address)
                except Exception as ex:
                    _LOGGER.debug("Invalid appliance address %s: %s", ip_address, ex)
                    raise _FlowException("invalid_ip_address", ip_address) from ex
                discovered = self.client.appliance_state(
                    address=ip_address, cloud=self.cloud
                )
        except ProtocolError as ex:
            raise _FlowException("connection_error", str(ex)) from ex
        except AuthenticationError as ex:
            raise _FlowException("invalid_auth", str(ex)) from ex
        except MideaNetworkError as ex:
            raise _FlowException("cannot_connect", str(ex)) from ex
        except MideaError as ex:
            raise _FlowException("not_discovered", str(ex)) from ex
        if discovered is None:
            raise _FlowException("not_discovered", appliance.address)
        return discovered

    async def _async_add_entry(self: _MideaFlow) -> FlowResult:
        supported_devices_conf = []
        for i, appliance in enumerate(self.appliances):
            if not supported_appliance(self.conf, appliance):
                continue
            device_conf = self.devices_conf[i]

            if device_conf.get(CONF_DISCOVERY) != DISCOVERY_IGNORE:
                device_conf |= {
                    CONF_API_VERSION: appliance.version,
                    CONF_ID: appliance.appliance_id,
                    CONF_IP_ADDRESS: (
                        appliance.address or device_conf[CONF_IP_ADDRESS] or UNKNOWN_IP
                    ),
                    CONF_NAME: appliance.name,
                    CONF_TOKEN_KEY: appliance.key,
                    CONF_TOKEN: appliance.token,
                    CONF_TYPE: appliance.type,
                    CONF_UNIQUE_ID: appliance.serial_number,
                }
                suggested_discovery = (
                    DISCOVERY_LAN
                    if address_ok(device_conf[CONF_IP_ADDRESS])
                    else DISCOVERY_WAIT
                )
                device_conf.get(CONF_DISCOVERY, suggested_discovery)
                supported_devices_conf.append(device_conf)
        self.devices_conf = supported_devices_conf
        self.conf[CONF_DEVICES] = self.devices_conf

        # Remove not used elements
        self.conf.pop(CONF_ADVANCED_SETTINGS, None)
        if self.config_entry:
            _LOGGER.debug("Updating configuration data %s", RedactedConf(self.conf))
            self.hass.config_entries.async_update_entry(
                entry=self.config_entry, data=self.conf
            )
            # Reload the config entry otherwise devices will remain unavailable
            self.hass.async_create_task(
                self.hass.config_entries.async_reload(self.config_entry.entry_id)
            )

        if not self.devices_conf:
            _LOGGER.debug("No configured appliances %s", RedactedConf(self.conf))
            return self.async_abort(reason="no_configured_devices")
        _LOGGER.debug("Creating configuration data %s", RedactedConf(self.conf))
        return self.async_create_entry(title=NAME, data=self.conf)

    async def _async_step_appliance(  # pylint: disable=too-many-locals
        self: _MideaFlow,
        step_id: str,
        user_input: dict[str, Any] | None = None,
    ) -> FlowResult:
        """Manage an appliances"""

        self.errors.clear()
        self.error_cause = ""
        appliance = self.appliances[self.appliance_idx]
        device_conf = self.devices_conf[self.appliance_idx]
        discovery_mode = device_conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE)
        ttl = device_conf.get(CONF_TTL, DEFAULT_TTL)
        ip_address = appliance.address or UNKNOWN_IP
        if user_input is not None:
            try:

                ip_address = user_input.get(
                    CONF_IP_ADDRESS, device_conf.get(CONF_IP_ADDRESS, UNKNOWN_IP)
                )
                self._check_ip_address_unique(ip_address)

                discovery_mode = user_input.get(CONF_DISCOVERY, discovery_mode)
                if discovery_mode not in [
                    DISCOVERY_WAIT,
                    DISCOVERY_LAN,
                    DISCOVERY_IGNORE,
                    DISCOVERY_CLOUD,
                ]:
                    discovery_mode = (
                        DISCOVERY_LAN if address_ok(ip_address) else DISCOVERY_CLOUD
                    )
                device_conf[CONF_DISCOVERY] = discovery_mode
                device_conf[CONF_TTL] = user_input.get(CONF_TTL, ttl)
                appliance.address = ip_address
                appliance.name = user_input.get(CONF_NAME, appliance.name)
                appliance.token = user_input.get(CONF_TOKEN, "")
                appliance.key = user_input.get(CONF_TOKEN_KEY, "")

                if not self.cloud:
                    await self.hass.async_add_executor_job(self._connect_to_cloud)

                discovered = await self.hass.async_add_executor_job(
                    self._validate_appliance,
                    appliance,
                    device_conf,
                )
                self.discovered_appliances[self.appliance_idx] = discovered

                if not self.indexes_to_process:
                    self._update_appliances_after_flow()

                    return await self._async_add_entry()

                self.appliance_idx = self.indexes_to_process.pop(0)
                appliance = self.appliances[self.appliance_idx]
                device_conf = self.devices_conf[self.appliance_idx]
                ip_address = appliance.address or UNKNOWN_IP
                user_input = None
                discovery_mode = DEFAULT_DISCOVERY_MODE
                ttl = DEFAULT_TTL

            except Exception as ex:  # pylint: disable=broad-except
                self._process_exception(ex)

        name = appliance.name
        extra = {
            "index": str(self.appliance_idx + 1),
            "count": str(len(self.appliances)),
            "serial_number": appliance.serial_number,
        }
        placeholders = self._placeholders(appliance, extra)
        schema_arg = {
            "name": name,
            "address": device_conf.get(CONF_IP_ADDRESS, ip_address),
            "token": device_conf.get(CONF_TOKEN, appliance.token),
            "token_key": device_conf.get(CONF_TOKEN_KEY, appliance.key),
            "ttl": device_conf.get(CONF_TTL, ttl),
            "discovery_mode": device_conf.get(CONF_DISCOVERY, discovery_mode),
        }
        schema = _appliance_schema(**schema_arg)
        return self.async_show_form(
            step_id=step_id,
            data_schema=schema,
            description_placeholders=placeholders,
            errors=self.errors,
            last_step=len(self.indexes_to_process) == 0,
        )

    def _check_ip_address_unique(self, ip_address) -> None:
        if address_ok(ip_address):
            for i in range(self.appliance_idx):
                if (
                    self.devices_conf[i].get(CONF_IP_ADDRESS) == ip_address
                    or ip_address == self.appliances[i].address
                ):
                    raise _FlowException(
                        "duplicate_ip_provided", self.appliances[i].name
                    )

    def _update_appliances_after_flow(self) -> None:
        for i, discovered in enumerate(self.discovered_appliances):
            if discovered:
                old_address = self.appliances[i].address
                self.appliances[i].update(discovered)
                if not discovered.address:
                    self.appliances[i].address = old_address

    def _placeholders(
        self: _MideaFlow, appliance: LanDevice = None, extra: dict[str, str] = None
    ) -> dict[str, str]:
        extra = extra or {}
        placeholders = {
            "cause": self.error_cause or "",
            **extra,
        }
        if appliance:
            placeholders[ATTR_ID] = (
                appliance.serial_number or f"{appliance.appliance_id} (Missing S/N)"
            )
            placeholders[ATTR_NAME] = appliance.name

        return placeholders


def _get_broadcast_addresses(user_input: dict[str, Any]) -> list[str]:
    address_entry = str(user_input.get(CONF_BROADCAST_ADDRESS, ""))
    addresses = [LOCAL_BROADCAST]
    specified_addresses = [
        addr.strip() for addr in address_entry.split(",") if addr.strip()
    ]
    for addr in specified_addresses:
        _LOGGER.debug("Trying IPv4 %s", addr)
        try:
            IPv4Network(addr)
            addresses.append(addr)
        except ValueError as ex:
            raise _FlowException("invalid_ip_range", str(ex)) from ex
        except Exception as ex:
            _LOGGER.debug("Invalid IP address %s", addr, exc_info=True)
            raise _FlowException("invalid_ip_range", addr) from ex
    return addresses


class _FlowException(Exception):
    def __init__(self, message, cause: str = None) -> None:
        super().__init__()
        self.message = message
        self.cause = cause


# pylint: disable=too-many-instance-attributes
class MideaConfigFlow(ConfigFlow, _MideaFlow, domain=DOMAIN):
    """Configuration flow for Midea dehumidifiers on local network uses
    discovery based on Midea cloud, so it first requires credentials for it.
    If some appliances are registered in the cloud, but not discovered, configuration
    flow will prompt for additional information.
    """

    VERSION = CURRENT_CONFIG_VERSION

    def __init__(self) -> None:
        super().__init__()
        self.discovered_appliances: list[LanDevice | None] = []
        self.appliances: list[LanDevice] = []
        self.config_entry: ConfigEntry | None = None
        self.advanced_settings = False

    @staticmethod
    @callback
    def async_get_options_flow(
        config_entry: ConfigEntry,
    ) -> OptionsFlow:
        """Define the config flow to handle options."""
        return MideaOptionsFlow(config_entry)

    def _connect_and_discover(self: MideaConfigFlow) -> None:
        """Validates that cloud credentials are valid and discovers local appliances"""

        self._connect_to_cloud()
        conf_addresses = self.conf.get(CONF_BROADCAST_ADDRESS, [])
        if isinstance(conf_addresses, str):
            conf_addresses = [conf_addresses]
        addresses = [
            str(IPv4Network(addr).broadcast_address) for addr in conf_addresses
        ]
        self.appliances.clear()
        self.appliances += self.client.find_appliances(self.cloud, addresses)
        self.devices_conf = [{} for _ in self.appliances]

    async def _validate_discovery_phase(
        self, user_input: dict[str, Any] | None
    ) -> FlowResult:
        assert user_input is not None
        self.conf[CONF_USERNAME] = user_input[CONF_USERNAME]
        self.conf[CONF_PASSWORD] = user_input[CONF_PASSWORD]

        if self.advanced_settings:
            assert self.conf is not None
            self.conf[CONF_MOBILE_APP] = user_input.get(CONF_MOBILE_APP, DEFAULT_APP)
            self.conf[CONF_INCLUDE] = user_input[CONF_INCLUDE]
            self.conf[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL]
            self.conf[CONF_DEBUG] = user_input[CONF_DEBUG]
            self.conf[CONF_BROADCAST_ADDRESS] = _get_broadcast_addresses(user_input)

        else:
            self.conf[CONF_MOBILE_APP] = user_input.get(CONF_MOBILE_APP, DEFAULT_APP)
            if user_input.get(CONF_ADVANCED_SETTINGS):
                return await self.async_step_advanced_settings()

            self.conf[CONF_BROADCAST_ADDRESS] = []
            self.conf[CONF_INCLUDE] = [APPLIANCE_TYPE_DEHUMIDIFIER]
            self.conf[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL

        if self.conf.get(CONF_DEBUG, False):
            await self.client.async_debug_mode(True)
        await self.hass.async_add_executor_job(self._connect_and_discover)

        self.indexes_to_process = [
            index
            for index, appliance in enumerate(self.appliances)
            if supported_appliance(self.conf, appliance)
            and not address_ok(appliance.address)
        ]
        if self.indexes_to_process:
            self.appliance_idx = self.indexes_to_process.pop(0)
            self.discovered_appliances = [None] * len(self.devices_conf)
            return await self.async_step_unreachable_appliance()

        return await self._async_add_entry()

    async def _do_validate(self, user_input: dict[str, Any]) -> FlowResult | None:
        try:
            return await self._validate_discovery_phase(user_input)
        except Exception as ex:  # pylint: disable=broad-except
            self._process_exception(ex)
        return None

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        self.advanced_settings = False
        if self._async_current_entries():
            return self.async_abort(reason="single_instance_allowed")

        self.errors.clear()
        self.error_cause = ""

        username = DEFAULT_USERNAME
        password = <PASSWORD>
        app = DEFAULT_APP
        if user_input is not None:
            username = user_input.get(CONF_USERNAME, username)
            password = user_input.get(CONF_PASSWORD, password)
            app = user_input.get(CONF_MOBILE_APP, app)
            res = await self._do_validate(user_input)
            if res:
                return res

        return self.async_show_form(
            step_id="user",
            data_schema=_user_schema(username=username, password=password, app=app),
            description_placeholders=self._placeholders(),
            errors=self.errors,
        )

    async def async_step_advanced_settings(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Step for managing advanced settings"""
        self.errors = {}
        self.error_cause = ""
        self.advanced_settings = True
        if user_input is not None:
            if res := await self._do_validate(user_input):
                return res
        else:
            user_input = {}

        username = user_input.get(
            CONF_USERNAME, self.conf.get(CONF_USERNAME, DEFAULT_USERNAME)
        )
        password = user_input.get(
            CONF_PASSWORD, self.conf.get(CONF_PASSWORD, DEFAULT_PASSWORD)
        )
        app = user_input.get(CONF_MOBILE_APP, DEFAULT_APP)
        broadcast_addresses = user_input.get(
            CONF_BROADCAST_ADDRESS, ",".join(self.conf.get(CONF_BROADCAST_ADDRESS, []))
        )

        return self.async_show_form(
            step_id="advanced_settings",
            data_schema=_advanced_settings_schema(
                username=username,
                password=password,
                app=app,
                broadcast_address=broadcast_addresses,
            ),
            description_placeholders=self._placeholders(),
            errors=self.errors,
        )

    async def async_step_unreachable_appliance(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Manage the appliances that were not discovered automatically on LAN."""

        return await self._async_step_appliance(
            step_id="unreachable_appliance",
            user_input=user_input,
        )

    async def _async_add_entry(self) -> FlowResult:
        assert self.conf is not None
        self.config_entry = await self.async_set_unique_id(self.conf[CONF_USERNAME])
        return await super()._async_add_entry()

    async def async_step_reauth(self, config) -> FlowResult:
        """Handle reauthorization request from Abode."""
        self.conf = {**config}

        return await self.async_step_reauth_confirm()

    async def async_step_reauth_confirm(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle reauthorization flow."""
        self.errors.clear()
        password = ""
        username = self.conf.get(CONF_USERNAME, "")
        app = self.conf.get(CONF_MOBILE_APP, DEFAULT_APP)
        if user_input is not None:
            extra_conf = {
                CONF_USERNAME: user_input.get(CONF_USERNAME, ""),
                CONF_PASSWORD: user_input.get(CONF_PASSWORD, ""),
                CONF_MOBILE_APP: user_input.get(CONF_MOBILE_APP, app),
            }
            try:
                await self.hass.async_add_executor_job(
                    self._connect_to_cloud, extra_conf
                )
            except Exception as ex:  # pylint: disable=broad-except
                self._process_exception(ex)
            else:
                self.conf[CONF_USERNAME] = username
                self.conf[CONF_PASSWORD] = password
                self.conf[CONF_MOBILE_APP] = app
                return await self._async_add_entry()

        return self.async_show_form(
            step_id="reauth_confirm",
            data_schema=_reauth_schema(
                username=username,
                password=password,
            ),
            description_placeholders=self._placeholders(),
            errors=self.errors,
        )


class MideaOptionsFlow(OptionsFlow, _MideaFlow):
    """Handle Midea options flow."""

    def __init__(self, config_entry: ConfigEntry) -> None:
        """Initialize Midea options flow."""
        super().__init__()
        self.config_entry = config_entry
        self.conf = {**config_entry.data}
        self.devices_conf = self.conf.get(CONF_DEVICES, [])

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Starts options flow"""
        self._build_appliance_list()
        return await self.async_step_appliance(user_input)

    async def async_step_appliance(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Options for an appliance"""

        return await self._async_step_appliance(
            step_id="appliance",
            user_input=user_input,
        )

    def _build_appliance_list(self) -> None:
        assert self.config_entry
        hub: Hub = self.hass.data[DOMAIN][self.config_entry.entry_id]
        self.appliances.clear()
        self.devices_conf = self.conf[CONF_DEVICES]
        for device in self.devices_conf:
            for coord in hub.coordinators:
                if device[CONF_UNIQUE_ID] == coord.appliance.serial_number:
                    self.appliances.append(coord.appliance)
                    break
            else:
                appliance = LanDevice(
                    appliance_id=device[CONF_ID],
                    serial_number=device[CONF_UNIQUE_ID],
                    appliance_type=device[CONF_TYPE],
                )
                appliance.name = device[CONF_NAME]
                appliance.address = device.get(CONF_IP_ADDRESS, UNKNOWN_IP)
                self.appliances.append(appliance)
        self.indexes_to_process = list(range(len(self.appliances)))
        self.appliance_idx = self.indexes_to_process.pop(0)
        self.discovered_appliances = [None] * len(self.devices_conf)

"""Config flow for Midea Air Appliance (Local) integration."""
from __future__ import annotations

from ipaddress import IPv4Address, IPv4Network
import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import (
    ATTR_ID,
    ATTR_NAME,
    CONF_API_VERSION,
    CONF_BROADCAST_ADDRESS,
    CONF_DEVICES,
    CONF_DISCOVERY,
    CONF_ID,
    CONF_INCLUDE,
    CONF_IP_ADDRESS,
    CONF_NAME,
    CONF_PASSWORD,
    CONF_SCAN_INTERVAL,
    CONF_TOKEN,
    CONF_TTL,
    CONF_TYPE,
    CONF_UNIQUE_ID,
    CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowHandler, FlowResult
from homeassistant.helpers import config_validation as cv
import voluptuous as vol

from midea_beautiful.cloud import MideaCloud
from midea_beautiful.exceptions import (
    AuthenticationError,
    CloudAuthenticationError,
    CloudError,
    MideaError,
    MideaNetworkError,
    ProtocolError,
    RetryLaterError,
)
from midea_beautiful.lan import LanDevice
from midea_beautiful.midea import (
    APPLIANCE_TYPE_DEHUMIDIFIER,
    SUPPORTED_APPS,
)

from custom_components.midea_dehumidifier_lan import Hub
from custom_components.midea_dehumidifier_lan.const import (
    NAME,
    CURRENT_CONFIG_VERSION,
    SUPPORTED_APPLIANCES,
    CONF_ADVANCED_SETTINGS,
    CONF_DEBUG,
    CONF_MOBILE_APP,
    CONF_TOKEN_KEY,
    DEFAULT_APP,
    DEFAULT_DISCOVERY_MODE,
    DEFAULT_PASSWORD,
    DEFAULT_SCAN_INTERVAL,
    DEFAULT_TTL,
    DEFAULT_USERNAME,
    DISCOVERY_CLOUD,
    DISCOVERY_IGNORE,
    DISCOVERY_LAN,
    DISCOVERY_MODE_LABELS,
    DISCOVERY_WAIT,
    DOMAIN,
    LOCAL_BROADCAST,
    UNKNOWN_IP,
)
from custom_components.midea_dehumidifier_lan.util import (
    MideaClient,
    RedactedConf,
    address_ok,
    supported_appliance,
)

_LOGGER = logging.getLogger(__name__)


def _appliance_schema(  # pylint: disable=too-many-arguments
    name: str,
    address: str = UNKNOWN_IP,
    ttl: int = DEFAULT_TTL,
    token: str = "",
    token_key: str = "",
    discovery_mode=DISCOVERY_WAIT,
) -> vol.Schema:
    return vol.Schema(
        {
            vol.Optional(CONF_DISCOVERY, default=str(discovery_mode)): vol.In(
                DISCOVERY_MODE_LABELS
            ),
            vol.Optional(
                CONF_IP_ADDRESS,
                default=address or UNKNOWN_IP,
            ): cv.string,
            vol.Required(CONF_NAME, default=name): cv.string,
            vol.Required(
                CONF_TTL,
                msg="Test",
                default=ttl,
                description={"suffix": "minutes"},
            ): cv.positive_int,
            vol.Optional(CONF_TOKEN, default=token or ""): cv.string,
            vol.Optional(CONF_TOKEN_KEY, default=token_key or ""): cv.string,
        }
    )


# pylint: disable=too-many-arguments
def _advanced_settings_schema(
    username: str = "",
    password: str = "",
    app: str = DEFAULT_APP,
    broadcast_address: str = "",
    appliances: list[str] = None,
    debug: bool = False,
) -> vol.Schema:
    appliances = appliances or [APPLIANCE_TYPE_DEHUMIDIFIER]
    return vol.Schema(
        {
            vol.Required(CONF_USERNAME, default=username): cv.string,
            vol.Required(CONF_PASSWORD, default=password): cv.string,
            vol.Optional(CONF_MOBILE_APP, default=app): vol.In(SUPPORTED_APPS.keys()),
            vol.Optional(CONF_BROADCAST_ADDRESS, default=broadcast_address): cv.string,
            vol.Required(
                CONF_SCAN_INTERVAL,
                msg="Test",
                default=DEFAULT_SCAN_INTERVAL,
                description={"suffix": "minutes"},
            ): cv.positive_int,
            vol.Required(CONF_INCLUDE, default=appliances): vol.All(
                cv.multi_select(SUPPORTED_APPLIANCES),
                vol.Length(min=1, msg="Must select at least one appliance category"),
            ),
            vol.Required(CONF_DEBUG, default=debug): bool,
        }
    )


def _reauth_schema(
    username: str,
    password: str,
) -> vol.Schema:
    return vol.Schema(
        {
            vol.Required(CONF_USERNAME, default=username): cv.string,
            vol.Required(CONF_PASSWORD, default=password): cv.string,
        }
    )


def _user_schema(username: str, password: str, app: str) -> vol.Schema:

    return vol.Schema(
        {
            vol.Required(CONF_USERNAME, default=username): cv.string,
            vol.Required(CONF_PASSWORD, default=password): cv.string,
            vol.Optional(CONF_MOBILE_APP, default=app): vol.In(SUPPORTED_APPS.keys()),
            vol.Required(CONF_ADVANCED_SETTINGS, default=False): bool,
        }
    )


# pylint: disable=too-many-instance-attributes
class _MideaFlow(FlowHandler):
    """Base class for Midea data flows"""

    def __init__(self) -> None:
        super().__init__()
        self.appliance_idx = -1
        self.appliances: list[LanDevice] = []
        self._client: MideaClient | None = None
        self.cloud: MideaCloud | None = None  # type: ignore
        self.conf = {}
        self.config_entry: ConfigEntry | None = None
        self.devices_conf: list[dict[str, Any]] = []
        self.discovered_appliances: list[LanDevice | None] = []
        self.error_cause: str = ""
        self.errors: dict[str, Any] = {}
        self.indexes_to_process = []

    @property
    def client(self) -> MideaClient:
        """Returns instance of MideaClient."""
        if not self._client:
            self._client = MideaClient(self.hass)
        return self._client

    def _process_exception(self: _MideaFlow, ex: Exception) -> None:
        if isinstance(ex, _FlowException):
            _LOGGER.warning(
                "Caught flow exception during appliance step %s", ex, exc_info=True
            )
            self.error_cause = str(ex.cause)
            self.errors["base"] = ex.message
        elif isinstance(ex, CloudAuthenticationError):
            self.error_cause = f"{ex.error_code} - {ex.message}"
            self.errors["base"] = "invalid_auth"
        elif isinstance(ex, CloudError):
            self.error_cause = f"{ex.error_code} - {ex.message}"
            self.errors["base"] = "midea_client"
        elif isinstance(ex, RetryLaterError):
            self.error_cause = f"{ex.error_code} - {ex.message}"
            self.errors["base"] = "retry_later"
        elif isinstance(ex, MideaError):
            self.error_cause = f"{ex.message}"
            self.errors["base"] = "midea_client"
        else:
            raise ex

    def _connect_to_cloud(self: _MideaFlow, extra_conf: dict[str, Any] = None) -> None:
        """Validates that cloud credentials are valid"""
        cfg = self.conf | (extra_conf or {})
        try:
            self.cloud = self.client.connect_to_cloud(cfg)
        except MideaError as ex:
            raise _FlowException("no_cloud", str(ex)) from ex

    def _validate_appliance(
        self: _MideaFlow, appliance: LanDevice, device_conf: dict
    ) -> LanDevice | None:
        """
        Validates that appliance configuration is correct and matches physical
        device
        """
        discovery_mode = device_conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE)
        if discovery_mode == DISCOVERY_IGNORE:
            _LOGGER.debug("Ignoring appliance %s", appliance)
            return None
        if discovery_mode == DISCOVERY_WAIT:
            _LOGGER.debug(
                "Attempt to discover appliance %s will be made later",
                appliance,
            )
            return None
        try:
            if discovery_mode == DISCOVERY_CLOUD:
                discovered = self.client.appliance_state(
                    appliance_id=appliance.appliance_id,
                    cloud=self.cloud,
                    use_cloud=True,
                )
            else:  # DISCOVERY_LAN
                ip_address = appliance.address
                if not address_ok(ip_address):
                    raise _FlowException("invalid_ip_address", ip_address)
                try:
                    IPv4Address(ip_address)
                except Exception as ex:
                    _LOGGER.debug("Invalid appliance address %s: %s", ip_address, ex)
                    raise _FlowException("invalid_ip_address", ip_address) from ex
                discovered = self.client.appliance_state(
                    address=ip_address, cloud=self.cloud
                )
        except ProtocolError as ex:
            raise _FlowException("connection_error", str(ex)) from ex
        except AuthenticationError as ex:
            raise _FlowException("invalid_auth", str(ex)) from ex
        except MideaNetworkError as ex:
            raise _FlowException("cannot_connect", str(ex)) from ex
        except MideaError as ex:
            raise _FlowException("not_discovered", str(ex)) from ex
        if discovered is None:
            raise _FlowException("not_discovered", appliance.address)
        return discovered

    async def _async_add_entry(self: _MideaFlow) -> FlowResult:
        supported_devices_conf = []
        for i, appliance in enumerate(self.appliances):
            if not supported_appliance(self.conf, appliance):
                continue
            device_conf = self.devices_conf[i]

            if device_conf.get(CONF_DISCOVERY) != DISCOVERY_IGNORE:
                device_conf |= {
                    CONF_API_VERSION: appliance.version,
                    CONF_ID: appliance.appliance_id,
                    CONF_IP_ADDRESS: (
                        appliance.address or device_conf[CONF_IP_ADDRESS] or UNKNOWN_IP
                    ),
                    CONF_NAME: appliance.name,
                    CONF_TOKEN_KEY: appliance.key,
                    CONF_TOKEN: appliance.token,
                    CONF_TYPE: appliance.type,
                    CONF_UNIQUE_ID: appliance.serial_number,
                }
                suggested_discovery = (
                    DISCOVERY_LAN
                    if address_ok(device_conf[CONF_IP_ADDRESS])
                    else DISCOVERY_WAIT
                )
                device_conf.get(CONF_DISCOVERY, suggested_discovery)
                supported_devices_conf.append(device_conf)
        self.devices_conf = supported_devices_conf
        self.conf[CONF_DEVICES] = self.devices_conf

        # Remove not used elements
        self.conf.pop(CONF_ADVANCED_SETTINGS, None)
        if self.config_entry:
            _LOGGER.debug("Updating configuration data %s", RedactedConf(self.conf))
            self.hass.config_entries.async_update_entry(
                entry=self.config_entry, data=self.conf
            )
            # Reload the config entry otherwise devices will remain unavailable
            self.hass.async_create_task(
                self.hass.config_entries.async_reload(self.config_entry.entry_id)
            )

        if not self.devices_conf:
            _LOGGER.debug("No configured appliances %s", RedactedConf(self.conf))
            return self.async_abort(reason="no_configured_devices")
        _LOGGER.debug("Creating configuration data %s", RedactedConf(self.conf))
        return self.async_create_entry(title=NAME, data=self.conf)

    async def _async_step_appliance(  # pylint: disable=too-many-locals
        self: _MideaFlow,
        step_id: str,
        user_input: dict[str, Any] | None = None,
    ) -> FlowResult:
        """Manage an appliances"""

        self.errors.clear()
        self.error_cause = ""
        appliance = self.appliances[self.appliance_idx]
        device_conf = self.devices_conf[self.appliance_idx]
        discovery_mode = device_conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY_MODE)
        ttl = device_conf.get(CONF_TTL, DEFAULT_TTL)
        ip_address = appliance.address or UNKNOWN_IP
        if user_input is not None:
            try:

                ip_address = user_input.get(
                    CONF_IP_ADDRESS, device_conf.get(CONF_IP_ADDRESS, UNKNOWN_IP)
                )
                self._check_ip_address_unique(ip_address)

                discovery_mode = user_input.get(CONF_DISCOVERY, discovery_mode)
                if discovery_mode not in [
                    DISCOVERY_WAIT,
                    DISCOVERY_LAN,
                    DISCOVERY_IGNORE,
                    DISCOVERY_CLOUD,
                ]:
                    discovery_mode = (
                        DISCOVERY_LAN if address_ok(ip_address) else DISCOVERY_CLOUD
                    )
                device_conf[CONF_DISCOVERY] = discovery_mode
                device_conf[CONF_TTL] = user_input.get(CONF_TTL, ttl)
                appliance.address = ip_address
                appliance.name = user_input.get(CONF_NAME, appliance.name)
                appliance.token = user_input.get(CONF_TOKEN, "")
                appliance.key = user_input.get(CONF_TOKEN_KEY, "")

                if not self.cloud:
                    await self.hass.async_add_executor_job(self._connect_to_cloud)

                discovered = await self.hass.async_add_executor_job(
                    self._validate_appliance,
                    appliance,
                    device_conf,
                )
                self.discovered_appliances[self.appliance_idx] = discovered

                if not self.indexes_to_process:
                    self._update_appliances_after_flow()

                    return await self._async_add_entry()

                self.appliance_idx = self.indexes_to_process.pop(0)
                appliance = self.appliances[self.appliance_idx]
                device_conf = self.devices_conf[self.appliance_idx]
                ip_address = appliance.address or UNKNOWN_IP
                user_input = None
                discovery_mode = DEFAULT_DISCOVERY_MODE
                ttl = DEFAULT_TTL

            except Exception as ex:  # pylint: disable=broad-except
                self._process_exception(ex)

        name = appliance.name
        extra = {
            "index": str(self.appliance_idx + 1),
            "count": str(len(self.appliances)),
            "serial_number": appliance.serial_number,
        }
        placeholders = self._placeholders(appliance, extra)
        schema_arg = {
            "name": name,
            "address": device_conf.get(CONF_IP_ADDRESS, ip_address),
            "token": device_conf.get(CONF_TOKEN, appliance.token),
            "token_key": device_conf.get(CONF_TOKEN_KEY, appliance.key),
            "ttl": device_conf.get(CONF_TTL, ttl),
            "discovery_mode": device_conf.get(CONF_DISCOVERY, discovery_mode),
        }
        schema = _appliance_schema(**schema_arg)
        return self.async_show_form(
            step_id=step_id,
            data_schema=schema,
            description_placeholders=placeholders,
            errors=self.errors,
            last_step=len(self.indexes_to_process) == 0,
        )

    def _check_ip_address_unique(self, ip_address) -> None:
        if address_ok(ip_address):
            for i in range(self.appliance_idx):
                if (
                    self.devices_conf[i].get(CONF_IP_ADDRESS) == ip_address
                    or ip_address == self.appliances[i].address
                ):
                    raise _FlowException(
                        "duplicate_ip_provided", self.appliances[i].name
                    )

    def _update_appliances_after_flow(self) -> None:
        for i, discovered in enumerate(self.discovered_appliances):
            if discovered:
                old_address = self.appliances[i].address
                self.appliances[i].update(discovered)
                if not discovered.address:
                    self.appliances[i].address = old_address

    def _placeholders(
        self: _MideaFlow, appliance: LanDevice = None, extra: dict[str, str] = None
    ) -> dict[str, str]:
        extra = extra or {}
        placeholders = {
            "cause": self.error_cause or "",
            **extra,
        }
        if appliance:
            placeholders[ATTR_ID] = (
                appliance.serial_number or f"{appliance.appliance_id} (Missing S/N)"
            )
            placeholders[ATTR_NAME] = appliance.name

        return placeholders


def _get_broadcast_addresses(user_input: dict[str, Any]) -> list[str]:
    address_entry = str(user_input.get(CONF_BROADCAST_ADDRESS, ""))
    addresses = [LOCAL_BROADCAST]
    specified_addresses = [
        addr.strip() for addr in address_entry.split(",") if addr.strip()
    ]
    for addr in specified_addresses:
        _LOGGER.debug("Trying IPv4 %s", addr)
        try:
            IPv4Network(addr)
            addresses.append(addr)
        except ValueError as ex:
            raise _FlowException("invalid_ip_range", str(ex)) from ex
        except Exception as ex:
            _LOGGER.debug("Invalid IP address %s", addr, exc_info=True)
            raise _FlowException("invalid_ip_range", addr) from ex
    return addresses


class _FlowException(Exception):
    def __init__(self, message, cause: str = None) -> None:
        super().__init__()
        self.message = message
        self.cause = cause


# pylint: disable=too-many-instance-attributes
class MideaConfigFlow(ConfigFlow, _MideaFlow, domain=DOMAIN):
    """Configuration flow for Midea dehumidifiers on local network uses
    discovery based on Midea cloud, so it first requires credentials for it.
    If some appliances are registered in the cloud, but not discovered, configuration
    flow will prompt for additional information.
    """

    VERSION = CURRENT_CONFIG_VERSION

    def __init__(self) -> None:
        super().__init__()
        self.discovered_appliances: list[LanDevice | None] = []
        self.appliances: list[LanDevice] = []
        self.config_entry: ConfigEntry | None = None
        self.advanced_settings = False

    @staticmethod
    @callback
    def async_get_options_flow(
        config_entry: ConfigEntry,
    ) -> OptionsFlow:
        """Define the config flow to handle options."""
        return MideaOptionsFlow(config_entry)

    def _connect_and_discover(self: MideaConfigFlow) -> None:
        """Validates that cloud credentials are valid and discovers local appliances"""

        self._connect_to_cloud()
        conf_addresses = self.conf.get(CONF_BROADCAST_ADDRESS, [])
        if isinstance(conf_addresses, str):
            conf_addresses = [conf_addresses]
        addresses = [
            str(IPv4Network(addr).broadcast_address) for addr in conf_addresses
        ]
        self.appliances.clear()
        self.appliances += self.client.find_appliances(self.cloud, addresses)
        self.devices_conf = [{} for _ in self.appliances]

    async def _validate_discovery_phase(
        self, user_input: dict[str, Any] | None
    ) -> FlowResult:
        assert user_input is not None
        self.conf[CONF_USERNAME] = user_input[CONF_USERNAME]
        self.conf[CONF_PASSWORD] = user_input[CONF_PASSWORD]

        if self.advanced_settings:
            assert self.conf is not None
            self.conf[CONF_MOBILE_APP] = user_input.get(CONF_MOBILE_APP, DEFAULT_APP)
            self.conf[CONF_INCLUDE] = user_input[CONF_INCLUDE]
            self.conf[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL]
            self.conf[CONF_DEBUG] = user_input[CONF_DEBUG]
            self.conf[CONF_BROADCAST_ADDRESS] = _get_broadcast_addresses(user_input)

        else:
            self.conf[CONF_MOBILE_APP] = user_input.get(CONF_MOBILE_APP, DEFAULT_APP)
            if user_input.get(CONF_ADVANCED_SETTINGS):
                return await self.async_step_advanced_settings()

            self.conf[CONF_BROADCAST_ADDRESS] = []
            self.conf[CONF_INCLUDE] = [APPLIANCE_TYPE_DEHUMIDIFIER]
            self.conf[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL

        if self.conf.get(CONF_DEBUG, False):
            await self.client.async_debug_mode(True)
        await self.hass.async_add_executor_job(self._connect_and_discover)

        self.indexes_to_process = [
            index
            for index, appliance in enumerate(self.appliances)
            if supported_appliance(self.conf, appliance)
            and not address_ok(appliance.address)
        ]
        if self.indexes_to_process:
            self.appliance_idx = self.indexes_to_process.pop(0)
            self.discovered_appliances = [None] * len(self.devices_conf)
            return await self.async_step_unreachable_appliance()

        return await self._async_add_entry()

    async def _do_validate(self, user_input: dict[str, Any]) -> FlowResult | None:
        try:
            return await self._validate_discovery_phase(user_input)
        except Exception as ex:  # pylint: disable=broad-except
            self._process_exception(ex)
        return None

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        self.advanced_settings = False
        if self._async_current_entries():
            return self.async_abort(reason="single_instance_allowed")

        self.errors.clear()
        self.error_cause = ""

        username = DEFAULT_USERNAME
        password = <PASSWORD>
        app = DEFAULT_APP
        if user_input is not None:
            username = user_input.get(CONF_USERNAME, username)
            password = user_input.get(CONF_PASSWORD, password)
            app = user_input.get(CONF_MOBILE_APP, app)
            res = await self._do_validate(user_input)
            if res:
                return res

        return self.async_show_form(
            step_id="user",
            data_schema=_user_schema(username=username, password=password, app=app),
            description_placeholders=self._placeholders(),
            errors=self.errors,
        )

    async def async_step_advanced_settings(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Step for managing advanced settings"""
        self.errors = {}
        self.error_cause = ""
        self.advanced_settings = True
        if user_input is not None:
            if res := await self._do_validate(user_input):
                return res
        else:
            user_input = {}

        username = user_input.get(
            CONF_USERNAME, self.conf.get(CONF_USERNAME, DEFAULT_USERNAME)
        )
        password = user_input.get(
            CONF_PASSWORD, self.conf.get(CONF_PASSWORD, DEFAULT_PASSWORD)
        )
        app = user_input.get(CONF_MOBILE_APP, DEFAULT_APP)
        broadcast_addresses = user_input.get(
            CONF_BROADCAST_ADDRESS, ",".join(self.conf.get(CONF_BROADCAST_ADDRESS, []))
        )

        return self.async_show_form(
            step_id="advanced_settings",
            data_schema=_advanced_settings_schema(
                username=username,
                password=password,
                app=app,
                broadcast_address=broadcast_addresses,
            ),
            description_placeholders=self._placeholders(),
            errors=self.errors,
        )

    async def async_step_unreachable_appliance(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Manage the appliances that were not discovered automatically on LAN."""

        return await self._async_step_appliance(
            step_id="unreachable_appliance",
            user_input=user_input,
        )

    async def _async_add_entry(self) -> FlowResult:
        assert self.conf is not None
        self.config_entry = await self.async_set_unique_id(self.conf[CONF_USERNAME])
        return await super()._async_add_entry()

    async def async_step_reauth(self, config) -> FlowResult:
        """Handle reauthorization request from Abode."""
        self.conf = {**config}

        return await self.async_step_reauth_confirm()

    async def async_step_reauth_confirm(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle reauthorization flow."""
        self.errors.clear()
        password = ""
        username = self.conf.get(CONF_USERNAME, "")
        app = self.conf.get(CONF_MOBILE_APP, DEFAULT_APP)
        if user_input is not None:
            extra_conf = {
                CONF_USERNAME: user_input.get(CONF_USERNAME, ""),
                CONF_PASSWORD: user_input.get(CONF_PASSWORD, ""),
                CONF_MOBILE_APP: user_input.get(CONF_MOBILE_APP, app),
            }
            try:
                await self.hass.async_add_executor_job(
                    self._connect_to_cloud, extra_conf
                )
            except Exception as ex:  # pylint: disable=broad-except
                self._process_exception(ex)
            else:
                self.conf[CONF_USERNAME] = username
                self.conf[CONF_PASSWORD] = password
                self.conf[CONF_MOBILE_APP] = app
                return await self._async_add_entry()

        return self.async_show_form(
            step_id="reauth_confirm",
            data_schema=_reauth_schema(
                username=username,
                password=password,
            ),
            description_placeholders=self._placeholders(),
            errors=self.errors,
        )


class MideaOptionsFlow(OptionsFlow, _MideaFlow):
    """Handle Midea options flow."""

    def __init__(self, config_entry: ConfigEntry) -> None:
        """Initialize Midea options flow."""
        super().__init__()
        self.config_entry = config_entry
        self.conf = {**config_entry.data}
        self.devices_conf = self.conf.get(CONF_DEVICES, [])

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Starts options flow"""
        self._build_appliance_list()
        return await self.async_step_appliance(user_input)

    async def async_step_appliance(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Options for an appliance"""

        return await self._async_step_appliance(
            step_id="appliance",
            user_input=user_input,
        )

    def _build_appliance_list(self) -> None:
        assert self.config_entry
        hub: Hub = self.hass.data[DOMAIN][self.config_entry.entry_id]
        self.appliances.clear()
        self.devices_conf = self.conf[CONF_DEVICES]
        for device in self.devices_conf:
            for coord in hub.coordinators:
                if device[CONF_UNIQUE_ID] == coord.appliance.serial_number:
                    self.appliances.append(coord.appliance)
                    break
            else:
                appliance = LanDevice(
                    appliance_id=device[CONF_ID],
                    serial_number=device[CONF_UNIQUE_ID],
                    appliance_type=device[CONF_TYPE],
                )
                appliance.name = device[CONF_NAME]
                appliance.address = device.get(CONF_IP_ADDRESS, UNKNOWN_IP)
                self.appliances.append(appliance)
        self.indexes_to_process = list(range(len(self.appliances)))
        self.appliance_idx = self.indexes_to_process.pop(0)
        self.discovered_appliances = [None] * len(self.devices_conf)

Config flow for Midea Air Appliance (Local) integration. # pylint: disable=too-many-arguments # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes Base class for Midea data flows # type: ignore Returns instance of MideaClient. Validates that cloud credentials are valid Validates that appliance configuration is correct and matches physical         device # DISCOVERY_LAN # Remove not used elements # Reload the config entry otherwise devices will remain unavailable # pylint: disable=too-many-locals Manage an appliances # pylint: disable=broad-except # pylint: disable=too-many-instance-attributes Configuration flow for Midea dehumidifiers on local network uses     discovery based on Midea cloud, so it first requires credentials for it.     If some appliances are registered in the cloud, but not discovered, configuration     flow will prompt for additional information. Define the config flow to handle options. Validates that cloud credentials are valid and discovers local appliances # pylint: disable=broad-except Step for managing advanced settings Manage the appliances that were not discovered automatically on LAN. Handle reauthorization request from Abode. Handle reauthorization flow. # pylint: disable=broad-except Handle Midea options flow. Initialize Midea options flow. Starts options flow Options for an appliance