"""PROTOTYP — wegwerfen. Frage: Lassen sich Logo und Containerfarbe
automatisch aus der Website eines Containerdienstes ziehen?

Reine Logik (crawlen, Kandidaten sammeln, Farbe bündeln). Das Vision-Modell
kommt als Funktion herein, damit sich die Logik ohne Netz nachvollziehen lässt.
"""
import base64
import io
import json
import re
import warnings
from urllib.parse import urljoin, urlparse

warnings.filterwarnings("ignore")
import requests
from bs4 import BeautifulSoup
from PIL import Image

UA = {"User-Agent": "Mozilla/5.0 (Macintosh) EV-Vorschau-Prototyp"}
MAX_SEITEN = 30
MAX_BILDER = 80
BILD_ENDUNGEN = (".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif")
LOGO_WORTE = re.compile(r"logo|brand|signet", re.I)


def hole(url, **kw):
    return requests.get(url, headers=UA, timeout=20, **kw)


def gleiche_seite(basis, url):
    a, b = urlparse(basis).netloc, urlparse(url).netloc
    return a.removeprefix("www.") == b.removeprefix("www.")


def seiten_finden(start):
    """Startseite, Sitemap und Links der Startseite, gedeckelt."""
    urls = [start]
    try:
        root = f"{urlparse(start).scheme}://{urlparse(start).netloc}"
        sm = hole(root + "/sitemap.xml")
        if sm.ok and "<loc>" in sm.text:
            locs = re.findall(r"<loc>(.*?)</loc>", sm.text)
            # Sitemap-Index: eine Ebene tiefer
            for loc in [l for l in locs if l.endswith(".xml")][:5]:
                sub = hole(loc)
                if sub.ok:
                    locs += re.findall(r"<loc>(.*?)</loc>", sub.text)
            urls += [l for l in locs if not l.endswith(".xml") and not l.lower().endswith(BILD_ENDUNGEN)]
    except requests.RequestException:
        pass
    try:
        soup = BeautifulSoup(hole(start).text, "html.parser")
        for a in soup.select("a[href]"):
            u = urljoin(start, a["href"]).split("#")[0]
            if u.startswith("http") and gleiche_seite(start, u) and not u.lower().endswith(BILD_ENDUNGEN + (".pdf",)):
                urls.append(u)
    except requests.RequestException:
        pass
    # Container-Seiten zuerst
    eindeutig = list(dict.fromkeys(urls))
    eindeutig.sort(key=lambda u: 0 if u == start else (1 if re.search(r"container|mulde|leistung|galerie|fuhrpark|ueber|über", u, re.I) else 2))
    return eindeutig[:MAX_SEITEN]


def groesste_aus_srcset(srcset):
    teile = [t.strip().split(" ") for t in srcset.split(",") if t.strip()]
    def breite(t):
        return int(re.sub(r"\D", "", t[1]) or 0) if len(t) > 1 else 0
    return max(teile, key=breite)[0] if teile else None


def bilder_sammeln(seite, html):
    """Alle Bild-URLs einer Seite mit Hinweisen, ob sie ein Logo sein könnten."""
    soup = BeautifulSoup(html, "html.parser")
    funde = []

    def dazu(src, logo_signal, quelle):
        if src and not src.startswith("data:"):
            funde.append({"url": urljoin(seite, src.strip()), "logo_signal": logo_signal, "quelle": quelle, "seite": seite})

    for img in soup.find_all("img"):
        src = img.get("data-src") or img.get("data-lazy-src") or img.get("src")
        if img.get("srcset") or img.get("data-srcset"):
            src = groesste_aus_srcset(img.get("data-srcset") or img.get("srcset")) or src
        merkmale = " ".join([src or "", img.get("alt", ""), " ".join(img.get("class", [])), img.get("id", "")])
        im_kopf = img.find_parent(["header", "nav"]) is not None or img.find_parent(class_=re.compile("header|logo|brand", re.I)) is not None
        signal = (2 if LOGO_WORTE.search(merkmale) else 0) + (1 if im_kopf else 0)
        dazu(src, signal, "img")
    for tag in soup.select("[style*='background']"):
        for u in re.findall(r"url\(['\"]?([^'\")]+)", tag["style"]):
            dazu(u, 0, "css")
    for m in soup.select("meta[property='og:image']"):
        dazu(m.get("content"), 0, "og:image")
    for s in soup.select("script[type='application/ld+json']"):
        for u in re.findall(r'"logo"\s*:\s*(?:\{[^}]*"url"\s*:\s*)?"([^"]+)"', s.string or ""):
            dazu(u.replace("\\/", "/"), 3, "schema.org")
    return funde


def normalisiert(url):
    """WordPress-Größenvarianten (-300x200) auf ein Bild zusammenlegen."""
    return re.sub(r"-\d+x\d+(?=\.\w+$)", "", url.split("?")[0])


def bild_laden(url):
    try:
        r = hole(url)
        if not r.ok:
            return None
        if url.lower().split("?")[0].endswith(".svg") or "svg" in r.headers.get("Content-Type", ""):
            return {"svg": True, "bytes": r.content}
        im = Image.open(io.BytesIO(r.content))
        im.load()
        return {"svg": False, "bild": im, "bytes": r.content}
    except Exception:
        return None


def als_jpeg_b64(im, kante=512):
    im = im.convert("RGBA")
    grund = Image.new("RGBA", im.size, (255, 255, 255, 255))
    grund.alpha_composite(im)
    grund = grund.convert("RGB")
    grund.thumbnail((kante, kante))
    buf = io.BytesIO()
    grund.save(buf, "JPEG", quality=80)
    return base64.b64encode(buf.getvalue()).decode()


