import base64
from pathlib import Path
import secrets
import string
from typing import Literal, NotRequired, TypedDict, Unpack
import unicodedata

import voluptuous as vol

import esphome.config_validation as cv
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
from esphome.core import CORE
from esphome.helpers import get_bool_env, write_file
from esphome.log import AnsiFore, color
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.util import safe_input, safe_print

CORE_BIG = r"""    _____ ____  _____  ______
   / ____/ __ \|  __ \|  ____|
  | |   | |  | | |__) | |__
  | |   | |  | |  _  /|  __|
  | |___| |__| | | \ \| |____
   \_____\____/|_|  \_\______|
"""
ESP_BIG = r"""      ______  _____ _____
     |  ____|/ ____|  __ \\
     | |__  | (___ | |__) |
     |  __|  \___ \|  ___/
     | |____ ____) | |
     |______|_____/|_|
"""
WIFI_BIG = r"""   __          ___ ______ _
   \ \        / (_)  ____(_)
    \ \  /\  / / _| |__   _
     \ \/  \/ / | |  __| | |
      \  /\  /  | | |    | |
       \/  \/   |_|_|    |_|
"""
OTA_BIG = r"""       ____ _______
      / __ \__   __|/\\
     | |  | | | |  /  \\
     | |  | | | | / /\ \\
     | |__| | | |/ ____ \\
      \____/  |_/_/    \_\\
"""

BASE_CONFIG = """esphome:
  name: {name}
"""

BASE_CONFIG_FRIENDLY = """esphome:
  name: {name}
  friendly_name: {friendly_name}
"""

LOGGER_CONFIG = """
# Enable logging
logger:
"""

API_CONFIG = """
# Enable Home Assistant API
api:
"""

ESP8266_CONFIG = """
esp8266:
  board: {board}
"""

ESP32_CONFIG = """
esp32:
  board: {board}
  framework:
    type: esp-idf
"""

RP2040_CONFIG = """
rp2040:
  board: {board}
"""

BK72XX_CONFIG = """
bk72xx:
  board: {board}
"""

LN882X_CONFIG = """
ln882x:
  board: {board}
"""

RTL87XX_CONFIG = """
rtl87xx:
  board: {board}
"""

HARDWARE_BASE_CONFIGS = {
    "ESP8266": ESP8266_CONFIG,
    "ESP32": ESP32_CONFIG,
    "RP2040": RP2040_CONFIG,
    "BK72XX": BK72XX_CONFIG,
    "LN882X": LN882X_CONFIG,
    "RTL87XX": RTL87XX_CONFIG,
}


def sanitize_double_quotes(value: str) -> str:
    return value.replace("\\", "\\\\").replace('"', '\\"')


class WizardFileKwargs(TypedDict):
    """Keyword arguments for wizard_file function."""

    name: str
    platform: Literal["ESP8266", "ESP32", "RP2040", "BK72XX", "LN882X", "RTL87XX"]
    board: str
    ssid: NotRequired[str]
    psk: NotRequired[str]
    ota_password: NotRequired[str]
    api_encryption_key: NotRequired[str]
    friendly_name: NotRequired[str]


def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str:
    letters = string.ascii_letters + string.digits
    ap_name_base = kwargs["name"].replace("_", " ").title()
    ap_name = f"{ap_name_base} Fallback Hotspot"
    if len(ap_name) > 32:
        ap_name = ap_name_base
    kwargs["fallback_name"] = ap_name
    kwargs["fallback_psk"] = "".join(secrets.choice(letters) for _ in range(12))

    base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG

    config = base.format(**kwargs)

    config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs)

    config += LOGGER_CONFIG

    if kwargs["board"] == "rpipico":
        return config

    config += API_CONFIG

    # Configure API encryption
    if "api_encryption_key" in kwargs:
        config += f'  encryption:\n    key: "{kwargs["api_encryption_key"]}"\n'

    # Configure OTA
    config += "\nota:\n"
    config += "  - platform: esphome\n"
    if "ota_password" in kwargs:
        config += f'    password: "{kwargs["ota_password"]}"'

    # Configuring wifi
    config += "\n\nwifi:\n"

    if "ssid" in kwargs:
        if kwargs["ssid"].startswith("!secret"):
            template = "  ssid: {ssid}\n  password: {psk}\n"
        else:
            template = """  ssid: "{ssid}"\n  password: "{psk}"\n"""
        config += template.format(**kwargs)
    else:
        config += """  # ssid: "My SSID"
  # password: "mypassword"

  networks:
"""

    # pylint: disable=consider-using-f-string
    if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "LN882X", "RTL87XX"]:
        config += """
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "{fallback_name}"
    password: "{fallback_psk}"

captive_portal:
    """.format(**kwargs)
    else:
        config += """
  # Enable fallback hotspot in case wifi connection fails
  ap:
    ssid: "{fallback_name}"
    password: "{fallback_psk}"
    """.format(**kwargs)

    return config


