import os
import random
import string
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):
    return value.replace("\\", "\\\\").replace('"', '\\"')


def wizard_file(**kwargs):
    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(random.choice(letters) for _ in range(12))

    if kwargs.get("friendly_name"):
        base = BASE_CONFIG_FRIENDLY
    else:
        base = 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
    if "password" in kwargs:
        config += f'  password: "{kwargs["password"]}"\n'
    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"]}"'
    elif "password" in kwargs:
        config += f'    password: "{kwargs["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


def wizard_write(path, **kwargs):
    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"]
    board = kwargs["board"]

    for key in ("ssid", "psk", "password", "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"]

    write_file(path, wizard_file(**kwargs))
    storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware)
    storage_path = ext_storage_path(os.path.basename(path))
    storage.save(storage_path)

    return True


if get_bool_env(ENV_QUICKWIZARD):

    def sleep(time):
        pass

else:
    from time import sleep


def safe_print_step(step, big):
    safe_print()
    safe_print()
    safe_print(f"============= STEP {step} =============")
    safe_print(big)
    safe_print("===================================")
    sleep(0.25)


def default_input(text, default):
    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):
    return "".join(
        c
        for c in unicodedata.normalize("NFD", str(value))
        if unicodedata.category(c) != "Mn"
    )


def wizard(path):
    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 not path.endswith(".yaml") and not path.endswith(".yml"):
        safe_print(
            f"Please make your configuration file {color(AnsiFore.CYAN, path)} have the extension .yaml or .yml"
        )
        return 1
    if os.path.exists(path):
        safe_print(
            f"Uh oh, it seems like {color(AnsiFore.CYAN, 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/rp2040.html"
        )
    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)

    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 not in ["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(
            f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?"
        )
        safe_print()
        sleep(0.25)
        safe_print("Press ENTER for no password")
        password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): "))
    else:
        ssid, password, psk = "", "", ""

    if not wizard_write(
        path=path,
        name=name,
        platform=platform,
        board=board,
        ssid=ssid,
        psk=psk,
        password=password,
    ):
        return 1

    safe_print()
    safe_print(
        color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ")
        + color(AnsiFore.BOLD_CYAN, 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.html#adding-some-features"
    )
    safe_print("  > to learn how to customize ESPHome and install it to your device.")
    return 0
