from datetime import date, time

from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel, Field, model_validator
from sqlmodel import Session, select

from app.db.session import get_session
from app.models import (
    AppSetting,
    Benefit,
    BenefitRedemption,
    Child,
    ChildSettings,
    CoinTransaction,
    CoinTransactionType,
    RoutineDaySetting,
    ScheduleBlock,
    ScheduleDay,
    SpecialDay,
    SpecialDayType,
    TaskInstance,
    TaskKind,
    TaskStatus,
    TaskTemplate,
)
from app.services.admin_auth import get_admin_session
from app.services.benefits import serialize_benefit
from app.services.schedule import serialize_routine_setting
from app.services.tasks import deactivate_task_template, ledger_for_child

router = APIRouter(prefix="/admin", tags=["admin-crud"])


def require_admin(
    x_admin_session: str | None = Header(default=None, alias="X-Admin-Session"),
    session: Session = Depends(get_session),
) -> None:
    get_admin_session(session, x_admin_session)


class ChildUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=80)
    color: str | None = Field(default=None, min_length=1, max_length=32)
    avatar: str | None = Field(default=None, min_length=1, max_length=16)
    sort_order: int | None = None


class ChildSettingsUpdate(BaseModel):
    language_quiz_language: str | None = Field(default=None, min_length=2, max_length=8)
    language_quiz_level: int | None = Field(default=None, ge=1, le=4)
    math_quiz_level: int | None = Field(default=None, ge=1, le=5)
    knowledge_quiz_level: int | None = Field(default=None, ge=1, le=4)


class TaskTemplateCreate(BaseModel):
    child_id: int | None = None
    slug: str = Field(min_length=1, max_length=120)
    title: str = Field(min_length=1, max_length=160)
    description: str = ""
    kind: TaskKind = TaskKind.required
    coin_value: int = 0
    penalty_value: int = 0
    active: bool = True
    applies_weekdays: str = "0,1,2,3,4"
    sort_order: int = 0


class TaskTemplateUpdate(BaseModel):
    slug: str | None = Field(default=None, min_length=1, max_length=120)
    title: str | None = Field(default=None, min_length=1, max_length=160)
    description: str | None = None
    kind: TaskKind | None = None
    coin_value: int | None = None
    penalty_value: int | None = None
    active: bool | None = None
    applies_weekdays: str | None = None
    sort_order: int | None = None


class BenefitCreate(BaseModel):
    slug: str = Field(min_length=1, max_length=120)
    title: str = Field(min_length=1, max_length=160)
    description: str = ""
    price: int = Field(ge=0)
    child_id: int | None = None
    active: bool = True
    sort_order: int = 0


class BenefitUpdate(BaseModel):
    slug: str | None = Field(default=None, min_length=1, max_length=120)
    title: str | None = Field(default=None, min_length=1, max_length=160)
    description: str | None = None
    price: int | None = Field(default=None, ge=0)
    child_id: int | None = None
    active: bool | None = None
    sort_order: int | None = None


class ManualCoinRequest(BaseModel):
    child_id: int
    amount: int
    comment: str = Field(min_length=1, max_length=240)
    reference_transaction_id: int | None = None
    idempotency_key: str | None = None

    @model_validator(mode="after")
    def validate_manual_coin(self):
        if not self.comment.strip():
            raise ValueError("Kommentar ist Pflicht")
        if self.amount == 0:
            raise ValueError("Betrag darf nicht 0 sein")
        return self


class VoidCoinRequest(BaseModel):
    comment: str = Field(default="", max_length=240)
    idempotency_key: str | None = None


class BulkVoidCoinRequest(BaseModel):
    transaction_ids: list[int] = Field(min_length=1, max_length=100)
    comment: str = Field(default="", max_length=240)
    idempotency_key: str | None = None


class BulkDeleteCoinRequest(BaseModel):
    child_id: int
    transaction_ids: list[int] = Field(min_length=1, max_length=200)
    confirm: str