class WizardWriteKwargs(TypedDict):
    """Keyword arguments for wizard_write function."""

    name: str
    type: Literal["basic", "empty", "upload"]
    # Required for "basic" type
    board: NotRequired[str]
    platform: NotRequired[str]
    ssid: NotRequired[str]
    psk: NotRequired[str]
    ota_password: NotRequired[str]
    api_encryption_key: NotRequired[str]
    friendly_name: NotRequired[str]
    # Required for "upload" type
    file_text: NotRequired[str]


def wizard_write(path: Path, **kwargs: Unpack[WizardWriteKwargs]) -> bool:
    from esphome.components.bk72xx import boards as bk72xx_boards
    from esphome.components.esp32 import boards as esp32_boards
    from esphome.components.esp8266 import boards as esp8266_boards
    from esphome.components.ln882x import boards as ln882x_boards
    from esphome.components.rp2040 import boards as rp2040_boards
    from esphome.components.rtl87xx import boards as rtl87xx_boards

    name = kwargs["name"]
    if kwargs["type"] == "empty":
        file_text = ""
        # Will be updated later after editing the file
        hardware = "UNKNOWN"
    elif kwargs["type"] == "upload":
        file_text = kwargs["file_text"]
        hardware = "UNKNOWN"
    else:  # "basic"
        board = kwargs["board"]

        for key in ("ssid", "psk", "ota_password"):
            if key in kwargs:
                kwargs[key] = sanitize_double_quotes(kwargs[key])
        if "platform" not in kwargs:
            if board in esp8266_boards.BOARDS:
                platform = "ESP8266"
            elif board in esp32_boards.BOARDS:
                platform = "ESP32"
            elif board in rp2040_boards.BOARDS:
                platform = "RP2040"
            elif board in bk72xx_boards.BOARDS:
                platform = "BK72XX"
            elif board in ln882x_boards.BOARDS:
                platform = "LN882X"
            elif board in rtl87xx_boards.BOARDS:
                platform = "RTL87XX"
            else:
                safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.'))
                return False
            kwargs["platform"] = platform
        hardware = kwargs["platform"]
        file_text = wizard_file(**kwargs)

    # Check if file already exists to prevent overwriting
    if path.exists() and path.is_file():
        safe_print(color(AnsiFore.RED, f'The file "{path}" already exists.'))
        return False

    write_file(path, file_text)
    storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware)
    storage_path = ext_storage_path(path.name)
    storage.save(storage_path)

    return True


if get_bool_env(ENV_QUICKWIZARD):

    def sleep(time: float) -> None:
        pass

else:
    from time import sleep


def safe_print_step(step: int, big: str) -> None:
    safe_print()
    safe_print()
    safe_print(f"============= STEP {step} =============")
    safe_print(big)
    safe_print("===================================")
    sleep(0.25)


def default_input(text: str, default: str) -> str:
    safe_print()
    safe_print(f"Press ENTER for default ({default})")
    return safe_input(text.format(default)) or default


# From https://stackoverflow.com/a/518232/8924614
def strip_accents(value: str) -> str:
    return "".join(
        c
        for c in unicodedata.normalize("NFD", str(value))
        if unicodedata.category(c) != "Mn"
    )


