"""WCAG 2.1/2.2 Accessibility-Checks fuer Web-Seiten."""

import math
import re
from typing import Any
from src.clients.html_fetcher import fetch_html

# WCAG 2.1 AA Regeln -- Kurzbeschreibung
WCAG_RULES = {
    "1.1.1": {
        "level": "A",
        "title": "Non-text Content",
        "desc": "Alle nicht-textuellen Inhalte brauchen eine Textalternative (alt, aria-label, etc.)",
    },
    "1.3.1": {
        "level": "A",
        "title": "Info and Relationships",
        "desc": "Informationsstruktur muss programmatisch ermittelbar sein (Ueberschriften, Listen, Tabellen)",
    },
    "1.4.1": {
        "level": "A",
        "title": "Use of Color",
        "desc": "Farbe darf nicht das einzige visuelle Mittel sein, um Informationen zu vermitteln",
    },
    "1.4.3": {
        "level": "AA",
        "title": "Contrast (Minimum)",
        "desc": "Normaler Text: Kontrast >= 4.5:1; Grosser Text (>=18pt): Kontrast >= 3:1",
    },
    "1.4.4": {
        "level": "AA",
        "title": "Resize Text",
        "desc": "Text kann bis 200% vergroessert werden ohne Verlust von Inhalt oder Funktionalitaet",
    },
    "2.1.1": {
        "level": "A",
        "title": "Keyboard",
        "desc": "Alle Funktionen muessen per Tastatur bedienbar sein",
    },
    "2.4.1": {
        "level": "A",
        "title": "Bypass Blocks",
        "desc": "Mechanismus zum Ueberspringen wiederholter Inhalte (Skip-Links, Landmarks)",
    },
    "2.4.2": {
        "level": "A",
        "title": "Page Titled",
        "desc": "Seiten muessen einen beschreibenden Titel haben",
    },
    "2.4.4": {
        "level": "A",
        "title": "Link Purpose",
        "desc": "Zweck jedes Links muss aus dem Link-Text oder Kontext ermittelbar sein",
    },
    "2.4.6": {
        "level": "AA",
        "title": "Headings and Labels",
        "desc": "Ueberschriften und Labels muessen beschreibend sein",
    },
    "3.1.1": {
        "level": "A",
        "title": "Language of Page",
        "desc": "Sprache der Seite muss im lang-Attribut angegeben sein",
    },
    "3.2.2": {
        "level": "A",
        "title": "On Input",
        "desc": "Formular-Elemente duerfen keine unerwarteten Kontextwechsel ausloesen",
    },
    "3.3.1": {
        "level": "A",
        "title": "Error Identification",
        "desc": "Eingabe-Fehler muessen erkannt und beschrieben werden",
    },
    "3.3.2": {
        "level": "A",
        "title": "Labels or Instructions",
        "desc": "Formular-Elemente muessen Labels oder Anweisungen haben",
    },
    "4.1.1": {
        "level": "A",
        "title": "Parsing",
        "desc": "HTML muss valide sein (keine doppelten IDs, korrekte Tag-Verschachtelung)",
    },
    "4.1.2": {
        "level": "A",
        "title": "Name, Role, Value",
        "desc": "Alle UI-Elemente brauchen zugaenglichen Namen, Rolle und Zustand (ARIA)",
    },
}


def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
    """Konvertiert Hex-Farbe (#RRGGBB oder #RGB) zu RGB-Tupel."""
    hex_color = hex_color.strip().lstrip("#")
    if len(hex_color) == 3:
        hex_color = "".join(c * 2 for c in hex_color)
    if len(hex_color) != 6:
        raise ValueError(f"Ungueltige Hex-Farbe: #{hex_color}")
    r = int(hex_color[0:2], 16)
    g = int(hex_color[2:4], 16)
    b = int(hex_color[4:6], 16)
    return r, g, b


def _relative_luminance(r: int, g: int, b: int) -> float:
    """Berechnet relative Luminanz nach WCAG 2.x Formel."""
    def linearize(c: int) -> float:
        s = c / 255.0
        return s / 12.92 if s <= 0.03928 else ((s + 0.055) / 1.055) ** 2.4

    return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)


