"""Config flow for Infrarize — 6-step UI wizard with device list selection."""
from __future__ import annotations

import json
import logging
import os

import aiofiles
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import (
    BooleanSelector,
    EntitySelector,
    EntitySelectorConfig,
    SelectSelector,
    SelectSelectorConfig,
    SelectSelectorMode,
    TextSelector,
    TextSelectorConfig,
    TextSelectorType,
)

from . import COMPONENT_ABS_DIR, Helper
from .const import (
    ALL_CONTROLLERS_SENTINEL,
    CONF_CONTROLLER_DATA,
    CONF_DELAY,
    CONF_DEVICE_CLASS,
    CONF_DEVICE_CODE,
    CONF_DEVICE_NAME,
    CONF_HUMIDITY_SENSOR,
    CONF_PLATFORM,
    CONF_POWER_SENSOR,
    CONF_POWER_SENSOR_RESTORE_STATE,
    CONF_SOURCE_NAMES,
    CONF_TEMPERATURE_SENSOR,
    CONTROLLER_HINTS,
    DEFAULT_DELAY,
    DEFAULT_DEVICE_NAMES,
    ENTITY_BASED_CONTROLLERS,
    INDEX_FILENAME,
    MANUAL_ENTRY_SENTINEL,
    PLATFORM_SUBDIR,
    PLATFORMS,
)

DOMAIN = "infrarize"
_LOGGER = logging.getLogger(__name__)


# ── Module-level helpers ──────────────────────────────────────────────────────

async def _load_device_json(platform: str, device_code: int) -> dict | None:
    """Return device JSON dict, downloading from GitHub if not cached locally."""
    subdir = PLATFORM_SUBDIR[platform]
    codes_dir = os.path.join(COMPONENT_ABS_DIR, "codes", subdir)
    os.makedirs(codes_dir, exist_ok=True)
    path = os.path.join(codes_dir, f"{device_code}.json")

    if not os.path.exists(path):
        source = (
            "https://raw.githubusercontent.com/"
            f"kobimx/Infrarize/master/codes/{subdir}/{device_code}.json"
        )
        try:
            await Helper.downloader(source, path)
        except Exception:
            return None

    try:
        async with aiofiles.open(path, mode="r") as f:
            return json.loads(await f.read())
    except Exception:
        return None


def _controller_schema(
    controller_type: str,
    current_data: str = "",
    current_delay: str = DEFAULT_DELAY,
) -> vol.Schema:
    """Build controller step schema; entity picker for Broadlink/Xiaomi, text for rest."""
    if controller_type in ENTITY_BASED_CONTROLLERS:
        data_field: object = EntitySelector(EntitySelectorConfig(domain="remote"))
    else:
        data_field = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))

    fields: dict = {}
    if current_data:
        fields[
            vol.Required(
                CONF_CONTROLLER_DATA,
                description={"suggested_value": current_data},
            )
        ] = data_field
    else:
        fields[vol.Required(CONF_CONTROLLER_DATA)] = data_field

    fields[
        vol.Optional(
            CONF_DELAY,
            description={"suggested_value": current_delay},
        )
    ] = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))

    return vol.Schema(fields)


