import hashlib
from pathlib import Path
import re

import requests

from esphome import pins
import esphome.codegen as cg
from esphome.components import light, sensor, uart
import esphome.config_validation as cv
from esphome.const import (
    CONF_CURRENT,
    CONF_GAMMA_CORRECT,
    CONF_MAX_BRIGHTNESS,
    CONF_MIN_BRIGHTNESS,
    CONF_OUTPUT_ID,
    CONF_POWER,
    CONF_UPDATE_INTERVAL,
    CONF_URL,
    CONF_VERSION,
    CONF_VOLTAGE,
    DEVICE_CLASS_CURRENT,
    DEVICE_CLASS_POWER,
    DEVICE_CLASS_VOLTAGE,
    STATE_CLASS_MEASUREMENT,
    UNIT_AMPERE,
    UNIT_VOLT,
    UNIT_WATT,
)
from esphome.core import CORE, HexInt

DOMAIN = "shelly_dimmer"
AUTO_LOAD = ["sensor"]
DEPENDENCIES = ["uart", "esp8266"]

shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer")
ShellyDimmer = shelly_dimmer_ns.class_(
    "ShellyDimmer", light.LightOutput, cg.PollingComponent, uart.UARTDevice
)

CONF_FIRMWARE = "firmware"
CONF_SHA256 = "sha256"
CONF_UPDATE = "update"

CONF_LEADING_EDGE = "leading_edge"
CONF_WARMUP_BRIGHTNESS = "warmup_brightness"
# CONF_WARMUP_TIME = "warmup_time"


CONF_NRST_PIN = "nrst_pin"
CONF_BOOT0_PIN = "boot0_pin"

KNOWN_FIRMWARE = {
    "51.5": (
        "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.5/shelly-dimmer-stm32_v51.5.bin",
        "553fc1d78ed113227af7683eaa9c26189a961c4ea9a48000fb5aa8f8ac5d7b60",
    ),
    "51.6": (
        "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin",
        "eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36",
    ),
    "51.7": (
        "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.7/shelly-dimmer-stm32_v51.7.bin",
        "7a20f1c967c469917368a79bc56498009045237080408cef7190743e08031889",
    ),
}


def parse_firmware_version(value):
    match = re.fullmatch(r"(\d+)\.(\d+)", value)
    if match is None:
        raise ValueError(f"Not a valid version number {value}")
    major = int(match[1])
    minor = int(match[2])
    return major, minor


def get_firmware(value):
    if not value[CONF_UPDATE]:
        return None

    def dl(url):
        try:
            req = requests.get(url, timeout=30)
            req.raise_for_status()
        except requests.exceptions.RequestException as e:
            raise cv.Invalid(f"Could not download firmware file ({url}): {e}") from e

        h = hashlib.new("sha256")
        h.update(req.content)
        return req.content, h.hexdigest()

    url = value[CONF_URL]

    if CONF_SHA256 in value:  # we have a hash, enable caching
        path = Path(CORE.data_dir) / DOMAIN / (value[CONF_SHA256] + "_fw_stm.bin")

        if not path.is_file():
            firmware_data, dl_hash = dl(url)

            if dl_hash != value[CONF_SHA256]:
                raise cv.Invalid(
                    f"Hash mismatch for {url}: {dl_hash} != {value[CONF_SHA256]}"
                )

            path.parent.mkdir(exist_ok=True, parents=True)
            path.write_bytes(firmware_data)

        else:
            firmware_data = path.read_bytes()
    else:  # no caching, download every time
        firmware_data, dl_hash = dl(url)

    return [HexInt(x) for x in firmware_data]


def validate_firmware(value):
    config = value.copy()
    if CONF_URL not in config:
        try:
            config[CONF_URL], config[CONF_SHA256] = KNOWN_FIRMWARE[config[CONF_VERSION]]
        except KeyError as e:
            raise cv.Invalid(
                f"Firmware {config[CONF_VERSION]} is unknown, please specify an '{CONF_URL}' ..."
            ) from e
    get_firmware(config)
    return config