class ResetCoinHistoryRequest(BaseModel):
    confirm: str
    delete_redemptions: bool = True


class SpecialDayCreate(BaseModel):
    child_id: int | None = None
    day: date
    day_type: SpecialDayType
    title: str = Field(min_length=1, max_length=160)
    notes: str = ""


class SpecialDayUpdate(BaseModel):
    child_id: int | None = None
    day: date | None = None
    day_type: SpecialDayType | None = None
    title: str | None = Field(default=None, min_length=1, max_length=160)
    notes: str | None = None


class SettingUpdate(BaseModel):
    value: str = Field(min_length=1, max_length=500)
    description: str | None = None


class ScheduleDayUpsert(BaseModel):
    child_id: int
    weekday: int = Field(ge=0, le=6)
    school_start: time | None = None
    school_end: time | None = None
    is_school_day: bool = True
    notes: str = ""


class ScheduleBlockCreate(BaseModel):
    schedule_day_id: int
    starts_at: time
    ends_at: time
    subject: str = Field(min_length=1, max_length=120)
    room: str = ""
    requires_sports_gear: bool = False
    sort_order: int = 0


class ScheduleBlockUpdate(BaseModel):
    starts_at: time | None = None
    ends_at: time | None = None
    subject: str | None = Field(default=None, min_length=1, max_length=120)
    room: str | None = None
    requires_sports_gear: bool | None = None
    sort_order: int | None = None


class RoutineDaySettingUpsert(BaseModel):
    child_id: int
    weekday: int = Field(ge=1, le=7)
    wake_time: time | None = None
    teeth_start: time | None = None
    leave_countdown_minutes: int = Field(default=10, ge=1, le=60)
    timer_hooks: str = Field(default="10,3", max_length=80)
    notes: str = Field(default="", max_length=500)
    active: bool = True


def _apply(model, data: dict) -> None:
    for key, value in data.items():
        setattr(model, key, value)


def _serialize_template(item: TaskTemplate) -> dict:
    return {
        "id": item.id,
        "child_id": item.child_id,
        "scope": "shared" if item.child_id is None else "child",
        "slug": item.slug,
        "title": item.title,
        "description": item.description,
        "kind": item.kind.value if hasattr(item.kind, "value") else item.kind,
        "coin_value": item.coin_value,
        "penalty_value": item.penalty_value,
        "active": item.active,
        "applies_weekdays": item.applies_weekdays,
        "sort_order": item.sort_order,
    }


def _task_tombstone_key(child_id: int | None, slug: str) -> str:
    return f"deleted_task_template:{child_id or 'shared'}:{slug}"


def _is_task_template_deleted(session: Session, item: TaskTemplate) -> bool:
    return session.get(AppSetting, _task_tombstone_key(item.child_id, item.slug)) is not None


def _mark_task_template_deleted(session: Session, item: TaskTemplate) -> None:
    key = _task_tombstone_key(item.child_id, item.slug)
    existing = session.get(AppSetting, key)
    if existing is None:
        session.add(AppSetting(key=key, value="true", description=f"Gelöschte Aufgabenvorlage: {item.title}"))
    else:
        existing.value = "true"
        session.add(existing)


def _delete_task_template(session: Session, template: TaskTemplate, today: date) -> dict:
    result = deactivate_task_template(session, template, today)
    _mark_task_template_deleted(session, template)
    instances = session.exec(
        select(TaskInstance).where(
            TaskInstance.task_template_id == template.id,
            TaskInstance.task_date >= today,
            TaskInstance.status != TaskStatus.done,
        )
    ).all()
    for instance in instances:
        session.delete(instance)
    template.active = False
    session.add(template)
    return {"cancelled_instances": result["cancelled_instances"], "deleted_instances": len(instances)}