def _contrast_ratio(l1: float, l2: float) -> float:
    """Berechnet Kontrastverhältnis aus zwei Luminanzwerten."""
    lighter = max(l1, l2)
    darker = min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)


async def check_color_contrast(
    foreground: str, background: str, font_size_pt: float = 12.0, bold: bool = False
) -> dict[str, Any]:
    """
    Prueft den Farbkontrast zwischen Vordergrund- und Hintergrundfarbe nach WCAG 2.1.

    Args:
        foreground: Vordergrundfarbe als Hex (#RRGGBB oder #RGB), z.B. '#000000'
        background: Hintergrundfarbe als Hex (#RRGGBB oder #RGB), z.B. '#ffffff'
        font_size_pt: Schriftgroesse in Punkt (Standard: 12pt)
        bold: True wenn Fettschrift

    Returns:
        Kontrastverhältnis, WCAG-Level und Empfehlungen
    """
    try:
        fg_rgb = _hex_to_rgb(foreground)
        bg_rgb = _hex_to_rgb(background)
    except ValueError as e:
        return {"error": str(e)}

    l_fg = _relative_luminance(*fg_rgb)
    l_bg = _relative_luminance(*bg_rgb)
    ratio = _contrast_ratio(l_fg, l_bg)

    # Grosser Text: >= 18pt normal oder >= 14pt fett
    is_large_text = font_size_pt >= 18.0 or (bold and font_size_pt >= 14.0)

    # WCAG 2.1 Schwellenwerte
    aa_threshold = 3.0 if is_large_text else 4.5
    aaa_threshold = 4.5 if is_large_text else 7.0

    passes_aa = ratio >= aa_threshold
    passes_aaa = ratio >= aaa_threshold

    return {
        "foreground": foreground,
        "background": background,
        "contrast_ratio": round(ratio, 2),
        "font_size_pt": font_size_pt,
        "bold": bold,
        "is_large_text": is_large_text,
        "wcag_aa": {
            "threshold": aa_threshold,
            "passes": passes_aa,
            "status": "PASS ✓" if passes_aa else "FAIL ✗",
        },
        "wcag_aaa": {
            "threshold": aaa_threshold,
            "passes": passes_aaa,
            "status": "PASS ✓" if passes_aaa else "FAIL ✗",
        },
        "recommendation": (
            "Kontrast genuegt WCAG 2.1 AAA."
            if passes_aaa
            else (
                "Kontrast genuegt WCAG 2.1 AA, aber nicht AAA."
                if passes_aa
                else f"Kontrast zu niedrig! Minimum fuer {'grossen' if is_large_text else 'normalen'} Text: {aa_threshold}:1"
            )
        ),
        "wcag_rule": "1.4.3 Contrast (Minimum)",
    }


