import asyncio
from homeassistant.components import frontend, http, panel_custom
from homeassistant.const import (
    CONF_DEFAULT,
    CONF_HEADERS,
    CONF_ICON,
    CONF_MODE,
    CONF_NAME,
    CONF_PATH,
    CONF_URL,
)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
import os
import re
import time
from typing import TYPE_CHECKING, cast
import voluptuous as vol
from yarl import URL

from .const import (
    DOMAIN,
    LOGGER as _LOGGER,
    API_BASE,
    URL_BASE,
    WorkMode,
    UIMode,
    RewriteMode,
    ConfMode,
)
from .config import IngressStore, IngressCfg, RewriteCfg
from .ingress import IngressView, std_header_name
from .websocket_api import async_register_websocket_api

if TYPE_CHECKING:
    from homeassistant.core import HomeAssistant, ServiceCall
    from homeassistant.config_entries import ConfigEntry
    from typing import Any, Final, TypedDict
    from .client import IngressClient

    class DomainData(TypedDict):
        client: IngressClient
        config: IngressStore
        panels: set[str]


VERSION = ""
CONNECT_TIMEOUT = 20

CONF_TITLE: "Final" = "title"
CONF_MATCH: "Final" = "match"
CONF_REPLACE: "Final" = "replace"
CONF_INDEX: "Final" = "index"
CONF_PARENT: "Final" = "parent"
CONF_WORK_MODE: "Final" = "work_mode"
CONF_UI_MODE: "Final" = "ui_mode"
CONF_REWRITE: "Final" = "rewrite"
CONF_COOKIE_NAME: "Final" = "cookie_name"
CONF_EXPIRE_TIME: "Final" = "expire_time"
CONF_STATIC_TOKEN: "Final" = "static_token"

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: cv.schema_with_slug_keys(
            vol.Schema(
                {
                    vol.Optional(panel_custom.CONF_REQUIRE_ADMIN, default=False): cv.boolean,
                    vol.Optional(CONF_TITLE): cv.string,
                    vol.Optional(CONF_ICON): cv.icon,
                    vol.Required(CONF_URL): vol.Any(
                        cv.string,
                        vol.Schema(
                            {
                                vol.Required(CONF_MATCH): cv.string,
                                vol.Required(CONF_REPLACE): cv.string,
                                vol.Required(CONF_DEFAULT): cv.string,
                            }
                        ),
                    ),
                    vol.Optional(CONF_INDEX, default=""): cv.string,
                    vol.Optional(CONF_PARENT): cv.string,
                    vol.Optional(CONF_WORK_MODE, default=WorkMode.INGRESS): vol.In(list(WorkMode)),
                    vol.Optional(CONF_UI_MODE, default=UIMode.NORMAL): vol.In(list(UIMode)),
                    vol.Optional(CONF_HEADERS): vol.Schema({str: cv.string}),
                    vol.Optional(CONF_REWRITE): [
                        vol.Schema(
                            {
                                vol.Required(CONF_MODE): vol.In(list(RewriteMode)),
                                vol.Optional(CONF_PATH): cv.string,
                                vol.Optional(CONF_NAME): cv.string,
                                vol.Required(CONF_MATCH): cv.string,
                                vol.Required(CONF_REPLACE): cv.string,
                            }
                        )
                    ],
                    vol.Optional(CONF_COOKIE_NAME): cv.string,
                    vol.Optional(CONF_STATIC_TOKEN): cv.string,
                    vol.Optional(CONF_EXPIRE_TIME): cv.positive_int,
                }
            )
        ),
    },
    extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass: "HomeAssistant", config) -> bool:
    # init once
    data = await _async_init(hass)

    # init config entry
    entries = hass.config_entries.async_entries(DOMAIN)
    if not entries:
        hass.async_create_task(
            hass.config_entries.flow.async_init(DOMAIN, context={"source": "import"})
        )

    # init yaml mode
    if not entries or entries[0].data[CONF_MODE] == ConfMode.YAML:
        await setup_domain(hass, data, config)

    return True


