"""Report service — all 5 sheet calculations for the DAK Berichtswesen.

Sheet 1: Auswertung KW gesamt     — weekly totals + year summary
Sheet 2: Auswertung nach Fachgebieten — per-KW per-Fallgruppe breakdown
Sheet 3: Auswertung Gutachten      — per-KW gutachten / alternative / bestaetigung
Sheet 4: Auswertung Therapieaenderungen — per-KW therapy-change metrics
Sheet 5: Auswertung ICD onko       — ICD code frequency for onko cases

All queries use SQLAlchemy (not pandas) against the cases / case_icd_codes tables.
"""

from __future__ import annotations

import logging
from typing import Any

from sqlalchemy import Integer, and_, func
from sqlalchemy.orm import Session

from app.config import get_settings
from app.models.case import Case, CaseICDCode

settings = get_settings()

logger = logging.getLogger(__name__)

# Canonical Fallgruppen in display order
FALLGRUPPEN = ("onko", "kardio", "intensiv", "galle", "sd")

# Report type definitions: each maps to a subset of Fallgruppen
REPORT_TYPES: dict[str, tuple[str, ...]] = {
    "gesamt": FALLGRUPPEN,
    "onko_intensiv": ("onko", "intensiv"),
    "galle_schild": ("galle", "sd"),
}

REPORT_TYPE_LABELS: dict[str, str] = {
    "gesamt": "Gesamt",
    "onko_intensiv": "Onko-Intensiv",
    "galle_schild": "Galle-Schild",
}

# Number of calendar weeks to include (ISO weeks 1..52; 53 is rare)
MAX_KW = 52


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _int(val: Any) -> int:
    """Safely coerce a query result to int (None -> 0)."""
    if val is None:
        return 0
    return int(val)


def _pct(part: int, total: int) -> float | None:
    """Return part/total as a float, or None if total==0."""
    if total == 0:
        return None
    return round(part / total, 4)


def _empty_weekly_row(kw: int) -> dict:
    """Return a zeroed-out weekly row template for Sheet 1."""
    return {
        "kw": kw,
        "erstberatungen": 0,
        "unterlagen": 0,
        "ablehnungen": 0,
        "keine_rm": 0,
        "gutachten": 0,
    }


def _empty_fg_weekly_row(kw: int, fallgruppen: tuple[str, ...] = FALLGRUPPEN) -> dict:
    """Return a zeroed-out weekly row template for Sheet 2."""
    row: dict[str, Any] = {"kw": kw}
    for fg in fallgruppen:
        row[fg] = {"anzahl": 0, "gutachten": 0, "keine_rm": 0}
    return row


def _empty_gutachten_weekly_row(kw: int, fallgruppen: tuple[str, ...] = FALLGRUPPEN) -> dict:
    """Return a zeroed-out weekly row template for Sheet 3."""
    row: dict[str, Any] = {"kw": kw}
    for group in ("gesamt",) + fallgruppen:
        row[group] = {"gutachten": 0, "alternative": 0, "bestaetigung": 0}
    return row


def _empty_ta_weekly_row(kw: int) -> dict:
    """Return a zeroed-out weekly row template for Sheet 4."""
    return {
        "kw": kw,
        "gutachten": 0,
        "ta_ja": 0,
        "ta_nein": 0,
        "diagnosekorrektur": 0,
        "unterversorgung": 0,
        "uebertherapie": 0,
    }


# ---------------------------------------------------------------------------
# Sheet 1: Auswertung KW gesamt
# ---------------------------------------------------------------------------

