"""
Trainingsrahmenprogramm — wiederverwendbare Vorlage (Bibliothek) über mehrere Session-Slots.

Zuordnung zu Trainingsgruppen / konkreten Einheiten erfolgt aus der Planung (Kopie + Lineage),
nicht über group_id oder training_unit_id am Rahmen.
Lesen wie Übungen (official / private / club); Schreiben nur Ersteller oder Plattform-Admin.
"""
from typing import Any, Dict, List, Optional, Sequence

from fastapi import APIRouter, Depends, HTTPException

from club_tenancy import (
    assert_valid_governance_visibility,
    is_platform_admin,
    library_content_visible_to_profile,
)
from db import get_db, get_cursor, r2d

from routers.training_planning import (
    _has_planning_role,
    _hydrate_training_unit_payload,
    _optional_positive_int,
    _insert_sections_from_legacy_exercises,
    _replace_unit_sections,
    _validate_variant_for_exercise,
)

from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql

router = APIRouter(prefix="/api", tags=["training_framework_programs"])
_VALID_VISIBILITY = frozenset({"private", "club", "official"})


def _fetch_framework_row(cur, framework_id: int) -> Dict[str, Any]:
    cur.execute("SELECT * FROM training_framework_programs WHERE id = %s", (framework_id,))
    r = cur.fetchone()
    if not r:
        raise HTTPException(status_code=404, detail="Trainingsrahmen nicht gefunden")
    return r2d(r)


def _framework_assert_readable(
    cur, row: Dict[str, Any], profile_id: int, role: Optional[str]
) -> None:
    if is_platform_admin(role):
        return
    if not library_content_visible_to_profile(
        cur,
        profile_id,
        row.get("visibility") or "private",
        row.get("club_id"),
        row.get("created_by"),
        role,
    ):
        raise HTTPException(status_code=403, detail="Keine Berechtigung für diesen Trainingsrahmen")


def _framework_assert_writable(cur, row: Dict[str, Any], profile_id: int, role: Optional[str]) -> None:
    if is_platform_admin(role):
        return
    if row.get("created_by") != profile_id:
        raise HTTPException(status_code=403, detail="Keine Berechtigung für diesen Trainingsrahmen")


def _framework_access(cur, framework_id: int, profile_id: int, role: str) -> Dict[str, Any]:
    row = _fetch_framework_row(cur, framework_id)
    _framework_assert_readable(cur, row, profile_id, role)
    return row


def _response_framework_detail(framework_id: int, profile_id: int, role: str) -> Dict[str, Any]:
    """Einzelabruf nach Schreiboperation (ohne FastAPI-Depends-Schleife)."""
    with get_db() as conn:
        cur = get_cursor(conn)
        row = _framework_access(cur, framework_id, profile_id, role)
        return _hydrate_framework(cur, row)


def _training_type_ids(cur, framework_id: int) -> List[int]:
    cur.execute(
        """
        SELECT training_type_id
        FROM training_framework_program_training_types
        WHERE framework_program_id = %s
        ORDER BY training_type_id
        """,
        (framework_id,),
    )
    return [r["training_type_id"] for r in cur.fetchall()]


def _target_group_ids(cur, framework_id: int) -> List[int]:
    cur.execute(
        """
        SELECT target_group_id
        FROM training_framework_program_target_groups
        WHERE framework_program_id = %s
        ORDER BY target_group_id
        """,
        (framework_id,),
    )
    return [r["target_group_id"] for r in cur.fetchall()]


