# retoor <retoor@molodetz.nl>

import html
import math
import re
from pathlib import Path

from devplacepy.config import TEMPLATES_DIR
from devplacepy import docs_api

K1 = 1.5
B = 0.75
_TOKEN = re.compile(r"[a-z0-9]+")
_static_docs = None


def _tokenize(text: str) -> list:
    return [t for t in _TOKEN.findall(text.lower()) if len(t) > 1]


def _demarkdown(text: str) -> str:
    text = re.sub(r"(?m)^[ \t]*```.*$", " ", text)
    text = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", text)
    text = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", text)
    text = re.sub(r"(?m)^[ \t]{0,3}#{1,6}[ \t]*", "", text)
    text = re.sub(r"(?m)^[ \t]{0,3}>[ \t]?", "", text)
    text = re.sub(r"(?m)^[ \t]*(?:[-*+]|\d+\.)[ \t]+", "", text)
    text = re.sub(
        r"(?m)^[ \t]*\|?(?:[ \t]*:?-{2,}:?[ \t]*\|)+[ \t]*:?-{0,}:?[ \t]*$", " ", text
    )
    text = text.replace("|", " ")
    text = re.sub(r"[`*~]", "", text)
    return text


def _strip(raw: str) -> str:
    raw = re.sub(r"(?is)<script\b.*?</script>", " ", raw)
    raw = re.sub(r"(?is)<style\b.*?</style>", " ", raw)
    raw = re.sub(r"{#.*?#}", " ", raw, flags=re.S)
    raw = re.sub(r"{%.*?%}", " ", raw, flags=re.S)
    raw = re.sub(r"{{.*?}}", " ", raw, flags=re.S)
    raw = html.unescape(raw)
    raw = re.sub(r"<[^>]+>", " ", raw)
    raw = _demarkdown(raw)
    return re.sub(r"\s+", " ", raw).strip()


def _prose_text(slug: str) -> str:
    path = Path(TEMPLATES_DIR) / "docs" / f"{slug}.html"
    try:
        return _strip(path.read_text(encoding="utf-8"))
    except OSError:
        return ""


def _group_text(group: dict) -> str:
    parts = [group.get("intro", "")]
    for ep in group.get("endpoints", []) or []:
        parts += [
            ep.get("method", ""),
            ep.get("path", ""),
            ep.get("title", ""),
            ep.get("summary", ""),
        ]
        parts += [str(note) for note in ep.get("notes", []) or []]
        for param in ep.get("params", []) or []:
            parts += [param.get("name", ""), param.get("description", "")]
    return _strip("\n".join(str(p) for p in parts))


def _collect_static_docs() -> list:
    global _static_docs
    if _static_docs is not None:
        return _static_docs
    from devplacepy.routers.docs import DOCS_PAGES
    from devplacepy.services.manager import service_manager

    docs = []
    for page in DOCS_PAGES:
        if page["kind"] == "live":
            continue
        slug, title = page["slug"], page["title"]
        if page["kind"] == "prose":
            body = _prose_text(slug)
        elif page.get("dynamic"):
            body = _group_text(
                docs_api.build_services_group(service_manager.describe_all(), "")
            )
        else:
            body = _group_text(docs_api.get_group(slug) or {})
        docs.append(
            {
                "slug": slug,
                "title": title,
                "admin": page.get("admin", False),
                "text": f"{title}\n{body}",
            }
        )
    _static_docs = docs
    return docs


def _live_docs(user) -> list:
    from devplacepy import docs_live

    facts = docs_live.build_live_facts(user)
    return [
        {
            "slug": "dashboard",
            "title": "Your dashboard",
            "admin": False,
            "text": "Your dashboard live data stats\n" + docs_live.live_text(facts),
        }
    ]


def _build(docs: list) -> dict:
    postings: dict = {}
    doc_len: list = []
    doc_text: list = []
    df: dict = {}
    for i, doc in enumerate(docs):
        tokens = _tokenize(doc["text"])
        doc_len.append(len(tokens) or 1)
        doc_text.append(doc["text"])
        freqs: dict = {}
        for token in tokens:
            freqs[token] = freqs.get(token, 0) + 1
        for token, count in freqs.items():
            postings.setdefault(token, []).append((i, count))
            df[token] = df.get(token, 0) + 1
    n = len(docs) or 1
    avgdl = (sum(doc_len) / n) if doc_len else 1.0
    idf = {t: math.log(1 + (n - d + 0.5) / (d + 0.5)) for t, d in df.items()}
    return {
        "docs": docs,
        "postings": postings,
        "doc_len": doc_len,
        "doc_text": doc_text,
        "avgdl": avgdl or 1.0,
        "idf": idf,
    }


def get_index(user=None) -> dict:
    return _build(list(_collect_static_docs()) + _live_docs(user))


def reset_index() -> None:
    global _static_docs
    _static_docs = None


def _snippet(text: str, terms: list, width: int = 280) -> str:
    low = text.lower()
    pos = -1
    for term in terms:
        found = low.find(term)
        if found != -1 and (pos == -1 or found < pos):
            pos = found
    if pos == -1:
        pos = 0
    start = max(0, pos - width // 3)
    end = min(len(text), start + width)
    fragment = html.escape(text[start:end])
    for term in sorted(set(terms), key=len, reverse=True):
        fragment = re.sub(
            r"(?i)(" + re.escape(html.escape(term)) + r")", r"<mark>\1</mark>", fragment
        )
    prefix = "… " if start > 0 else ""
    suffix = " …" if end < len(text) else ""
    return prefix + fragment + suffix


def _rank(query: str, user, is_admin: bool, limit: int) -> tuple:
    terms = _tokenize(query or "")
    if not terms:
        return [], None
    idx = get_index(user)
    scores: dict = {}
    for term in set(terms):
        plist = idx["postings"].get(term)
        if not plist:
            continue
        idf_t = idx["idf"][term]
        for i, tf in plist:
            dl = idx["doc_len"][i]
            denom = tf + K1 * (1 - B + B * dl / idx["avgdl"])
            scores[i] = scores.get(i, 0.0) + idf_t * (tf * (K1 + 1)) / denom
    ranked = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
    selected = []
    for i, score in ranked:
        doc = idx["docs"][i]
        if doc["admin"] and not is_admin:
            continue
        selected.append((i, doc, round(score, 3)))
        if len(selected) >= limit:
            break
    return selected, idx


def search(query: str, user=None, is_admin: bool = False, limit: int = 20) -> list:
    selected, idx = _rank(query, user, is_admin, limit)
    if not selected:
        return []
    terms = _tokenize(query)
    return [
        {
            "slug": doc["slug"],
            "title": doc["title"],
            "score": score,
            "url": f"/docs/{doc['slug']}.html",
            "snippet": _snippet(idx["doc_text"][i], terms),
        }
        for i, doc, score in selected
    ]


def search_pages(
    query: str,
    user=None,
    is_admin: bool = False,
    limit: int = 6,
    content_chars: int = 2400,
) -> list:
    selected, idx = _rank(query, user, is_admin, limit)
    return [
        {
            "title": doc["title"],
            "slug": doc["slug"],
            "url": f"/docs/{doc['slug']}.html",
            "score": score,
            "content": idx["doc_text"][i][:content_chars],
        }
        for i, doc, score in selected
    ]