def calculate_sheet1_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict:
    """Calculate *Auswertung KW gesamt*.

    Returns::

        {
            "summary": {
                "erstberatungen": int,
                "ablehnungen": int,
                "unterlagen": int,
                "keine_rueckmeldung": int,
                "gutachten": int,
            },
            "weekly": [
                {"kw": 1, "erstberatungen": X, "unterlagen": X,
                 "ablehnungen": X, "keine_rm": X, "gutachten": X},
                ...  # kw 1..52
            ]
        }

    Business rules (matching the Excel formulas):

    * Erstberatungen = total cases for the KW
    * Unterlagen     = cases where unterlagen == True
    * Ablehnungen    = cases where ablehnung == True
    * Gutachten      = cases where gutachten == True
    * Keine RM       = Unterlagen - Gutachten  (derived, per KW row)

    If *max_kw* is given, only data up to and including that KW is included.
    """
    # One query: group by kw, count the four flags
    filters = [Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr]
    if fallgruppen is not None:
        filters.append(Case.fallgruppe.in_(fallgruppen))
    if max_kw is not None:
        filters.append(Case.kw <= max_kw)
    rows = (
        db.query(
            Case.kw,
            func.count(Case.id).label("erstberatungen"),
            func.sum(Case.unterlagen.cast(Integer)).label("unterlagen"),
            func.sum(Case.ablehnung.cast(Integer)).label("ablehnungen"),
            func.sum(Case.gutachten.cast(Integer)).label("gutachten"),
        )
        .filter(*filters)
        .group_by(Case.kw)
        .all()
    )

    # Build a lookup kw -> values
    kw_map: dict[int, dict] = {}
    for row in rows:
        kw = _int(row.kw)
        unterlagen = _int(row.unterlagen)
        gutachten = _int(row.gutachten)
        kw_map[kw] = {
            "kw": kw,
            "erstberatungen": _int(row.erstberatungen),
            "unterlagen": unterlagen,
            "ablehnungen": _int(row.ablehnungen),
            "keine_rm": unterlagen - gutachten,
            "gutachten": gutachten,
        }

    # Build full 1..52 list (filling gaps with zeros)
    weekly = []
    for kw in range(1, MAX_KW + 1):
        weekly.append(kw_map.get(kw, _empty_weekly_row(kw)))

    # Summary (sums across all weeks)
    total_erst = sum(w["erstberatungen"] for w in weekly)
    total_abl = sum(w["ablehnungen"] for w in weekly)
    total_unt = sum(w["unterlagen"] for w in weekly)
    total_keine = sum(w["keine_rm"] for w in weekly)
    total_gut = sum(w["gutachten"] for w in weekly)

    summary = {
        "erstberatungen": total_erst,
        "ablehnungen": total_abl,
        "unterlagen": total_unt,
        "keine_rueckmeldung": total_keine,
        "gutachten": total_gut,
    }

    return {"summary": summary, "weekly": weekly}


# ---------------------------------------------------------------------------
# Sheet 2: Auswertung nach Fachgebieten
# ---------------------------------------------------------------------------

def calculate_sheet2_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict:
    """Calculate *Auswertung nach Fachgebieten*.

    Per KW, per Fallgruppe: Anzahl, Gutachten, Keine RM/Ablehnung.

    Returns::

        {
            "weekly": [
                {
                    "kw": 1,
                    "onko":     {"anzahl": X, "gutachten": X, "keine_rm": X},
                    "kardio":   {...},
                    "intensiv": {...},
                    "galle":    {...},
                    "sd":       {...},
                },
                ...
            ]
        }

    Keine RM/Ablehnung = Anzahl - Gutachten  (per the Excel formula).

    If *max_kw* is given, only data up to and including that KW is included.
    If *fallgruppen* is given, only those Fallgruppen are included.
    """
    fgs = fallgruppen or FALLGRUPPEN
    filters = [Case.versicherung == settings.VERSICHERUNG_FILTER, Case.jahr == jahr]
    if fallgruppen is not None:
        filters.append(Case.fallgruppe.in_(fallgruppen))
    if max_kw is not None:
        filters.append(Case.kw <= max_kw)
    rows = (
        db.query(
            Case.kw,
            Case.fallgruppe,
            func.count(Case.id).label("anzahl"),
            func.sum(Case.gutachten.cast(Integer)).label("gutachten"),
        )
        .filter(*filters)
        .group_by(Case.kw, Case.fallgruppe)
        .all()
    )

    # Build kw -> fg -> values
    kw_map: dict[int, dict] = {}
    for row in rows:
        kw = _int(row.kw)
        fg = row.fallgruppe
        if fg not in fgs:
            logger.warning("Unknown fallgruppe '%s' in case data, skipping", fg)
            continue
        if kw not in kw_map:
            kw_map[kw] = _empty_fg_weekly_row(kw, fgs)
        anzahl = _int(row.anzahl)
        gutachten = _int(row.gutachten)
        kw_map[kw][fg] = {
            "anzahl": anzahl,
            "gutachten": gutachten,
            "keine_rm": anzahl - gutachten,
        }

    weekly = []
    for kw in range(1, MAX_KW + 1):
        weekly.append(kw_map.get(kw, _empty_fg_weekly_row(kw, fgs)))

    return {"weekly": weekly}