def _options_schema(platform: str, defaults: dict | None = None) -> vol.Schema:
    """Build optional-sensors schema for the given platform."""
    d = defaults or {}

    def _opt(key: str) -> vol.Optional:
        val = d.get(key)
        if val is not None and val != "":
            return vol.Optional(key, description={"suggested_value": val})
        return vol.Optional(key)

    # Power sensors are only ever checked for on/off state (see
    # InfrarizeClimate._async_power_sensor_changed and the equivalent methods in
    # fan.py, light.py, media_player.py, which compare .state to STATE_ON/STATE_OFF)
    # so restrict the picker to domains that expose a binary on/off state.
    power_sensor_selector = EntitySelector(
        EntitySelectorConfig(domain=["binary_sensor", "switch", "input_boolean"])
    )

    if platform == "climate":
        return vol.Schema(
            {
                _opt(CONF_TEMPERATURE_SENSOR): EntitySelector(
                    EntitySelectorConfig(domain="sensor", device_class="temperature")
                ),
                _opt(CONF_HUMIDITY_SENSOR): EntitySelector(
                    EntitySelectorConfig(domain="sensor", device_class="humidity")
                ),
                _opt(CONF_POWER_SENSOR): power_sensor_selector,
                vol.Optional(
                    CONF_POWER_SENSOR_RESTORE_STATE,
                    default=bool(d.get(CONF_POWER_SENSOR_RESTORE_STATE, False)),
                ): BooleanSelector(),
            }
        )

    if platform == "media_player":
        src_existing = d.get(CONF_SOURCE_NAMES, {})
        src_str = (
            json.dumps(src_existing)
            if isinstance(src_existing, dict) and src_existing
            else ""
        )
        fields: dict = {
            _opt(CONF_POWER_SENSOR): power_sensor_selector,
            vol.Optional(
                CONF_DEVICE_CLASS,
                description={"suggested_value": d.get(CONF_DEVICE_CLASS, "tv")},
            ): TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)),
        }
        src_key = (
            vol.Optional(
                CONF_SOURCE_NAMES,
                description={"suggested_value": src_str},
            )
            if src_str
            else vol.Optional(CONF_SOURCE_NAMES)
        )
        fields[src_key] = TextSelector(
            TextSelectorConfig(type=TextSelectorType.TEXT, multiline=True)
        )
        return vol.Schema(fields)

    # fan and light
    return vol.Schema({_opt(CONF_POWER_SENSOR): power_sensor_selector})


def _parse_options(platform: str, user_input: dict) -> dict:
    """Clean and coerce options form input before storing."""
    out: dict = {}

    def _maybe(key: str) -> None:
        val = user_input.get(key, "")
        if isinstance(val, str):
            val = val.strip()
        if val:
            out[key] = val

    if platform == "climate":
        _maybe(CONF_TEMPERATURE_SENSOR)
        _maybe(CONF_HUMIDITY_SENSOR)
        _maybe(CONF_POWER_SENSOR)
        out[CONF_POWER_SENSOR_RESTORE_STATE] = bool(
            user_input.get(CONF_POWER_SENSOR_RESTORE_STATE, False)
        )
    elif platform == "media_player":
        _maybe(CONF_POWER_SENSOR)
        out[CONF_DEVICE_CLASS] = (
            (user_input.get(CONF_DEVICE_CLASS) or "tv").strip() or "tv"
        )
        src_raw = user_input.get(CONF_SOURCE_NAMES, "")
        if isinstance(src_raw, str) and src_raw.strip():
            try:
                parsed = json.loads(src_raw.strip())
                if isinstance(parsed, dict):
                    out[CONF_SOURCE_NAMES] = parsed
            except json.JSONDecodeError:
                pass
        elif isinstance(src_raw, dict) and src_raw:
            out[CONF_SOURCE_NAMES] = src_raw
    else:  # fan, light
        _maybe(CONF_POWER_SENSOR)

    return out


def _device_placeholders(device_data: dict, platform: str) -> dict:
    """Build description_placeholders dict for the options step."""
    p: dict = {
        "manufacturer": device_data.get("manufacturer", "—"),
        "models": ", ".join(device_data.get("supportedModels", [])) or "—",
        "controller_type": device_data.get("supportedController", "—"),
        "device_code": str(device_data.get("device_code", "—")),
    }
    if platform == "climate":
        p["temp_range"] = (
            f"{device_data.get('minTemperature', '—')} – "
            f"{device_data.get('maxTemperature', '—')} °C"
        )
        p["operation_modes"] = ", ".join(device_data.get("operationModes", []))
        p["fan_modes"] = ", ".join(device_data.get("fanModes", []))
        swing = device_data.get("swingModes", [])
        p["swing_modes"] = ", ".join(swing) if swing else "none"
    return p


# ── Config Flow ───────────────────────────────────────────────────────────────