def wizard(path: Path) -> int:
    from esphome.components.bk72xx import boards as bk72xx_boards
    from esphome.components.esp32 import boards as esp32_boards
    from esphome.components.esp8266 import boards as esp8266_boards
    from esphome.components.ln882x import boards as ln882x_boards
    from esphome.components.rp2040 import boards as rp2040_boards
    from esphome.components.rtl87xx import boards as rtl87xx_boards

    if path.suffix not in (".yaml", ".yml"):
        safe_print(
            f"Please make your configuration file {color(AnsiFore.CYAN, str(path))} have the extension .yaml or .yml"
        )
        return 1
    if path.exists():
        safe_print(
            f"Uh oh, it seems like {color(AnsiFore.CYAN, str(path))} already exists, please delete that file first or chose another configuration file."
        )
        return 2

    CORE.config_path = path

    safe_print("Hi there!")
    sleep(1.5)
    safe_print("I'm the wizard of ESPHome :)")
    sleep(1.25)
    safe_print("And I'm here to help you get started with ESPHome.")
    sleep(2.0)
    safe_print(
        "In 4 steps I'm going to guide you through creating a basic "
        "configuration file for your custom firmware. Yay!"
    )
    sleep(3.0)
    safe_print()
    safe_print_step(1, CORE_BIG)
    safe_print(
        f"First up, please choose a {color(AnsiFore.GREEN, 'name')} for your node."
    )
    safe_print(
        "It should be a unique name that can be used to identify the device later."
    )
    sleep(1)
    safe_print(
        f"For example, I like calling the node in my living room {color(AnsiFore.BOLD_WHITE, 'livingroom')}."
    )
    safe_print()
    sleep(1)
    name = safe_input(color(AnsiFore.BOLD_WHITE, "(name): "))

    while True:
        try:
            name = cv.valid_name(name)
            break
        except vol.Invalid:
            safe_print(
                color(
                    AnsiFore.RED,
                    f'Oh noes, "{name}" isn\'t a valid name. Names can only '
                    f"include numbers, lower-case letters and hyphens. ",
                )
            )
            name = strip_accents(name).lower().replace(" ", "-")
            name = strip_accents(name).lower().replace("_", "-")
            name = "".join(c for c in name if c in ALLOWED_NAME_CHARS)
            safe_print(
                f'Shall I use "{color(AnsiFore.CYAN, name)}" as the name instead?'
            )
            sleep(0.5)
            name = default_input("(name [{}]): ", name)

    safe_print(f'Great! Your node is now called "{color(AnsiFore.CYAN, name)}".')
    sleep(1)
    safe_print_step(2, ESP_BIG)
    safe_print(
        "Now I'd like to know what microcontroller you're using so that I can compile "
        "firmwares for it."
    )

    wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2040"]
    safe_print(
        "Please choose one of the supported microcontrollers "
        "(Use ESP8266 for Sonoff devices)."
    )
    while True:
        sleep(0.5)
        safe_print()
        platform = safe_input(
            color(AnsiFore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")
        )
        try:
            platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper())
            break
        except vol.Invalid:
            safe_print(
                f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.'
            )
    safe_print(
        f"Thanks! You've chosen {color(AnsiFore.CYAN, platform)} as your platform."
    )
    safe_print()
    sleep(1)

    if platform == "ESP32":
        board_link = (
            "https://docs.platformio.org/en/latest/platforms/espressif32.html#boards"
        )
    elif platform == "ESP8266":
        board_link = (
            "https://docs.platformio.org/en/latest/platforms/espressif8266.html#boards"
        )
    elif platform == "RP2040":
        board_link = "https://www.raspberrypi.com/documentation/microcontrollers/silicon.html#rp2040"
    elif platform in ["BK72XX", "LN882X", "RTL87XX"]:
        board_link = "https://docs.libretiny.eu/docs/status/supported/"
    else:
        raise NotImplementedError("Unknown platform!")

    safe_print(
        f"Next, I need to know what {color(AnsiFore.GREEN, 'board')} you're using."
    )
    sleep(0.5)
    safe_print(f"Please go to {color(AnsiFore.GREEN, board_link)} and choose a board.")
    if platform == "ESP32":
        safe_print(f"(Type {color(AnsiFore.GREEN, 'esp01_1m')} for Sonoff devices)")
    safe_print()
    # Don't sleep because user needs to copy link
    if platform == "ESP32":
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcu-32s")}".')
        boards_list = esp32_boards.BOARDS.items()
    elif platform == "ESP8266":
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcuv2")}".')
        boards_list = esp8266_boards.BOARDS.items()
    elif platform == "BK72XX":
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".')
        boards_list = bk72xx_boards.BOARDS.items()
    elif platform == "LN882X":
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wl2s")}".')
        boards_list = ln882x_boards.BOARDS.items()
    elif platform == "RTL87XX":
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".')
        boards_list = rtl87xx_boards.BOARDS.items()
    elif platform == "RP2040":
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "rpipicow")}".')
        boards_list = rp2040_boards.BOARDS.items()

    else:
        raise NotImplementedError("Unknown platform!")

    boards = []
    safe_print("Options:")
    for board_id, board_data in boards_list:
        safe_print(f" - {board_id} - {board_data['name']}")
        boards.append(board_id.lower())

    while True:
        board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): "))
        try:
            board = vol.All(vol.Lower, vol.Any(*boards))(board)
            break
        except vol.Invalid:
            safe_print(
                color(
                    AnsiFore.RED, f'Sorry, I don\'t think the board "{board}" exists.'
                )
            )
            safe_print()
            sleep(0.25)
            safe_print()

    safe_print(f"Way to go! You've chosen {color(AnsiFore.CYAN, board)} as your board.")
    safe_print()
    sleep(1)

    # Do not create wifi if the board does not support it
    if board != "rpipico":
        safe_print_step(3, WIFI_BIG)
        safe_print("In this step, I'm going to create the configuration for WiFi.")
        safe_print()
        sleep(1)
        safe_print(
            f"First, what's the {color(AnsiFore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
        )
        sleep(1.5)
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "Abraham Linksys")}".')
        while True:
            ssid = safe_input(color(AnsiFore.BOLD_WHITE, "(ssid): "))
            try:
                ssid = cv.ssid(ssid)
                break
            except vol.Invalid:
                safe_print(
                    color(
                        AnsiFore.RED,
                        f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.',
                    )
                )
                safe_print()
                sleep(1)

        safe_print(
            f'Thank you very much! You\'ve just chosen "{color(AnsiFore.CYAN, ssid)}" as your SSID.'
        )
        safe_print()
        sleep(0.75)

        safe_print(
            f"Now please state the {color(AnsiFore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
        )
        safe_print()
        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "PASSWORD42")}"')
        sleep(0.5)
        psk = safe_input(color(AnsiFore.BOLD_WHITE, "(PSK): "))
        safe_print(
            "Perfect! WiFi is now set up (you can create static IPs and so on later)."
        )
        sleep(1.5)

        safe_print_step(4, OTA_BIG)
        safe_print(
            "Almost there! ESPHome can automatically upload custom firmwares over WiFi "
            "(over the air) and integrates into Home Assistant with a native API."
        )
        safe_print()
        sleep(0.5)

        # Generate encryption key (32 bytes, base64 encoded) for secure API communication
        noise_psk = secrets.token_bytes(32)
        api_encryption_key = base64.b64encode(noise_psk).decode()

        safe_print(
            "For secure API communication, I've generated a random encryption key."
        )
        safe_print()
        safe_print(
            f"Your {color(AnsiFore.GREEN, 'API encryption key')} is: "
            f"{color(AnsiFore.BOLD_WHITE, api_encryption_key)}"
        )
        safe_print()
        safe_print("You'll need this key when adding the device to Home Assistant.")
        sleep(1)

        safe_print()
        safe_print(
            f"Do you want to set a {color(AnsiFore.GREEN, 'password')} for OTA updates? "
            "This can be insecure if you do not trust the WiFi network."
        )
        safe_print()
        sleep(0.25)
        safe_print("Press ENTER for no password")
        ota_password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): "))
    else:
        ssid, psk = "", ""
        api_encryption_key = None
        ota_password = ""

    kwargs = {
        "path": path,
        "name": name,
        "platform": platform,
        "board": board,
        "ssid": ssid,
        "psk": psk,
        "type": "basic",
    }
    if api_encryption_key:
        kwargs["api_encryption_key"] = api_encryption_key
    if ota_password:
        kwargs["ota_password"] = ota_password

    if not wizard_write(**kwargs):
        return 1

    safe_print()
    safe_print(
        color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ")
        + color(AnsiFore.BOLD_CYAN, str(path))
    )
    safe_print()
    safe_print("Next steps:")
    safe_print("  > Follow the rest of the getting started guide:")
    safe_print(
        "  > https://esphome.io/guides/getting_started_command_line/#adding-some-features"
    )
    safe_print("  > to learn how to customize ESPHome and install it to your device.")
    return 0