def _serialize_special_day(item: SpecialDay) -> dict:
    return {
        "id": item.id,
        "child_id": item.child_id,
        "day": item.day.isoformat(),
        "day_type": item.day_type.value if hasattr(item.day_type, "value") else item.day_type,
        "title": item.title,
        "notes": item.notes,
    }


def _serialize_schedule_day(item: ScheduleDay) -> dict:
    return {
        "id": item.id,
        "child_id": item.child_id,
        "weekday": item.weekday,
        "school_start": item.school_start.isoformat() if item.school_start else None,
        "school_end": item.school_end.isoformat() if item.school_end else None,
        "is_school_day": item.is_school_day,
        "notes": item.notes,
    }


def _serialize_schedule_block(item: ScheduleBlock) -> dict:
    return {
        "id": item.id,
        "schedule_day_id": item.schedule_day_id,
        "starts_at": item.starts_at.isoformat(),
        "ends_at": item.ends_at.isoformat(),
        "subject": item.subject,
        "room": item.room,
        "requires_sports_gear": item.requires_sports_gear,
        "sort_order": item.sort_order,
    }


def _serialize_child_with_settings(session: Session, child: Child) -> dict:
    settings = session.exec(select(ChildSettings).where(ChildSettings.child_id == child.id)).first()
    payload = child.model_dump()
    payload["settings"] = None if settings is None else {
        "language_quiz_language": settings.language_quiz_language,
        "language_quiz_level": settings.language_quiz_level,
        "math_quiz_level": settings.math_quiz_level,
        "knowledge_quiz_level": settings.knowledge_quiz_level,
    }
    return payload


def _coin_tx_payload(tx: CoinTransaction) -> dict:
    return {
        "id": tx.id,
        "amount": tx.amount,
        "reserved_amount": tx.reserved_amount,
        "comment": tx.comment,
        "reference_transaction_id": tx.reference_transaction_id,
    }


def _void_coin_transaction(session: Session, transaction_id: int, comment: str = "", idempotency_key: str | None = None) -> CoinTransaction:
    original = session.get(CoinTransaction, transaction_id)
    if original is None:
        raise HTTPException(status_code=404, detail={"code": "transaction_not_found", "message": f"Buchung #{transaction_id} nicht gefunden"})
    key = idempotency_key or f"void:{transaction_id}"
    existing = session.exec(select(CoinTransaction).where(CoinTransaction.idempotency_key == key)).first()
    if existing is not None:
        return existing
    already_voided = session.exec(
        select(CoinTransaction).where(
            CoinTransaction.reference_transaction_id == transaction_id,
            CoinTransaction.idempotency_key == f"void:{transaction_id}",
        )
    ).first()
    if already_voided is not None:
        return already_voided
    reason = comment.strip() or f"Storno zu Buchung #{transaction_id}"
    tx = CoinTransaction(
        child_id=original.child_id,
        transaction_type=CoinTransactionType.manual,
        amount=-original.amount,
        reserved_amount=-original.reserved_amount,
        comment=f"{reason} (Storno zu Buchung #{transaction_id})",
        reference_transaction_id=transaction_id,
        idempotency_key=key,
    )
    session.add(tx)
    session.flush()
    return tx


def _dependent_coin_transaction_ids(session: Session, child_id: int, transaction_ids: list[int]) -> set[int]:
    pending = set(transaction_ids)
    collected: set[int] = set()
    while pending:
        current = pending - collected
        if not current:
            break
        collected.update(current)
        dependents = session.exec(
            select(CoinTransaction.id).where(
                CoinTransaction.child_id == child_id,
                CoinTransaction.reference_transaction_id.in_(current),  # type: ignore[attr-defined]
            )
        ).all()
        pending.update(int(item) for item in dependents if item is not None)
    return collected


@router.get("/children", dependencies=[Depends(require_admin)])
def admin_children(session: Session = Depends(get_session)) -> dict:
    children = session.exec(select(Child).order_by(Child.sort_order)).all()
    return {"children": [_serialize_child_with_settings(session, child) for child in children]}


