from __future__ import annotations

from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any

import fitz  # PyMuPDF
import numpy as np
from PIL import Image


ITEMS = [
    {"id": "i01", "category": "Anmeldebüro & Kursverlauf", "text": "Der Prozess von der Ankündigung bis zur Teilnahme war unkompliziert"},
    {"id": "i02", "category": "Anmeldebüro & Kursverlauf", "text": "Die Organisation des Kurses (z. B. Zeitplan, Struktur) ist für mich klar und nachvollziehbar."},
    {"id": "i03", "category": "Anmeldebüro & Kursverlauf", "text": "Ich fühle mich über die Abläufe und Termine gut informiert."},
    {"id": "i04", "category": "Gruppenseminare", "text": "Die Inhalte der Gruppenseminare sind für meine berufliche Situation relevant."},
    {"id": "i05", "category": "Gruppenseminare", "text": "Es wurden die erwarteten Themen behandelt."},
    {"id": "i06", "category": "Einzelcoaching", "text": "Die Einzelcoachings helfen mir, meine beruflichen Ziele zu klären und umzusetzen."},
    {"id": "i07", "category": "Einzelcoaching", "text": "Ich erhalte Unterstützung bei der Stellensuche und der Erstellung der Bewerbungsunterlagen."},
    {"id": "i08", "category": "Einzelcoaching", "text": "Mein Coach geht auf meine persönliche Situation und Fragen ein."},
    {"id": "i09", "category": "Einzelcoaching", "text": "Ich erhalte konkrete und umsetzbare Tipps für meinen Bewerbungsprozess."},
    {"id": "i10", "category": "Eigenarbeit", "text": "Die technische Ausstattung (Laptop, Internet) ist ausreichend für meine Bewerbungsaktivitäten."},
    {"id": "i11", "category": "Eigenarbeit", "text": "Bei Fragen während der Eigenarbeit erhalte ich ausreichend Unterstützung."},
    {"id": "i12", "category": "Eigenarbeit", "text": "Ich fühle mich sicherer im Umgang mit digitalen Tools für Bewerbungen."},
    {"id": "i13", "category": "Gesamtbewertung", "text": "Der Kurs hat mir geholfen, ein besseres Verständnis für den aktuellen Arbeitsmarkt zu erhalten."},
    {"id": "i14", "category": "Gesamtbewertung", "text": "Ich habe praktische Tipps für den Bewerbungsprozess erhalten."},
    {"id": "i15", "category": "Gesamtbewertung", "text": "Ich würde den Kurs anderen in meiner Situation weiterempfehlen."},
]

# Coordinates calibrated against the bfz questionnaire rendered at 2x:
# base image size 1191x1682, page rect 595.2x840.95.
BASE_W = 1191.0
BASE_H = 1682.0
X_CENTERS = np.array([927.0, 961.0, 995.0, 1028.0, 1062.0])
Y_ODD = np.array([655.0, 707.0, 759.0, 875.0, 925.0, 1044.0, 1095.0, 1146.0, 1197.0, 1327.0, 1378.0, 1430.0])
Y_EVEN = np.array([280.0, 335.0, 386.0])
X_BOUNDS = np.array([(X_CENTERS[i] + X_CENTERS[i + 1]) / 2 for i in range(4)])


@dataclass
class CellResult:
    value: int | None
    confidence: str
    method: str
    details: str = ""


@dataclass
class QuestionnaireResult:
    questionnaire_id: int
    pdf_pages: str
    values: list[int | None]
    cells: list[CellResult]


def _assign_x(x: float, sx: float) -> int:
    bounds = X_BOUNDS * sx
    return int(1 + np.sum(x > bounds))


def _clip(arr: np.ndarray, y0: int, y1: int, x0: int, x1: int) -> np.ndarray:
    h, w = arr.shape[:2]
    return arr[max(0, y0):min(h, y1), max(0, x0):min(w, x1)]


