"""Configuration management for WebDrop Bridge application."""

import json
import logging
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List

from dotenv import load_dotenv

logger = logging.getLogger(__name__)

DEFAULT_BRAND_ID = "webdrop_bridge"
DEFAULT_CONFIG_DIR_NAME = "webdrop_bridge"
DEFAULT_UPDATE_BASE_URL = "https://git.him-tools.de"
DEFAULT_UPDATE_REPO = "HIM-public/webdrop-bridge"
DEFAULT_UPDATE_CHANNEL = "stable"
DEFAULT_UPDATE_MANIFEST_NAME = "release-manifest.json"
DEFAULT_ACTIVE_BRANDING_ID = "default"
DEFAULT_APP_ICON_PATH = "resources/icons/app.ico"
DEFAULT_TOOLBAR_ICON_HOME = "resources/icons/home.ico"
DEFAULT_TOOLBAR_ICON_RELOAD = "resources/icons/reload.ico"
DEFAULT_TOOLBAR_ICON_OPEN = "resources/icons/open.ico"
DEFAULT_TOOLBAR_ICON_OPENWITH = "resources/icons/openwith.ico"


class ConfigurationError(Exception):
    """Raised when configuration is invalid."""

    pass


@dataclass
class URLMapping:
    """Maps an Azure Blob Storage URL prefix to a local drive path."""

    url_prefix: str
    local_path: str

    def __post_init__(self):
        """Validate mapping configuration."""
        if not self.url_prefix.startswith(("http://", "https://")):
            raise ConfigurationError(
                f"URL prefix must start with http:// or https://: {self.url_prefix}"
            )
        # Ensure URL prefix ends with /
        if not self.url_prefix.endswith("/"):
            self.url_prefix += "/"
        # Normalize local path
        self.local_path = str(Path(self.local_path))