@router.patch("/children/{child_id}", dependencies=[Depends(require_admin)])
def update_child(child_id: int, payload: ChildUpdate, session: Session = Depends(get_session)) -> dict:
    child = session.get(Child, child_id)
    if child is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    _apply(child, payload.model_dump(exclude_unset=True))
    session.add(child)
    session.commit()
    session.refresh(child)
    return {"child": _serialize_child_with_settings(session, child)}


@router.patch("/children/{child_id}/settings", dependencies=[Depends(require_admin)])
def update_child_settings(child_id: int, payload: ChildSettingsUpdate, session: Session = Depends(get_session)) -> dict:
    child = session.get(Child, child_id)
    if child is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    settings = session.exec(select(ChildSettings).where(ChildSettings.child_id == child_id)).first()
    if settings is None:
        settings = ChildSettings(child_id=child_id)
    _apply(settings, payload.model_dump(exclude_unset=True))
    session.add(settings)
    session.commit()
    session.refresh(settings)
    return {"child": _serialize_child_with_settings(session, child)}


def _sync_open_instances_from_template(session: Session, template: TaskTemplate, start_date: date) -> int:
    instances = session.exec(
        select(TaskInstance).where(
            TaskInstance.task_template_id == template.id,
            TaskInstance.task_date >= start_date,
            TaskInstance.status == TaskStatus.open,
        )
    ).all()
    for instance in instances:
        instance.title = template.title
        instance.kind = template.kind
        instance.coin_value = template.coin_value
        instance.penalty_value = template.penalty_value
        session.add(instance)
    return len(instances)


@router.get("/task-templates", dependencies=[Depends(require_admin)])
def list_task_templates(session: Session = Depends(get_session)) -> dict:
    rows = session.exec(select(TaskTemplate).order_by(TaskTemplate.child_id, TaskTemplate.sort_order)).all()
    visible_rows = [row for row in rows if not _is_task_template_deleted(session, row)]
    return {"task_templates": [_serialize_template(row) for row in visible_rows]}


@router.post("/task-templates", status_code=201, dependencies=[Depends(require_admin)])
def create_task_template(payload: TaskTemplateCreate, session: Session = Depends(get_session)) -> dict:
    if payload.child_id is not None and session.get(Child, payload.child_id) is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    row = TaskTemplate(**payload.model_dump())
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"task_template": _serialize_template(row)}


@router.patch("/task-templates/{template_id}", dependencies=[Depends(require_admin)])
def update_task_template(template_id: int, payload: TaskTemplateUpdate, session: Session = Depends(get_session)) -> dict:
    row = session.get(TaskTemplate, template_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "task_template_not_found", "message": "Aufgabenvorlage nicht gefunden"})
    data = payload.model_dump(exclude_unset=True)
    was_active = row.active
    _apply(row, data)
    cancelled_instances = 0
    synced_instances = 0
    if was_active and data.get("active") is False:
        result = deactivate_task_template(session, row, date.today())
        cancelled_instances = result["cancelled_instances"]
    else:
        session.add(row)
        if any(key in data for key in ("title", "kind", "coin_value", "penalty_value")):
            synced_instances = _sync_open_instances_from_template(session, row, date.today())
    session.commit()
    session.refresh(row)
    return {"ok": True, "task_template": _serialize_template(row), "cancelled_instances": cancelled_instances, "synced_instances": synced_instances}


@router.delete("/task-templates/{template_id}", dependencies=[Depends(require_admin)])
def delete_task_template(template_id: int, session: Session = Depends(get_session)) -> dict:
    row = session.get(TaskTemplate, template_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "task_template_not_found", "message": "Aufgabenvorlage nicht gefunden"})
    result = _delete_task_template(session, row, date.today())
    session.commit()
    session.refresh(row)
    return {
        "ok": True,
        "deleted": True,
        "task_template": _serialize_template(row),
        "cancelled_instances": result["cancelled_instances"],
        "deleted_instances": result["deleted_instances"],
    }


