"""
Building xOpat visualization configurations and URLs.

Usage examples:

    # Simplest - just a slide
    display(server, "slide.tif")

    # Slide with one mask, default shader
    display(server, "slide.tif", masks=["mask.tif"])

    # Slide with masks and custom shaders
    from xopat_deploy.shaders import heatmap, classify
    display(server, "slide.tif", masks=[
        Mask("mask.tif", shader=heatmap(color="#ff0000")),
        Mask("classes.tif", shader=classify(classes=3)),
    ])

    # Full control via Layer object
    layer = Layer("slide.tif")
    layer.add_mask("mask.tif", name="Tumor", shader=heatmap(color="#ff0000"))
    display(server, layer)
"""

import json
import hashlib
from pathlib import Path


class Mask:
    """
    A single overlay mask with optional shader configuration.

    Args:
        path: Path to the mask file (relative to data_dir)
        shader: Shader config dict from shaders.py, e.g. heatmap(), classify()
                If None, defaults to heatmap with yellow color
        name: Display name shown in xOpat UI panel
        visible: Whether the layer is visible by default
    """

    def __init__(self, path, shader=None, name=None, visible=True):
        self.path = path
        self.shader = shader
        self.name = name or Path(path).stem
        self.visible = visible


class Layer:
    """
    A slide with optional mask overlays.
    Use this for full control over the visualization.

    Args:
        slide: Path to the slide file (relative to data_dir)
        name: Display name for the visualization
    """

    def __init__(self, slide, name="Visualization"):
        self.slide = slide
        self.name = name
        self._masks = []

    def add_mask(self, path, shader=None, name=None, visible=True):
        """
        Add a mask overlay to this layer.

        Args:
            path: Path to the mask file (relative to data_dir)
            shader: Shader config dict from shaders.py
            name: Display name in xOpat UI
            visible: Whether visible by default
        """
        self._masks.append(Mask(path, shader=shader, name=name, visible=visible))
        return self

    @property
    def masks(self):
        return self._masks


def _default_shader(data_index, mask):
    """Build shader dict for a mask, using provided shader config or default heatmap."""
    from .shaders import heatmap as default_heatmap

    shader_conf = mask.shader if mask.shader is not None else default_heatmap()

    return {
        "name": mask.name,
        "type": shader_conf["type"],
        "visible": 1 if mask.visible else 0,
        "fixed": False,
        "dataReferences": [data_index],
        "params": shader_conf.get("params", {}),
    }


def build_visualization_config(slide, masks=None, name="Visualization"):
    """
    Build the full xOpat JSON configuration dict.

    Args:
        slide: Path to slide file
        masks: List of Mask objects or plain path strings
        name: Visualization name

    Returns:
        dict: xOpat-compatible configuration
    """
    # normalize masks to Mask objects
    normalized_masks = []
    if masks:
        for m in masks:
            if isinstance(m, str):
                normalized_masks.append(Mask(m))
            elif isinstance(m, Mask):
                normalized_masks.append(m)
            else:
                raise ValueError(f"masks must be strings or Mask objects, got {type(m)}")

    # build data array - slide is always index 0
    data = [slide] + [m.path for m in normalized_masks]

    # build shaders for each mask
    shaders = {}
    for i, mask in enumerate(normalized_masks):
        data_index = i + 1  # slide is 0, masks start at 1
        shader_id = f"shader_{i}"
        shaders[shader_id] = _default_shader(data_index, mask)

    config = {
        "data": data,
        "background": [{"dataReference": 0}],
        "visualizations": [
            {
                "name": name,
                "shaders": shaders,
            }
        ],
    }

    return config


def build_post_html(config, xopat_base_url, width="100%", height=800):
    """
    Build HTML with an IFrame that receives xOpat visualization config via POST.
    Uses the same retry logic as the simple display() for JupyterHub compatibility.

    Args:
        config: dict from build_visualization_config()
        xopat_base_url: Base URL of xOpat, e.g. "http://127.0.0.1:9000"
        width: IFrame width
        height: IFrame height

    Returns:
        str: HTML string to render in Jupyter
    """
    config_json = json.dumps(config)
    uid = hashlib.md5(config_json.encode()).hexdigest()[:8]
    action = xopat_base_url.rstrip("/") + "/"
    config_escaped = config_json.replace("'", "&#39;")

    return f"""
<div id="status-{uid}">Loading...</div>
<iframe
    name="xopat-{uid}"
    id="xopat-{uid}"
    width="{width}"
    height="{height}"
    style="border:1px solid #ccc; visibility: hidden;">
</iframe>
<form id="form-{uid}" method="POST"
    action="{action}"
    target="xopat-{uid}"
    style="display:none;">
    <input type="hidden" name="visualization" value='{config_escaped}'>
</form>
<script>
(function() {{
    const iframe = document.getElementById('xopat-{uid}');
    const status = document.getElementById('status-{uid}');
    const maxRetries = 15;
    let attempt = 0;

    function isErrorPage() {{
        try {{
            const body = iframe.contentDocument?.body?.innerText || '';
            return body.includes('500') || body.includes('Internal server error');
        }} catch(e) {{
            return false;
        }}
    }}

    function retry() {{
        if (attempt >= maxRetries) {{
            status.textContent = 'Failed to load after 15 retries.';
            iframe.style.visibility = 'visible';
            return;
        }}
        attempt++;
        status.textContent = 'Retrying... attempt ' + attempt + '/15';
        setTimeout(() => {{
            document.getElementById('form-{uid}').submit();
        }}, 2000);
    }}

    iframe.onload = function() {{
        if (isErrorPage()) {{
            retry();
        }} else {{
            status.textContent = 'Ready.';
            iframe.style.visibility = 'visible';
        }}
    }};

    document.getElementById('form-{uid}').submit();
}})();
</script>
"""


def build_simple_url(slide, xopat_base_url):
    """
    Build simple xOpat URL for a slide without any masks.
    Uses the ?slides= query parameter (existing behavior).

    Args:
        slide: Path to slide file
        xopat_base_url: Base URL of xOpat

    Returns:
        str: Simple xOpat URL
    """
    slide_q = slide.replace(">", "%3E")
    return f"{xopat_base_url.rstrip('/')}/?slides={slide_q}"