async def check_url_accessibility(url: str) -> dict[str, Any]:
    """
    Fuehrt automatische WCAG 2.1 AA Accessibility-Checks fuer eine URL durch.

    Prueft: Bildtextalternativen, Seitentitel, Sprach-Attribut, Ueberschriften-Hierarchie,
    Formular-Labels, Link-Texte, ARIA-Grundregeln, doppelte IDs.

    Args:
        url: Zu pruefende URL (z.B. 'https://example.com')

    Returns:
        Liste aller gefundenen Probleme nach WCAG-Kriterium sortiert
    """
    try:
        html, soup = await fetch_html(url)
    except Exception as e:
        return {"error": f"URL konnte nicht abgerufen werden: {e}", "url": url}

    issues = []
    warnings = []
    passed = []

    # --- 2.4.2: Seitentitel ---
    title_tag = soup.find("title")
    if not title_tag or not title_tag.get_text(strip=True):
        issues.append({
            "wcag": "2.4.2",
            "level": "A",
            "title": "Page Titled",
            "issue": "Seite hat keinen <title>-Tag oder der Titel ist leer.",
        })
    else:
        passed.append("2.4.2 Page Titled: <title> vorhanden")

    # --- 3.1.1: Sprache ---
    html_tag = soup.find("html")
    if html_tag:
        lang = html_tag.get("lang", "").strip()
        if not lang:
            issues.append({
                "wcag": "3.1.1",
                "level": "A",
                "title": "Language of Page",
                "issue": "<html>-Tag fehlt das lang-Attribut (z.B. lang='de' oder lang='en').",
            })
        else:
            passed.append(f"3.1.1 Language of Page: lang='{lang}' vorhanden")

    # --- 1.1.1: Bilder ohne Alt-Text ---
    images = soup.find_all("img")
    imgs_missing_alt = []
    imgs_empty_alt = 0
    for img in images:
        alt = img.get("alt")
        if alt is None:
            src = img.get("src", "")[:60]
            imgs_missing_alt.append(src)
        elif alt.strip() == "":
            imgs_empty_alt += 1  # Leeres alt ist OK fuer dekorative Bilder

    if imgs_missing_alt:
        issues.append({
            "wcag": "1.1.1",
            "level": "A",
            "title": "Non-text Content",
            "issue": f"{len(imgs_missing_alt)} Bild(er) ohne alt-Attribut gefunden.",
            "details": imgs_missing_alt[:10],
        })
    else:
        passed.append(f"1.1.1 Non-text Content: Alle {len(images)} Bilder haben alt-Attribut")

    # --- 1.3.1: Ueberschriften-Hierarchie ---
    headings = []
    for level in range(1, 7):
        for h in soup.find_all(f"h{level}"):
            headings.append((level, h.get_text(strip=True)[:80]))

    h1_count = sum(1 for h in headings if h[0] == 1)
    if h1_count == 0:
        issues.append({
            "wcag": "1.3.1",
            "level": "A",
            "title": "Info and Relationships",
            "issue": "Kein <h1>-Element gefunden. Jede Seite sollte genau einen H1 haben.",
        })
    elif h1_count > 1:
        warnings.append({
            "wcag": "1.3.1",
            "level": "A",
            "title": "Info and Relationships",
            "warning": f"{h1_count} <h1>-Elemente gefunden. Empfehlung: Genau 1 H1 pro Seite.",
        })

    # Ueberschriften-Sprung pruefen (z.B. H1 -> H3 ohne H2)
    prev_level = 0
    for level, text in headings:
        if prev_level > 0 and level > prev_level + 1:
            issues.append({
                "wcag": "1.3.1",
                "level": "A",
                "title": "Info and Relationships",
                "issue": f"Ueberschriften-Hierarchie-Sprung: H{prev_level} -> H{level} ('{text[:40]}'). H{prev_level+1} fehlt.",
            })
        prev_level = level

    if headings:
        passed.append(f"Ueberschriften-Struktur: {len(headings)} Headings gefunden")

    # --- 3.3.2: Formular-Labels ---
    inputs = soup.find_all(["input", "textarea", "select"])
    unlabeled_inputs = []
    for inp in inputs:
        input_type = inp.get("type", "text").lower()
        # Hidden, Submit, Button, Reset brauchen kein Label
        if input_type in ("hidden", "submit", "button", "reset", "image"):
            continue
        input_id = inp.get("id")
        aria_label = inp.get("aria-label", "").strip()
        aria_labelledby = inp.get("aria-labelledby", "").strip()
        # Label per id-for suchen
        has_label = bool(aria_label or aria_labelledby)
        if not has_label and input_id:
            label = soup.find("label", attrs={"for": input_id})
            has_label = label is not None
        # Umschlossendes label pruefen
        if not has_label:
            parent = inp.parent
            while parent:
                if parent.name == "label":
                    has_label = True
                    break
                parent = parent.parent
        if not has_label:
            name = inp.get("name", inp.get("placeholder", "?"))[:40]
            unlabeled_inputs.append(name)

    if unlabeled_inputs:
        issues.append({
            "wcag": "3.3.2",
            "level": "A",
            "title": "Labels or Instructions",
            "issue": f"{len(unlabeled_inputs)} Formular-Element(e) ohne zugaengliches Label.",
            "details": unlabeled_inputs[:10],
        })
    elif inputs:
        passed.append(f"3.3.2 Labels or Instructions: Alle Formular-Elemente haben Labels")

    # --- 2.4.4: Link-Texte ---
    links = soup.find_all("a", href=True)
    bad_links = []
    generic_texts = {"hier", "here", "click here", "hier klicken", "more", "mehr", "read more", "weiterlesen", "link", "..."}
    for link in links:
        text = link.get_text(strip=True)
        aria_label = link.get("aria-label", "").strip()
        title = link.get("title", "").strip()
        accessible_name = aria_label or title or text
        if not accessible_name:
            img_alts = [img.get("alt", "") for img in link.find_all("img")]
            accessible_name = " ".join(filter(None, img_alts)).strip()
        if not accessible_name:
            bad_links.append(f"<a href='{link.get('href','')[:40]}'>kein Text</a>")
        elif accessible_name.lower() in generic_texts:
            bad_links.append(f"'{accessible_name}' (generischer Link-Text)")

    if bad_links:
        issues.append({
            "wcag": "2.4.4",
            "level": "A",
            "title": "Link Purpose",
            "issue": f"{len(bad_links)} Link(s) mit fehlendem oder generischem Text.",
            "details": bad_links[:10],
        })
    elif links:
        passed.append(f"2.4.4 Link Purpose: {len(links)} Links geprueft, kein generischer Text")

    # --- 4.1.1: Doppelte IDs ---
    all_ids = [tag.get("id") for tag in soup.find_all(id=True)]
    seen = set()
    duplicate_ids = []
    for id_val in all_ids:
        if id_val in seen:
            if id_val not in duplicate_ids:
                duplicate_ids.append(id_val)
        seen.add(id_val)

    if duplicate_ids:
        issues.append({
            "wcag": "4.1.1",
            "level": "A",
            "title": "Parsing",
            "issue": f"{len(duplicate_ids)} doppelte ID(s) gefunden.",
            "details": duplicate_ids[:10],
        })
    else:
        passed.append("4.1.1 Parsing: Keine doppelten IDs")

    # --- 2.4.1: Skip-Links / Landmarks ---
    landmarks = soup.find_all(["main", "nav", "header", "footer", "aside"])
    landmark_roles = soup.find_all(attrs={"role": True})
    role_values = [el.get("role") for el in landmark_roles]
    has_main = bool(soup.find("main")) or "main" in role_values
    skip_links = [a for a in soup.find_all("a") if "#" in a.get("href", "") and a.get_text(strip=True)]

    if not has_main and not skip_links:
        warnings.append({
            "wcag": "2.4.1",
            "level": "A",
            "title": "Bypass Blocks",
            "warning": "Weder <main>-Element noch Skip-Links gefunden. Screen-Reader-Nutzer koennen Navigation nicht ueberspringen.",
        })
    else:
        passed.append("2.4.1 Bypass Blocks: main-Element oder Skip-Links vorhanden")

    # --- 4.1.2: Buttons ohne zugaenglichen Namen ---
    buttons = soup.find_all("button")
    unlabeled_buttons = []
    for btn in buttons:
        text = btn.get_text(strip=True)
        aria_label = btn.get("aria-label", "").strip()
        aria_labelledby = btn.get("aria-labelledby", "").strip()
        title = btn.get("title", "").strip()
        if not (text or aria_label or aria_labelledby or title):
            unlabeled_buttons.append(str(btn)[:60])

    if unlabeled_buttons:
        issues.append({
            "wcag": "4.1.2",
            "level": "A",
            "title": "Name, Role, Value",
            "issue": f"{len(unlabeled_buttons)} Button(s) ohne zugaenglichen Namen.",
            "details": unlabeled_buttons[:5],
        })
    elif buttons:
        passed.append(f"4.1.2 Name, Role, Value: Alle {len(buttons)} Buttons haben zugaenglichen Namen")

    return {
        "url": url,
        "issues_count": len(issues),
        "warnings_count": len(warnings),
        "passed_count": len(passed),
        "wcag_level": "AA",
        "issues": issues,
        "warnings": warnings,
        "passed": passed,
        "summary": (
            f"{len(issues)} WCAG-Probleme, {len(warnings)} Warnungen, {len(passed)} Checks bestanden."
        ),
    }