@router.get("/benefits", dependencies=[Depends(require_admin)])
def admin_list_benefits(session: Session = Depends(get_session)) -> dict:
    rows = session.exec(select(Benefit).order_by(Benefit.sort_order, Benefit.title)).all()
    return {"benefits": [serialize_benefit(row) for row in rows]}


@router.post("/benefits", status_code=201, dependencies=[Depends(require_admin)])
def admin_create_benefit(payload: BenefitCreate, session: Session = Depends(get_session)) -> dict:
    row = Benefit(**payload.model_dump())
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"benefit": serialize_benefit(row)}


@router.patch("/benefits/{benefit_id}", dependencies=[Depends(require_admin)])
def admin_update_benefit(benefit_id: int, payload: BenefitUpdate, session: Session = Depends(get_session)) -> dict:
    row = session.get(Benefit, benefit_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "benefit_not_found", "message": "Belohnung nicht gefunden"})
    _apply(row, payload.model_dump(exclude_unset=True))
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"benefit": serialize_benefit(row)}


@router.delete("/benefits/{benefit_id}", dependencies=[Depends(require_admin)])
def admin_delete_benefit(benefit_id: int, session: Session = Depends(get_session)) -> dict:
    row = session.get(Benefit, benefit_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "benefit_not_found", "message": "Belohnung nicht gefunden"})
    row.active = False
    session.add(row)
    session.commit()
    return {"ok": True}


@router.get("/coins/{child_id}/ledger", dependencies=[Depends(require_admin)])
def admin_coin_ledger(child_id: int, session: Session = Depends(get_session)) -> dict:
    child = session.get(Child, child_id)
    if child is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    ledger = ledger_for_child(session, child_id)
    ledger["child"] = child.model_dump()
    return ledger


@router.post("/coins/manual", dependencies=[Depends(require_admin)])
def manual_coin(payload: ManualCoinRequest, session: Session = Depends(get_session)) -> dict:
    if session.get(Child, payload.child_id) is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    key = payload.idempotency_key or f"manual:{payload.child_id}:{payload.amount}:{payload.comment}:{payload.reference_transaction_id or 'none'}"
    existing = session.exec(select(CoinTransaction).where(CoinTransaction.idempotency_key == key)).first()
    if existing is None:
        comment = payload.comment.strip()
        if payload.reference_transaction_id:
            reference = session.get(CoinTransaction, payload.reference_transaction_id)
            if reference is None or reference.child_id != payload.child_id:
                raise HTTPException(status_code=404, detail={"code": "reference_transaction_not_found", "message": "Referenzbuchung nicht gefunden"})
            comment = f"{comment} (Korrektur zu Buchung #{payload.reference_transaction_id})"
        existing = CoinTransaction(
            child_id=payload.child_id,
            transaction_type=CoinTransactionType.manual,
            amount=payload.amount,
            reserved_amount=0,
            comment=comment,
            reference_transaction_id=payload.reference_transaction_id,
            idempotency_key=key,
        )
        session.add(existing)
        session.commit()
        session.refresh(existing)
    return {"transaction": {"id": existing.id, "amount": existing.amount, "comment": existing.comment}, "ledger": ledger_for_child(session, payload.child_id)}


@router.post("/coins/{transaction_id}/void", dependencies=[Depends(require_admin)])
def void_coin_transaction(transaction_id: int, payload: VoidCoinRequest, session: Session = Depends(get_session)) -> dict:
    original = session.get(CoinTransaction, transaction_id)
    if original is None:
        raise HTTPException(status_code=404, detail={"code": "transaction_not_found", "message": "Buchung nicht gefunden"})
    tx = _void_coin_transaction(session, transaction_id, payload.comment, payload.idempotency_key)
    session.commit()
    session.refresh(tx)
    return {"transaction": _coin_tx_payload(tx), "ledger": ledger_for_child(session, original.child_id)}