class InfrarizeConfigFlow(ConfigFlow, domain=DOMAIN):
    """Handle multi-step UI config flow for Infrarize."""

    VERSION = 1

    def __init__(self) -> None:
        self._platform: str = ""
        self._controller_filter: str = ALL_CONTROLLERS_SENTINEL
        self._manufacturer: str = ""
        self._device_code: int = 0
        self._device_data: dict = {}
        self._device_name: str = ""
        self._controller_data: str = ""
        self._delay: str = DEFAULT_DELAY
        self._index_cache: list[dict] | None = None

    # ── Index helpers ─────────────────────────────────────────────────────────

    async def _get_index(self) -> list[dict]:
        """Load and cache index entries for the current platform."""
        if self._index_cache is not None:
            return self._index_cache
        path = os.path.join(COMPONENT_ABS_DIR, INDEX_FILENAME)
        try:
            async with aiofiles.open(path, mode="r") as f:
                data = json.loads(await f.read())
            self._index_cache = data.get(self._platform, [])
        except Exception:
            _LOGGER.warning("Could not load %s", INDEX_FILENAME)
            self._index_cache = []
        return self._index_cache

    def _filtered(self, entries: list[dict]) -> list[dict]:
        if self._controller_filter == ALL_CONTROLLERS_SENTINEL:
            return entries
        return [e for e in entries if e.get("controller") == self._controller_filter]

    # ── Step 1: platform ──────────────────────────────────────────────────────

    async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
        if user_input is not None:
            self._platform = user_input["platform"]
            return await self.async_step_controller_filter()

        return self.async_show_form(
            step_id="user",
            data_schema=vol.Schema(
                {
                    vol.Required("platform"): SelectSelector(
                        SelectSelectorConfig(
                            options=[
                                {"label": v, "value": k}
                                for k, v in PLATFORMS.items()
                            ],
                            mode=SelectSelectorMode.LIST,
                        )
                    )
                }
            ),
        )

    # ── Step 2: controller filter ─────────────────────────────────────────────

    async def async_step_controller_filter(
        self, user_input: dict | None = None
    ) -> FlowResult:
        entries = await self._get_index()

        if user_input is not None:
            self._controller_filter = user_input.get(
                "controller_type", ALL_CONTROLLERS_SENTINEL
            )
            return await self.async_step_manufacturer()

        available = sorted({e["controller"] for e in entries})
        options = [{"label": "Any (show all)", "value": ALL_CONTROLLERS_SENTINEL}]
        options += [{"label": c, "value": c} for c in available]

        return self.async_show_form(
            step_id="controller_filter",
            data_schema=vol.Schema(
                {
                    vol.Required(
                        "controller_type", default=ALL_CONTROLLERS_SENTINEL
                    ): SelectSelector(
                        SelectSelectorConfig(
                            options=options, mode=SelectSelectorMode.LIST
                        )
                    )
                }
            ),
        )

    # ── Step 3: manufacturer ──────────────────────────────────────────────────

    async def async_step_manufacturer(
        self, user_input: dict | None = None
    ) -> FlowResult:
        entries = self._filtered(await self._get_index())
        manufacturers = sorted({e["manufacturer"] for e in entries})

        if user_input is not None:
            sel = user_input.get("manufacturer", "")
            if sel == MANUAL_ENTRY_SENTINEL:
                return await self.async_step_device_manual()
            self._manufacturer = sel
            return await self.async_step_device_select()

        options = [
            {"label": "— Enter code manually —", "value": MANUAL_ENTRY_SENTINEL}
        ]
        options += [{"label": m, "value": m} for m in manufacturers]

        return self.async_show_form(
            step_id="manufacturer",
            data_schema=vol.Schema(
                {
                    vol.Required("manufacturer"): SelectSelector(
                        SelectSelectorConfig(
                            options=options, mode=SelectSelectorMode.DROPDOWN
                        )
                    )
                }
            ),
        )

    # ── Step 4a: device from index list ───────────────────────────────────────

    async def async_step_device_select(
        self, user_input: dict | None = None
    ) -> FlowResult:
        entries = [
            e
            for e in self._filtered(await self._get_index())
            if e.get("manufacturer") == self._manufacturer
        ]
        errors: dict[str, str] = {}

        if user_input is not None:
            sel = user_input.get("device_code", "")
            if sel == MANUAL_ENTRY_SENTINEL:
                return await self.async_step_device_manual()
            try:
                device_code = int(sel)
            except (ValueError, TypeError):
                errors["device_code"] = "invalid_device_code"
            else:
                device_data = await _load_device_json(self._platform, device_code)
                if device_data is None:
                    errors["device_code"] = "invalid_device_code"
                else:
                    self._device_code = device_code
                    self._device_data = device_data
                    models = device_data.get("supportedModels", [])
                    self._device_name = (
                        f"{self._manufacturer} {models[0]}"
                        if models
                        else DEFAULT_DEVICE_NAMES[self._platform]
                    )
                    return await self.async_step_device_name()

        options = [
            {"label": "— Enter code manually —", "value": MANUAL_ENTRY_SENTINEL}
        ]
        for e in sorted(entries, key=lambda x: x["code"]):
            models_str = ", ".join(e.get("models", []))
            options.append(
                {"label": f"{e['code']} — {models_str}", "value": str(e["code"])}
            )

        return self.async_show_form(
            step_id="device_select",
            data_schema=vol.Schema(
                {
                    vol.Required("device_code"): SelectSelector(
                        SelectSelectorConfig(
                            options=options, mode=SelectSelectorMode.DROPDOWN
                        )
                    )
                }
            ),
            description_placeholders={"manufacturer": self._manufacturer},
            errors=errors,
        )

    # ── Step 4b: manual code entry ────────────────────────────────────────────

    async def async_step_device_manual(
        self, user_input: dict | None = None
    ) -> FlowResult:
        errors: dict[str, str] = {}

        if user_input is not None:
            try:
                device_code = int(user_input["device_code"])
                if device_code <= 0:
                    raise ValueError("non-positive")
            except (ValueError, KeyError, TypeError):
                errors["device_code"] = "invalid_device_code"
            else:
                device_data = await _load_device_json(self._platform, device_code)
                if device_data is None:
                    errors["device_code"] = "invalid_device_code"
                else:
                    self._device_code = device_code
                    self._device_data = device_data
                    manufacturer = device_data.get("manufacturer", "")
                    models = device_data.get("supportedModels", [])
                    self._manufacturer = manufacturer
                    self._device_name = (
                        f"{manufacturer} {models[0]}"
                        if models
                        else DEFAULT_DEVICE_NAMES[self._platform]
                    )
                    return await self.async_step_device_name()

        return self.async_show_form(
            step_id="device_manual",
            data_schema=vol.Schema(
                {
                    vol.Required("device_code"): TextSelector(
                        TextSelectorConfig(type=TextSelectorType.NUMBER)
                    )
                }
            ),
            errors=errors,
        )

    # ── Step 4c: confirm / edit name ──────────────────────────────────────────

    async def async_step_device_name(
        self, user_input: dict | None = None
    ) -> FlowResult:
        if user_input is not None:
            name = (user_input.get("name") or "").strip() or self._device_name
            self._device_name = name
            return await self.async_step_controller()

        return self.async_show_form(
            step_id="device_name",
            data_schema=vol.Schema(
                {
                    vol.Required(
                        "name",
                        description={"suggested_value": self._device_name},
                    ): TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
                }
            ),
            description_placeholders={
                "manufacturer": self._device_data.get("manufacturer", ""),
                "models": ", ".join(self._device_data.get("supportedModels", [])),
                "controller_type": self._device_data.get("supportedController", ""),
                "device_code": str(self._device_code),
            },
        )

    # ── Step 5: controller data ───────────────────────────────────────────────

    async def async_step_controller(
        self, user_input: dict | None = None
    ) -> FlowResult:
        errors: dict[str, str] = {}
        controller_type = self._device_data.get("supportedController", "")
        hint = CONTROLLER_HINTS.get(controller_type, "Enter the controller connection string.")

        if user_input is not None:
            controller_data = user_input.get(CONF_CONTROLLER_DATA, "")
            if isinstance(controller_data, str):
                controller_data = controller_data.strip()
            if not controller_data:
                errors[CONF_CONTROLLER_DATA] = "controller_data_required"
            else:
                self._controller_data = controller_data
                delay_raw = user_input.get(CONF_DELAY, DEFAULT_DELAY)
                try:
                    float(delay_raw)
                    self._delay = str(delay_raw)
                except (ValueError, TypeError):
                    self._delay = DEFAULT_DELAY

                uid = (
                    f"infrarize_{self._platform}_{self._device_code}"
                    f"_{self._controller_data}"
                )
                await self.async_set_unique_id(uid)
                self._abort_if_unique_id_configured()

                return await self.async_step_options_platform()

        return self.async_show_form(
            step_id="controller",
            data_schema=_controller_schema(
                controller_type, self._controller_data, self._delay
            ),
            description_placeholders={
                "controller_type": controller_type,
                "controller_hint": hint,
            },
            errors=errors,
        )

    # ── Step 6: optional sensors / platform settings ──────────────────────────

    async def async_step_options_platform(
        self, user_input: dict | None = None
    ) -> FlowResult:
        if user_input is not None:
            return self._create_entry(user_input)

        return self.async_show_form(
            step_id="options_platform",
            data_schema=_options_schema(self._platform),
            description_placeholders=_device_placeholders(
                self._device_data, self._platform
            ),
        )

    # ── Entry creation ────────────────────────────────────────────────────────

    def _create_entry(self, options_input: dict) -> FlowResult:
        try:
            delay_float = float(self._delay)
        except (ValueError, TypeError):
            delay_float = 0.5

        # Immutable identification stored in data
        data: dict = {
            CONF_PLATFORM:    self._platform,
            CONF_DEVICE_CODE: self._device_code,
            CONF_DEVICE_NAME: self._device_name,
        }

        # Mutable settings stored in options (editable via options flow)
        options: dict = {
            CONF_CONTROLLER_DATA: self._controller_data,
            CONF_DELAY:           delay_float,
        }
        options.update(_parse_options(self._platform, options_input))

        mfr = self._device_data.get("manufacturer", "Infrarize")
        title = f"{mfr} — {self._device_name}"
        return self.async_create_entry(title=title, data=data, options=options)

    @staticmethod
    @callback
    def async_get_options_flow(config_entry: ConfigEntry) -> InfrarizeOptionsFlow:
        return InfrarizeOptionsFlow(config_entry)