def hex_zu_rgb(h):
    h = h.lstrip("#")
    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))


def farbe_buendeln(treffer):
    """Treffer [{farbe_hex, sicherheit}] -> (hex, anteil). Nahe Farben bilden eine Gruppe,
    die gewichtsstärkste Gruppe gewinnt, ihr gewichteter Mittelwert ist das Ergebnis."""
    gruppen = []
    for t in treffer:
        rgb = hex_zu_rgb(t["farbe_hex"])
        for g in gruppen:
            if sum((a - b) ** 2 for a, b in zip(rgb, g["zentrum"])) ** 0.5 < 70:
                g["mitglieder"].append((rgb, t["sicherheit"]))
                break
        else:
            gruppen.append({"zentrum": rgb, "mitglieder": [(rgb, t["sicherheit"])]})
    if not gruppen:
        return None, 0
    gesamt = sum(w for g in gruppen for _, w in g["mitglieder"])
    beste = max(gruppen, key=lambda g: sum(w for _, w in g["mitglieder"]))
    gewicht = sum(w for _, w in beste["mitglieder"])
    mittel = [round(sum(c[i] * w for c, w in beste["mitglieder"]) / gewicht) for i in range(3)]
    return "#%02X%02X%02X" % tuple(mittel), round(gewicht / gesamt, 2)


def extrahieren(start, vision_container, vision_logo, protokoll=print):
    seiten = seiten_finden(start)
    protokoll(f"  {len(seiten)} Seiten")
    funde = []
    for s in seiten:
        try:
            r = hole(s)
            if r.ok and "html" in r.headers.get("Content-Type", ""):
                funde += bilder_sammeln(s, r.text)
        except requests.RequestException:
            continue

    # zusammenlegen, stärkstes Logo-Signal behalten
    bilder = {}
    for f in funde:
        k = normalisiert(f["url"])
        if k not in bilder or f["logo_signal"] > bilder[k]["logo_signal"]:
            bilder[k] = {**f, "haeufigkeit": bilder.get(k, {}).get("haeufigkeit", 0)}
        bilder[k]["haeufigkeit"] += 1
    protokoll(f"  {len(bilder)} verschiedene Bilder")

    # Logo: Signal, dann Häufigkeit (steht auf jeder Seite im Kopf)
    logo_kandidaten = sorted([b for b in bilder.values() if b["logo_signal"] > 0],
                             key=lambda b: (b["logo_signal"], b["haeufigkeit"]), reverse=True)[:6]
    geladen_logo = []
    for b in logo_kandidaten:
        d = bild_laden(b["url"])
        if d:
            geladen_logo.append({**b, **d})
    logo = vision_logo(geladen_logo) if geladen_logo else None

    # Containerfotos: große Rasterbilder, keine Logos
    logo_urls = {normalisiert(b["url"]) for b in logo_kandidaten}
    fotos = []
    for k, b in list(bilder.items())[: MAX_BILDER * 2]:
        if k in logo_urls or k.lower().endswith(".svg"):
            continue
        d = bild_laden(b["url"])
        if d and not d["svg"] and min(d["bild"].size) >= 200:
            fotos.append({**b, **d})
        if len(fotos) >= MAX_BILDER:
            break
    protokoll(f"  {len(fotos)} Fotos ab 200 px an das Vision-Modell")
    urteile = vision_container(fotos)
    treffer = [u for u in urteile if u["ist_container"] and u["sicherheit"] >= 0.6 and u["farbe_hex"]]
    farbe, anteil = farbe_buendeln(treffer)

    quelle = "container"
    if not farbe and logo and logo.get("farbe_hex"):
        farbe, anteil, quelle = logo["farbe_hex"], None, "logo"
    return {
        "website": start,
        "seiten": len(seiten),
        "bilder": len(bilder),
        "fotos_geprueft": len(fotos),
        "logo": logo,
        "logo_kandidaten": [b["url"] for b in logo_kandidaten],
        "farbe": farbe,
        "farbe_anteil": anteil,
        "farbe_quelle": quelle if farbe else None,
        "container_treffer": sorted(treffer, key=lambda t: -t["sicherheit"]),
        "container_verworfen": [u for u in urteile if u not in treffer and u["ist_container"]],
    }


def akzentfarbe(start):
    """Häufigste bunte Farbe aus Inline-Styles und eingebundenem CSS der Startseite."""
    import colorsys
    from collections import Counter
    r = hole(start)
    soup = BeautifulSoup(r.text, "html.parser")
    texte = [r.text]
    for l in soup.select("link[rel=stylesheet][href]")[:15]:
        try:
            texte.append(hole(urljoin(start, l["href"])).text)
        except requests.RequestException:
            pass
    zaehler = Counter()
    for t in texte:
        for h in re.findall(r"#([0-9a-fA-F]{6})\b", t):
            zaehler["#" + h.upper()] += 1
        for a, b, c in re.findall(r"rgba?\((\d+),\s*(\d+),\s*(\d+)", t):
            zaehler["#%02X%02X%02X" % (int(a), int(b), int(c))] += 1
    bunt = {}
    for h, n in zaehler.items():
        rr, gg, bb = hex_zu_rgb(h)
        _, l, s = colorsys.rgb_to_hls(rr / 255, gg / 255, bb / 255)
        if s > 0.35 and 0.15 < l < 0.85:
            bunt[h] = n
    return sorted(bunt.items(), key=lambda x: -x[1])[:5]