@router.post("/coins/bulk-void", dependencies=[Depends(require_admin)])
def bulk_void_coin_transactions(payload: BulkVoidCoinRequest, session: Session = Depends(get_session)) -> dict:
    ids = list(dict.fromkeys(payload.transaction_ids))
    originals = session.exec(select(CoinTransaction).where(CoinTransaction.id.in_(ids))).all()  # type: ignore[attr-defined]
    found_ids = {tx.id for tx in originals}
    missing = [tx_id for tx_id in ids if tx_id not in found_ids]
    if missing:
        raise HTTPException(status_code=404, detail={"code": "transaction_not_found", "message": f"Buchung nicht gefunden: {missing[0]}"})
    if len({tx.child_id for tx in originals}) > 1:
        raise HTTPException(status_code=400, detail={"code": "mixed_children", "message": "Bitte Buchungen nur pro Kind gemeinsam stornieren"})
    created = []
    for tx_id in ids:
        created.append(_void_coin_transaction(session, tx_id, payload.comment, f"{payload.idempotency_key or 'bulk-void'}:{tx_id}"))
    child_id = originals[0].child_id
    session.commit()
    return {"transactions": [_coin_tx_payload(tx) for tx in created], "ledger": ledger_for_child(session, child_id)}


@router.delete("/coins/bulk", dependencies=[Depends(require_admin)])
def delete_coin_transactions(payload: BulkDeleteCoinRequest, session: Session = Depends(get_session)) -> dict:
    if payload.confirm != "DELETE_COIN_BUCHUNGEN":
        raise HTTPException(status_code=400, detail={"code": "confirmation_required", "message": "Bestätigung DELETE_COIN_BUCHUNGEN fehlt"})
    if session.get(Child, payload.child_id) is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    ids_to_delete = _dependent_coin_transaction_ids(session, payload.child_id, payload.transaction_ids)
    rows = session.exec(
        select(CoinTransaction).where(
            CoinTransaction.child_id == payload.child_id,
            CoinTransaction.id.in_(ids_to_delete),  # type: ignore[attr-defined]
        )
    ).all()
    found_ids = {tx.id for tx in rows}
    missing = [tx_id for tx_id in payload.transaction_ids if tx_id not in found_ids]
    if missing:
        raise HTTPException(status_code=404, detail={"code": "transaction_not_found", "message": f"Buchung nicht gefunden: {missing[0]}"})
    for tx in rows:
        session.delete(tx)
    session.commit()
    return {"ok": True, "deleted": len(rows), "ledger": ledger_for_child(session, payload.child_id)}


@router.post("/coins/{child_id}/reset", dependencies=[Depends(require_admin)])
def reset_coin_history(child_id: int, payload: ResetCoinHistoryRequest, session: Session = Depends(get_session)) -> dict:
    if payload.confirm != "COINS_AUF_NULL":
        raise HTTPException(status_code=400, detail={"code": "confirmation_required", "message": "Bestätigung COINS_AUF_NULL fehlt"})
    if session.get(Child, child_id) is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    transactions = session.exec(select(CoinTransaction).where(CoinTransaction.child_id == child_id)).all()
    for tx in transactions:
        session.delete(tx)
    redemptions_deleted = 0
    if payload.delete_redemptions:
        redemptions = session.exec(select(BenefitRedemption).where(BenefitRedemption.child_id == child_id)).all()
        redemptions_deleted = len(redemptions)
        for redemption in redemptions:
            session.delete(redemption)
    session.commit()
    return {"ok": True, "deleted": len(transactions), "redemptions_deleted": redemptions_deleted, "ledger": ledger_for_child(session, child_id)}


