"""Manifest validation."""
from __future__ import annotations

from enum import IntEnum
import json
from pathlib import Path
import subprocess
from typing import Any
from urllib.parse import urlparse

from awesomeversion import (
    AwesomeVersion,
    AwesomeVersionException,
    AwesomeVersionStrategy,
)
import voluptuous as vol
from voluptuous.humanize import humanize_error

from homeassistant.const import Platform
from homeassistant.helpers import config_validation as cv

from .model import Config, Integration

DOCUMENTATION_URL_SCHEMA = "https"
DOCUMENTATION_URL_HOST = "www.home-assistant.io"
DOCUMENTATION_URL_PATH_PREFIX = "/integrations/"
DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"}


class QualityScale(IntEnum):
    """Supported manifest quality scales."""

    INTERNAL = -1
    SILVER = 1
    GOLD = 2
    PLATINUM = 3


SUPPORTED_QUALITY_SCALES = [enum.name.lower() for enum in QualityScale]
SUPPORTED_IOT_CLASSES = [
    "assumed_state",
    "calculated",
    "cloud_polling",
    "cloud_push",
    "local_polling",
    "local_push",
]

# List of integrations that are supposed to have no IoT class
NO_IOT_CLASS = [
    *{platform.value for platform in Platform},
    "api",
    "application_credentials",
    "auth",
    "automation",
    "blueprint",
    "color_extractor",
    "config",
    "configurator",
    "counter",
    "default_config",
    "device_automation",
    "device_tracker",
    "diagnostics",
    "discovery",
    "downloader",
    "ffmpeg",
    "file_upload",
    "frontend",
    "hardkernel",
    "hardware",
    "history",
    "homeassistant",
    "homeassistant_alerts",
    "homeassistant_hardware",
    "homeassistant_sky_connect",
    "homeassistant_yellow",
    "image_upload",
    "input_boolean",
    "input_button",
    "input_datetime",
    "input_number",
    "input_select",
    "input_text",
    "intent_script",
    "intent",
    "logbook",
    "logger",
    "lovelace",
    "map",
    "media_source",
    "my",
    "onboarding",
    "panel_custom",
    "panel_iframe",
    "plant",
    "profiler",
    "proxy",
    "python_script",
    "raspberry_pi",
    "repairs",
    "safe_mode",
    "schedule",
    "script",
    "search",
    "system_health",
    "system_log",
    "tag",
    "timer",
    "trace",
    "webhook",
    "websocket_api",
    "zone",
]


def documentation_url(value: str) -> str:
    """Validate that a documentation url has the correct path and domain."""
    if value in DOCUMENTATION_URL_EXCEPTIONS:
        return value

    parsed_url = urlparse(value)
    if parsed_url.scheme != DOCUMENTATION_URL_SCHEMA:
        raise vol.Invalid("Documentation url is not prefixed with https")
    if parsed_url.netloc == DOCUMENTATION_URL_HOST and not parsed_url.path.startswith(
        DOCUMENTATION_URL_PATH_PREFIX
    ):
        raise vol.Invalid(
            "Documentation url does not begin with www.home-assistant.io/integrations"
        )

    return value


def verify_lowercase(value: str) -> str:
    """Verify a value is lowercase."""
    if value.lower() != value:
        raise vol.Invalid("Value needs to be lowercase")

    return value


def verify_uppercase(value: str) -> str:
    """Verify a value is uppercase."""
    if value.upper() != value:
        raise vol.Invalid("Value needs to be uppercase")

    return value


def verify_version(value: str) -> str:
    """Verify the version."""
    try:
        AwesomeVersion(
            value,
            ensure_strategy=[
                AwesomeVersionStrategy.CALVER,
                AwesomeVersionStrategy.SEMVER,
                AwesomeVersionStrategy.SIMPLEVER,
                AwesomeVersionStrategy.BUILDVER,
                AwesomeVersionStrategy.PEP440,
            ],
        )
    except AwesomeVersionException:
        raise vol.Invalid(f"'{value}' is not a valid version.")
    return value


def verify_wildcard(value: str) -> str:
    """Verify the matcher contains a wildcard."""
    if "*" not in value:
        raise vol.Invalid(f"'{value}' needs to contain a wildcard matcher")
    return value


INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
    {
        vol.Required("domain"): str,
        vol.Required("name"): str,
        vol.Optional("integration_type", default="hub"): vol.In(
            [
                "device",
                "entity",
                "hardware",
                "helper",
                "hub",
                "service",
                "system",
            ]
        ),
        vol.Optional("config_flow"): bool,
        vol.Optional("mqtt"): [str],
        vol.Optional("zeroconf"): [
            vol.Any(
                str,
                vol.All(
                    cv.deprecated("macaddress"),
                    cv.deprecated("model"),
                    cv.deprecated("manufacturer"),
                    vol.Schema(
                        {
                            vol.Required("type"): str,
                            vol.Optional("macaddress"): vol.All(
                                str, verify_uppercase, verify_wildcard
                            ),
                            vol.Optional("manufacturer"): vol.All(
                                str, verify_lowercase
                            ),
                            vol.Optional("model"): vol.All(str, verify_lowercase),
                            vol.Optional("name"): vol.All(str, verify_lowercase),
                            vol.Optional("properties"): vol.Schema(
                                {str: verify_lowercase}
                            ),
                        }
                    ),
                ),
            )
        ],
        vol.Optional("ssdp"): vol.Schema(
            vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))])
        ),
        vol.Optional("bluetooth"): [
            vol.Schema(
                {
                    vol.Optional("connectable"): bool,
                    vol.Optional("service_uuid"): vol.All(str, verify_lowercase),
                    vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase),
                    vol.Optional("local_name"): vol.All(str),
                    vol.Optional("manufacturer_id"): int,
                    vol.Optional("manufacturer_data_start"): [int],
                }
            )
        ],
        vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}),
        vol.Optional("dhcp"): [
            vol.Schema(
                {
                    vol.Optional("macaddress"): vol.All(
                        str, verify_uppercase, verify_wildcard
                    ),
                    vol.Optional("hostname"): vol.All(str, verify_lowercase),
                    vol.Optional("registered_devices"): cv.boolean,
                }
            )
        ],
        vol.Optional("usb"): [
            vol.Schema(
                {
                    vol.Optional("vid"): vol.All(str, verify_uppercase),
                    vol.Optional("pid"): vol.All(str, verify_uppercase),
                    vol.Optional("serial_number"): vol.All(str, verify_lowercase),
                    vol.Optional("manufacturer"): vol.All(str, verify_lowercase),
                    vol.Optional("description"): vol.All(str, verify_lowercase),
                    vol.Optional("known_devices"): [str],
                }
            )
        ],
        vol.Required("documentation"): vol.All(
            vol.Url(), documentation_url  # pylint: disable=no-value-for-parameter
        ),
        vol.Optional(
            "issue_tracker"
        ): vol.Url(),  # pylint: disable=no-value-for-parameter
        vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES),
        vol.Optional("requirements"): [str],
        vol.Optional("dependencies"): [str],
        vol.Optional("after_dependencies"): [str],
        vol.Required("codeowners"): [str],
        vol.Optional("loggers"): [str],
        vol.Optional("disabled"): str,
        vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES),
    }
)

VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
    {
        vol.Required("domain"): str,
        vol.Required("name"): str,
        vol.Required("integration_type"): "virtual",
        vol.Exclusive("iot_standards", "virtual_integration"): [
            vol.Any("homekit", "zigbee", "zwave")
        ],
        vol.Exclusive("supported_by", "virtual_integration"): str,
    }
)


def manifest_schema(value: dict[str, Any]) -> vol.Schema:
    """Validate integration manifest."""
    if value.get("integration_type") == "virtual":
        return VIRTUAL_INTEGRATION_MANIFEST_SCHEMA(value)
    return INTEGRATION_MANIFEST_SCHEMA(value)


CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend(
    {
        vol.Optional("version"): vol.All(str, verify_version),
    }
)


def validate_version(integration: Integration) -> None:
    """Validate the version of the integration.

    Will be removed when the version key is no longer optional for custom integrations.
    """
    if not integration.manifest.get("version"):
        integration.add_error("manifest", "No 'version' key in the manifest file.")
        return


def validate_manifest(integration: Integration, core_components_dir: Path) -> None:
    """Validate manifest."""
    try:
        if integration.core:
            manifest_schema(integration.manifest)
        else:
            CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
    except vol.Invalid as err:
        integration.add_error(
            "manifest", f"Invalid manifest: {humanize_error(integration.manifest, err)}"
        )

    if (domain := integration.manifest["domain"]) != integration.path.name:
        integration.add_error("manifest", "Domain does not match dir name")

    if not integration.core and (core_components_dir / domain).exists():
        integration.add_warning(
            "manifest", "Domain collides with built-in core integration"
        )

    if domain in NO_IOT_CLASS and "iot_class" in integration.manifest:
        integration.add_error("manifest", "Domain should not have an IoT Class")

    if (
        domain not in NO_IOT_CLASS
        and "iot_class" not in integration.manifest
        and integration.manifest.get("integration_type") != "virtual"
    ):
        integration.add_error("manifest", "Domain is missing an IoT Class")

    if (
        integration.manifest.get("integration_type") == "virtual"
        and (supported_by := integration.manifest.get("supported_by"))
        and not (core_components_dir / supported_by).exists()
    ):
        integration.add_error(
            "manifest",
            "Virtual integration points to non-existing supported_by integration",
        )

    if (
        (quality_scale := integration.manifest.get("quality_scale"))
        and QualityScale[quality_scale.upper()] > QualityScale.SILVER
        and not integration.manifest.get("codeowners")
    ):
        integration.add_error(
            "manifest",
            f"{quality_scale} integration does not have a code owner",
        )

    if not integration.core:
        validate_version(integration)


_SORT_KEYS = {"domain": ".domain", "name": ".name"}


def _sort_manifest_keys(key: str) -> str:
    return _SORT_KEYS.get(key, key)


def sort_manifest(integration: Integration) -> bool:
    """Sort manifest."""
    keys = list(integration.manifest.keys())
    if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys:
        manifest = {key: integration.manifest[key] for key in keys_sorted}
        integration.manifest_path.write_text(json.dumps(manifest, indent=2))
        integration.add_error(
            "manifest",
            "Manifest keys have been sorted: domain, name, then alphabetical order",
        )
        return True
    return False


def validate(integrations: dict[str, Integration], config: Config) -> None:
    """Handle all integrations manifests."""
    core_components_dir = config.root / "homeassistant/components"
    manifests_resorted = []
    for integration in integrations.values():
        validate_manifest(integration, core_components_dir)
        if not integration.errors:
            if sort_manifest(integration):
                manifests_resorted.append(integration.manifest_path)
    if manifests_resorted:
        subprocess.run(
            ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"]
            + manifests_resorted,
            stdout=subprocess.DEVNULL,
        )
