"""IndexNow — instant re-crawl pings to Bing, Yandex and other participating
engines (not Google) after fixes are applied.

How it works: you place a key file at your site root (a random key whose
filename IS the key, containing that same key as its only line). When URLs
change, the tool sends the engines a list of changed URLs plus the key; the
engine fetches yoursite.com/<key>.txt to confirm you own the site, then
re-crawls only what changed.

Three setup modes for the key file (user picks in settings):
  • "auto"     — the apply flow writes the key file to your root itself
  • "manual"   — the tool generates the key + file; you upload it once yourself
  • "approval" — the key file is proposed like any other fix and uploaded
                 only after you approve it

After the key file exists, IndexNow is purely OUTBOUND: it only pings the
engines over HTTPS and never writes to your server again.
"""
from __future__ import annotations
import secrets
import urllib.parse
import urllib.request
import json
from pathlib import Path

import yaml

APP_DIR = Path(__file__).parent.parent
IN_CFG = APP_DIR / "indexnow.yaml"

# Participating endpoints. One ping to any IndexNow-participating engine is
# shared with the others, but submitting to a couple directly is more robust.
ENDPOINTS = [
    "https://api.indexnow.org/indexnow",
    "https://www.bing.com/indexnow",
]
DEFAULT_CFG = {
    "enabled": False,
    "mode": "manual",          # auto | manual | approval
    "key": "",                 # 8-128 hex chars; generated on demand
    "key_uploaded": False,     # set True once the file is known to be live
}


def generate_key() -> str:
    """32-char hex key — valid IndexNow key (a-f0-9, 8..128 chars)."""
    return secrets.token_hex(16)


def key_filename(key: str) -> str:
    return f"{key}.txt"


def key_file_contents(key: str) -> str:
    """The file's only line is the key itself."""
    return key + "\n"


def key_url(site_url: str, key: str) -> str:
    root = site_url.rstrip("/")
    return f"{root}/{key_filename(key)}"


def load_cfg() -> dict:
    if not IN_CFG.exists():
        save_cfg(dict(DEFAULT_CFG))
    cfg = yaml.safe_load(IN_CFG.read_text(encoding="utf-8")) or {}
    for k, v in DEFAULT_CFG.items():
        cfg.setdefault(k, v)
    return cfg


def save_cfg(cfg: dict):
    IN_CFG.write_text(yaml.safe_dump(cfg, sort_keys=False), encoding="utf-8")


def ensure_key(cfg: dict) -> str:
    """Return the configured key, generating and storing one if absent.
    Mutates cfg in place (caller persists it)."""
    key = (cfg.get("key") or "").strip()
    if not key:
        key = generate_key()
        cfg["key"] = key
        cfg["key_uploaded"] = False
    return key


def submit(site_url: str, changed_urls: list[str], key: str,
           log=print, transport=None) -> dict:
    """Ping the engines with the changed URLs. Returns a per-endpoint result.
    Never raises — a failed ping must never break the apply flow."""
    urls = [u for u in dict.fromkeys(changed_urls) if u.startswith("http")]
    if not urls:
        return {"submitted": 0, "results": {}, "note": "no crawlable URLs"}

    host = urllib.parse.urlparse(site_url).netloc
    payload = json.dumps({
        "host": host,
        "key": key,
        "keyLocation": key_url(site_url, key),
        "urlList": urls,
    }).encode()

    opener = transport or _http_post
    results = {}
    for ep in ENDPOINTS:
        try:
            status = opener(ep, payload)
            results[ep] = status
        except Exception as e:                     # never propagate
            results[ep] = f"error: {type(e).__name__}: {e}"
    ok = sum(1 for v in results.values() if isinstance(v, int) and 200 <= v < 300)
    log(f"  IndexNow: pinged {len(urls)} URL(s) — "
        f"{ok}/{len(ENDPOINTS)} endpoint(s) accepted")
    return {"submitted": len(urls), "results": results, "endpoints_ok": ok}


def _http_post(url: str, data: bytes) -> int:
    req = urllib.request.Request(
        url, data=data,
        headers={"Content-Type": "application/json; charset=utf-8"},
        method="POST")
    with urllib.request.urlopen(req, timeout=15) as resp:
        return resp.status


def to_absolute(site_url: str, rel_files: list[str]) -> list[str]:
    """Map applied relative paths to absolute page URLs worth submitting.
    Skips non-page assets (images, .htaccess, key files, sitemaps handled
    separately)."""
    root = site_url.rstrip("/")
    out = []
    for rel in rel_files:
        low = rel.lower()
        if low.endswith((".php", ".html", ".htm")) or rel in ("", "index.php"):
            out.append(f"{root}/{rel}".rstrip("/") if rel not in ("index.php",)
                       else root + "/")
        elif low in ("sitemap.xml", "llms.txt", "robots.txt"):
            out.append(f"{root}/{rel}")
    return out