@router.get("/special-days", dependencies=[Depends(require_admin)])
def list_special_days(session: Session = Depends(get_session)) -> dict:
    rows = session.exec(select(SpecialDay).order_by(SpecialDay.day)).all()
    return {"special_days": [_serialize_special_day(row) for row in rows]}


@router.post("/special-days", status_code=201, dependencies=[Depends(require_admin)])
def create_special_day(payload: SpecialDayCreate, session: Session = Depends(get_session)) -> dict:
    row = SpecialDay(**payload.model_dump())
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"special_day": _serialize_special_day(row)}


@router.patch("/special-days/{special_day_id}", dependencies=[Depends(require_admin)])
def update_special_day(special_day_id: int, payload: SpecialDayUpdate, session: Session = Depends(get_session)) -> dict:
    row = session.get(SpecialDay, special_day_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "special_day_not_found", "message": "Spezialtag nicht gefunden"})
    _apply(row, payload.model_dump(exclude_unset=True))
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"special_day": _serialize_special_day(row)}


@router.delete("/special-days/{special_day_id}", dependencies=[Depends(require_admin)])
def delete_special_day(special_day_id: int, session: Session = Depends(get_session)) -> dict:
    row = session.get(SpecialDay, special_day_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "special_day_not_found", "message": "Spezialtag nicht gefunden"})
    session.delete(row)
    session.commit()
    return {"ok": True}


def _int_setting(session: Session, key: str, default: int) -> int:
    row = session.get(AppSetting, key)
    if row is None:
        return default
    try:
        return max(0, int(row.value))
    except ValueError:
        return default


def _str_setting(session: Session, key: str, default: str) -> str:
    row = session.get(AppSetting, key)
    return row.value if row else default


@router.get("/settings/public")
def public_settings(session: Session = Depends(get_session)) -> dict:
    warning = session.get(AppSetting, "departure_warning_sound_enabled")
    return {
        "departure_warning_sound_enabled": (warning.value.lower() == "true") if warning else True,
        "language_quiz_coin_value": _int_setting(session, "language_quiz_coin_value", 1),
        "math_quiz_coin_value": _int_setting(session, "math_quiz_coin_value", 1),
        "knowledge_quiz_coin_value": _int_setting(session, "knowledge_quiz_coin_value", 1),
        "screen_time_default_daily_minutes": _int_setting(session, "screen_time_default_daily_minutes", 40),
        "screen_time_exchange_coin_cost": _int_setting(session, "screen_time_exchange_coin_cost", 4),
        "screen_time_exchange_minutes": _int_setting(session, "screen_time_exchange_minutes", 10),
        "screen_time_alarm_sound": _str_setting(session, "screen_time_alarm_sound", "classic"),
    }


@router.get("/settings", dependencies=[Depends(require_admin)])
def list_settings(session: Session = Depends(get_session)) -> dict:
    rows = session.exec(select(AppSetting).order_by(AppSetting.key)).all()
    return {"settings": [row.model_dump() for row in rows]}


@router.patch("/settings/{key}", dependencies=[Depends(require_admin)])
def update_setting(key: str, payload: SettingUpdate, session: Session = Depends(get_session)) -> dict:
    row = session.get(AppSetting, key)
    if row is None:
        row = AppSetting(key=key, value=payload.value, description=payload.description or "")
    else:
        row.value = payload.value
        if payload.description is not None:
            row.description = payload.description
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"setting": row.model_dump()}


@router.get("/schedule-routines", dependencies=[Depends(require_admin)])
def list_routine_day_settings(session: Session = Depends(get_session)) -> dict:
    children = session.exec(select(Child).order_by(Child.sort_order)).all()
    child_lookup = {child.id: child for child in children}
    rows = session.exec(select(RoutineDaySetting).order_by(RoutineDaySetting.child_id, RoutineDaySetting.weekday)).all()
    return {"routine_settings": [serialize_routine_setting(row, child_lookup.get(row.child_id)) for row in rows]}