async def validate_aria(html_snippet: str) -> dict[str, Any]:
    """
    Validiert ARIA-Attribute in einem HTML-Snippet.

    Prueft: aria-* Attribute auf gueltigen Werten, role-Attribute auf bekannte Werte,
    required ARIA-Attribute fuer bestimmte Rollen.

    Args:
        html_snippet: HTML-Code-Snippet (kein vollstaendiges Dokument noetig)

    Returns:
        Liste der ARIA-Probleme mit WCAG-Referenz
    """
    soup = BeautifulSoup(html_snippet, "lxml")
    issues = []
    passed = []

    # Bekannte ARIA-Rollen (Subset der wichtigsten)
    valid_roles = {
        "alert", "alertdialog", "application", "article", "banner", "button",
        "cell", "checkbox", "columnheader", "combobox", "complementary",
        "contentinfo", "definition", "dialog", "directory", "document",
        "feed", "figure", "form", "grid", "gridcell", "group", "heading",
        "img", "link", "list", "listbox", "listitem", "log", "main",
        "marquee", "math", "menu", "menubar", "menuitem", "menuitemcheckbox",
        "menuitemradio", "navigation", "none", "note", "option", "presentation",
        "progressbar", "radio", "radiogroup", "region", "row", "rowgroup",
        "rowheader", "scrollbar", "search", "searchbox", "separator",
        "slider", "spinbutton", "status", "switch", "tab", "table",
        "tablist", "tabpanel", "term", "textbox", "timer", "toolbar",
        "tooltip", "tree", "treegrid", "treeitem",
    }

    # Boolean ARIA-Attribute
    boolean_aria = {
        "aria-atomic", "aria-busy", "aria-disabled", "aria-expanded",
        "aria-grabbed", "aria-haspopup", "aria-hidden", "aria-invalid",
        "aria-live", "aria-modal", "aria-multiline", "aria-multiselectable",
        "aria-pressed", "aria-readonly", "aria-required", "aria-selected",
    }
    valid_boolean = {"true", "false"}

    elements = soup.find_all(True)
    for el in elements:
        # Role pruefen
        role = el.get("role")
        if role and role not in valid_roles:
            issues.append({
                "wcag": "4.1.2",
                "element": str(el)[:80],
                "issue": f"Unbekannte ARIA-Rolle: role='{role}'",
            })

        # ARIA-Attribute pruefen
        for attr, val in el.attrs.items():
            if not attr.startswith("aria-"):
                continue
            # aria-labelledby und aria-describedby: Wert muss vorhandene ID sein
            if attr in ("aria-labelledby", "aria-describedby"):
                ids = val.split() if isinstance(val, str) else val
                for ref_id in ids:
                    if not soup.find(id=ref_id):
                        issues.append({
                            "wcag": "4.1.2",
                            "element": str(el)[:80],
                            "issue": f"{attr}='{ref_id}' verweist auf nicht vorhandene ID.",
                        })
            # Boolean-Attribute pruefen
            elif attr in boolean_aria:
                v = val.lower() if isinstance(val, str) else str(val).lower()
                if v not in valid_boolean and v not in {"mixed", "undefined"}:
                    issues.append({
                        "wcag": "4.1.2",
                        "element": str(el)[:80],
                        "issue": f"{attr}='{val}' -- erwartete 'true' oder 'false'.",
                    })

        # role=img braucht aria-label
        if role == "img":
            if not el.get("aria-label") and not el.get("aria-labelledby"):
                issues.append({
                    "wcag": "1.1.1",
                    "element": str(el)[:80],
                    "issue": "Element mit role='img' hat kein aria-label oder aria-labelledby.",
                })

        # role=button/link ohne Name
        if role in ("button", "link"):
            text = el.get_text(strip=True)
            if not text and not el.get("aria-label") and not el.get("aria-labelledby"):
                issues.append({
                    "wcag": "4.1.2",
                    "element": str(el)[:80],
                    "issue": f"Element mit role='{role}' hat keinen zugaenglichen Namen.",
                })

    if not issues:
        passed.append("Alle ARIA-Attribute im Snippet sind gueltig.")

    return {
        "issues_count": len(issues),
        "issues": issues,
        "passed": passed,
        "wcag_rule": "4.1.2 Name, Role, Value",
    }