def validate_sha256(value):
    value = cv.string(value)
    if not re.fullmatch(r"[0-9a-fA-F]{64}", value):
        raise ValueError(f"Not a valid SHA256 hex string: {value}")
    return value


def validate_version(value):
    parse_firmware_version(value)
    return value


CONFIG_SCHEMA = (
    light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
        {
            cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ShellyDimmer),
            cv.Optional(CONF_FIRMWARE, default="51.6"): cv.maybe_simple_value(
                {
                    cv.Optional(CONF_URL): cv.url,
                    cv.Optional(CONF_SHA256): validate_sha256,
                    cv.Required(CONF_VERSION): validate_version,
                    cv.Optional(CONF_UPDATE, default=False): cv.boolean,
                },
                validate_firmware,  # converts a simple version key to generate the full url
                key=CONF_VERSION,
            ),
            cv.Optional(CONF_NRST_PIN, default="GPIO5"): pins.gpio_output_pin_schema,
            cv.Optional(CONF_BOOT0_PIN, default="GPIO4"): pins.gpio_output_pin_schema,
            cv.Optional(CONF_LEADING_EDGE, default=False): cv.boolean,
            cv.Optional(CONF_WARMUP_BRIGHTNESS, default=100): cv.uint16_t,
            # cv.Optional(CONF_WARMUP_TIME, default=20): cv.uint16_t,
            cv.Optional(CONF_MIN_BRIGHTNESS, default=0): cv.uint16_t,
            cv.Optional(CONF_MAX_BRIGHTNESS, default=1000): cv.uint16_t,
            cv.Optional(CONF_POWER): sensor.sensor_schema(
                unit_of_measurement=UNIT_WATT,
                accuracy_decimals=1,
                device_class=DEVICE_CLASS_POWER,
                state_class=STATE_CLASS_MEASUREMENT,
            ),
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
                unit_of_measurement=UNIT_VOLT,
                accuracy_decimals=1,
                device_class=DEVICE_CLASS_VOLTAGE,
                state_class=STATE_CLASS_MEASUREMENT,
            ),
            cv.Optional(CONF_CURRENT): sensor.sensor_schema(
                unit_of_measurement=UNIT_AMPERE,
                device_class=DEVICE_CLASS_CURRENT,
                accuracy_decimals=2,
                state_class=STATE_CLASS_MEASUREMENT,
            ),
            # Change the default gamma_correct setting.
            cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
        }
    )
    .extend(cv.polling_component_schema("10s"))
    .extend(uart.UART_DEVICE_SCHEMA)
)


async def to_code(config):
    fw_hex = get_firmware(config[CONF_FIRMWARE])
    fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION])

    if fw_hex is not None:
        cg.add_define("USE_SHD_FIRMWARE_DATA", fw_hex)
    cg.add_define("USE_SHD_FIRMWARE_MAJOR_VERSION", fw_major)
    cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor)

    var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
    await cg.register_component(var, config)
    config.pop(
        CONF_UPDATE_INTERVAL
    )  # drop UPDATE_INTERVAL as it does not apply to the light component

    await light.register_light(var, config)
    await uart.register_uart_device(var, config)

    nrst_pin = await cg.gpio_pin_expression(config[CONF_NRST_PIN])
    cg.add(var.set_nrst_pin(nrst_pin))
    boot0_pin = await cg.gpio_pin_expression(config[CONF_BOOT0_PIN])
    cg.add(var.set_boot0_pin(boot0_pin))

    cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE]))
    cg.add(var.set_warmup_brightness(config[CONF_WARMUP_BRIGHTNESS]))
    # cg.add(var.set_warmup_time(config[CONF_WARMUP_TIME]))
    cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS]))
    cg.add(var.set_max_brightness(config[CONF_MAX_BRIGHTNESS]))

    for key in [CONF_POWER, CONF_VOLTAGE, CONF_CURRENT]:
        if key not in config:
            continue

        conf = config[key]
        sens = await sensor.new_sensor(conf)
        cg.add(getattr(var, f"set_{key}_sensor")(sens))
