"""Runtime branding template management for the shared application."""

from __future__ import annotations

import json
import logging
import os
import platform
import shutil
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any

from webdrop_bridge.config import DEFAULT_CONFIG_DIR_NAME, Config, ConfigurationError

logger = logging.getLogger(__name__)

DEFAULT_BRANDING_TEMPLATE_ID = "default"
DEFAULT_LOGO_PATH = "resources/icons/app.png"
SUPPORTED_LOGO_SUFFIXES = {".png", ".jpg", ".jpeg", ".bmp", ".svg", ".ico", ".icns"}


@dataclass(frozen=True)
class BrandingTemplate:
    """Serializable runtime branding template."""

    template_id: str
    display_name: str
    app_name: str
    window_title: str = ""
    logo_path: str = ""
    app_icon_path_windows: str = "resources/icons/app.ico"
    app_icon_path_macos: str = "resources/icons/app.icns"
    toolbar_icon_home: str = "resources/icons/home.ico"
    toolbar_icon_reload: str = "resources/icons/reload.ico"
    toolbar_icon_open: str = "resources/icons/open.ico"
    toolbar_icon_openwith: str = "resources/icons/openwith.ico"
    accent_color: str = "#667eea"

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "BrandingTemplate":
        """Create a template from a JSON-compatible dictionary."""
        template_id = str(data.get("template_id") or data.get("id") or "").strip()
        display_name = str(data.get("display_name") or template_id or "").strip()
        app_name = str(data.get("app_name") or display_name or "").strip()

        if not template_id:
            raise ConfigurationError("Branding template requires a template_id")
        if not display_name:
            raise ConfigurationError("Branding template requires a display_name")
        if not app_name:
            raise ConfigurationError("Branding template requires an app_name")

        return cls(
            template_id=template_id,
            display_name=display_name,
            app_name=app_name,
            window_title=str(data.get("window_title", "")),
            logo_path=str(data.get("logo_path", "")),
            app_icon_path_windows=str(data.get("app_icon_path_windows", "resources/icons/app.ico")),
            app_icon_path_macos=str(data.get("app_icon_path_macos", "resources/icons/app.icns")),
            toolbar_icon_home=str(data.get("toolbar_icon_home", "resources/icons/home.ico")),
            toolbar_icon_reload=str(data.get("toolbar_icon_reload", "resources/icons/reload.ico")),
            toolbar_icon_open=str(data.get("toolbar_icon_open", "resources/icons/open.ico")),
            toolbar_icon_openwith=str(
                data.get("toolbar_icon_openwith", "resources/icons/openwith.ico")
            ),
            accent_color=str(data.get("accent_color", "#667eea")),
        )

    def to_dict(self) -> dict[str, Any]:
        """Convert the template to a JSON-compatible dictionary."""
        return asdict(self)

    def get_app_icon_path(self) -> str:
        """Return the best app icon path for the current platform."""
        if platform.system() == "Darwin":
            return self.app_icon_path_macos
        return self.app_icon_path_windows


BUILTIN_BRANDING_TEMPLATES: dict[str, BrandingTemplate] = {
    "default": BrandingTemplate(
        template_id="default",
        display_name="Default",
        app_name="WebDrop Bridge",
        window_title="",
        logo_path=DEFAULT_LOGO_PATH,
        accent_color="#667eea",
    ),
    "agravity": BrandingTemplate(
        template_id="agravity",
        display_name="Agravity",
        app_name="Agravity Bridge",
        window_title="",
        logo_path=DEFAULT_LOGO_PATH,
        accent_color="#2d7d6e",
    ),
}


