import param
import panel as pn
import re
import markdown as _md
import requests
from urllib.parse import unquote


class Glossinator(param.Parameterized):

    glossary = param.Dict(default={}, constant=True, doc="A dictionary mapping terms to their tooltip content (HTML allowed).")

    markdown = param.String(default="", allow_refs=True, doc="Markdown text to display with glossary tooltips.")

    html = param.String(default="", doc="HTML content to display with glossary tooltips")
    html_pane = param.ClassSelector(class_=pn.pane.HTML, default=None, doc="The HTML pane displaying the glossary content.")

    tooltip_background_color = param.Color(default="#2d2d2d", doc="Tooltip background color.")
    tooltip_background_opacity = param.Integer(default=100, bounds=(0, 100), doc="Tooltip background opacity (0–100).")
    tooltip_text_color = param.Color(default="#f0f0f0", doc="Tooltip text color.")
    tooltip_max_width = param.Integer(default=280, bounds=(50, None), doc="Max tooltip width in pixels.")
    tooltip_border_radius = param.Integer(default=6, bounds=(0, None), doc="Tooltip border radius in pixels.")

    def __init__(self, **params):
        super().__init__(**params)
        self._resolved_glossary = self._resolve_glossary()
        self.param.watch(self._apply_glossary, ["markdown"])
        self.param.watch(self.make_pane, ["html"])
        self._apply_glossary(None)

    def _resolve_glossary(self) -> dict:
        """Return glossary with Wikipedia URLs replaced by fetched tooltip HTML."""
        resolved = {}
        _wiki_re = re.compile(r'https?://[a-z-]+\.wikipedia\.org/wiki/.+')
        for term, value in self.glossary.items():
            if _wiki_re.match(value):
                resolved[term] = Glossinator.wikipedia_tooltip(value)
            else:
                resolved[term] = value
        return resolved

    def _apply_glossary(self, event) -> str:
        """Convert markdown text to HTML and wrap glossary key occurrences in tooltip spans."""
        
        html = _md.markdown(self.markdown, extensions=["extra"])
        if not self.glossary:
            self.param.update(html=html)
            return
        # Build the overlay's inline style from params so no shadow-DOM CSS is needed.
        r_bg = int(self.tooltip_background_color[1:3], 16)
        g_bg = int(self.tooltip_background_color[3:5], 16)
        b_bg = int(self.tooltip_background_color[5:7], 16)
        bg_opacity = self.tooltip_background_opacity / 100
        overlay_style = (
            f"position:fixed;padding:8px 12px;"
            f"border-radius:{self.tooltip_border_radius}px;"
            f"font-size:0.85em;max-width:{self.tooltip_max_width}px;width:max-content;"
            f"background:rgba({r_bg},{g_bg},{b_bg},{bg_opacity});"
            f"color:{self.tooltip_text_color};"
            "z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,0.3);"
            "line-height:1.5;white-space:normal;overflow-wrap:break-word;"
            "pointer-events:auto;top:0;left:0;"
            "visibility:hidden;opacity:0;transition:opacity 0.15s;"
        )
        # Portal approach: move overlay to document.body to escape shadow-DOM
        # overflow/containment clipping entirely.
        # Mouseenter cancels any pending hide timer so the cursor can move
        # from the term into the overlay and click links.
        enter_js = (
            "(function(term){"
            "if(window._hvGlsTimer){clearTimeout(window._hvGlsTimer);window._hvGlsTimer=null;}"
            "var ov=document.getElementById('hv-gls-ov');"
            "if(!ov){"
            "ov=document.createElement('div');ov.id='hv-gls-ov';document.body.appendChild(ov);"
            "ov.addEventListener('mouseenter',function(){if(window._hvGlsTimer){clearTimeout(window._hvGlsTimer);window._hvGlsTimer=null;}});"
            "ov.addEventListener('mouseleave',function(){window._hvGlsTimer=setTimeout(function(){ov.style.visibility='hidden';ov.style.opacity='0';},150);});"
            "}"
            "ov.setAttribute('style','" + overlay_style + "');"
            "var c=term.querySelector('.hv-gls-c');"
            "ov.innerHTML=c?c.innerHTML:'';"
            "ov.style.fontFamily=window.getComputedStyle(term).fontFamily;"
            "ov.style.visibility='visible';ov.style.opacity='1';"
            "var tr=term.getBoundingClientRect();"
            "var tw=ov.offsetWidth,th=ov.offsetHeight;"
            "var left=tr.left+tr.width/2-tw/2;"
            "var top=tr.top-th-4;"
            "if(left+tw>window.innerWidth-8)left=window.innerWidth-tw-8;"
            "if(left<8)left=8;"
            "if(top<8)top=tr.bottom+4;"
            "ov.style.left=left+'px';ov.style.top=top+'px';"
            "})(this)"
        )
        leave_js = (
            "window._hvGlsTimer=setTimeout(function(){"
            "var ov=document.getElementById('hv-gls-ov');"
            "if(ov){ov.style.visibility='hidden';ov.style.opacity='0';}"
            "},150);"
        )

        sorted_terms = sorted(self.glossary.keys(), key=len, reverse=True)
        combined_pattern = '|'.join(re.escape(t) for t in sorted_terms)
        lower_to_tip = {t.lower(): self._resolved_glossary[t] for t in sorted_terms}

        def replacer(m, _enter=enter_js, _leave=leave_js, _tips=lower_to_tip):
            matched = m.group(0)
            tip = _tips[matched.lower()]
            return (
                f'<span class="hv-glossary-term" onmouseenter="{_enter}" onmouseleave="{_leave}">{matched}'
                f'<span class="hv-gls-c" style="display:none">{tip}</span></span>'
            )

        # Split on HTML tags; even-index parts are text nodes, odd-index are tags
        parts = re.split(r'(<[^>]+>)', html)
        for idx, part in enumerate(parts):
            if idx % 2 == 1:
                continue  # skip HTML tags
            parts[idx] = re.sub(combined_pattern, replacer, part, flags=re.IGNORECASE)
        self.param.update(html=''.join(parts))

    def _build_css(self) -> str:
        return """
.hv-glossary-term {
    position: relative;
    display: inline-block;
    text-decoration: underline dotted;
    cursor: help;
}
"""

    def make_pane(self, event):
        if self.html_pane is None:
            self.param.update(html_pane=pn.pane.HTML(
                self.html,
                align="center",
                sizing_mode="stretch_both",
                styles={"font-size": "medium"},
                stylesheets=[self._build_css()],
            ))
        else:
            self.html_pane.object = self.html

    @staticmethod
    def wikipedia_tooltip(url: str) -> str:
        m = re.match(r'https?://([a-z-]+)\.wikipedia\.org/wiki/(.+)', url)
        if not m:
            raise ValueError(f"Invalid Wikipedia URL: {url!r}")
        lang, page_title = m.group(1), unquote(m.group(2))
        api_url = f"https://{lang}.wikipedia.org/api/rest_v1/page/summary/{page_title}"
        r = requests.get(api_url, headers={"User-Agent": "holoviz-utils/1.0"})
        r.raise_for_status()
        data = r.json()

        title       = data.get("title", page_title)
        description = data.get("description", "")
        extract_html = data.get("extract_html", "")
        # <p> inside a <span> is invalid HTML; replace with block-display spans
        extract_html = re.sub(r'<p\b[^>]*>', '<span style="display:block;margin:0.4em 0">', extract_html)
        extract_html = re.sub(r'</p>', '</span>', extract_html)
        article_url  = data["content_urls"]["desktop"]["page"]
        thumbnail    = data.get("thumbnail")

        img = ""
        if thumbnail:
            img = (
                f'<img src="{thumbnail["source"]}" '
                f'style="float:right; max-width:80px; max-height:80px; '
                f'margin:0 0 6px 10px; border-radius:3px; object-fit:cover;" />'
            )

        desc = ""
        if description:
            desc = (
                f'<span style="display:block; color:#72777d; font-size:0.85em; '
                f'margin:2px 0 6px;">{description}</span>'
            )

        link = (
            f'<a href="{article_url}" target="_blank" '
            f'style="display:block; margin-top:8px; font-size:0.85em; color:#3366cc;">'
            f'Read on Wikipedia →</a>'
        )

        return f'{img}<b>{title}</b>{desc}{extract_html}{link}'

    
if __name__ == "__main__":

    from holoviz_utils.dataviz import FloatingPane

    glossary = {
        "This": "An example term that will be explained in the tooltip.",
        "AETI": "https://en.wikipedia.org/wiki/Evapotranspiration",
        "Python": "A high-level programming language.",
        "Markdown": "A lightweight markup language for formatting text.",
        "HTML": "The standard markup language for creating web pages."
    }
    md_text = "This is a sample AETI text mentioning Python, Markdown, and AETI."
    
    gls = Glossinator(
        glossary=glossary, 
        markdown=md_text,
        tooltip_background_opacity=50,
        )
    
    fp = FloatingPane(
        background_pane = pn.Column(styles={"background-color": "green"}, sizing_mode="stretch_both"),
        append_params = False,
        floating_panel_content = [gls.html_pane],
    )

    fp.view.show()