def _hydrate_framework(cur, row: Dict[str, Any]) -> Dict[str, Any]:
    fid = row["id"]
    cur.execute(
        """
        SELECT id, framework_program_id, sort_order, title, notes
        FROM training_framework_goals
        WHERE framework_program_id = %s
        ORDER BY sort_order
        """,
        (fid,),
    )
    row["goals"] = [r2d(g) for g in cur.fetchall()]
    cur.execute(
        """
        SELECT id, framework_program_id, sort_order, title, notes
        FROM training_framework_slots
        WHERE framework_program_id = %s
        ORDER BY sort_order
        """,
        (fid,),
    )
    slots = [r2d(s) for s in cur.fetchall()]
    for s in slots:
        cur.execute(
            "SELECT id FROM training_units WHERE framework_slot_id = %s",
            (s["id"],),
        )
        row_b = cur.fetchone()
        if not row_b:
            s["blueprint_training_unit_id"] = None
            s["sections"] = []
            s["exercises"] = []
            continue
        uid = row_b["id"]
        s["blueprint_training_unit_id"] = uid
        unit_min: Dict[str, Any] = {"id": uid}
        _hydrate_training_unit_payload(cur, unit_min)
        s["sections"] = unit_min.get("sections", [])
        s["exercises"] = unit_min.get("exercises", [])
    row["slots"] = slots
    row["training_type_ids"] = _training_type_ids(cur, fid)
    row["target_group_ids"] = _target_group_ids(cur, fid)
    return row


def _assert_visibility(val: Optional[str]) -> Optional[str]:
    if val is None:
        return None
    if val not in _VALID_VISIBILITY:
        raise HTTPException(
            status_code=400,
            detail="visibility muss private, club oder official sein",
        )
    return val


def _parse_positive_int_ids(raw: Any, label: str) -> List[int]:
    if raw is None:
        return []
    if not isinstance(raw, list):
        raise HTTPException(status_code=400, detail=f"{label} muss eine Liste von IDs sein")
    out: List[int] = []
    for item in raw:
        if item in (None, ""):
            continue
        try:
            n = int(item)
        except (TypeError, ValueError):
            raise HTTPException(status_code=400, detail=f"{label}: ungültige ID") from None
        if n <= 0:
            raise HTTPException(status_code=400, detail=f"{label}: ungültige ID")
        if n not in out:
            out.append(n)
    return out


def _replace_training_types(cur, framework_id: int, ids: Sequence[int]) -> None:
    cur.execute(
        "DELETE FROM training_framework_program_training_types WHERE framework_program_id = %s",
        (framework_id,),
    )
    for tid in ids:
        cur.execute(
            """
            INSERT INTO training_framework_program_training_types (framework_program_id, training_type_id)
            VALUES (%s, %s)
            ON CONFLICT DO NOTHING
            """,
            (framework_id, tid),
        )


def _replace_target_groups(cur, framework_id: int, ids: Sequence[int]) -> None:
    cur.execute(
        "DELETE FROM training_framework_program_target_groups WHERE framework_program_id = %s",
        (framework_id,),
    )
    for gid in ids:
        cur.execute(
            """
            INSERT INTO training_framework_program_target_groups (framework_program_id, target_group_id)
            VALUES (%s, %s)
            ON CONFLICT DO NOTHING
            """,
            (framework_id, gid),
        )


def _insert_goal_rows(cur, framework_id: int, goals_in: List[Any]) -> None:
    if not goals_in:
        raise HTTPException(status_code=400, detail="Mindestens ein Entwicklungsziel (goals) ist erforderlich")
    for gi, g in enumerate(goals_in):
        title_g = (g.get("title") or "").strip()
        if not title_g:
            raise HTTPException(status_code=400, detail="Jedes Ziel braucht ein nicht-leeres title")
        order_ix = g.get("sort_order")
        if order_ix is None:
            order_ix = gi
        cur.execute(
            """
            INSERT INTO training_framework_goals (
                framework_program_id, sort_order, title, notes
            ) VALUES (%s, %s, %s, %s)
            """,
            (framework_id, int(order_ix), title_g[:500], g.get("notes")),
        )


def _insert_default_blueprint_section(cur, blueprint_unit_id: int) -> None:
    """Leerer Ablauf, falls noch keine Sektionen existieren."""
    cur.execute(
        "SELECT 1 FROM training_unit_sections WHERE training_unit_id = %s",
        (blueprint_unit_id,),
    )
    if cur.fetchone():
        return
    _replace_unit_sections(
        cur,
        blueprint_unit_id,
        [{"title": "Ablauf", "order_index": 0, "guidance_notes": None, "items": []}],
    )


