"""
Skybolt - High-performance asset caching for multi-page applications

Reads the render-map.json generated by @skybolt/vite-plugin and outputs
optimized HTML tags with intelligent caching via Service Workers.
"""

from __future__ import annotations

import json
from html import escape
from pathlib import Path
from typing import Any
from urllib.parse import unquote


class Skybolt:
    """
    Skybolt asset renderer.

    Reads the render-map.json generated by @skybolt/vite-plugin and outputs
    optimized HTML tags with intelligent caching via Service Workers.
    """

    VERSION = "3.2.0"

    def __init__(
        self,
        render_map_path: str | Path,
        cookies: dict[str, str] | None = None,
        cdn_url: str | None = None,
    ) -> None:
        """
        Create a new Skybolt instance.

        Args:
            render_map_path: Path to render-map.json generated by Vite plugin
            cookies: Cookie dictionary (defaults to empty dict)
            cdn_url: Optional CDN URL prefix (e.g., 'https://cdn.example.com')

        Raises:
            FileNotFoundError: If render map cannot be read
            json.JSONDecodeError: If render map contains invalid JSON
        """
        render_map_path = Path(render_map_path)
        self._map: dict[str, Any] = json.loads(render_map_path.read_text())
        self._client_cache = self._parse_cookie(
            (cookies or {}).get("sb_assets", "")
        )
        self._cdn_url = cdn_url.rstrip("/") if cdn_url else None

    def css(self, entry: str) -> str:
        """
        Render CSS asset.

        On first visit: inlines CSS with sb-* attributes for caching
        On repeat visit: outputs <link> tag (Service Worker serves from cache)

        Args:
            entry: Source file path (e.g., 'src/css/main.css')

        Returns:
            HTML string
        """
        asset = self._map.get("assets", {}).get(entry)

        if asset is None:
            return self._comment(f"Skybolt: asset not found: {entry}")

        url = self._resolve_url(asset["url"])

        # Client has current version - external link (SW serves from cache)
        if self._has_cached(entry, asset["hash"]):
            return f'<link rel="stylesheet" href="{escape(url)}">'

        # First visit - inline with cache attributes
        return (
            f'<style'
            f' sb-asset="{escape(entry)}:{escape(asset["hash"])}"'
            f' sb-url="{escape(url)}">'
            f'{asset["content"]}'
            f'</style>'
        )

    def script(self, entry: str, module: bool = True) -> str:
        """
        Render JavaScript asset.

        On first visit: inlines JS with sb-* attributes for caching
        On repeat visit: outputs <script> tag (Service Worker serves from cache)

        Args:
            entry: Source file path (e.g., 'src/js/app.js')
            module: Whether to load as ES module (default: True)

        Returns:
            HTML string
        """
        asset = self._map.get("assets", {}).get(entry)

        if asset is None:
            return self._comment(f"Skybolt: asset not found: {entry}")

        url = self._resolve_url(asset["url"])
        type_attr = ' type="module"' if module else ""

        # Client has current version - external script (SW serves from cache)
        if self._has_cached(entry, asset["hash"]):
            return f'<script{type_attr} src="{escape(url)}"></script>'

        # First visit - inline with cache attributes
        return (
            f'<script{type_attr}'
            f' sb-asset="{escape(entry)}:{escape(asset["hash"])}"'
            f' sb-url="{escape(url)}">'
            f'{asset["content"]}'
            f'</script>'
        )

    def preload(
        self,
        entry: str,
        as_type: str,
        mime_type: str | None = None,
        crossorigin: str | None = None,
        fetchpriority: str | None = None,
    ) -> str:
        """
        Render preload link for critical resources.

        Use this for fonts, images, or other resources that should load early.
        Preloaded resources are not cached by Skybolt's Service Worker.

        Args:
            entry: Source file path or direct URL
            as_type: Resource type ('image', 'font', 'style', 'script', 'fetch')
            mime_type: MIME type (e.g., 'font/woff2', 'image/webp')
            crossorigin: Crossorigin attribute ('anonymous', 'use-credentials')
            fetchpriority: Fetch priority ('high', 'low', 'auto')

        Returns:
            HTML string
        """
        # Try to resolve from assets, fall back to using entry as URL
        url = self.get_asset_url(entry) or entry
        url = self._resolve_url(url)

        attrs = {
            "rel": "preload",
            "href": url,
            "as": as_type,
        }

        if mime_type is not None:
            attrs["type"] = mime_type

        if crossorigin is not None:
            attrs["crossorigin"] = crossorigin

        if fetchpriority is not None:
            attrs["fetchpriority"] = fetchpriority

        return self._build_tag("link", attrs)

    def launch_script(self) -> str:
        """
        Render the Skybolt client launcher.

        Call this once in <head> before other Skybolt assets.
        Outputs config meta tag and client script.

        Returns:
            HTML string
        """
        sw_path = self._map.get("serviceWorker", {}).get("path", "/skybolt-sw.js")
        config = json.dumps({"swPath": sw_path}, separators=(",", ":"))

        return (
            f'<meta name="skybolt-config" content="{escape(config)}">\n'
            f'<script type="module">{self._map["client"]["script"]}</script>'
        )

    def get_asset_url(self, entry: str) -> str | None:
        """
        Get URL for an asset (for manual use cases).

        Args:
            entry: Source file path

        Returns:
            Asset URL or None if not found
        """
        url = self._map.get("assets", {}).get(entry, {}).get("url")
        return self._resolve_url(url) if url else None

    def get_asset_hash(self, entry: str) -> str | None:
        """
        Get hash for an asset (for manual use cases).

        Args:
            entry: Source file path

        Returns:
            Asset hash or None if not found
        """
        return self._map.get("assets", {}).get(entry, {}).get("hash")

    def _resolve_url(self, url: str) -> str:
        """Resolve URL with optional CDN prefix."""
        if self._cdn_url is None:
            return url

        # Don't prefix absolute URLs
        if url.startswith(("http://", "https://", "//")):
            return url

        return f"{self._cdn_url}{url}"

    def _has_cached(self, entry: str, hash_value: str) -> bool:
        """Check if client has a specific asset version cached."""
        return self._client_cache.get(entry) == hash_value

    def _parse_cookie(self, cookie: str) -> dict[str, str]:
        """Parse sb_assets cookie into name => hash map."""
        if not cookie:
            return {}

        decoded = unquote(cookie)
        cache: dict[str, str] = {}

        for pair in decoded.split(","):
            # Find last colon (hash doesn't contain colons, but paths might)
            colon_pos = pair.rfind(":")
            if colon_pos != -1:
                name = pair[:colon_pos]
                hash_value = pair[colon_pos + 1:]
                cache[name] = hash_value

        return cache

    def _build_tag(self, tag: str, attrs: dict[str, str]) -> str:
        """Build an HTML tag."""
        attr_str = " ".join(
            f'{key}="{escape(value)}"' for key, value in attrs.items()
        )
        return f"<{tag} {attr_str}>"

    def _comment(self, text: str) -> str:
        """Generate HTML comment (for errors/debugging)."""
        return f"<!-- {escape(text)} -->"