async def async_setup_entry(hass: "HomeAssistant", entry: "ConfigEntry") -> bool:
    if entry.data[CONF_MODE] != ConfMode.AGENT:
        return True

    from homeassistant.const import EVENT_HOMEASSISTANT_STOP
    from .client import create_client
    from .client.exceptions import ClientException

    # init client
    client = create_client(hass, entry.data[CONF_URL], async_get_clientsession(hass))
    client.on_client_event("ready", on_remote_ready)

    # get config
    config = None
    try:
        async with asyncio.timeout(CONNECT_TIMEOUT):
            await client.connect()
            config = await get_remote_config(hass, client)
    except ClientException as err:
        raise ConfigEntryNotReady("Failed to connect to ingress agent") from err
    except Exception as err:
        _LOGGER.exception("Failed to connect to ingress agent")
        raise ConfigEntryNotReady("Unknown error connecting to the ingress agent") from err
    finally:
        if config is None:
            client.disconnect()

    entry.async_on_unload(
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, lambda e: client.disconnect())
    )

    # setup config
    data: DomainData = hass.data[DOMAIN]
    data["client"] = client
    await setup_domain(hass, data, config)
    return True


async def on_remote_ready(hass: "HomeAssistant", client: "IngressClient", _: "Any"):
    await client.send_command({"type": "subscribe"})


async def get_remote_config(hass: "HomeAssistant", client: "IngressClient") -> dict[str, "Any"]:
    # TODO
    return {DOMAIN: {}}


async def async_unload_entry(hass: "HomeAssistant", entry: "ConfigEntry") -> bool:
    client: IngressClient
    if client := hass.data[DOMAIN].pop("client", None):
        client.disconnect()
    return True


async def setup_domain(hass: "HomeAssistant", data: "DomainData", config) -> None:
    # clean
    for name in data["panels"]:
        frontend.async_remove_panel(hass, name)
    data["panels"].clear()
    data["config"].clear()

    # parse config
    config, panels = _parse_config(hass, config)

    # add ingress
    now = int(time.time())
    for cfg in config:
        data["config"].add_ingress(cfg, now)

    # add panels
    data["panels"].update(panels)
    await asyncio.gather(*(panel_custom.async_register_panel(hass, **v) for v in panels.values()))


async def _async_init(hass: "HomeAssistant") -> "DomainData":
    """Init DomainData and service once."""
    data: DomainData = hass.data.setdefault(DOMAIN, {})
    if "config" not in cast(dict, data):
        from homeassistant.const import SERVICE_RELOAD
        from homeassistant.helpers.reload import async_integration_yaml_config
        from homeassistant.helpers.service import async_register_admin_service

        # get version
        global VERSION
        VERSION = await get_version(hass, DOMAIN)

        # register static url
        static_dir = os.path.join(os.path.dirname(__file__), "www")
        await _async_register_static_paths(hass.http, [(URL_BASE, static_dir, False)])

        # register ingress view
        config = IngressStore()
        hass.http.register_view(IngressView(hass, config, async_get_clientsession(hass)))

        # register websocket api
        await async_register_websocket_api(hass)

        # register reload config
        async def reload_config(call: "ServiceCall"):
            data: DomainData = hass.data[DOMAIN]
            if client := data.get("client"):
                config = await get_remote_config(hass, client)
            else:
                config = await async_integration_yaml_config(hass, DOMAIN)
            await setup_domain(hass, data, config or {})
            hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)

        async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, reload_config)

        # init data
        data.update(config=config, panels=set())
    return data


async def get_version(hass: "HomeAssistant", domain: str) -> str:
    version = "unknown"
    try:
        from homeassistant.loader import async_get_integration

        integration = await async_get_integration(hass, domain)
        version = integration.version or version
    except Exception:
        pass
    return version


async def _async_register_static_paths(hass_http: "http.HomeAssistantHTTP", configs):
    if hasattr(hass_http, "async_register_static_paths"):
        await hass_http.async_register_static_paths([http.StaticPathConfig(*c) for c in configs])
    else:
        for c in configs:
            hass_http.register_static_path(*c)