def _insert_slots_and_blueprints(
    cur,
    framework_id: int,
    slots_in: Optional[List[Any]],
    profile_id: int,
) -> None:
    if slots_in is None:
        return
    for si, slot in enumerate(slots_in):
        order_ix = slot.get("sort_order")
        if order_ix is None:
            order_ix = si
        title_s = slot.get("title")
        if title_s is not None:
            title_s = title_s.strip() or None

        cur.execute(
            """
            INSERT INTO training_framework_slots (
                framework_program_id, sort_order, title, notes, training_unit_id
            ) VALUES (%s, %s, %s, %s, NULL)
            RETURNING id
            """,
            (
                framework_id,
                int(order_ix),
                title_s,
                slot.get("notes"),
            ),
        )
        sid = cur.fetchone()["id"]

        cur.execute(
            """
            INSERT INTO training_units (
                group_id, planned_date,
                planned_time_start, planned_time_end, planned_focus,
                status, notes, trainer_notes,
                created_by, plan_template_id, framework_slot_id
            ) VALUES (
                NULL, NULL,
                NULL, NULL, NULL,
                'planned', NULL, NULL,
                %s, NULL, %s
            )
            RETURNING id
            """,
            (profile_id, sid),
        )
        bid = cur.fetchone()["id"]

        sections_in = slot.get("sections")
        exercises_in = slot.get("exercises")

        if sections_in is not None:
            if len(sections_in) == 0:
                _insert_default_blueprint_section(cur, bid)
            else:
                _replace_unit_sections(cur, bid, sections_in)
        elif exercises_in is not None and len(exercises_in) > 0:
            for raw in exercises_in:
                eid = raw.get("exercise_id")
                if not eid:
                    continue
                vid = _optional_positive_int(raw.get("exercise_variant_id"), "exercise_variant_id")
                _validate_variant_for_exercise(cur, int(eid), vid)
            _insert_sections_from_legacy_exercises(cur, bid, exercises_in)
        else:
            _insert_default_blueprint_section(cur, bid)


@router.get("/training-framework-programs")
def list_training_framework_programs(tenant: TenantContext = Depends(get_tenant_context)):
    profile_id = tenant.profile_id
    role = tenant.global_role
    with get_db() as conn:
        cur = get_cursor(conn)
        base_sel = """
            SELECT fp.*,
                fa.name AS focus_area_name,
                sd.name AS style_direction_name,
                (SELECT COUNT(*)::int FROM training_framework_goals g WHERE g.framework_program_id = fp.id)
                    AS goals_count,
                (SELECT COUNT(*)::int FROM training_framework_slots s WHERE s.framework_program_id = fp.id)
                    AS slots_count,
                (SELECT COUNT(*)::int FROM training_framework_program_training_types t
                    WHERE t.framework_program_id = fp.id) AS training_types_count,
                (SELECT COUNT(*)::int FROM training_framework_program_target_groups tg
                    WHERE tg.framework_program_id = fp.id) AS target_groups_count,
                (
                    SELECT STRING_AGG(typ.name::text, ', ' ORDER BY typ.sort_order NULLS LAST, typ.name)
                    FROM training_framework_program_training_types j
                    JOIN training_types typ ON typ.id = j.training_type_id
                    WHERE j.framework_program_id = fp.id
                ) AS training_type_names_agg,
                (
                    SELECT STRING_AGG(tg.name::text, ', ' ORDER BY tg.name)
                    FROM training_framework_program_target_groups j
                    JOIN target_groups tg ON tg.id = j.target_group_id
                    WHERE j.framework_program_id = fp.id
                ) AS target_group_names_agg
            FROM training_framework_programs fp
            LEFT JOIN focus_areas fa ON fa.id = fp.focus_area_id
            LEFT JOIN style_directions sd ON sd.id = fp.style_direction_id
        """
        vis_clause, vis_params = library_content_visibility_sql(
            alias="fp",
            profile_id=profile_id,
            role=role,
            effective_club_id=tenant.effective_club_id,
        )
        cur.execute(
            base_sel
            + f""" WHERE ({vis_clause})
                ORDER BY fp.updated_at DESC NULLS LAST, fp.title""",
            vis_params,
        )
        return [r2d(r) for r in cur.fetchall()]