# ── Options Flow ──────────────────────────────────────────────────────────────


class InfrarizeOptionsFlow(OptionsFlow):
    """Edit controller and sensor settings for an existing Infrarize entry."""

    def __init__(self, config_entry: ConfigEntry) -> None:
        self._entry = config_entry
        self._platform: str = config_entry.data.get(CONF_PLATFORM, "")
        self._device_code: int = config_entry.data.get(CONF_DEVICE_CODE, 0)
        self._device_data: dict = {}
        # Pre-fill from current options
        self._controller_data: str = config_entry.options.get(CONF_CONTROLLER_DATA, "")
        self._delay: str = str(config_entry.options.get(CONF_DELAY, DEFAULT_DELAY))

    async def async_step_init(self, user_input: dict | None = None) -> FlowResult:
        if not self._device_data:
            self._device_data = (
                await _load_device_json(self._platform, self._device_code) or {}
            )
        return await self.async_step_controller()

    async def async_step_controller(
        self, user_input: dict | None = None
    ) -> FlowResult:
        errors: dict[str, str] = {}
        controller_type = self._device_data.get("supportedController", "")
        hint = CONTROLLER_HINTS.get(controller_type, "Enter the controller connection string.")

        if user_input is not None:
            controller_data = user_input.get(CONF_CONTROLLER_DATA, "")
            if isinstance(controller_data, str):
                controller_data = controller_data.strip()
            if not controller_data:
                errors[CONF_CONTROLLER_DATA] = "controller_data_required"
            else:
                self._controller_data = controller_data
                delay_raw = user_input.get(CONF_DELAY, DEFAULT_DELAY)
                try:
                    float(delay_raw)
                    self._delay = str(delay_raw)
                except (ValueError, TypeError):
                    self._delay = DEFAULT_DELAY
                return await self.async_step_options_platform()

        return self.async_show_form(
            step_id="controller",
            data_schema=_controller_schema(
                controller_type, self._controller_data, self._delay
            ),
            description_placeholders={
                "controller_type": controller_type,
                "controller_hint": hint,
            },
            errors=errors,
        )

    async def async_step_options_platform(
        self, user_input: dict | None = None
    ) -> FlowResult:
        if user_input is not None:
            try:
                delay_float = float(self._delay)
            except (ValueError, TypeError):
                delay_float = 0.5

            updated: dict = {
                CONF_CONTROLLER_DATA: self._controller_data,
                CONF_DELAY:           delay_float,
            }
            updated.update(_parse_options(self._platform, user_input))
            return self.async_create_entry(title="", data=updated)

        return self.async_show_form(
            step_id="options_platform",
            data_schema=_options_schema(self._platform, defaults=self._entry.options),
            description_placeholders=_device_placeholders(
                self._device_data, self._platform
            ),
        )
