import base64
from pathlib import Path
import re
import secrets

import requests
from ruamel.yaml import YAML

from esphome import git
import esphome.codegen as cg
from esphome.components.packages import validate_source_shorthand
import esphome.config_validation as cv
from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI
import esphome.final_validate as fv
from esphome.yaml_util import dump

dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")

# payload is in `esphomelib` mdns record, which only exists if api
# is enabled
DEPENDENCIES = ["api"]
CODEOWNERS = ["@esphome/core"]


def validate_import_url(value):
    value = cv.string_strict(value)
    value = cv.Length(max=255)(value)
    validate_source_shorthand(value)
    return value


def validate_full_url(config):
    if not config[CONF_IMPORT_FULL_CONFIG]:
        return config
    source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL])
    if CONF_REF not in source:
        raise cv.Invalid(
            "Must specify a ref (branch or tag) to import from when importing full config"
        )
    return config


CONF_PACKAGE_IMPORT_URL = "package_import_url"
CONF_IMPORT_FULL_CONFIG = "import_full_config"

CONFIG_SCHEMA = cv.All(
    cv.Schema(
        {
            cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url,
            cv.Optional(CONF_IMPORT_FULL_CONFIG, default=False): cv.boolean,
        }
    ),
    validate_full_url,
)


def _final_validate(config):
    full_config = fv.full_config.get()[CONF_ESPHOME]
    if CONF_PROJECT not in full_config:
        raise cv.Invalid(
            "Dashboard import requires the `esphome` -> `project` information to be provided."
        )


FINAL_VALIDATE_SCHEMA = _final_validate

WIFI_CONFIG = """

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
"""


async def to_code(config):
    cg.add_define("USE_DASHBOARD_IMPORT")
    url = config[CONF_PACKAGE_IMPORT_URL]
    if config[CONF_IMPORT_FULL_CONFIG]:
        url += "?full_config"
    cg.add(dashboard_import_ns.set_package_import_url(cg.FlashStringLiteral(url)))


def import_config(
    path: str,
    name: str,
    friendly_name: str | None,
    project_name: str,
    import_url: str,
    network: str = CONF_WIFI,
    encryption: bool = False,
) -> None:
    """Materialise a dashboard-imported device's YAML on disk.

    Used by:
    - device-builder (esphome/device-builder) — called from the
      ``devices/import`` WS handler to seed the YAML for an adopted
      factory firmware. Coordinate before changing the kwargs or the
      generated YAML's top-level keys; both consumers depend on the
      output shape (``esphome.name`` / ``packages:`` import url) to
      route subsequent compile + flash operations.
    """
    p = Path(path)

    if p.exists():
        raise FileExistsError

    git_file = git.GitFile.from_shorthand(import_url)

    if git_file.query and "full_config" in git_file.query:
        url = git_file.raw_url
        try:
            req = requests.get(url, timeout=30)
            req.raise_for_status()
        except requests.exceptions.RequestException as e:
            raise ValueError(f"Error while fetching {url}: {e}") from e

        contents = req.text
        yaml = YAML()
        loaded_yaml = yaml.load(contents)
        if (
            "name_add_mac_suffix" in loaded_yaml["esphome"]
            and loaded_yaml["esphome"]["name_add_mac_suffix"]
        ):
            loaded_yaml["esphome"]["name_add_mac_suffix"] = False
            name_val = loaded_yaml["esphome"]["name"]
            sub_pattern = re.compile(r"\$\{?([a-zA-Z-_]+)\}?")
            if match := sub_pattern.match(name_val):
                name_sub = match.group(1)
                if name_sub in loaded_yaml["substitutions"]:
                    loaded_yaml["substitutions"][name_sub] = name
                else:
                    raise ValueError(
                        f"Name substitution {name_sub} not found in substitutions"
                    )
            else:
                loaded_yaml["esphome"]["name"] = name
            if friendly_name is not None:
                friendly_name_val = loaded_yaml["esphome"]["friendly_name"]
                if match := sub_pattern.match(friendly_name_val):
                    friendly_name_sub = match.group(1)
                    if friendly_name_sub in loaded_yaml["substitutions"]:
                        loaded_yaml["substitutions"][friendly_name_sub] = friendly_name
                    else:
                        raise ValueError(
                            f"Friendly name substitution {friendly_name_sub} not found in substitutions"
                        )
                else:
                    loaded_yaml["esphome"]["friendly_name"] = friendly_name

            with p.open("w", encoding="utf8") as f:
                yaml.dump(loaded_yaml, f)
        else:
            with p.open("w", encoding="utf8") as f:
                f.write(contents)

    else:
        substitutions = {"name": name}
        esphome_core = {"name": "${name}", "name_add_mac_suffix": False}
        if friendly_name:
            substitutions["friendly_name"] = friendly_name
            esphome_core["friendly_name"] = "${friendly_name}"
        config = {
            "substitutions": substitutions,
            "packages": {project_name: import_url},
            "esphome": esphome_core,
        }
        if encryption:
            noise_psk = secrets.token_bytes(32)
            key = base64.b64encode(noise_psk).decode()
            config["api"] = {"encryption": {"key": key}}

        output = dump(config)

        if network == CONF_WIFI:
            output += WIFI_CONFIG

        p.write_text(output, encoding="utf8")