def _parse_config(
    hass: "HomeAssistant", config
) -> tuple[list[IngressCfg], dict[str, dict[str, "Any"]]]:
    cfgs: list[IngressCfg] = []
    panels: dict[str, dict[str, Any]] = {}
    ingresses, children = {}, []
    placeholder = "$http_x_ingress_path"
    entrypoint = f"{URL_BASE}/entrypoint.js?v={VERSION}"
    for name, data in config.get(DOMAIN, {}).items():
        cfg: dict[str, Any]
        ingress_cfg = None
        work_mode = data[CONF_WORK_MODE]
        url, front_url = data[CONF_URL], {}
        if isinstance(url, dict):
            url, front_url = url[CONF_DEFAULT], url
        if work_mode not in (WorkMode.IFRAME, WorkMode.CUSTOM):
            # init backend config
            ingress_cfg = dict(mode=work_mode, name=name, entry=name)
            ingress_path, sub_path = f"{API_BASE}/{name}", ""
            if work_mode != WorkMode.HASSIO:
                url = url.replace(placeholder, ingress_path).rstrip("/")
                if "://" not in url:
                    url = f"http://{url}"
                pos = url.find("/", url.index("://") + 3)
                if pos > 0:
                    url, sub_path = url[:pos], url[pos:]
                headers = {}
                for k, v in data.get(CONF_HEADERS, {}).items():
                    headers[std_header_name(k)] = v.replace(placeholder, ingress_path)
                ingress_cfg.update(
                    origin=URL(url),
                    sub_path=sub_path,
                    headers=headers,
                    cookie_name=data.get(CONF_COOKIE_NAME),
                    expire_time=data.get(CONF_EXPIRE_TIME),
                    static_token=data.get(CONF_STATIC_TOKEN),
                )
                url += sub_path
                if work_mode in (WorkMode.INGRESS, WorkMode.SUBAPP):
                    rewrites: list[RewriteCfg] = []
                    if rewrite := data.get(CONF_REWRITE):
                        ingress_path = re.escape(ingress_path)
                        for item in rewrite:
                            item[CONF_REPLACE] = item[CONF_REPLACE].replace(
                                placeholder, ingress_path
                            )
                            rewrites.append(RewriteCfg(**item))
                    ingress_cfg.update(rewrites=rewrites)
            ingress_cfg = IngressCfg(**ingress_cfg)
            cfgs.append(ingress_cfg)
            # init frontend config
            cfg = {"token": ingress_cfg.token, "index": data[CONF_INDEX].lstrip("/")}
            if work_mode == WorkMode.AUTH:
                cfg["url"] = front_url
                cfg["field"] = ingress_cfg.cookie_name
            elif work_mode == WorkMode.HASSIO:
                cfg["addon"] = url
            elif work_mode == WorkMode.INGRESS:
                ingresses[name] = ingress_cfg
        else:
            cfg = {"url": front_url}
            if data[CONF_INDEX]:
                url = url.rstrip("/")
                cfg["index"] = data[CONF_INDEX].lstrip("/")
        if front_url.get(CONF_MATCH):
            front_url[CONF_DEFAULT] = url
        elif "url" in cfg:
            cfg["url"] = url

        cfg["ui_mode"] = work_mode if work_mode == WorkMode.CUSTOM else data[CONF_UI_MODE]
        title, icon = data.get(CONF_TITLE), data.get(CONF_ICON)
        if parent := data.get(CONF_PARENT):
            if name.startswith(parent) and name[len(parent) : len(parent) + 1] == "_":
                name = name[len(parent) + 1 :]
            if ingress_cfg:
                ingress_cfg.entry = f"{parent}/{name}"
            if title:
                cfg["title"] = title
            if icon:
                cfg["icon"] = icon
            children.append((name, parent, cfg, ingress_cfg))
            continue

        panels[name] = dict(
            webcomponent_name="ha-panel-ingress",
            js_url=entrypoint,
            frontend_url_path=name,
            sidebar_title=title,
            sidebar_icon=icon,
            require_admin=data[panel_custom.CONF_REQUIRE_ADMIN],
            embed_iframe=False,
            config=cfg,
        )

    for child, parent, cfg, ingress_cfg in children:
        if ingress_cfg and ingress_cfg.mode == WorkMode.SUBAPP:
            if parent not in ingresses:
                _LOGGER.error(
                    "parent ingress[%s] not found, skip subapp[%s]!", parent, ingress_cfg.name
                )
                continue
            # ingress: add subapp to parent's sub_apps
            pi_cfg = ingresses[parent]
            if not pi_cfg.sub_apps:
                pi_cfg.sub_apps = []
            pi_cfg.sub_apps.append(ingress_cfg)
            # panel: use parent's parent if parent is a child panel
            if parent not in panels:
                parent = pi_cfg.entry.partition("/")
                parent, child = parent[0], f"{parent[2]}-{child}"
                ingress_cfg.entry = f"{parent}/{child}"
        if parent not in panels:
            _LOGGER.error("parent panel[%s] not found, skip child panel[%s]!", parent, child)
            continue
        # panel: add child to parent's children
        panels[parent]["config"].setdefault("children", {})[child] = cfg

    return cfgs, panels
