"""
SirvClient - SDK for building Sirv URLs and HTML tags for images, spins, videos, 3D models, and galleries.

Example::

    sirv = SirvClient(domain='demo.sirv.com', defaults={'q': 80})
    url = sirv.url('/image.jpg', {'w': 300, 'format': 'webp'})
    html = sirv.image('/photo.jpg', alt='A photo')
"""

from typing import Any, Dict, List, Optional, Sequence, Tuple
from urllib.parse import quote
import math


class SirvClient:
    """SDK for building Sirv URLs and HTML tags."""

    def __init__(self, domain: str = '', defaults: Optional[Dict[str, Any]] = None) -> None:
        """
        Create a SirvClient instance.

        Args:
            domain: Sirv domain (e.g. 'demo.sirv.com').
            defaults: Default query parameters merged into every URL.
        """
        if not domain:
            raise ValueError('domain is required')
        self._domain = domain.rstrip('/')
        self._defaults = defaults or {}

    def _flatten(self, obj: Dict[str, Any], prefix: str = '') -> List[Tuple[str, str]]:
        """
        Flatten a nested dict into dot-notation key-value pairs.

        {'crop': {'type': 'face', 'pad': {'width': 10}}}
        => [('crop.type', 'face'), ('crop.pad.width', '10')]
        """
        entries: List[Tuple[str, str]] = []
        for key, value in obj.items():
            full_key = f'{prefix}.{key}' if prefix else key
            if value is not None and isinstance(value, dict):
                entries.extend(self._flatten(value, full_key))
            elif value is not None:
                entries.append((full_key, str(value)))
        return entries

    def _build_query(self, params: Optional[Dict[str, Any]] = None) -> str:
        """Build a query string from merged defaults + params."""
        merged = {**self._defaults, **(params or {})}
        entries = self._flatten(merged)
        if not entries:
            return ''
        parts = [f'{quote(k, safe="")}={quote(v, safe="")}' for k, v in entries]
        return '?' + '&'.join(parts)

    def url(self, path: str, params: Optional[Dict[str, Any]] = None) -> str:
        """
        Build a full Sirv URL.

        Args:
            path: Asset path (e.g. '/image.jpg').
            params: Transformation parameters (nested dicts are flattened to dot-notation).

        Returns:
            Full HTTPS URL string.
        """
        normalized_path = path if path.startswith('/') else '/' + path
        return f'https://{self._domain}{normalized_path}{self._build_query(params)}'

    def src_set(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
        *,
        widths: Optional[List[int]] = None,
        min_width: Optional[int] = None,
        max_width: Optional[int] = None,
        tolerance: float = 0.15,
        device_pixel_ratios: Optional[List[int]] = None,
    ) -> str:
        """
        Generate a srcset string for responsive images.

        Three modes (checked in order):
        1. Explicit widths list.
        2. Auto-range with min_width/max_width/tolerance.
        3. Device pixel ratios with variable quality.

        Args:
            path: Image path.
            params: Transformation parameters.
            widths: Explicit list of widths.
            min_width: Minimum width for auto-generation.
            max_width: Maximum width for auto-generation.
            tolerance: Tolerance for auto-generating widths (0-1), default 0.15.
            device_pixel_ratios: DPR values (e.g. [1, 2, 3]).

        Returns:
            srcset string, or empty string if no mode matched.
        """
        params = params or {}

        if widths is not None:
            return ', '.join(
                f'{self.url(path, {**params, "w": w})} {w}w'
                for w in widths
            )

        if min_width is not None and max_width is not None:
            generated = self._generate_widths(min_width, max_width, tolerance)
            return ', '.join(
                f'{self.url(path, {**params, "w": w})} {w}w'
                for w in generated
            )

        if device_pixel_ratios is not None:
            base_q = params.get('q', self._defaults.get('q', 80))
            entries = []
            for dpr in device_pixel_ratios:
                q = self._dpr_quality(base_q, dpr)
                dpr_params = {**params, 'q': q}
                if 'w' in params:
                    dpr_params['w'] = params['w'] * dpr
                if 'h' in params:
                    dpr_params['h'] = params['h'] * dpr
                entries.append(f'{self.url(path, dpr_params)} {dpr}x')
            return ', '.join(entries)

        return ''

    def _dpr_quality(self, base_q: int, dpr: int) -> int:
        """
        Calculate quality for a given DPR.
        Higher DPR uses lower quality since pixels are smaller.
        Formula: round(base_q * 0.75^(dpr-1))
        """
        if dpr <= 1:
            return base_q
        return round(base_q * math.pow(0.75, dpr - 1))

    def _generate_widths(self, min_w: int, max_w: int, tolerance: float) -> List[int]:
        """Generate widths between min and max using a tolerance step."""
        widths: List[int] = []
        current = float(min_w)
        while current < max_w:
            widths.append(round(current))
            current *= 1 + tolerance * 2
        widths.append(round(max_w))
        return widths

    def _serialize_viewer_options(self, opts: Dict[str, Any]) -> str:
        """
        Serialize viewer options to semicolon-separated format for data-options.
        {'autostart': 'visible', 'threshold': 200} => 'autostart:visible;threshold:200'
        """
        return ';'.join(f'{k}:{v}' for k, v in opts.items())

    @staticmethod
    def _escape_attr(s: str) -> str:
        """Escape HTML attribute values."""
        return (
            s.replace('&', '&amp;')
             .replace('"', '&quot;')
             .replace('<', '&lt;')
             .replace('>', '&gt;')
        )

    def image(
        self,
        path: str,
        *,
        transform: Optional[Dict[str, Any]] = None,
        viewer: Optional[Dict[str, Any]] = None,
        alt: Optional[str] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """
        Generate an <img> tag for a Sirv image.

        Args:
            path: Image path.
            transform: Transformation parameters for the URL.
            viewer: Viewer options for data-options attribute.
            alt: Alt text.
            class_name: Additional CSS class(es).

        Returns:
            HTML img tag string.
        """
        src = self.url(path, transform or {})
        cls = f'Sirv {class_name}' if class_name else 'Sirv'
        html = f'<img class="{cls}" data-src="{self._escape_attr(src)}"'
        if alt is not None:
            html += f' alt="{self._escape_attr(alt)}"'
        if viewer:
            html += f' data-options="{self._escape_attr(self._serialize_viewer_options(viewer))}"'
        html += '>'
        return html

    def zoom(
        self,
        path: str,
        *,
        transform: Optional[Dict[str, Any]] = None,
        viewer: Optional[Dict[str, Any]] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """
        Generate a <div> tag for a Sirv zoom viewer.

        Args:
            path: Image path.
            transform: Transformation parameters.
            viewer: Viewer options.
            class_name: Additional CSS class(es).

        Returns:
            HTML div tag string.
        """
        return self._viewer_div(path, 'zoom', transform=transform, viewer=viewer, class_name=class_name)

    def spin(
        self,
        path: str,
        *,
        transform: Optional[Dict[str, Any]] = None,
        viewer: Optional[Dict[str, Any]] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """
        Generate a <div> tag for a Sirv spin viewer.

        Args:
            path: Path to .spin file.
            viewer: Viewer options.
            class_name: Additional CSS class(es).

        Returns:
            HTML div tag string.
        """
        return self._viewer_div(path, None, transform=transform, viewer=viewer, class_name=class_name)

    def video(
        self,
        path: str,
        *,
        transform: Optional[Dict[str, Any]] = None,
        viewer: Optional[Dict[str, Any]] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """
        Generate a <div> tag for a Sirv video.

        Args:
            path: Video path.
            viewer: Viewer options.
            class_name: Additional CSS class(es).

        Returns:
            HTML div tag string.
        """
        return self._viewer_div(path, None, transform=transform, viewer=viewer, class_name=class_name)

    def model(
        self,
        path: str,
        *,
        transform: Optional[Dict[str, Any]] = None,
        viewer: Optional[Dict[str, Any]] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """
        Generate a <div> tag for a Sirv 3D model viewer.

        Args:
            path: Path to .glb file.
            viewer: Viewer options.
            class_name: Additional CSS class(es).

        Returns:
            HTML div tag string.
        """
        return self._viewer_div(path, None, transform=transform, viewer=viewer, class_name=class_name)

    def _viewer_div(
        self,
        path: str,
        data_type: Optional[str] = None,
        *,
        transform: Optional[Dict[str, Any]] = None,
        viewer: Optional[Dict[str, Any]] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """Internal helper to generate viewer div tags."""
        src = self.url(path, transform or {})
        cls = f'Sirv {class_name}' if class_name else 'Sirv'
        html = f'<div class="{cls}" data-src="{self._escape_attr(src)}"'
        if data_type:
            html += f' data-type="{data_type}"'
        if viewer:
            html += f' data-options="{self._escape_attr(self._serialize_viewer_options(viewer))}"'
        html += '></div>'
        return html

    def gallery(
        self,
        items: List[Dict[str, Any]],
        *,
        viewer: Optional[Dict[str, Any]] = None,
        class_name: Optional[str] = None,
    ) -> str:
        """
        Generate a gallery container with multiple assets.

        Args:
            items: Gallery items, each a dict with keys:
                - src (str): Asset path.
                - type (str, optional): Asset type override (e.g. 'zoom', 'spin').
                - transform (dict, optional): Per-item transformation params.
                - viewer (dict, optional): Per-item viewer options.
            viewer: Gallery-level viewer options.
            class_name: Additional CSS class(es) for the gallery container.

        Returns:
            HTML string with nested divs.
        """
        cls = f'Sirv {class_name}' if class_name else 'Sirv'
        html = f'<div class="{cls}"'
        if viewer:
            html += f' data-options="{self._escape_attr(self._serialize_viewer_options(viewer))}"'
        html += '>'

        for item in items:
            src = self.url(item['src'], item.get('transform') or {})
            child = f'<div data-src="{self._escape_attr(src)}"'
            if item.get('type'):
                child += f' data-type="{item["type"]}"'
            if item.get('viewer'):
                child += f' data-options="{self._escape_attr(self._serialize_viewer_options(item["viewer"]))}"'
            child += '></div>'
            html += child

        html += '</div>'
        return html

    def script_tag(
        self,
        *,
        modules: Optional[List[str]] = None,
        async_attr: bool = True,
    ) -> str:
        """
        Generate a <script> tag to load Sirv JS.

        Args:
            modules: Specific modules to load (e.g. ['spin', 'zoom']).
            async_attr: Whether to add async attribute (default True).

        Returns:
            HTML script tag string.
        """
        filename = 'sirv'
        if modules:
            filename = 'sirv.' + '.'.join(modules)
        html = f'<script src="https://scripts.sirv.com/sirvjs/v3/{filename}.js"'
        if async_attr:
            html += ' async'
        html += '></script>'
        return html