@router.put("/schedule-routines", dependencies=[Depends(require_admin)])
def upsert_routine_day_setting(payload: RoutineDaySettingUpsert, session: Session = Depends(get_session)) -> dict:
    child = session.get(Child, payload.child_id)
    if child is None:
        raise HTTPException(status_code=404, detail={"code": "child_not_found", "message": "Kind nicht gefunden"})
    hooks = ",".join(part.strip() for part in payload.timer_hooks.split(",") if part.strip()) or "10,3"
    row = session.exec(select(RoutineDaySetting).where(RoutineDaySetting.child_id == payload.child_id, RoutineDaySetting.weekday == payload.weekday)).first()
    data = payload.model_dump()
    data["timer_hooks"] = hooks
    if row is None:
        row = RoutineDaySetting(**data)
    else:
        _apply(row, data)
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"routine_setting": serialize_routine_setting(row, child)}


@router.get("/schedule-days", dependencies=[Depends(require_admin)])
def list_schedule_days(session: Session = Depends(get_session)) -> dict:
    rows = session.exec(select(ScheduleDay).order_by(ScheduleDay.child_id, ScheduleDay.weekday)).all()
    return {"schedule_days": [_serialize_schedule_day(row) for row in rows]}


@router.put("/schedule-days", dependencies=[Depends(require_admin)])
def upsert_schedule_day(payload: ScheduleDayUpsert, session: Session = Depends(get_session)) -> dict:
    row = session.exec(select(ScheduleDay).where(ScheduleDay.child_id == payload.child_id, ScheduleDay.weekday == payload.weekday)).first()
    if row is None:
        row = ScheduleDay(**payload.model_dump())
    else:
        _apply(row, payload.model_dump())
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"schedule_day": _serialize_schedule_day(row)}


@router.get("/schedule-blocks", dependencies=[Depends(require_admin)])
def list_schedule_blocks(session: Session = Depends(get_session)) -> dict:
    rows = session.exec(select(ScheduleBlock).order_by(ScheduleBlock.schedule_day_id, ScheduleBlock.sort_order)).all()
    return {"schedule_blocks": [_serialize_schedule_block(row) for row in rows]}


@router.post("/schedule-blocks", status_code=201, dependencies=[Depends(require_admin)])
def create_schedule_block(payload: ScheduleBlockCreate, session: Session = Depends(get_session)) -> dict:
    day = session.get(ScheduleDay, payload.schedule_day_id)
    if day is None:
        raise HTTPException(status_code=404, detail={"code": "schedule_day_not_found", "message": "Stundenplantag nicht gefunden"})
    row = ScheduleBlock(**payload.model_dump())
    row.child_id = day.child_id
    row.weekday = day.weekday
    row.title = payload.subject
    row.subject = payload.subject
    row.active = True
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"schedule_block": _serialize_schedule_block(row)}


@router.patch("/schedule-blocks/{block_id}", dependencies=[Depends(require_admin)])
def update_schedule_block(block_id: int, payload: ScheduleBlockUpdate, session: Session = Depends(get_session)) -> dict:
    row = session.get(ScheduleBlock, block_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "schedule_block_not_found", "message": "Stundenplanblock nicht gefunden"})
    data = payload.model_dump(exclude_unset=True)
    _apply(row, data)
    if "subject" in data:
        row.title = row.subject or ""
    session.add(row)
    session.commit()
    session.refresh(row)
    return {"schedule_block": _serialize_schedule_block(row)}


@router.delete("/schedule-blocks/{block_id}", dependencies=[Depends(require_admin)])
def delete_schedule_block(block_id: int, session: Session = Depends(get_session)) -> dict:
    row = session.get(ScheduleBlock, block_id)
    if row is None:
        raise HTTPException(status_code=404, detail={"code": "schedule_block_not_found", "message": "Stundenplanblock nicht gefunden"})
    session.delete(row)
    session.commit()
    return {"ok": True}