async def check_heading_structure(url: str) -> dict[str, Any]:
    """
    Analysiert die Ueberschriften-Hierarchie einer Webseite nach WCAG 1.3.1.

    Prueft: H1-Einzigartigkeit, keine Hierarchie-Spruenge, leere Ueberschriften.

    Args:
        url: Zu pruefende URL

    Returns:
        Ueberschriften-Baum und alle gefundenen Probleme
    """
    try:
        _, soup = await fetch_html(url)
    except Exception as e:
        return {"error": f"URL konnte nicht abgerufen werden: {e}"}

    headings = []
    for level in range(1, 7):
        for h in soup.find_all(f"h{level}"):
            text = h.get_text(strip=True)
            headings.append({"level": level, "text": text, "empty": not bool(text)})

    issues = []
    h1_count = sum(1 for h in headings if h["level"] == 1)

    if h1_count == 0:
        issues.append("Kein H1 gefunden -- jede Seite sollte genau einen H1 haben.")
    elif h1_count > 1:
        issues.append(f"{h1_count} H1-Elemente gefunden -- empfohlen: genau 1 H1 pro Seite.")

    prev_level = 0
    for h in headings:
        if h["empty"]:
            issues.append(f"Leere H{h['level']}-Ueberschrift gefunden.")
        if prev_level > 0 and h["level"] > prev_level + 1:
            issues.append(
                f"Hierarchie-Sprung: H{prev_level} -> H{h['level']} ('{h['text'][:40]}') -- H{prev_level+1} fehlt."
            )
        prev_level = h["level"]

    return {
        "url": url,
        "heading_count": len(headings),
        "headings": headings,
        "issues_count": len(issues),
        "issues": issues,
        "wcag_rule": "1.3.1 Info and Relationships / 2.4.6 Headings and Labels",
        "status": "PASS ✓" if not issues else "FAIL ✗",
    }