# ---------------------------------------------------------------------------
# Sheet 3: Auswertung Gutachten
# ---------------------------------------------------------------------------

def calculate_sheet3_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict:
    """Calculate *Auswertung Gutachten*.

    Per KW, per group (gesamt + 5 Fallgruppen):
    Gutachten count, Alternative, Bestaetigung.

    Returns::

        {
            "weekly": [
                {
                    "kw": 1,
                    "gesamt":   {"gutachten": X, "alternative": X, "bestaetigung": X},
                    "onko":     {...},
                    "kardio":   {...},
                    "intensiv": {...},
                    "galle":    {...},
                    "sd":       {...},
                },
                ...
            ]
        }

    In the Excel:
    - Per Fallgruppe: Gutachten = count, Alternative = count where typ='Alternative',
      Bestaetigung = Gutachten - Alternative
    - Gesamt = sum across all Fallgruppen

    If *max_kw* is given, only data up to and including that KW is included.
    If *fallgruppen* is given, only those Fallgruppen are included.
    """
    fgs = fallgruppen or FALLGRUPPEN
    filters = [
        Case.versicherung == settings.VERSICHERUNG_FILTER,
        Case.jahr == jahr,
        Case.gutachten == True,  # noqa: E712
    ]
    if fallgruppen is not None:
        filters.append(Case.fallgruppe.in_(fallgruppen))
    if max_kw is not None:
        filters.append(Case.kw <= max_kw)
    rows = (
        db.query(
            Case.kw,
            Case.fallgruppe,
            func.count(Case.id).label("gutachten"),
            func.sum(
                (Case.gutachten_typ == "Alternative").cast(Integer)
            ).label("alternative"),
        )
        .filter(*filters)
        .group_by(Case.kw, Case.fallgruppe)
        .all()
    )

    kw_map: dict[int, dict] = {}
    for row in rows:
        kw = _int(row.kw)
        fg = row.fallgruppe
        if fg not in fgs:
            continue
        if kw not in kw_map:
            kw_map[kw] = _empty_gutachten_weekly_row(kw, fgs)

        gutachten = _int(row.gutachten)
        alternative = _int(row.alternative)
        kw_map[kw][fg] = {
            "gutachten": gutachten,
            "alternative": alternative,
            "bestaetigung": gutachten - alternative,
        }

    # Compute gesamt (sum of included Fallgruppen per KW)
    for kw_data in kw_map.values():
        total_g = sum(kw_data[fg]["gutachten"] for fg in fgs)
        total_a = sum(kw_data[fg]["alternative"] for fg in fgs)
        kw_data["gesamt"] = {
            "gutachten": total_g,
            "alternative": total_a,
            "bestaetigung": total_g - total_a,
        }

    weekly = []
    for kw in range(1, MAX_KW + 1):
        weekly.append(kw_map.get(kw, _empty_gutachten_weekly_row(kw, fgs)))

    return {"weekly": weekly}


# ---------------------------------------------------------------------------
# Sheet 4: Auswertung Therapieaenderungen
# ---------------------------------------------------------------------------