def _detect_page_values(img: Image.Image, rows: np.ndarray) -> list[CellResult]:
    arr = np.asarray(img.convert("RGB"))
    h, w = arr.shape[:2]
    sx = w / BASE_W
    sy = h / BASE_H

    xs = (X_CENTERS * sx).astype(int)
    ys = (rows * sy).astype(int)
    search_x0 = int(900 * sx)
    search_x1 = int(1115 * sx)

    r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2]
    # Blue/pen marks in the source scans; intentionally permissive.
    blue = (b > 80) & (b > r + 25) & (b > g + 10) & (r < 210) & (g < 210)
    # Black pencil/pen marks. Template smiley ink is also dark, so this is fallback only.
    dark = (r < 155) & (g < 155) & (b < 155)

    results: list[CellResult] = []
    for row_idx, y in enumerate(ys):
        # Blue mark search in the smiley strip, including the common checkmark area above the row.
        band = _clip(blue, int(y - 40 * sy), int(y + 30 * sy), search_x0, search_x1)
        pts = np.argwhere(band)
        if len(pts) >= max(6, int(6 * sx * sy)):
            x_positions = pts[:, 1] + search_x0
            # Left quantile finds the selected smiley even for broad ticks crossing the cell.
            xq = float(np.quantile(x_positions, 0.15))
            value = _assign_x(xq, sx)
            confidence = "high" if len(pts) >= 60 else "low"
            details = f"blue_pixels={len(pts)}, x15={xq:.1f}"
            if len(pts) < 50:
                details += "; bitte prüfen: wenige Pixel / evtl. Randnotiz"
            results.append(CellResult(value, confidence, "blue", details))
            continue

        # Fallback: detect extra dark ink by comparing the five smiley cells.
        scores: list[int] = []
        for x in xs:
            crop = _clip(dark, int(y - 26 * sy), int(y + 26 * sy), int(x - 20 * sx), int(x + 20 * sx))
            scores.append(int(crop.sum()))
        ordered = sorted(scores)
        excess = ordered[-1] - ordered[-2] if len(ordered) >= 2 else 0
        max_score = max(scores) if scores else 0
        if excess >= max(45, int(45 * sx * sy)) or max_score >= max(360, int(360 * sx * sy)):
            value = int(np.argmax(scores) + 1)
            confidence = "medium" if excess >= 70 else "low"
            results.append(CellResult(value, confidence, "dark", f"scores={scores}, excess={excess}; schwarze Markierung/Fallback"))
        else:
            results.append(CellResult(None, "blank", "none", f"scores={scores}, excess={excess}"))

    return results


def extract_pdf(pdf_path: str | Path, render_dir: str | Path | None = None) -> dict[str, Any]:
    """Extract 1-5 smiley values from a scanned bfz evaluation PDF.

    Assumes questionnaire pages come in pairs: first page with 12 scaled items,
    second page with 3 Gesamtbewertung items.
    """
    pdf_path = Path(pdf_path)
    doc = fitz.open(pdf_path)
    page_count = doc.page_count
    if page_count < 2:
        raise ValueError("PDF enthält weniger als 2 Seiten; erwartet werden Fragebogen-Seitenpaare.")

    render_path = Path(render_dir) if render_dir else None
    if render_path:
        render_path.mkdir(parents=True, exist_ok=True)

    rendered: list[Image.Image] = []
    for i, page in enumerate(doc):
        pix = page.get_pixmap(matrix=fitz.Matrix(2, 2), alpha=False)
        img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
        rendered.append(img)
        if render_path:
            img.save(render_path / f"page_{i + 1:02d}.png")

    questionnaires: list[QuestionnaireResult] = []
    pair_count = page_count // 2
    for qidx in range(pair_count):
        page_a_idx = qidx * 2
        page_b_idx = qidx * 2 + 1
        cells = []
        cells.extend(_detect_page_values(rendered[page_a_idx], Y_ODD))
        if page_b_idx < page_count:
            cells.extend(_detect_page_values(rendered[page_b_idx], Y_EVEN))
        else:
            cells.extend([CellResult(None, "blank", "missing_page", "zweite Seite fehlt") for _ in range(3)])
        questionnaires.append(
            QuestionnaireResult(
                questionnaire_id=qidx + 1,
                pdf_pages=f"{page_a_idx + 1}-{page_b_idx + 1 if page_b_idx < page_count else '?'}",
                values=[c.value for c in cells],
                cells=cells,
            )
        )

    warnings: list[str] = []
    if page_count % 2:
        warnings.append("Ungerade Seitenzahl: letzte Seite wurde nicht als vollständiger Fragebogen gewertet.")
    low = sum(1 for q in questionnaires for c in q.cells if c.confidence in {"low", "blank"})
    if low:
        warnings.append(f"{low} Felder sind leer oder niedrig sicher erkannt – in der Tabelle prüfen. Ja, leider genau der langweilige Teil.")

    return {
        "page_count": page_count,
        "questionnaire_count": len(questionnaires),
        "items": ITEMS,
        "questionnaires": [
            {**asdict(q), "cells": [asdict(c) for c in q.cells]} for q in questionnaires
        ],
        "warnings": warnings,
    }