@router.get("/training-framework-programs/{framework_id}")
def get_training_framework_program(
    framework_id: int, tenant: TenantContext = Depends(get_tenant_context)
):
    profile_id = tenant.profile_id
    role = tenant.global_role
    return _response_framework_detail(framework_id, profile_id, role)


@router.post("/training-framework-programs")
def create_training_framework_program(
    data: dict, tenant: TenantContext = Depends(get_tenant_context)
):
    profile_id = tenant.profile_id
    role = tenant.global_role
    if not _has_planning_role(role):
        raise HTTPException(status_code=403, detail="Nur Planungsberechtigte dürfen Rahmenprogramme anlegen")

    title = (data.get("title") or "").strip()
    if not title:
        raise HTTPException(status_code=400, detail="title ist Pflicht")

    vis = data.get("visibility") or "private"
    vis = _assert_visibility(vis)
    club_id = data.get("club_id")
    if club_id in ("", []):
        club_id = None
    if vis == "club" and club_id is None:
        club_id = tenant.effective_club_id

    goals_in = data.get("goals")
    slots_in = data.get("slots")
    if not isinstance(goals_in, list) or not goals_in:
        raise HTTPException(status_code=400, detail="goals als Liste mit mindestens einem Eintrag ist Pflicht")

    fa_id = _optional_positive_int(data.get("focus_area_id"), "focus_area_id")
    sd_id = _optional_positive_int(data.get("style_direction_id"), "style_direction_id")
    tt_ids = _parse_positive_int_ids(data.get("training_type_ids"), "training_type_ids")
    tg_ids = _parse_positive_int_ids(data.get("target_group_ids"), "target_group_ids")

    with get_db() as conn:
        cur = get_cursor(conn)
        assert_valid_governance_visibility(cur, profile_id, role, vis, club_id)
        cur.execute(
            """
            INSERT INTO training_framework_programs (
                title, description,
                planned_period_start, planned_period_end,
                visibility, club_id, created_by,
                focus_area_id, style_direction_id
            ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
            RETURNING id
            """,
            (
                title[:200],
                data.get("description"),
                data.get("planned_period_start"),
                data.get("planned_period_end"),
                vis,
                club_id,
                profile_id,
                fa_id,
                sd_id,
            ),
        )
        fid = cur.fetchone()["id"]
        _insert_goal_rows(cur, fid, goals_in)
        _insert_slots_and_blueprints(cur, fid, slots_in, profile_id)
        _replace_training_types(cur, fid, tt_ids)
        _replace_target_groups(cur, fid, tg_ids)
        conn.commit()

    return _response_framework_detail(fid, profile_id, role)