class BrandingManager:
    """Manage runtime branding templates independently from saved setups."""

    def __init__(self, base_dir: Path | None = None) -> None:
        env_dir = os.getenv("WEBDROP_BRANDING_DIR")
        resolved_base = Path(env_dir).resolve() if env_dir and base_dir is None else base_dir
        self.base_dir = resolved_base or self._default_base_dir()
        self.templates_dir = self.base_dir / "templates"
        self.assets_dir = self.base_dir / "assets"
        self.active_branding_path = self.base_dir / "active_branding.json"
        self.ensure_builtin_templates()

    @staticmethod
    def _default_base_dir() -> Path:
        """Return the shared branding storage directory."""
        return Config.get_default_config_path(DEFAULT_CONFIG_DIR_NAME).parent / "branding"

    def ensure_builtin_templates(self) -> None:
        """Ensure built-in templates exist on disk for discovery and later editing."""
        self.templates_dir.mkdir(parents=True, exist_ok=True)
        self.assets_dir.mkdir(parents=True, exist_ok=True)

        for template in BUILTIN_BRANDING_TEMPLATES.values():
            template_path = self.templates_dir / f"{template.template_id}.json"
            if not template_path.exists():
                template_path.write_text(json.dumps(template.to_dict(), indent=2), encoding="utf-8")

    def list_templates(self) -> list[BrandingTemplate]:
        """List all available templates with built-ins guaranteed."""
        self.ensure_builtin_templates()

        templates: dict[str, BrandingTemplate] = {}
        for template_path in self.templates_dir.glob("*.json"):
            try:
                data = json.loads(template_path.read_text(encoding="utf-8"))
                template = BrandingTemplate.from_dict(data)
                templates[template.template_id] = template
            except (OSError, json.JSONDecodeError, ConfigurationError) as exc:
                logger.warning("Skipping invalid branding template %s: %s", template_path, exc)

        for template_id, template in BUILTIN_BRANDING_TEMPLATES.items():
            templates.setdefault(template_id, template)

        ordered_templates = sorted(
            templates.values(),
            key=lambda template: (
                template.template_id != DEFAULT_BRANDING_TEMPLATE_ID,
                template.display_name.lower(),
            ),
        )
        return ordered_templates

    def has_template(self, template_id: str) -> bool:
        """Return whether a template with the given id exists."""
        return any(template.template_id == template_id for template in self.list_templates())

    def load_template(self, template_id: str) -> BrandingTemplate:
        """Load a template by id, falling back to the default template if missing."""
        for template in self.list_templates():
            if template.template_id == template_id:
                return template

        logger.warning("Branding template '%s' not found. Falling back to default.", template_id)
        return BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID]

    def save_template(self, template: BrandingTemplate) -> Path:
        """Save or update a branding template on disk."""
        if not template.template_id:
            raise ConfigurationError("Branding template requires a template_id")
        if template.template_id in BUILTIN_BRANDING_TEMPLATES:
            raise ConfigurationError(f"Cannot overwrite built-in branding: {template.template_id}")

        stored_logo_path = ""
        if template.logo_path:
            stored_logo_path = self._copy_logo_asset(template.logo_path, template.template_id)

        stored_template = BrandingTemplate(
            template_id=template.template_id,
            display_name=template.display_name,
            app_name=template.app_name,
            window_title=template.window_title,
            logo_path=stored_logo_path or template.logo_path,
            app_icon_path_windows=stored_logo_path or template.app_icon_path_windows,
            app_icon_path_macos=stored_logo_path or template.app_icon_path_macos,
            toolbar_icon_home=template.toolbar_icon_home,
            toolbar_icon_reload=template.toolbar_icon_reload,
            toolbar_icon_open=template.toolbar_icon_open,
            toolbar_icon_openwith=template.toolbar_icon_openwith,
            accent_color=template.accent_color,
        )

        self.templates_dir.mkdir(parents=True, exist_ok=True)
        template_path = self.templates_dir / f"{stored_template.template_id}.json"
        template_path.write_text(json.dumps(stored_template.to_dict(), indent=2), encoding="utf-8")
        logger.info("Branding template saved: %s", stored_template.template_id)
        return template_path

    def build_template(
        self,
        *,
        template_id: str,
        display_name: str,
        app_name: str = "",
        window_title: str = "",
        logo_path: str = "",
    ) -> BrandingTemplate:
        """Build a validated branding template from editable UI fields."""
        safe_id = self._slugify(template_id.strip() or display_name)
        safe_name = display_name.strip()
        logo = logo_path.strip()
        resolved_app_name = (app_name or display_name).strip()

        if not safe_id:
            raise ConfigurationError("Branding requires a name")
        if not safe_name:
            raise ConfigurationError("Branding requires a display name")
        if logo:
            logo_file = self._resolve_asset_path(logo)
            if logo_file is None or not logo_file.exists() or not logo_file.is_file():
                raise ConfigurationError(f"Logo file not found: {logo}")
            if logo_file.suffix.lower() not in SUPPORTED_LOGO_SUFFIXES:
                raise ConfigurationError(
                    "Unsupported logo format. Use PNG, JPG, BMP, SVG, ICO, or ICNS."
                )

        return BrandingTemplate(
            template_id=safe_id,
            display_name=safe_name,
            app_name=resolved_app_name,
            window_title=window_title.strip(),
            logo_path=logo,
            app_icon_path_windows=logo or "resources/icons/app.ico",
            app_icon_path_macos=logo or "resources/icons/app.icns",
        )

    @staticmethod
    def _slugify(value: str) -> str:
        """Convert a human-readable branding name into a stable id."""
        return "".join(c.lower() if c.isalnum() else "_" for c in value).strip("_")

    @staticmethod
    def _resolve_asset_path(configured_path: str) -> Path | None:
        """Resolve a branding asset path in dev and packaged layouts."""
        if not configured_path:
            return None

        path = Path(configured_path)
        candidates = [path] if path.is_absolute() else [Path.cwd() / path]
        if not path.is_absolute():
            project_root = Path(__file__).resolve().parents[3]
            candidates.append(project_root / path)

        for candidate in candidates:
            if candidate.exists():
                return candidate

        return None

    def _copy_logo_asset(self, configured_path: str, template_id: str) -> str:
        """Copy a user-selected logo into managed branding storage."""
        resolved_path = self._resolve_asset_path(configured_path)
        if resolved_path is None or not resolved_path.exists() or not resolved_path.is_file():
            raise ConfigurationError(f"Logo file not found: {configured_path}")

        self.assets_dir.mkdir(parents=True, exist_ok=True)
        target_path = self.assets_dir / f"{template_id}{resolved_path.suffix.lower()}"
        if resolved_path.resolve() != target_path.resolve():
            shutil.copy2(resolved_path, target_path)

        return str(target_path)

    def export_template(self, template_id: str, export_path: Path) -> Path:
        """Export a branding into a shareable JSON file plus optional logo asset."""
        template = self.load_template(template_id)
        export_path.parent.mkdir(parents=True, exist_ok=True)

        export_data = template.to_dict()
        if template.logo_path:
            resolved_logo = self._resolve_asset_path(template.logo_path)
            if resolved_logo and resolved_logo.exists() and resolved_logo.is_file():
                export_logo_path = export_path.parent / resolved_logo.name
                if resolved_logo.resolve() != export_logo_path.resolve():
                    shutil.copy2(resolved_logo, export_logo_path)
                export_data["logo_path"] = export_logo_path.name
                export_data["app_icon_path_windows"] = export_logo_path.name
                export_data["app_icon_path_macos"] = export_logo_path.name

        export_path.write_text(json.dumps(export_data, indent=2), encoding="utf-8")
        logger.info("Branding template exported: %s -> %s", template_id, export_path)
        return export_path

    def import_template(self, import_path: Path) -> BrandingTemplate:
        """Import a branding from a previously exported JSON file."""
        if not import_path.exists() or not import_path.is_file():
            raise ConfigurationError(f"Branding import file not found: {import_path}")

        try:
            data = json.loads(import_path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError) as exc:
            raise ConfigurationError(f"Invalid branding import file: {import_path}") from exc

        template = BrandingTemplate.from_dict(data)
        imported_template_id = template.template_id
        if imported_template_id in BUILTIN_BRANDING_TEMPLATES:
            imported_template_id = self._slugify(f"{template.display_name}_imported")

        imported_logo_path = ""
        if template.logo_path:
            candidate_logo = Path(template.logo_path)
            if not candidate_logo.is_absolute():
                candidate_logo = import_path.parent / candidate_logo
            imported_logo_path = self._copy_logo_asset(str(candidate_logo), imported_template_id)

        imported_template = BrandingTemplate(
            template_id=imported_template_id,
            display_name=template.display_name,
            app_name=template.app_name,
            window_title=template.window_title,
            logo_path=imported_logo_path,
            app_icon_path_windows=imported_logo_path or template.app_icon_path_windows,
            app_icon_path_macos=imported_logo_path or template.app_icon_path_macos,
            toolbar_icon_home=template.toolbar_icon_home,
            toolbar_icon_reload=template.toolbar_icon_reload,
            toolbar_icon_open=template.toolbar_icon_open,
            toolbar_icon_openwith=template.toolbar_icon_openwith,
            accent_color=template.accent_color,
        )
        self.save_template(imported_template)
        logger.info("Branding template imported: %s", imported_template.template_id)
        return imported_template

    def delete_template(self, template_id: str) -> None:
        """Delete a user template while protecting built-ins."""
        if template_id in BUILTIN_BRANDING_TEMPLATES:
            raise ConfigurationError(f"Cannot delete built-in branding template: {template_id}")

        template_path = self.templates_dir / f"{template_id}.json"
        if not template_path.exists():
            raise ConfigurationError(f"Branding template not found: {template_id}")

        template_path.unlink()
        logger.info("Branding template deleted: %s", template_id)

    def get_active_branding_id(self) -> str:
        """Return the persisted active branding selection."""
        if not self.active_branding_path.exists():
            return DEFAULT_BRANDING_TEMPLATE_ID

        try:
            data = json.loads(self.active_branding_path.read_text(encoding="utf-8"))
            template_id = str(data.get("active_branding_id", DEFAULT_BRANDING_TEMPLATE_ID))
            return template_id if self.has_template(template_id) else DEFAULT_BRANDING_TEMPLATE_ID
        except (OSError, json.JSONDecodeError):
            return DEFAULT_BRANDING_TEMPLATE_ID

    def set_active_branding_id(self, template_id: str) -> None:
        """Persist the active branding selection."""
        if not self.has_template(template_id):
            raise ConfigurationError(f"Branding template not found: {template_id}")

        self.base_dir.mkdir(parents=True, exist_ok=True)
        self.active_branding_path.write_text(
            json.dumps({"active_branding_id": template_id}, indent=2),
            encoding="utf-8",
        )
        logger.info("Active branding set to %s", template_id)

    def apply_to_config(self, config: Config) -> BrandingTemplate:
        """Apply the active branding template to cosmetic config fields only."""
        requested_id = (getattr(config, "active_branding_id", "") or "").strip()
        if not requested_id or requested_id == DEFAULT_BRANDING_TEMPLATE_ID:
            requested_id = self.get_active_branding_id()

        template = self.load_template(requested_id)
        default_app_name = BUILTIN_BRANDING_TEMPLATES[DEFAULT_BRANDING_TEMPLATE_ID].app_name
        known_app_names = {known_template.app_name for known_template in self.list_templates()}
        known_title_prefixes = {f"{app_name} v" for app_name in known_app_names}

        config.active_branding_id = template.template_id
        config.branding_display_name = template.display_name

        if (
            template.template_id != DEFAULT_BRANDING_TEMPLATE_ID
            or not config.app_name
            or config.app_name in known_app_names
        ):
            config.app_name = template.app_name or default_app_name

        if (
            template.template_id != DEFAULT_BRANDING_TEMPLATE_ID
            or not config.window_title
            or any(config.window_title.startswith(prefix) for prefix in known_title_prefixes)
        ):
            config.window_title = (
                template.window_title or f"{config.app_name} v{config.app_version}"
            )

        config.logo_path = template.logo_path
        config.app_icon_path = template.get_app_icon_path()
        config.toolbar_icon_home = template.toolbar_icon_home
        config.toolbar_icon_reload = template.toolbar_icon_reload
        config.toolbar_icon_open = template.toolbar_icon_open
        config.toolbar_icon_openwith = template.toolbar_icon_openwith

        return template