@dataclass
class Config:
    """Application configuration loaded from environment variables or JSON file.

    Attributes:
        app_name: Application display name
        app_version: Application version (semantic versioning)
        log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
        log_file: Optional log file path
        allowed_roots: List of whitelisted root directories for file access
        allowed_urls: List of whitelisted URL domains/patterns (empty = no restriction)
        webapp_url: URL to load in embedded web application (default: https://dev.agravity.io/)
        url_mappings: List of Azure URL to local path mappings
        check_file_exists: Whether to validate that files exist before drag
        auto_check_updates: Whether to automatically check for updates
        update_check_interval_hours: Hours between update checks
        window_width: Initial window width in pixels
        window_height: Initial window height in pixels
        window_title: Main window title (default: "{app_name} v{app_version}")
        enable_logging: Whether to write logs to file
        enable_checkout: Whether to check asset checkout status and show checkout dialog
            on drag. Disabled by default as checkout support is optional.
        brand_id: Stable brand identifier used for packaging and update selection
        config_dir_name: AppData/config directory name for this branded variant
        update_base_url: Base Forgejo URL used for release checks
        update_repo: Forgejo repository containing shared releases
        update_channel: Update channel name used by release manifest selection
        update_manifest_name: Asset name of the shared release manifest

    Raises:
        ConfigurationError: If configuration values are invalid
    """

    app_name: str
    app_version: str
    log_level: str
    log_file: Path | None
    allowed_roots: List[Path]
    allowed_urls: List[str]
    webapp_url: str
    url_mappings: List[URLMapping] = field(default_factory=list)
    check_file_exists: bool = True
    auto_check_updates: bool = True
    update_check_interval_hours: int = 24
    window_width: int = 1024
    window_height: int = 768
    window_title: str = ""
    enable_logging: bool = True
    enable_checkout: bool = False
    language: str = "auto"
    active_branding_id: str = DEFAULT_ACTIVE_BRANDING_ID
    branding_display_name: str = "Default"
    logo_path: str = ""
    app_icon_path: str = DEFAULT_APP_ICON_PATH
    toolbar_icon_home: str = DEFAULT_TOOLBAR_ICON_HOME
    toolbar_icon_reload: str = DEFAULT_TOOLBAR_ICON_RELOAD
    toolbar_icon_open: str = DEFAULT_TOOLBAR_ICON_OPEN
    toolbar_icon_openwith: str = DEFAULT_TOOLBAR_ICON_OPENWITH
    brand_id: str = DEFAULT_BRAND_ID
    config_dir_name: str = DEFAULT_CONFIG_DIR_NAME
    update_base_url: str = DEFAULT_UPDATE_BASE_URL
    update_repo: str = DEFAULT_UPDATE_REPO
    update_channel: str = DEFAULT_UPDATE_CHANNEL
    update_manifest_name: str = DEFAULT_UPDATE_MANIFEST_NAME

    @classmethod
    def from_file(cls, config_path: Path) -> "Config":
        """Load configuration from JSON file.

        Args:
            config_path: Path to configuration file

        Returns:
            Config: Configured instance from JSON file

        Raises:
            ConfigurationError: If configuration file is invalid
        """
        if not config_path.exists():
            raise ConfigurationError(f"Configuration file not found: {config_path}")

        try:
            with open(config_path, "r", encoding="utf-8") as f:
                data = json.load(f)
        except (json.JSONDecodeError, IOError) as e:
            raise ConfigurationError(f"Failed to load configuration: {e}") from e

        # Get version from package
        from webdrop_bridge import __version__

        # Parse URL mappings
        mappings = [
            URLMapping(url_prefix=m["url_prefix"], local_path=m["local_path"])
            for m in data.get("url_mappings", [])
        ]

        # Parse allowed roots
        allowed_roots = [Path(p).resolve() for p in data.get("allowed_roots", [])]

        # Validate allowed roots exist
        for root in allowed_roots:
            if not root.exists():
                logger.warning(f"Allowed root does not exist: {root}")
            elif not root.is_dir():
                raise ConfigurationError(f"Allowed root is not a directory: {root}")

        brand_id = data.get("brand_id", DEFAULT_BRAND_ID)
        config_dir_name = data.get("config_dir_name", cls._slugify_config_dir_name(brand_id))

        # Get log file path
        log_file = None
        if data.get("enable_logging", True):
            log_file_str = data.get("log_file", None)
            if log_file_str:
                log_file = Path(log_file_str)
                # If relative path, resolve relative to app data directory instead of cwd
                if not log_file.is_absolute():
                    log_file = Config.get_default_log_dir(config_dir_name) / log_file
            else:
                # Use default log path in app data
                log_file = Config.get_default_log_path(config_dir_name)

        app_name = data.get("app_name", "WebDrop Bridge")
        stored_window_title = data.get("window_title", "")

        # Regenerate default window titles on version upgrade
        # If the stored title matches the pattern "{app_name} v{version}", regenerate it
        # with the current version. This ensures the title updates automatically on upgrades.
        import re

        version_pattern = re.compile(rf"^{re.escape(app_name)}\s+v[\d.]+$")
        if stored_window_title and version_pattern.match(stored_window_title):
            # Detected a default-pattern title with old version, regenerate
            window_title = f"{app_name} v{__version__}"
        elif stored_window_title:
            # Custom window title, keep it as-is
            window_title = stored_window_title
        else:
            # No window title specified, use default
            window_title = f"{app_name} v{__version__}"

        config = cls(
            app_name=app_name,
            app_version=__version__,
            log_level=data.get("log_level", "INFO").upper(),
            log_file=log_file,
            allowed_roots=allowed_roots,
            allowed_urls=data.get("allowed_urls", []),
            webapp_url=data.get("webapp_url", "https://dev.agravity.io/"),
            url_mappings=mappings,
            check_file_exists=data.get("check_file_exists", True),
            auto_check_updates=data.get("auto_check_updates", True),
            update_check_interval_hours=data.get("update_check_interval_hours", 24),
            window_width=data.get("window_width", 1024),
            window_height=data.get("window_height", 768),
            window_title=window_title,
            enable_logging=data.get("enable_logging", True),
            enable_checkout=data.get("enable_checkout", False),
            language=data.get("language", "auto"),
            active_branding_id=data.get("active_branding_id", DEFAULT_ACTIVE_BRANDING_ID),
            logo_path=data.get("logo_path", ""),
            app_icon_path=data.get("app_icon_path", DEFAULT_APP_ICON_PATH),
            toolbar_icon_home=data.get("toolbar_icon_home", DEFAULT_TOOLBAR_ICON_HOME),
            toolbar_icon_reload=data.get("toolbar_icon_reload", DEFAULT_TOOLBAR_ICON_RELOAD),
            toolbar_icon_open=data.get("toolbar_icon_open", DEFAULT_TOOLBAR_ICON_OPEN),
            toolbar_icon_openwith=data.get("toolbar_icon_openwith", DEFAULT_TOOLBAR_ICON_OPENWITH),
            brand_id=brand_id,
            config_dir_name=config_dir_name,
            update_base_url=data.get("update_base_url", DEFAULT_UPDATE_BASE_URL),
            update_repo=data.get("update_repo", DEFAULT_UPDATE_REPO),
            update_channel=data.get("update_channel", DEFAULT_UPDATE_CHANNEL),
            update_manifest_name=data.get("update_manifest_name", DEFAULT_UPDATE_MANIFEST_NAME),
        )
        return cls._apply_runtime_branding(config)

    @classmethod
    def from_env(cls, env_file: str | None = None) -> "Config":
        """Load configuration from environment variables.

        Looks for a .env file in the current working directory and loads
        environment variables from it. All unset variables use sensible defaults.

        Args:
            env_file: Optional path to .env file to load. If None, loads default .env

        Returns:
            Config: Configured instance with environment values or defaults

        Raises:
            ConfigurationError: If any configured value is invalid
        """
        # Load .env file if it exists
        load_dotenv(env_file)

        # Extract and validate configuration values
        app_name = os.getenv("APP_NAME", "WebDrop Bridge")
        # Version always comes from __init__.py for consistency
        from webdrop_bridge import __version__

        app_version = __version__
        brand_id = os.getenv("BRAND_ID", DEFAULT_BRAND_ID)
        config_dir_name = os.getenv("APP_CONFIG_DIR_NAME", cls._slugify_config_dir_name(brand_id))

        log_level = os.getenv("LOG_LEVEL", "INFO").upper()
        log_file_str = os.getenv("LOG_FILE", None)
        allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public")
        allowed_urls_str = os.getenv("ALLOWED_URLS", "")
        webapp_url = os.getenv("WEBAPP_URL", "https://dev.agravity.io/")
        window_width = int(os.getenv("WINDOW_WIDTH", "1024"))
        window_height = int(os.getenv("WINDOW_HEIGHT", "768"))
        # Window title defaults to app_name + version if not specified
        default_title = f"{app_name} v{app_version}"
        window_title = os.getenv("WINDOW_TITLE", default_title)
        enable_logging = os.getenv("ENABLE_LOGGING", "true").lower() == "true"
        enable_checkout = os.getenv("ENABLE_CHECKOUT", "false").lower() == "true"
        language = os.getenv("LANGUAGE", "auto")
        active_branding_id = os.getenv("BRAND_TEMPLATE", DEFAULT_ACTIVE_BRANDING_ID)
        app_icon_path = os.getenv("APP_ICON_PATH", DEFAULT_APP_ICON_PATH)
        toolbar_icon_home = os.getenv("TOOLBAR_ICON_HOME", DEFAULT_TOOLBAR_ICON_HOME)
        toolbar_icon_reload = os.getenv("TOOLBAR_ICON_RELOAD", DEFAULT_TOOLBAR_ICON_RELOAD)
        toolbar_icon_open = os.getenv("TOOLBAR_ICON_OPEN", DEFAULT_TOOLBAR_ICON_OPEN)
        toolbar_icon_openwith = os.getenv("TOOLBAR_ICON_OPENWITH", DEFAULT_TOOLBAR_ICON_OPENWITH)
        update_base_url = os.getenv("UPDATE_BASE_URL", DEFAULT_UPDATE_BASE_URL)
        update_repo = os.getenv("UPDATE_REPO", DEFAULT_UPDATE_REPO)
        update_channel = os.getenv("UPDATE_CHANNEL", DEFAULT_UPDATE_CHANNEL)
        update_manifest_name = os.getenv("UPDATE_MANIFEST_NAME", DEFAULT_UPDATE_MANIFEST_NAME)

        # Validate log level
        valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
        if log_level not in valid_levels:
            raise ConfigurationError(
                f"Invalid LOG_LEVEL: {log_level}. " f"Must be one of: {', '.join(valid_levels)}"
            )

        # Validate and parse allowed roots
        try:
            allowed_roots = []
            for p in allowed_roots_str.split(","):
                root_path = Path(p.strip()).resolve()
                if not root_path.exists():
                    logger.warning(f"Allowed root does not exist: {p.strip()}")
                elif not root_path.is_dir():
                    raise ConfigurationError(f"Allowed root '{p.strip()}' is not a directory")
                else:
                    allowed_roots.append(root_path)
        except ConfigurationError:
            raise
        except (ValueError, OSError) as e:
            raise ConfigurationError(
                f"Invalid ALLOWED_ROOTS: {allowed_roots_str}. Error: {e}"
            ) from e

        # Validate window dimensions
        if window_width <= 0 or window_height <= 0:
            raise ConfigurationError(
                f"Window dimensions must be positive: " f"{window_width}x{window_height}"
            )

        # Create log file path if logging enabled
        log_file = None
        if enable_logging:
            if log_file_str:
                log_file = Path(log_file_str)
                # If relative path, resolve relative to app data directory instead of cwd
                if not log_file.is_absolute():
                    log_file = Config.get_default_log_dir(config_dir_name) / log_file
            else:
                # Use default log path in app data
                log_file = Config.get_default_log_path(config_dir_name)

        # Validate webapp URL is not empty
        if not webapp_url:
            raise ConfigurationError("WEBAPP_URL cannot be empty")

        # Parse allowed URLs (empty string = no restriction)
        allowed_urls = (
            [url.strip() for url in allowed_urls_str.split(",") if url.strip()]
            if allowed_urls_str
            else []
        )

        # Parse URL mappings (Azure Blob Storage → Local Paths)
        # Format: url_prefix1=local_path1;url_prefix2=local_path2
        url_mappings_str = os.getenv("URL_MAPPINGS", "")
        url_mappings = []
        if url_mappings_str:
            try:
                for mapping in url_mappings_str.split(";"):
                    mapping = mapping.strip()
                    if not mapping:
                        continue
                    if "=" not in mapping:
                        raise ConfigurationError(
                            f"Invalid URL mapping format: {mapping}. Expected 'url=path'"
                        )
                    url_prefix, local_path_str = mapping.split("=", 1)
                    url_mappings.append(
                        URLMapping(url_prefix=url_prefix.strip(), local_path=local_path_str.strip())
                    )
            except (ValueError, OSError) as e:
                raise ConfigurationError(
                    f"Invalid URL_MAPPINGS: {url_mappings_str}. Error: {e}"
                ) from e

        config = cls(
            app_name=app_name,
            app_version=app_version,
            log_level=log_level,
            log_file=log_file,
            allowed_roots=allowed_roots,
            allowed_urls=allowed_urls,
            webapp_url=webapp_url,
            url_mappings=url_mappings,
            window_width=window_width,
            window_height=window_height,
            window_title=window_title,
            enable_logging=enable_logging,
            enable_checkout=enable_checkout,
            language=language,
            active_branding_id=active_branding_id,
            app_icon_path=app_icon_path,
            toolbar_icon_home=toolbar_icon_home,
            toolbar_icon_reload=toolbar_icon_reload,
            toolbar_icon_open=toolbar_icon_open,
            toolbar_icon_openwith=toolbar_icon_openwith,
            brand_id=brand_id,
            config_dir_name=config_dir_name,
            update_base_url=update_base_url,
            update_repo=update_repo,
            update_channel=update_channel,
            update_manifest_name=update_manifest_name,
        )
        return cls._apply_runtime_branding(config)

    def to_file(self, config_path: Path) -> None:
        """Save configuration to JSON file.

        Args:
            config_path: Path to save configuration to

        Creates parent directories if they don't exist.
        """
        data = {
            "app_name": self.app_name,
            "webapp_url": self.webapp_url,
            "url_mappings": [
                {"url_prefix": m.url_prefix, "local_path": m.local_path} for m in self.url_mappings
            ],
            "allowed_roots": [str(p) for p in self.allowed_roots],
            "allowed_urls": self.allowed_urls,
            "check_file_exists": self.check_file_exists,
            "auto_check_updates": self.auto_check_updates,
            "update_check_interval_hours": self.update_check_interval_hours,
            "log_level": self.log_level,
            "log_file": str(self.log_file) if self.log_file else None,
            "window_width": self.window_width,
            "window_height": self.window_height,
            "window_title": self.window_title,
            "enable_logging": self.enable_logging,
            "enable_checkout": self.enable_checkout,
            "language": self.language,
            "active_branding_id": self.active_branding_id,
            "brand_id": self.brand_id,
            "config_dir_name": self.config_dir_name,
            "update_base_url": self.update_base_url,
            "update_repo": self.update_repo,
            "update_channel": self.update_channel,
            "update_manifest_name": self.update_manifest_name,
        }

        config_path.parent.mkdir(parents=True, exist_ok=True)
        with open(config_path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)

    @staticmethod
    def _apply_runtime_branding(config: "Config") -> "Config":
        """Apply the persisted runtime branding template to cosmetic fields."""
        try:
            from webdrop_bridge.core.branding_manager import BrandingManager

            BrandingManager().apply_to_config(config)
        except Exception as e:
            logger.warning(f"Failed to apply runtime branding: {e}")
        return config

    @staticmethod
    def load_bootstrap_env(env_file: str | None = None) -> Path | None:
        """Load a bootstrap .env before configuration path lookup.

        This lets branded builds decide their config directory before the main
        config file is loaded.

        Args:
            env_file: Optional explicit .env path

        Returns:
            Path to the loaded .env file, or None if nothing was loaded
        """
        candidate_paths: list[Path] = []
        if env_file:
            candidate_paths.append(Path(env_file).resolve())
        else:
            if getattr(sys, "frozen", False):
                exe_dir = Path(sys.executable).resolve().parent
                # One-folder fallback: some packagers place data files in _internal.
                candidate_paths.append(exe_dir / ".env")
                candidate_paths.append(exe_dir / "_internal" / ".env")

                # PyInstaller runtime extraction directory (one-file and one-folder).
                meipass = getattr(sys, "_MEIPASS", None)
                if meipass:
                    candidate_paths.append(Path(meipass).resolve() / ".env")

            candidate_paths.append(Path.cwd() / ".env")
            candidate_paths.append(Path(__file__).resolve().parents[2] / ".env")

        for path in candidate_paths:
            if path.exists():
                load_dotenv(path, override=False)
                logger.debug(f"Loaded bootstrap environment from {path}")
                return path

        return None

    @staticmethod
    def _slugify_config_dir_name(value: str) -> str:
        """Convert brand-like identifiers into a filesystem-safe directory name."""
        sanitized = "".join(c.lower() if c.isalnum() else "_" for c in value).strip("_")
        return sanitized or DEFAULT_CONFIG_DIR_NAME

    @staticmethod
    def get_default_config_dir_name() -> str:
        """Get the default config directory name from environment or fallback."""
        return os.getenv("APP_CONFIG_DIR_NAME", DEFAULT_CONFIG_DIR_NAME)

    @staticmethod
    def get_default_config_path(config_dir_name: str | None = None) -> Path:
        """Get the default configuration file path.

        Returns:
            Path to default config file in user's AppData/Roaming
        """
        import platform

        if platform.system() == "Windows":
            base = Path.home() / "AppData" / "Roaming"
        else:
            base = Path.home() / ".config"
        return base / (config_dir_name or Config.get_default_config_dir_name()) / "config.json"

    @staticmethod
    def get_default_log_dir(config_dir_name: str | None = None) -> Path:
        """Get the default directory for log files.

        Always uses user's AppData directory to ensure permissions work
        correctly in both development and installed scenarios.

        Returns:
            Path to default logs directory in user's AppData/Roaming
        """
        import platform

        if platform.system() == "Windows":
            base = Path.home() / "AppData" / "Roaming"
        else:
            base = Path.home() / ".local" / "share"
        return base / (config_dir_name or Config.get_default_config_dir_name()) / "logs"

    @staticmethod
    def get_default_log_path(config_dir_name: str | None = None) -> Path:
        """Get the default log file path.

        Returns:
            Path to default log file in user's AppData/Roaming/webdrop_bridge/logs
        """
        dir_name = config_dir_name or Config.get_default_config_dir_name()
        return Config.get_default_log_dir(dir_name) / f"{dir_name}.log"

    def get_config_path(self) -> Path:
        """Get the default config file path for this configured brand."""
        return self.get_default_config_path(self.config_dir_name)

    def get_cache_dir(self) -> Path:
        """Get the update/cache directory for this configured brand."""
        return self.get_default_config_path(self.config_dir_name).parent / "cache"

    def __repr__(self) -> str:
        """Return developer-friendly representation."""
        return (
            f"Config(app={self.app_name} v{self.app_version}, "
            f"brand={self.brand_id}, "
            f"log_level={self.log_level}, "
            f"allowed_roots={len(self.allowed_roots)} dirs, "
            f"window={self.window_width}x{self.window_height})"
        )
