"""Comment format helpers — pure logic, no Qt (parity plan Q6b).

The shared ``emdsInfo.sqlite`` convention for rich comments:

* canonical TEXT columns (``workspaces.description``, ``studies.comments``,
  ``analyses.description``, task ``parameters["comments"]`` …) always hold
  PLAIN TEXT, so every existing view in both products keeps working;
* the full rich body rides in a metadata-JSON sidecar under
  :data:`SIDECAR_HTML_KEY` — the same pattern the ArcGIS add-in already uses
  for its RTF sidecar (``emds_comments_rtf``/``emds_comments_plain_text``).

On read the editor resolves, in order: HTML sidecar → legacy RTF blob
detection (some migrated ``analyses.description`` values contain literal
``{\\rtf1`` payloads copied verbatim from EMDS9-era tables — best-effort
stripped to plain text) → plain text.
"""

from __future__ import annotations

import re
from typing import Any, Dict, Optional, Tuple

# QGIS-authored rich comments (HTML, QTextDocument.toHtml()).
SIDECAR_HTML_KEY = "emds_comments_html"
# ArcGIS-authored sidecars (never written by QGIS; read-compat only).
ARCGIS_RTF_KEY = "emds_comments_rtf"
ARCGIS_PLAIN_KEY = "emds_comments_plain_text"

_HTML_RE = re.compile(
    r"^\s*(?:<!DOCTYPE\s|<html[\s>]|<head[\s>]|<body[\s>]|<p[\s>]|<div[\s>]"
    r"|<span[\s>]|<b[\s>]|<i[\s>]|<ul[\s>]|<ol[\s>]|<h[1-6][\s>])",
    re.IGNORECASE,
)
_RTF_RE = re.compile(r"^\s*\{\\rtf1")

# RTF control words / groups for the best-effort plain-text extraction.
_RTF_DESTINATIONS = re.compile(
    r"\{(?:\\\*)?\\(?:fonttbl|colortbl|stylesheet|info|pict|themedata|"
    r"listtable|listoverridetable|generator)[^{}]*(?:\{[^{}]*\})*[^{}]*\}",
    re.IGNORECASE,
)
_RTF_NEWLINES = re.compile(r"\\(?:par|line)\b")
_RTF_HEX = re.compile(r"\\'([0-9a-fA-F]{2})")
_RTF_CONTROL = re.compile(r"\\[a-zA-Z]+-?\d*\s?")


def looks_like_html(text: Any) -> bool:
    """True only for text that is CONFIDENTLY markup: an HTML-ish opening
    prefix AND evidence of real structure (a closing tag or an <html>/
    <!DOCTYPE> preamble). A bare prefix is not enough — legacy plain
    comments like "<i think we should re-run>" must never be parsed as
    HTML, because QTextEdit.setHtml would silently mangle them and the
    next save would destroy the original text."""
    if not isinstance(text, str) or not _HTML_RE.match(text):
        return False
    head = text[:200].lower()
    return "</" in text or head.startswith("<!doctype") or "<html" in head


def looks_like_rtf(text: Any) -> bool:
    return isinstance(text, str) and bool(_RTF_RE.match(text))


def rtf_to_plain_text(rtf: str) -> str:
    """Best-effort plain text from a legacy RTF blob. Not a full parser —
    good enough to rescue migrated EMDS9 comments for display/editing."""
    if not isinstance(rtf, str):
        return ""
    text = _RTF_DESTINATIONS.sub("", rtf)
    # Protect RTF escapes FIRST: \\ \{ \} are literal characters, and must
    # not be eaten by the control-word / brace stripping below.
    text = (
        text.replace("\\\\", "\x00")
        .replace("\\{", "\x01")
        .replace("\\}", "\x02")
    )
    text = _RTF_NEWLINES.sub("\n", text)
    text = _RTF_HEX.sub(lambda m: bytes.fromhex(m.group(1)).decode("cp1252", "replace"), text)
    text = _RTF_CONTROL.sub("", text)
    text = text.replace("{", "").replace("}", "")
    text = text.replace("\x00", "\\").replace("\x01", "{").replace("\x02", "}")
    # Collapse the whitespace debris the stripping leaves behind.
    lines = [re.sub(r"[ \t]+", " ", line).strip() for line in text.split("\n")]
    return "\n".join(line for line in lines if line).strip()


def resolve_comment_content(
    plain: Any, sidecar_html: Any = None
) -> Tuple[str, str]:
    """(kind, content) an editor should load: ``("html", …)`` or
    ``("plain", …)``. The HTML sidecar wins; a legacy RTF blob in the
    canonical column degrades to extracted plain text."""
    if isinstance(sidecar_html, str) and sidecar_html.strip():
        return "html", sidecar_html
    text = plain if isinstance(plain, str) else ("" if plain is None else str(plain))
    if looks_like_rtf(text):
        return "plain", rtf_to_plain_text(text)
    if looks_like_html(text):
        return "html", text
    return "plain", text


def sidecar_from_metadata(metadata: Any) -> Optional[str]:
    """The HTML sidecar out of a metadata dict (already-parsed JSON)."""
    if isinstance(metadata, dict):
        value = metadata.get(SIDECAR_HTML_KEY)
        if isinstance(value, str) and value.strip():
            return value
    return None


def apply_sidecar(
    metadata: Optional[Dict[str, Any]],
    html: Optional[str],
    plain: Optional[str] = None,
) -> Dict[str, Any]:
    """Return *metadata* (copied) with the HTML sidecar set or removed.

    A QGIS save SUPERSEDES any ArcGIS-authored rich sidecar: the stale RTF
    copy is cleared (else the ArcGIS comments editor would keep preferring
    it over the text just saved) and, when *plain* is given, the ArcGIS
    plain-text mirror is refreshed to match the canonical column.
    """
    result = dict(metadata or {})
    if html and html.strip():
        result[SIDECAR_HTML_KEY] = html
    else:
        result.pop(SIDECAR_HTML_KEY, None)
    if ARCGIS_RTF_KEY in result:
        result[ARCGIS_RTF_KEY] = ""
    if plain is not None and ARCGIS_PLAIN_KEY in result:
        result[ARCGIS_PLAIN_KEY] = plain
    return result