def calculate_sheet4_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict:
    """Calculate *Auswertung Therapieaenderungen*.

    Per KW: Gutachten count, TA Ja, TA Nein, Diagnosekorrektur,
    Unterversorgung, Uebertherapie.

    Returns::

        {
            "weekly": [
                {
                    "kw": 1,
                    "gutachten": X,
                    "ta_ja": X,
                    "ta_nein": X,
                    "diagnosekorrektur": X,
                    "unterversorgung": X,
                    "uebertherapie": X,
                },
                ...
            ]
        }

    If *max_kw* is given, only data up to and including that KW is included.
    If *fallgruppen* is given, only those Fallgruppen are included.
    """
    filters = [
        Case.versicherung == settings.VERSICHERUNG_FILTER,
        Case.jahr == jahr,
        Case.gutachten == True,  # noqa: E712
    ]
    if fallgruppen is not None:
        filters.append(Case.fallgruppe.in_(fallgruppen))
    if max_kw is not None:
        filters.append(Case.kw <= max_kw)
    rows = (
        db.query(
            Case.kw,
            func.count(Case.id).label("gutachten"),
            func.sum(
                (Case.therapieaenderung == "Ja").cast(Integer)
            ).label("ta_ja"),
            func.sum(
                (Case.therapieaenderung == "Nein").cast(Integer)
            ).label("ta_nein"),
            func.sum(Case.ta_diagnosekorrektur.cast(Integer)).label("diagnosekorrektur"),
            func.sum(Case.ta_unterversorgung.cast(Integer)).label("unterversorgung"),
            func.sum(Case.ta_uebertherapie.cast(Integer)).label("uebertherapie"),
        )
        .filter(*filters)
        .group_by(Case.kw)
        .all()
    )

    kw_map: dict[int, dict] = {}
    for row in rows:
        kw = _int(row.kw)
        kw_map[kw] = {
            "kw": kw,
            "gutachten": _int(row.gutachten),
            "ta_ja": _int(row.ta_ja),
            "ta_nein": _int(row.ta_nein),
            "diagnosekorrektur": _int(row.diagnosekorrektur),
            "unterversorgung": _int(row.unterversorgung),
            "uebertherapie": _int(row.uebertherapie),
        }

    weekly = []
    for kw in range(1, MAX_KW + 1):
        weekly.append(kw_map.get(kw, _empty_ta_weekly_row(kw)))

    return {"weekly": weekly}


# ---------------------------------------------------------------------------
# Sheet 5: Auswertung ICD onko
# ---------------------------------------------------------------------------

def calculate_sheet5_data(db: Session, jahr: int, max_kw: int | None = None, fallgruppen: tuple[str, ...] | None = None) -> dict:
    """Calculate *Auswertung ICD onko*.

    Returns sorted list of ICD codes from onko cases with counts.

    Query: case_icd_codes JOIN cases
           WHERE cases.fallgruppe = 'onko' AND cases.jahr = jahr
           GROUP BY UPPER(icd_code)
           ORDER BY count DESC, icd_code ASC

    Returns::

        {
            "icd_codes": [
                {"icd": "C18", "count": 17},
                {"icd": "C50", "count": 12},
                ...
            ]
        }

    If *max_kw* is given, only data up to and including that KW is included.
    If *fallgruppen* is given and ``'onko'`` is not in the set, returns an
    empty list (ICD codes are only relevant for onko cases).
    """
    fgs = fallgruppen or FALLGRUPPEN
    if "onko" not in fgs:
        return {"icd_codes": []}

    filter_conditions = [
        Case.versicherung == settings.VERSICHERUNG_FILTER,
        Case.fallgruppe == "onko",
        Case.jahr == jahr,
    ]
    if max_kw is not None:
        filter_conditions.append(Case.kw <= max_kw)
    rows = (
        db.query(
            func.upper(CaseICDCode.icd_code).label("icd"),
            func.count(CaseICDCode.id).label("cnt"),
        )
        .join(Case, CaseICDCode.case_id == Case.id)
        .filter(and_(*filter_conditions))
        .group_by(func.upper(CaseICDCode.icd_code))
        .order_by(func.count(CaseICDCode.id).desc(), func.upper(CaseICDCode.icd_code))
        .all()
    )

    icd_codes = [{"icd": row.icd, "count": _int(row.cnt)} for row in rows]
    return {"icd_codes": icd_codes}


# ---------------------------------------------------------------------------
# Dashboard KPIs
# ---------------------------------------------------------------------------