async def check_images_alt(url: str) -> dict[str, Any]:
    """
    Prueft alle Bilder einer Webseite auf Alt-Text (WCAG 1.1.1).

    Args:
        url: Zu pruefende URL

    Returns:
        Alle Bilder mit Alt-Text-Status
    """
    try:
        _, soup = await fetch_html(url)
    except Exception as e:
        return {"error": f"URL konnte nicht abgerufen werden: {e}"}

    images = soup.find_all("img")
    results = []
    issues = []

    for img in images:
        src = img.get("src", "")[:80]
        alt = img.get("alt")
        aria_label = img.get("aria-label", "").strip()
        role = img.get("role", "")

        if role == "presentation" or alt == "":
            # Dekoratives Bild -- leeres alt ist korrekt
            status = "dekorativ"
        elif alt is None and not aria_label:
            status = "FEHLT"
            issues.append(src)
        elif alt or aria_label:
            status = "OK"
        else:
            status = "FEHLT"
            issues.append(src)

        results.append({
            "src": src,
            "alt": alt,
            "aria_label": aria_label if aria_label else None,
            "status": status,
        })

    return {
        "url": url,
        "total_images": len(images),
        "missing_alt": len(issues),
        "images": results,
        "wcag_rule": "1.1.1 Non-text Content",
        "status": "PASS ✓" if not issues else "FAIL ✗",
    }


async def check_form_labels(url: str) -> dict[str, Any]:
    """
    Prueft alle Formular-Elemente einer Webseite auf zugaengliche Labels (WCAG 3.3.2).

    Args:
        url: Zu pruefende URL

    Returns:
        Alle Formular-Elemente mit Label-Status
    """
    try:
        _, soup = await fetch_html(url)
    except Exception as e:
        return {"error": f"URL konnte nicht abgerufen werden: {e}"}

    inputs = soup.find_all(["input", "textarea", "select"])
    results = []
    issues = []

    skip_types = {"hidden", "submit", "button", "reset", "image"}

    for inp in inputs:
        input_type = inp.get("type", "text").lower()
        if input_type in skip_types:
            continue

        input_id = inp.get("id", "")
        name = inp.get("name", inp.get("placeholder", ""))[:40]
        aria_label = inp.get("aria-label", "").strip()
        aria_labelledby = inp.get("aria-labelledby", "").strip()

        # Label per for-Attribut suchen
        label_text = None
        if input_id:
            label_el = soup.find("label", attrs={"for": input_id})
            if label_el:
                label_text = label_el.get_text(strip=True)

        # Umschlossendes label pruefen
        if not label_text:
            parent = inp.parent
            while parent:
                if parent.name == "label":
                    label_text = parent.get_text(strip=True)
                    break
                parent = parent.parent

        has_label = bool(aria_label or aria_labelledby or label_text)
        status = "OK" if has_label else "FEHLT"
        if not has_label:
            issues.append(name)

        results.append({
            "type": input_type,
            "name": name,
            "id": input_id,
            "label": label_text or aria_label or aria_labelledby,
            "status": status,
        })

    return {
        "url": url,
        "total_inputs": len(results),
        "missing_labels": len(issues),
        "inputs": results,
        "wcag_rule": "3.3.2 Labels or Instructions",
        "status": "PASS ✓" if not issues else "FAIL ✗",
    }


