from __future__ import annotations

import hmac
import os
from pathlib import Path
from typing import Any

from esphome.core import CORE
from esphome.helpers import get_bool_env

from .util.password import password_hash

# Sentinel file name used for CORE.config_path when dashboard initializes.
# This ensures .parent returns the config directory instead of root.
_DASHBOARD_SENTINEL_FILE = "___DASHBOARD_SENTINEL___.yaml"


class DashboardSettings:
    """Settings for the dashboard."""

    __slots__ = (
        "config_dir",
        "password_hash",
        "username",
        "using_password",
        "on_ha_addon",
        "cookie_secret",
        "absolute_config_dir",
        "verbose",
    )

    def __init__(self) -> None:
        """Initialize the dashboard settings."""
        self.config_dir: Path = None
        self.password_hash: bytes = b""
        self.username: str = ""
        self.using_password: bool = False
        self.on_ha_addon: bool = False
        self.cookie_secret: str | None = None
        self.absolute_config_dir: Path | None = None
        self.verbose: bool = False

    def parse_args(self, args: Any) -> None:
        """Parse the arguments."""
        self.on_ha_addon: bool = args.ha_addon
        password = args.password or os.getenv("PASSWORD") or ""
        if not self.on_ha_addon:
            self.username = args.username or os.getenv("USERNAME") or ""
            self.using_password = bool(password)
        if self.using_password:
            self.password_hash = password_hash(password)
        self.config_dir = Path(args.configuration)
        self.absolute_config_dir = self.config_dir.resolve()
        self.verbose = args.verbose
        # Set to a sentinel file so .parent gives us the config directory.
        # Previously this was `os.path.join(self.config_dir, ".")` which worked because
        # os.path.dirname("/config/.") returns "/config", but Path("/config/.").parent
        # normalizes to Path("/config") first, then .parent returns Path("/"), breaking
        # secret resolution. Using a sentinel file ensures .parent gives the correct directory.
        CORE.config_path = self.config_dir / _DASHBOARD_SENTINEL_FILE

    @property
    def relative_url(self) -> str:
        return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL") or "/"

    @property
    def status_use_mqtt(self) -> bool:
        return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT")

    @property
    def using_ha_addon_auth(self) -> bool:
        if not self.on_ha_addon:
            return False
        return not get_bool_env("DISABLE_HA_AUTHENTICATION")

    @property
    def using_auth(self) -> bool:
        return self.using_password or self.using_ha_addon_auth

    @property
    def streamer_mode(self) -> bool:
        return get_bool_env("ESPHOME_STREAMER_MODE")

    def check_password(self, username: str, password: str) -> bool:
        if not self.using_auth:
            return True
        # Compare in constant running time (to prevent timing attacks)
        username_matches = hmac.compare_digest(
            username.encode("utf-8"), self.username.encode("utf-8")
        )
        password_matches = hmac.compare_digest(
            self.password_hash, password_hash(password)
        )
        return username_matches and password_matches

    def rel_path(self, *args: Any) -> Path:
        """Return a path relative to the ESPHome config folder."""
        joined_path = self.config_dir / Path(*args)
        # Raises ValueError if not relative to ESPHome config folder
        joined_path.resolve().relative_to(self.absolute_config_dir)
        return joined_path