def calculate_dashboard_kpis(db: Session, jahr: int, max_kw: int | None = None) -> dict:
    """Calculate live KPIs for the dashboard.

    If *max_kw* is given, only cases up to and including that calendar week
    are counted.  This is used for fair year-over-year comparisons.

    Returns::

        {
            "total_cases": int,
            "pending_icd": int,
            "pending_coding": int,
            "total_gutachten": int,
            "total_ablehnungen": int,
            "total_unterlagen": int,
            "fallgruppen": {"onko": X, "kardio": X, "intensiv": X, "galle": X, "sd": X},
            "gutachten_typen": {"alternative": X, "bestaetigung": X, "uncodiert": X},
        }
    """
    # Base filters for this portal's insurance + year (+ optional KW cutoff)
    v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER
    base_filters = [v_filter, Case.jahr == jahr]
    if max_kw is not None:
        base_filters.append(Case.kw <= max_kw)

    # Total cases for the year
    total_cases = (
        db.query(func.count(Case.id)).filter(*base_filters).scalar() or 0
    )

    # Cases without ICD codes entered
    pending_icd = (
        db.query(func.count(Case.id))
        .filter(*base_filters, Case.icd == None)  # noqa: E711
        .scalar()
        or 0
    )

    # Gutachten without gutachten_typ (need coding)
    pending_coding = (
        db.query(func.count(Case.id))
        .filter(
            *base_filters,
            Case.gutachten == True,  # noqa: E712
            Case.gutachten_typ == None,  # noqa: E711
        )
        .scalar()
        or 0
    )

    # Gutachten totals
    total_gutachten = (
        db.query(func.count(Case.id))
        .filter(*base_filters, Case.gutachten == True)  # noqa: E712
        .scalar()
        or 0
    )

    # Ablehnungen
    total_ablehnungen = (
        db.query(func.count(Case.id))
        .filter(*base_filters, Case.ablehnung == True)  # noqa: E712
        .scalar()
        or 0
    )

    # Unterlagen
    total_unterlagen = (
        db.query(func.count(Case.id))
        .filter(*base_filters, Case.unterlagen == True)  # noqa: E712
        .scalar()
        or 0
    )

    # Per-Fallgruppe counts
    fg_rows = (
        db.query(Case.fallgruppe, func.count(Case.id).label("cnt"))
        .filter(*base_filters)
        .group_by(Case.fallgruppe)
        .all()
    )
    fallgruppen = {fg: 0 for fg in FALLGRUPPEN}
    for row in fg_rows:
        if row.fallgruppe in fallgruppen:
            fallgruppen[row.fallgruppe] = _int(row.cnt)

    # Gutachten type breakdown
    typ_rows = (
        db.query(Case.gutachten_typ, func.count(Case.id).label("cnt"))
        .filter(*base_filters, Case.gutachten == True)  # noqa: E712
        .group_by(Case.gutachten_typ)
        .all()
    )
    gutachten_typen = {"alternative": 0, "bestaetigung": 0, "uncodiert": 0}
    for row in typ_rows:
        if row.gutachten_typ == "Alternative":
            gutachten_typen["alternative"] = _int(row.cnt)
        elif row.gutachten_typ == "Bestätigung":
            gutachten_typen["bestaetigung"] = _int(row.cnt)
        else:
            gutachten_typen["uncodiert"] = _int(row.cnt)

    # Abrechnung stats (only among cases with gutachten)
    total_abgerechnet = (
        db.query(func.count(Case.id))
        .filter(
            *base_filters,
            Case.gutachten == True,  # noqa: E712
            Case.abgerechnet == True,  # noqa: E712
        )
        .scalar()
        or 0
    )
    pending_abrechnung = total_gutachten - total_abgerechnet

    return {
        "total_cases": total_cases,
        "pending_icd": pending_icd,
        "pending_coding": pending_coding,
        "total_gutachten": total_gutachten,
        "total_abgerechnet": total_abgerechnet,
        "pending_abrechnung": pending_abrechnung,
        "total_ablehnungen": total_ablehnungen,
        "total_unterlagen": total_unterlagen,
        "fallgruppen": fallgruppen,
        "gutachten_typen": gutachten_typen,
    }


# ---------------------------------------------------------------------------
# Dashboard: Yearly KW comparison (multi-year)
# ---------------------------------------------------------------------------