async def generate_a11y_report(url: str) -> dict[str, Any]:
    """
    Erstellt einen vollstaendigen Accessibility-Bericht fuer eine URL.
    Kombiniert alle Checks: URL-Accessibility, Bilder, Formulare, Ueberschriften.

    Args:
        url: Zu pruefende URL

    Returns:
        Vollstaendiger A11y-Bericht mit Gesamt-Score und Empfehlungen
    """
    # Alle Checks parallel durchfuehren
    import asyncio
    results = await asyncio.gather(
        check_url_accessibility(url),
        check_images_alt(url),
        check_form_labels(url),
        check_heading_structure(url),
        return_exceptions=True,
    )

    general, images, forms, headings = results

    # Fehler abfangen
    if isinstance(general, Exception):
        return {"error": str(general), "url": url}

    total_issues = 0
    if not isinstance(general, Exception):
        total_issues += general.get("issues_count", 0)
    if not isinstance(images, Exception):
        total_issues += images.get("missing_alt", 0)
    if not isinstance(forms, Exception):
        total_issues += forms.get("missing_labels", 0)
    if not isinstance(headings, Exception):
        total_issues += headings.get("issues_count", 0)

    # Score berechnen (0-100, hoeher = besser)
    max_issues = 20
    score = max(0, round(100 - (total_issues / max_issues * 100)))

    return {
        "url": url,
        "accessibility_score": score,
        "total_issues": total_issues,
        "wcag_level_checked": "WCAG 2.1 AA",
        "general_checks": general if not isinstance(general, Exception) else {"error": str(general)},
        "images": images if not isinstance(images, Exception) else {"error": str(images)},
        "forms": forms if not isinstance(forms, Exception) else {"error": str(forms)},
        "headings": headings if not isinstance(headings, Exception) else {"error": str(headings)},
        "recommendation": (
            "Ausgezeichnet! Kaum Accessibility-Probleme gefunden."
            if score >= 90
            else (
                "Gut, aber einige Probleme sollten behoben werden."
                if score >= 70
                else (
                    "Mehrere Accessibility-Probleme gefunden. Bearbeitung empfohlen."
                    if score >= 50
                    else "Erhebliche Accessibility-Probleme. Sofortige Bearbeitung empfohlen."
                )
            )
        ),
    }


async def list_wcag_rules(level: str = "AA") -> dict[str, Any]:
    """
    Listet alle WCAG 2.1 Regeln fuer das angegebene Konformitaetslevel.

    Args:
        level: Konformitaetslevel -- 'A', 'AA' oder 'AAA' (Standard: 'AA')

    Returns:
        Liste aller relevanten WCAG-Regeln mit Beschreibung
    """
    level = level.upper().strip()
    if level not in ("A", "AA", "AAA"):
        level = "AA"

    level_map = {"A": ["A"], "AA": ["A", "AA"], "AAA": ["A", "AA", "AAA"]}
    include_levels = level_map[level]

    filtered = {
        k: v for k, v in WCAG_RULES.items() if v["level"] in include_levels
    }

    return {
        "wcag_version": "2.1",
        "requested_level": level,
        "rules_count": len(filtered),
        "rules": [
            {
                "criterion": k,
                "level": v["level"],
                "title": v["title"],
                "description": v["desc"],
            }
            for k, v in sorted(filtered.items())
        ],
    }
