"""Config flow to configure Heos."""

from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

from pyheos import (
    CommandAuthenticationError,
    ConnectionState,
    Heos,
    HeosError,
    HeosOptions,
)
import voluptuous as vol

from homeassistant.config_entries import (
    SOURCE_IGNORE,
    ConfigFlow,
    ConfigFlowResult,
    OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import selector
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo

from .const import DOMAIN, ENTRY_TITLE
from .coordinator import HeosConfigEntry

_LOGGER = logging.getLogger(__name__)

AUTH_SCHEMA = vol.Schema(
    {
        vol.Optional(CONF_USERNAME): selector.TextSelector(),
        vol.Optional(CONF_PASSWORD): selector.TextSelector(
            selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
        ),
    }
)


async def _validate_host(host: str, errors: dict[str, str]) -> bool:
    """Validate host is reachable, return True, otherwise populate errors and return False."""
    heos = Heos(HeosOptions(host, events=False, heart_beat=False))
    try:
        await heos.connect()
    except HeosError:
        errors[CONF_HOST] = "cannot_connect"
        return False
    finally:
        await heos.disconnect()
    return True


async def _validate_auth(
    user_input: dict[str, str], entry: HeosConfigEntry, errors: dict[str, str]
) -> bool:
    """Validate authentication by signing in or out, otherwise populate errors if needed."""
    can_validate = (
        hasattr(entry, "runtime_data")
        and entry.runtime_data.heos.connection_state is ConnectionState.CONNECTED
    )
    if not user_input:
        # Log out (neither username nor password provided)
        if not can_validate:
            return True
        try:
            await entry.runtime_data.heos.sign_out()
        except HeosError:
            errors["base"] = "unknown"
            _LOGGER.exception("Unexpected error occurred during sign-out")
            return False
        else:
            _LOGGER.debug("Successfully signed-out of HEOS Account")
            return True

    # Ensure both username and password are provided
    authentication = CONF_USERNAME in user_input or CONF_PASSWORD in user_input
    if authentication and CONF_USERNAME not in user_input:
        errors[CONF_USERNAME] = "username_missing"
        return False
    if authentication and CONF_PASSWORD not in user_input:
        errors[CONF_PASSWORD] = "password_missing"
        return False

    # Attempt to login (both username and password provided)
    if not can_validate:
        return True
    try:
        await entry.runtime_data.heos.sign_in(
            user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
        )
    except CommandAuthenticationError as err:
        errors["base"] = "invalid_auth"
        _LOGGER.warning("Failed to sign-in to HEOS Account: %s", err)
        return False
    except HeosError:
        errors["base"] = "unknown"
        _LOGGER.exception("Unexpected error occurred during sign-in")
        return False
    else:
        _LOGGER.debug(
            "Successfully signed-in to HEOS Account: %s",
            entry.runtime_data.heos.signed_in_username,
        )
        return True


def _get_current_hosts(entry: HeosConfigEntry) -> set[str]:
    """Get a set of current hosts from the entry."""
    hosts = set(entry.data[CONF_HOST])
    if hasattr(entry, "runtime_data"):
        hosts.update(
            player.ip_address
            for player in entry.runtime_data.heos.players.values()
            if player.ip_address is not None
        )
    return hosts


class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
    """Define a flow for HEOS."""

    VERSION = 1

    def __init__(self) -> None:
        """Initialize the HEOS flow."""
        self._discovered_host: str | None = None

    @staticmethod
    @callback
    def async_get_options_flow(config_entry: HeosConfigEntry) -> OptionsFlow:
        """Create the options flow."""
        return HeosOptionsFlowHandler()

    async def async_step_ssdp(
        self, discovery_info: SsdpServiceInfo
    ) -> ConfigFlowResult:
        """Handle a discovered Heos device."""
        # Store discovered host
        if TYPE_CHECKING:
            assert discovery_info.ssdp_location

        hostname = urlparse(discovery_info.ssdp_location).hostname
        assert hostname is not None

        return await self._async_handle_discovered(hostname)

    async def async_step_zeroconf(
        self, discovery_info: ZeroconfServiceInfo
    ) -> ConfigFlowResult:
        """Handle zeroconf discovery."""
        return await self._async_handle_discovered(discovery_info.host)

    async def async_step_confirm_discovery(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Confirm discovered HEOS system."""
        if user_input is not None:
            assert self._discovered_host is not None
            return self.async_create_entry(
                title=ENTRY_TITLE, data={CONF_HOST: self._discovered_host}
            )

        self._set_confirm_only()
        return self.async_show_form(step_id="confirm_discovery")

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Obtain host and validate connection."""
        await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
        self._abort_if_unique_id_configured(error="single_instance_allowed")
        # Try connecting to host if provided
        errors: dict[str, str] = {}
        host = None
        if user_input is not None:
            host = user_input[CONF_HOST]
            if await _validate_host(host, errors):
                return self.async_create_entry(
                    title=ENTRY_TITLE, data={CONF_HOST: host}
                )

        # Return form
        return self.async_show_form(
            step_id="user",
            data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}),
            errors=errors,
        )

    async def async_step_reconfigure(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Allow reconfiguration of entry."""
        entry = self._get_reconfigure_entry()
        host = entry.data[CONF_HOST]  # Get current host value
        errors: dict[str, str] = {}
        if user_input is not None:
            host = user_input[CONF_HOST]
            if await _validate_host(host, errors):
                return self.async_update_reload_and_abort(
                    entry, data_updates={CONF_HOST: host}
                )
        return self.async_show_form(
            step_id="reconfigure",
            data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}),
            errors=errors,
        )

    async def async_step_reauth(
        self, entry_data: Mapping[str, Any]
    ) -> ConfigFlowResult:
        """Perform reauthentication after auth failure event."""
        return await self.async_step_reauth_confirm()

    async def async_step_reauth_confirm(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Validate account credentials and update options."""
        errors: dict[str, str] = {}
        entry: HeosConfigEntry = self._get_reauth_entry()
        if user_input is not None:
            if await _validate_auth(user_input, entry, errors):
                return self.async_update_reload_and_abort(entry, options=user_input)

        return self.async_show_form(
            step_id="reauth_confirm",
            errors=errors,
            data_schema=self.add_suggested_values_to_schema(
                AUTH_SCHEMA, user_input or entry.options
            ),
        )

    async def _async_handle_discovered(self, hostname: str) -> ConfigFlowResult:
        entry: HeosConfigEntry | None = await self.async_set_unique_id(DOMAIN)
        # Abort early when discovery is ignored or host is part of the current system
        if entry and (
            entry.source == SOURCE_IGNORE or hostname in _get_current_hosts(entry)
        ):
            return self.async_abort(reason="single_instance_allowed")

        # Connect to discovered host and get system information
        heos = Heos(HeosOptions(hostname, events=False, heart_beat=False))
        try:
            await heos.connect()
            system_info = await heos.get_system_info()
        except HeosError as error:
            _LOGGER.debug(
                "Failed to retrieve system information from discovered HEOS device %s",
                hostname,
                exc_info=error,
            )
            return self.async_abort(reason="cannot_connect")
        finally:
            await heos.disconnect()

        # Select the preferred host, if available
        if system_info.preferred_hosts and system_info.preferred_hosts[0].ip_address:
            hostname = system_info.preferred_hosts[0].ip_address

        # Move to confirmation when not configured
        if entry is None:
            self._discovered_host = hostname
            return await self.async_step_confirm_discovery()

        # Only update if the configured host isn't part of the discovered hosts to ensure new players that come online don't trigger a reload
        if entry.data[CONF_HOST] not in [host.ip_address for host in system_info.hosts]:
            _LOGGER.debug(
                "Updated host %s to discovered host %s", entry.data[CONF_HOST], hostname
            )
            return self.async_update_reload_and_abort(
                entry,
                data_updates={CONF_HOST: hostname},
                reason="reconfigure_successful",
            )
        return self.async_abort(reason="single_instance_allowed")


class HeosOptionsFlowHandler(OptionsFlow):
    """Define HEOS options flow."""

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Manage the options."""
        errors: dict[str, str] = {}
        if user_input is not None:
            if await _validate_auth(user_input, self.config_entry, errors):
                return self.async_create_entry(data=user_input)

        return self.async_show_form(
            errors=errors,
            step_id="init",
            data_schema=self.add_suggested_values_to_schema(
                AUTH_SCHEMA, user_input or self.config_entry.options
            ),
        )