def calculate_yearly_kw_comparison(db: Session) -> list[dict]:
    """Return per-KW total case counts for each year from 2022 onwards.

    Returns a list of dicts like::

        [
            {"kw": 1, "2022": 5, "2023": 8, "2024": 12, ...},
            {"kw": 2, "2022": 3, "2023": 7, "2024": 9, ...},
            ...
        ]
    """
    v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER

    rows = (
        db.query(
            Case.jahr,
            Case.kw,
            func.count(Case.id).label("cnt"),
        )
        .filter(v_filter, Case.jahr >= 2022)
        .group_by(Case.jahr, Case.kw)
        .all()
    )

    # Build lookup: {kw: {year: count}}
    kw_data: dict[int, dict[int, int]] = {}
    all_years: set[int] = set()
    for row in rows:
        yr = int(row.jahr)
        kw = int(row.kw)
        all_years.add(yr)
        if kw not in kw_data:
            kw_data[kw] = {}
        kw_data[kw][yr] = int(row.cnt)

    sorted_years = sorted(all_years)
    result = []
    for kw in range(1, 53):
        entry: dict[str, Any] = {"kw": kw}
        for yr in sorted_years:
            entry[str(yr)] = kw_data.get(kw, {}).get(yr, 0)
        result.append(entry)

    return result


# ---------------------------------------------------------------------------
# Dashboard: Top ICD codes
# ---------------------------------------------------------------------------

def calculate_top_icd(db: Session, jahr: int, limit: int = 10) -> list[dict]:
    """Return the most common ICD codes across all cases for the given year.

    Uses the ``icd`` text field on cases (not the junction table) so it
    covers all Fallgruppen, not just onko.

    Returns::

        [{"icd": "C50.9", "count": 42}, {"icd": "I25.1", "count": 31}, ...]
    """
    v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER

    rows = (
        db.query(
            func.upper(func.trim(Case.icd)).label("icd_code"),
            func.count(Case.id).label("cnt"),
        )
        .filter(
            v_filter,
            Case.jahr == jahr,
            Case.icd != None,  # noqa: E711
            Case.icd != "",
        )
        .group_by(func.upper(func.trim(Case.icd)))
        .order_by(func.count(Case.id).desc())
        .limit(limit)
        .all()
    )

    return [{"icd": row.icd_code, "count": int(row.cnt)} for row in rows]


# ---------------------------------------------------------------------------
# Dashboard: Top Gutachter
# ---------------------------------------------------------------------------

def calculate_top_gutachter(db: Session, jahr: int, limit: int = 10) -> list[dict]:
    """Return the most prolific Gutachter for the given year.

    Returns::

        [{"gutachter": "Dr. Müller", "count": 42}, ...]
    """
    v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER

    rows = (
        db.query(
            func.trim(Case.gutachter).label("name"),
            func.count(Case.id).label("cnt"),
        )
        .filter(
            v_filter,
            Case.jahr == jahr,
            Case.gutachten == True,  # noqa: E712
            Case.gutachter != None,  # noqa: E711
            Case.gutachter != "",
        )
        .group_by(func.trim(Case.gutachter))
        .order_by(func.count(Case.id).desc())
        .limit(limit)
        .all()
    )

    return [{"gutachter": row.name, "count": int(row.cnt)} for row in rows]


# ---------------------------------------------------------------------------
# Dashboard: Gutachten-Statistik (combined sheet3 + sheet4 + KPIs)
# ---------------------------------------------------------------------------