@router.put("/training-framework-programs/{framework_id}")
def update_training_framework_program(
    framework_id: int, data: dict, tenant: TenantContext = Depends(get_tenant_context)
):
    profile_id = tenant.profile_id
    role = tenant.global_role
    if not _has_planning_role(role):
        raise HTTPException(status_code=403, detail="Keine Berechtigung")

    with get_db() as conn:
        cur = get_cursor(conn)
        row_prev = _fetch_framework_row(cur, framework_id)
        _framework_assert_writable(cur, row_prev, profile_id, role)

        merged_vis = row_prev.get("visibility") or "private"
        merged_club = row_prev.get("club_id")
        if "visibility" in data:
            v_m = _assert_visibility(data.get("visibility"))
            if v_m is None:
                raise HTTPException(status_code=400, detail="visibility fehlt")
            merged_vis = v_m
        if "club_id" in data:
            merged_club = data.get("club_id")
            if merged_club in ("", []):
                merged_club = None
        if "visibility" in data or "club_id" in data:
            assert_valid_governance_visibility(cur, profile_id, role, merged_vis, merged_club)

        header_fields = []
        header_params: List[Any] = []

        if "title" in data:
            tit = (data.get("title") or "").strip()
            if not tit:
                raise HTTPException(status_code=400, detail="title ist Pflicht")
            header_fields.append("title = %s")
            header_params.append(tit[:200])

        if "description" in data:
            header_fields.append("description = %s")
            header_params.append(data.get("description"))
        if "planned_period_start" in data:
            header_fields.append("planned_period_start = %s")
            header_params.append(data.get("planned_period_start"))
        if "planned_period_end" in data:
            header_fields.append("planned_period_end = %s")
            header_params.append(data.get("planned_period_end"))

        if "visibility" in data:
            header_fields.append("visibility = %s")
            header_params.append(merged_vis)
        if "club_id" in data:
            header_fields.append("club_id = %s")
            header_params.append(merged_club)

        if "focus_area_id" in data:
            fidv = data.get("focus_area_id")
            header_fields.append("focus_area_id = %s")
            header_params.append(
                None if fidv in (None, "") else _optional_positive_int(fidv, "focus_area_id")
            )
        if "style_direction_id" in data:
            sidv = data.get("style_direction_id")
            header_fields.append("style_direction_id = %s")
            header_params.append(
                None if sidv in (None, "") else _optional_positive_int(sidv, "style_direction_id")
            )

        if header_fields:
            header_fields.append("updated_at = NOW()")
            header_params.append(framework_id)
            cur.execute(
                f"""
                UPDATE training_framework_programs
                SET {", ".join(header_fields)}
                WHERE id = %s
                """,
                tuple(header_params),
            )

        if "training_type_ids" in data:
            tt_ids = _parse_positive_int_ids(data.get("training_type_ids"), "training_type_ids")
            _replace_training_types(cur, framework_id, tt_ids)

        if "target_group_ids" in data:
            tg_ids = _parse_positive_int_ids(data.get("target_group_ids"), "target_group_ids")
            _replace_target_groups(cur, framework_id, tg_ids)

        if "goals" in data:
            goals_in = data["goals"]
            if not isinstance(goals_in, list) or not goals_in:
                raise HTTPException(status_code=400, detail="goals als Liste mit mindestens einem Eintrag ist Pflicht")
            cur.execute(
                "DELETE FROM training_framework_goals WHERE framework_program_id = %s",
                (framework_id,),
            )
            _insert_goal_rows(cur, framework_id, goals_in)

        if "slots" in data:
            cur.execute(
                "DELETE FROM training_framework_slots WHERE framework_program_id = %s",
                (framework_id,),
            )
            _insert_slots_and_blueprints(cur, framework_id, data.get("slots") or [], profile_id)

        if header_fields or "goals" in data or "slots" in data or "training_type_ids" in data or "target_group_ids" in data:
            cur.execute(
                "UPDATE training_framework_programs SET updated_at = NOW() WHERE id = %s",
                (framework_id,),
            )

        conn.commit()

    return _response_framework_detail(framework_id, profile_id, role)


@router.delete("/training-framework-programs/{framework_id}")
def delete_training_framework_program(
    framework_id: int, tenant: TenantContext = Depends(get_tenant_context)
):
    profile_id = tenant.profile_id
    role = tenant.global_role
    with get_db() as conn:
        cur = get_cursor(conn)
        row_fw = _fetch_framework_row(cur, framework_id)
        _framework_assert_writable(cur, row_fw, profile_id, role)
        cur.execute(
            "DELETE FROM training_framework_programs WHERE id = %s",
            (framework_id,),
        )
        conn.commit()
    return {"ok": True}