def calculate_gutachten_statistik(db: Session, jahr: int) -> dict:
    """Return combined gutachten statistics for the statistics page.

    Combines:
    - KPI aggregates (total, bestätigung, alternative, uncodiert, therapy changes)
    - Sheet 3 weekly data (gutachten types per KW)
    - Sheet 4 weekly data (therapy changes per KW)

    Returns::

        {
            "kpis": {
                "total_gutachten": int,
                "bestaetigung": int,
                "alternative": int,
                "uncodiert": int,
                "ta_ja": int,
                "ta_nein": int,
                "diagnosekorrektur": int,
                "unterversorgung": int,
                "uebertherapie": int,
            },
            "gutachten_weekly": [{"kw": 1, "bestaetigung": X, "alternative": X}, ...],
            "therapie_weekly": [{"kw": 1, "ta_ja": X, "ta_nein": X, ...}, ...],
        }
    """
    v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER

    # KPI aggregates
    total_gutachten = (
        db.query(func.count(Case.id))
        .filter(v_filter, Case.jahr == jahr, Case.gutachten == True)  # noqa: E712
        .scalar() or 0
    )
    typ_rows = (
        db.query(Case.gutachten_typ, func.count(Case.id).label("cnt"))
        .filter(v_filter, Case.jahr == jahr, Case.gutachten == True)  # noqa: E712
        .group_by(Case.gutachten_typ)
        .all()
    )
    bestaetigung = 0
    alternative = 0
    uncodiert = 0
    for row in typ_rows:
        cnt = _int(row.cnt)
        if row.gutachten_typ == "Bestätigung":
            bestaetigung = cnt
        elif row.gutachten_typ == "Alternative":
            alternative = cnt
        else:
            uncodiert = cnt

    # Therapy change aggregates
    ta_row = (
        db.query(
            func.sum((Case.therapieaenderung == "Ja").cast(Integer)).label("ta_ja"),
            func.sum((Case.therapieaenderung == "Nein").cast(Integer)).label("ta_nein"),
            func.sum(Case.ta_diagnosekorrektur.cast(Integer)).label("dk"),
            func.sum(Case.ta_unterversorgung.cast(Integer)).label("uv"),
            func.sum(Case.ta_uebertherapie.cast(Integer)).label("ut"),
        )
        .filter(v_filter, Case.jahr == jahr, Case.gutachten == True)  # noqa: E712
        .first()
    )

    # Sheet 3 weekly (simplified: gesamt only)
    sheet3 = calculate_sheet3_data(db, jahr)
    gutachten_weekly = [
        {
            "kw": w["kw"],
            "bestaetigung": w["gesamt"]["bestaetigung"],
            "alternative": w["gesamt"]["alternative"],
        }
        for w in sheet3["weekly"]
    ]

    # Sheet 4 weekly
    sheet4 = calculate_sheet4_data(db, jahr)
    therapie_weekly = sheet4["weekly"]

    return {
        "kpis": {
            "total_gutachten": total_gutachten,
            "bestaetigung": bestaetigung,
            "alternative": alternative,
            "uncodiert": uncodiert,
            "ta_ja": _int(ta_row.ta_ja) if ta_row else 0,
            "ta_nein": _int(ta_row.ta_nein) if ta_row else 0,
            "diagnosekorrektur": _int(ta_row.dk) if ta_row else 0,
            "unterversorgung": _int(ta_row.uv) if ta_row else 0,
            "uebertherapie": _int(ta_row.ut) if ta_row else 0,
        },
        "gutachten_weekly": gutachten_weekly,
        "therapie_weekly": therapie_weekly,
    }


# ---------------------------------------------------------------------------
# Full report generation (all 5 sheets)
# ---------------------------------------------------------------------------

def generate_full_report(db: Session, jahr: int, kw: int | None = None, report_type: str = "gesamt") -> dict:
    """Generate complete report data for all 5 sheets.

    If *kw* is given, only data up to and including that calendar week is
    included in the report.  This allows generating historical reports
    that reflect the state at a specific point in the year.

    If *report_type* is given, only the Fallgruppen belonging to that type
    are included (e.g. ``"onko_intensiv"`` → only onko + intensiv).

    Returns::

        {
            "jahr": int,
            "kw": int | None,
            "report_type": str,
            "fallgruppen": list[str],
            "sheet1": {...},
            "sheet2": {...},
            "sheet3": {...},
            "sheet4": {...},
            "sheet5": {...},
        }
    """
    fallgruppen = REPORT_TYPES.get(report_type, FALLGRUPPEN)
    fg_arg = fallgruppen if report_type != "gesamt" else None
    logger.info("Generating full report for jahr=%d, kw=%s, report_type=%s", jahr, kw, report_type)

    return {
        "jahr": jahr,
        "kw": kw,
        "report_type": report_type,
        "fallgruppen": list(fallgruppen),
        "sheet1": calculate_sheet1_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
        "sheet2": calculate_sheet2_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
        "sheet3": calculate_sheet3_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
        "sheet4": calculate_sheet4_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
        "sheet5": calculate_sheet5_data(db, jahr, max_kw=kw, fallgruppen=fg_arg),
    }
