import asyncio
import math
from datetime import date, timedelta
from uuid import UUID

from fastapi import HTTPException
from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from decimal import Decimal

from app.models.absence import Absence, AbsenceStatus
from app.models.absence_type import AbsenceCategory, AbsenceType
from app.models.audit_log import AuditLog
from app.models.company import Company
from app.models.overtime_balance import OvertimeBalance
from app.models.public_holiday import PublicHoliday
from app.models.user import User, UserRole
from app.models.vacation_balance import VacationBalance
from app.models.work_schedule import WorkSchedule
from app.schemas.absence import AbsenceCreate, AbsenceReject, AbsenceTypeCreate, AbsenceTypeUpdate

_manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)


class AbsenceService:

    # ── AbsenceTypes ─────────────────────────────────────────────────────────

    async def list_types(self, company_id: UUID, db: AsyncSession) -> list[AbsenceType]:
        result = await db.scalars(
            select(AbsenceType)
            .where(AbsenceType.company_id == company_id, AbsenceType.is_active == True)
            .order_by(AbsenceType.name)
        )
        return list(result.all())

    async def create_type(
        self, company_id: UUID, data: AbsenceTypeCreate, db: AsyncSession
    ) -> AbsenceType:
        at = AbsenceType(company_id=company_id, **data.model_dump())
        db.add(at)
        await db.flush()
        return at

    async def update_type(
        self, type_id: UUID, company_id: UUID, data: AbsenceTypeUpdate, db: AsyncSession
    ) -> AbsenceType:
        at = await self._get_type_or_404(type_id, company_id, db)
        for field, value in data.model_dump(exclude_none=True).items():
            setattr(at, field, value)
        return at

    async def create_defaults_for_company(self, company_id: UUID, db: AsyncSession) -> None:
        """Standard-Abwesenheitstypen + Standard-Arbeitsplan für ein neues Unternehmen anlegen."""
        defaults = [
            {
                "name": "Urlaub", "color": "#3B82F6", "category": AbsenceCategory.VACATION,
                "requires_approval": True, "deducts_vacation": True, "is_paid": True,
            },
            {
                "name": "Krankheit", "color": "#EF4444", "category": AbsenceCategory.SICK,
                "requires_approval": False, "deducts_vacation": False, "is_paid": True,
                "requires_certificate": True, "certificate_after_days": 3,
            },
            {
                "name": "Freizeitausgleich", "color": "#F59E0B", "category": AbsenceCategory.OVERTIME_COMP,
                "requires_approval": True, "deducts_vacation": False,
                "affects_overtime_balance": True, "is_paid": True,
            },
            {
                "name": "Weiterbildung", "color": "#8B5CF6", "category": AbsenceCategory.TRAINING,
                "requires_approval": True, "deducts_vacation": False, "is_paid": True,
                "max_days_per_year": 5,
            },
            {
                "name": "Dienstreise", "color": "#06B6D4", "category": AbsenceCategory.BUSINESS_TRIP,
                "requires_approval": True, "deducts_vacation": False, "is_paid": True,
            },
            {
                "name": "Homeoffice", "color": "#10B981", "category": AbsenceCategory.OTHER,
                "requires_approval": True, "deducts_vacation": False, "is_paid": True,
            },
            {
                "name": "Sonderurlaub", "color": "#84CC16", "category": AbsenceCategory.VACATION,
                "requires_approval": True, "deducts_vacation": True, "is_paid": True,
            },
        ]
        for d in defaults:
            db.add(AbsenceType(company_id=company_id, **d))

        # Standard-Arbeitsplan: Mo–Fr 8h
        schedule = WorkSchedule(
            company_id=company_id,
            name="Vollzeit (40h)",
            valid_from=date.today(),
        )
        db.add(schedule)
        await db.flush()

    # ── Absences ──────────────────────────────────────────────────────────────

    async def list_absences(
        self,
        company_id: UUID,
        current_user: User,
        db: AsyncSession,
        user_id: UUID | None = None,
        type_id: UUID | None = None,
        status: AbsenceStatus | None = None,
        year: int | None = None,
        as_substitute: bool = False,
    ) -> tuple[int, list[Absence]]:
        q = (
            select(Absence)
            .join(User, Absence.user_id == User.id)
            .where(User.company_id == company_id)
        )
        if as_substitute:
            # Anträge, in denen der aktuelle User als Vertretung eingetragen ist
            q = q.where(Absence.substitute_id == current_user.id)
        elif current_user.role == UserRole.EMPLOYEE:
            q = q.where(Absence.user_id == current_user.id)
        elif user_id:
            q = q.where(Absence.user_id == user_id)

        if type_id:
            q = q.where(Absence.type_id == type_id)
        if status:
            q = q.where(Absence.status == status)
        if year:
            q = q.where(Absence.start_date >= date(year, 1, 1), Absence.end_date <= date(year, 12, 31))

        total = await db.scalar(select(func.count()).select_from(q.subquery())) or 0
        result = await db.scalars(q.order_by(Absence.start_date.desc()))
        return total, list(result.all())

    async def get_by_id(self, absence_id: UUID, current_user: User, db: AsyncSession) -> Absence:
        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        if absence.user_id != current_user.id and current_user.role == UserRole.EMPLOYEE:
            raise HTTPException(status_code=403, detail="Keine Berechtigung.")
        return absence

    async def create_absence(
        self,
        data: AbsenceCreate,
        current_user: User,
        db: AsyncSession,
    ) -> tuple[Absence, list[str]]:
        # AbsenceType validieren
        absence_type = await self._get_type_or_404(data.type_id, current_user.company_id, db)

        # Arbeitstage berechnen
        holidays = await self._get_holiday_dates(
            current_user.company_id, data.start_date.year, db
        )
        working_days = self._calc_working_days(
            data.start_date, data.end_date, holidays,
            data.half_day_start, data.half_day_end
        )

        # Krankmeldungen dürfen auch Wochenenden/Feiertage umfassen (0 Arbeitstage erlaubt)
        if working_days <= 0 and absence_type.category != AbsenceCategory.SICK:
            raise HTTPException(status_code=400, detail="Keine Arbeitstage im ausgewählten Zeitraum.")

        # Urlaubskonto prüfen wenn Urlaub abgezogen werden soll
        warnings: list[str] = []
        if absence_type.deducts_vacation:
            balance = await self._get_or_create_balance(current_user.id, data.start_date.year, db)
            company = await db.get(Company, current_user.company_id)
            _, expired = self._carryover_expired(company, data.start_date.year)
            available = self.effective_available(balance, expired)
            if available < working_days:
                warnings.append(
                    f"Urlaubskonto reicht möglicherweise nicht aus: "
                    f"{available} Tage verfügbar, {working_days} Tage beantragt."
                )

        # Überschneidung mit eigenen Abwesenheiten prüfen
        overlap = await db.scalar(
            select(Absence).where(
                and_(
                    Absence.user_id == current_user.id,
                    Absence.status != AbsenceStatus.CANCELLED,
                    Absence.status != AbsenceStatus.REJECTED,
                    Absence.start_date <= data.end_date,
                    Absence.end_date >= data.start_date,
                )
            )
        )
        if overlap:
            warnings.append("Überschneidung mit bestehender Abwesenheit im selben Zeitraum.")

        # Vertreter prüfen: gleiche Firma + im Zeitraum selbst nicht abwesend (nur Warnung)
        if data.substitute_id:
            substitute = await db.get(User, data.substitute_id)
            if substitute is None or substitute.company_id != current_user.company_id:
                raise HTTPException(status_code=404, detail="Vertretung nicht gefunden.")
            if substitute.id == current_user.id:
                raise HTTPException(status_code=400, detail="Man kann sich nicht selbst vertreten.")
            sub_overlap = await db.scalar(
                select(Absence).where(
                    and_(
                        Absence.user_id == data.substitute_id,
                        Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.APPROVED]),
                        Absence.start_date <= data.end_date,
                        Absence.end_date >= data.start_date,
                    )
                )
            )
            if sub_overlap:
                warnings.append(
                    f"Gewählte Vertretung ({substitute.full_name}) ist im Zeitraum selbst abwesend."
                )

        status = AbsenceStatus.PENDING if absence_type.requires_approval else AbsenceStatus.APPROVED
        approved_by = None if absence_type.requires_approval else current_user.id

        # Krankmeldung: AU-Pflicht-Datum automatisch berechnen.
        # Reihenfolge: AbsenceType.certificate_after_days (override) → Company default.
        certificate_required_by: date | None = None
        if absence_type.category == AbsenceCategory.SICK and absence_type.requires_certificate:
            company = await db.get(Company, current_user.company_id)
            company_default = company.sick_note_required_after_days if company else 3
            threshold = absence_type.certificate_after_days or company_default
            certificate_required_by = data.start_date + timedelta(days=threshold)

        absence = Absence(
            user_id=current_user.id,
            type_id=data.type_id,
            start_date=data.start_date,
            end_date=data.end_date,
            half_day_start=data.half_day_start,
            half_day_end=data.half_day_end,
            working_days=working_days,
            fza_hours=data.fza_hours if hasattr(data, "fza_hours") else None,
            status=status,
            approved_by=approved_by,
            substitute_id=data.substitute_id,
            note=data.note,
            certificate_required_by=certificate_required_by,
        )
        db.add(absence)
        await db.flush()

        # Bei automatischer Genehmigung Konto abziehen + Vertretung benachrichtigen
        if not absence_type.requires_approval:
            if absence_type.deducts_vacation:
                await self._deduct_vacation(current_user.id, data.start_date.year, int(working_days), db)
            if absence.substitute_id:
                await self._notify_substitute(absence, db)

        return absence, warnings

    async def update_absence(
        self, absence_id: UUID, data: "AbsenceUpdate", current_user: User, db: AsyncSession
    ) -> Absence:
        from app.schemas.absence import AbsenceUpdate
        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        # Mitarbeiter: nur eigene; Manager: gleiche Company
        if current_user.role == UserRole.EMPLOYEE:
            if absence.user_id != current_user.id:
                raise HTTPException(status_code=403, detail="Keine Berechtigung.")
        else:
            owner = await db.get(User, absence.user_id)
            if owner is None or owner.company_id != current_user.company_id:
                raise HTTPException(status_code=403, detail="Zugriff verweigert.")
        is_manager = current_user.role in _manager_roles
        if absence.status not in (AbsenceStatus.PENDING, AbsenceStatus.APPROVED):
            raise HTTPException(status_code=409, detail="Nur ausstehende oder genehmigte Anträge können bearbeitet werden.")
        if absence.status == AbsenceStatus.APPROVED and not is_manager:
            # Mitarbeiter stellt Änderungswunsch → Begründung Pflicht, Status zurück auf pending
            if not data.correction_note or not data.correction_note.strip():
                raise HTTPException(status_code=422, detail="Änderungsgrund ist bei genehmigten Anträgen Pflicht.")

        if data.type_id is not None:
            await self._get_type_or_404(data.type_id, current_user.company_id, db)
            absence.type_id = data.type_id
        if data.start_date is not None:
            absence.start_date = data.start_date
        if data.end_date is not None:
            absence.end_date = data.end_date
        if data.half_day_start is not None:
            absence.half_day_start = data.half_day_start
        if data.half_day_end is not None:
            absence.half_day_end = data.half_day_end
        if data.substitute_id is not None:
            absence.substitute_id = data.substitute_id
        if data.note is not None:
            absence.note = data.note
        if data.correction_note is not None:
            absence.correction_note = data.correction_note.strip() or None

        # Genehmigter Antrag: Mitarbeiter-Änderung → zurück auf pending (erneute Genehmigung)
        was_approved = absence.status == AbsenceStatus.APPROVED
        if was_approved and not is_manager:
            absence.status = AbsenceStatus.PENDING
            absence.approved_by = None

        # Arbeitstage neu berechnen
        holiday_dates = await self._get_holiday_dates(current_user.company_id, absence.start_date.year, db)
        absence.working_days = Decimal(str(
            self._calc_working_days(absence.start_date, absence.end_date,
                                    holiday_dates, absence.half_day_start, absence.half_day_end)
        ))

        # Audit-Log
        action = "absence_change_request" if (was_approved and not is_manager) else "absence_updated"
        db.add(AuditLog(
            user_id=current_user.id,
            action=action,
            entity_type="absence",
            entity_id=absence.id,
            old_value={"status": "approved" if was_approved else "pending"},
            new_value={
                "status": absence.status.value,
                "start_date": str(absence.start_date),
                "end_date": str(absence.end_date),
                "working_days": float(absence.working_days),
                "correction_note": absence.correction_note,
            },
        ))
        return absence

    async def cancel_absence(
        self, absence_id: UUID, current_user: User, db: AsyncSession
    ) -> Absence:
        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")

        is_admin = current_user.role in (UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
        original_status = absence.status  # vor Änderung merken

        if absence.user_id != current_user.id and not is_admin:
            raise HTTPException(status_code=403, detail="Nur eigene Anträge können storniert werden.")

        if absence.status == AbsenceStatus.APPROVED:
            if not is_admin:
                raise HTTPException(
                    status_code=409,
                    detail="Genehmigte Anträge können nur von HR/Admin storniert werden."
                )
            # Überstunden zurückbuchen wenn Freizeitausgleich
            absence_type = await db.get(AbsenceType, absence.type_id)
            if absence_type and absence_type.affects_overtime_balance:
                await self._refund_overtime(
                    absence.user_id, absence.working_days, db, fza_hours=absence.fza_hours
                )
        elif absence.status not in (AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED):
            raise HTTPException(
                status_code=409,
                detail="Nur ausstehende oder genehmigte Anträge können storniert werden."
            )

        absence.status = AbsenceStatus.CANCELLED

        # Audit-Log (DSGVO)
        db.add(AuditLog(
            company_id=current_user.company_id,
            user_id=current_user.id,
            action="absence_cancelled",
            entity_type="absence",
            entity_id=absence.id,
            old_value={"status": original_status.value},
            new_value={
                "status": "cancelled",
                "cancelled_by": str(current_user.id),
                "cancelled_by_name": current_user.full_name,
                "absence_user_id": str(absence.user_id),
                "start_date": str(absence.start_date),
                "end_date": str(absence.end_date),
                "working_days": float(absence.working_days),
                **({"fza_hours_refunded": True} if original_status == AbsenceStatus.APPROVED else {}),
            },
        ))

        from app.services.caldav_service import caldav_service
        asyncio.create_task(caldav_service.sync_removed_bg(absence.id))

        return absence

    async def approve_absence(
        self, absence_id: UUID, current_user: User, db: AsyncSession
    ) -> Absence:
        if current_user.role not in (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN):
            raise HTTPException(status_code=403, detail="Keine Berechtigung.")

        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        requester = await db.get(User, absence.user_id)
        if requester is None or requester.company_id != current_user.company_id:
            raise HTTPException(status_code=403, detail="Zugriff verweigert.")
        if absence.user_id == current_user.id:
            raise HTTPException(
                status_code=409,
                detail="Eigene Abwesenheitsanträge können nicht selbst genehmigt werden."
            )

        # Storno-Anfrage genehmigen → Antrag tatsächlich stornieren + Rückbuchung
        if absence.status == AbsenceStatus.CANCELLATION_REQUESTED:
            await self._apply_cancellation(absence, current_user, db, from_request=True)
            return absence, []

        # Zwei-Stufen-Genehmigung (firmenweit opt-in, optional ab Schwellwert)
        company = await db.get(Company, current_user.company_id)
        two_stage = bool(
            company and company.two_stage_approval_enabled
            and absence.working_days >= (company.two_stage_min_days or 0)
        )
        _final_roles = (UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)

        if absence.status == AbsenceStatus.FIRST_APPROVED:
            # Zweite (finale) Stufe – nur HR/Admin und ein anderer Genehmiger als Stufe 1
            if current_user.role not in _final_roles:
                raise HTTPException(status_code=403, detail="Nur HR/Admin kann die finale Genehmigung erteilen.")
            if absence.first_approved_by == current_user.id:
                raise HTTPException(status_code=409, detail="Die finale Genehmigung muss von einer anderen Person erfolgen.")
            # → fällt durch zur finalen Genehmigung unten
        elif absence.status == AbsenceStatus.PENDING:
            if two_stage:
                absence.status = AbsenceStatus.FIRST_APPROVED
                absence.first_approved_by = current_user.id
                db.add(AuditLog(
                    company_id=current_user.company_id, user_id=current_user.id,
                    action="absence_first_approved", entity_type="absence", entity_id=absence.id,
                    old_value={"status": "pending"},
                    new_value={"status": "first_approved", "first_approved_by": str(current_user.id),
                               "absence_user_id": str(absence.user_id)},
                ))
                await self._add_system_comment(
                    absence, current_user.company_id, current_user.id,
                    f"Erste Stufe genehmigt von {current_user.full_name} – wartet auf finale Genehmigung.", db,
                )
                return absence, []
            # einstufig → direkt finale Genehmigung unten
        else:
            raise HTTPException(status_code=409, detail="Nur ausstehende Anträge können genehmigt werden.")

        absence.status = AbsenceStatus.APPROVED
        absence.approved_by = current_user.id

        absence_type = await db.get(AbsenceType, absence.type_id)

        # Urlaubskonto abziehen wenn nötig
        if absence_type and absence_type.deducts_vacation:
            await self._deduct_vacation(absence.user_id, absence.start_date.year, int(absence.working_days), db)

        # Überstundenkonto abziehen wenn Freizeitausgleich
        fza_warnings: list[str] = []
        if absence_type and absence_type.affects_overtime_balance:
            fza_warnings = await self._deduct_overtime(
                absence.user_id, absence.working_days, db, fza_hours=absence.fza_hours
            )

        # Audit-Log (DSGVO)
        db.add(AuditLog(
            company_id=current_user.company_id,
            user_id=current_user.id,
            action="absence_approved",
            entity_type="absence",
            entity_id=absence.id,
            old_value={"status": "pending"},
            new_value={
                "status": "approved",
                "approved_by": str(current_user.id),
                "approved_by_name": current_user.full_name,
                "absence_user_id": str(absence.user_id),
                "start_date": str(absence.start_date),
                "end_date": str(absence.end_date),
                "working_days": float(absence.working_days),
            },
        ))

        # CalDAV-Sync (fire & forget – Fehler blockieren nicht die Genehmigung)
        from app.services.caldav_service import caldav_service
        asyncio.create_task(caldav_service.sync_approved_bg(absence.id))

        # Vertretung benachrichtigen
        if absence.substitute_id:
            await self._notify_substitute(absence, db)

        return absence, fza_warnings

    async def reject_absence(
        self, absence_id: UUID, data: AbsenceReject, current_user: User, db: AsyncSession
    ) -> Absence:
        if current_user.role not in (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN):
            raise HTTPException(status_code=403, detail="Keine Berechtigung.")

        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        requester = await db.get(User, absence.user_id)
        if requester is None or requester.company_id != current_user.company_id:
            raise HTTPException(status_code=403, detail="Zugriff verweigert.")

        # Storno-Anfrage ablehnen → Antrag bleibt genehmigt
        if absence.status == AbsenceStatus.CANCELLATION_REQUESTED:
            absence.status = AbsenceStatus.APPROVED
            db.add(AuditLog(
                company_id=current_user.company_id,
                user_id=current_user.id,
                action="absence_cancellation_rejected",
                entity_type="absence",
                entity_id=absence.id,
                old_value={"status": "cancellation_requested"},
                new_value={"status": "approved", "rejection_reason": data.rejection_reason,
                           "absence_user_id": str(absence.user_id)},
            ))
            await self._add_system_comment(
                absence, current_user.company_id, current_user.id,
                f"Stornierung abgelehnt von {current_user.full_name}: {data.rejection_reason}", db,
            )
            return absence

        if absence.status not in (AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED):
            raise HTTPException(status_code=409, detail="Nur ausstehende Anträge können abgelehnt werden.")

        absence.status = AbsenceStatus.REJECTED
        absence.approved_by = current_user.id
        absence.rejection_reason = data.rejection_reason

        # Audit-Log (DSGVO)
        db.add(AuditLog(
            company_id=current_user.company_id,
            user_id=current_user.id,
            action="absence_rejected",
            entity_type="absence",
            entity_id=absence.id,
            old_value={"status": "pending"},
            new_value={
                "status": "rejected",
                "rejection_reason": absence.rejection_reason,
                "rejected_by": str(current_user.id),
                "rejected_by_name": current_user.full_name,
                "absence_user_id": str(absence.user_id),
                "start_date": str(absence.start_date),
                "end_date": str(absence.end_date),
                "working_days": float(absence.working_days),
            },
        ))

        from app.services.caldav_service import caldav_service
        asyncio.create_task(caldav_service.sync_removed_bg(absence.id))

        return absence

    async def get_calendar(
        self,
        company_id: UUID,
        year: int,
        month: int | None,
        db: AsyncSession,
    ) -> list[dict]:
        q = (
            select(Absence, User, AbsenceType)
            .join(User, Absence.user_id == User.id)
            .join(AbsenceType, Absence.type_id == AbsenceType.id)
            .where(
                User.company_id == company_id,
                Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED, AbsenceStatus.APPROVED]),
            )
        )
        if month:
            start = date(year, month, 1)
            end = date(year, month, 28) + timedelta(days=4)
            end = end.replace(day=1) - timedelta(days=1)
            q = q.where(Absence.start_date <= end, Absence.end_date >= start)
        else:
            q = q.where(
                Absence.start_date >= date(year, 1, 1),
                Absence.end_date <= date(year, 12, 31),
            )

        result = await db.execute(q.order_by(Absence.start_date))
        rows = result.all()

        calendar = []
        for absence, user, atype in rows:
            calendar.append({
                "user_id": user.id,
                "user_name": user.full_name,
                "absence_id": absence.id,
                "type_name": atype.name,
                "type_color": atype.color,
                "start_date": absence.start_date,
                "end_date": absence.end_date,
                "status": absence.status,
                "working_days": absence.working_days,
            })
        return calendar

    # ── Urlaubskonto ──────────────────────────────────────────────────────────

    async def get_balance(self, user_id: UUID, year: int, db: AsyncSession) -> VacationBalance:
        return await self._get_or_create_balance(user_id, year, db)

    async def get_pending_days(self, user_id: UUID, year: int, db: AsyncSession) -> float:
        """Summe der Arbeitstage aus ausstehenden Anträgen die Urlaub abziehen."""
        q = (
            select(func.sum(Absence.working_days))
            .join(AbsenceType, Absence.type_id == AbsenceType.id)
            .where(
                Absence.user_id == user_id,
                Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED]),
                AbsenceType.deducts_vacation.is_(True),
                func.extract("year", Absence.start_date) == year,
            )
        )
        result = await db.scalar(q)
        return float(result or 0)

    # ── Feiertage ─────────────────────────────────────────────────────────────

    async def list_holidays(
        self, year: int, country: str, state: str | None, db: AsyncSession
    ) -> list[PublicHoliday]:
        q = select(PublicHoliday).where(
            PublicHoliday.year == year, PublicHoliday.country == country
        )
        if state:
            q = q.where(or_(PublicHoliday.state == state, PublicHoliday.state.is_(None)))
        result = await db.scalars(q.order_by(PublicHoliday.date))
        return list(result.all())

    async def create_holiday(self, data, db: AsyncSession) -> PublicHoliday:
        holiday = PublicHoliday(
            country=data.country,
            state=data.state,
            date=data.date,
            name=data.name,
            year=data.date.year,
        )
        db.add(holiday)
        await db.flush()
        return holiday

    # ── Helpers ───────────────────────────────────────────────────────────────

    async def _get_type_or_404(
        self, type_id: UUID, company_id: UUID, db: AsyncSession
    ) -> AbsenceType:
        at = await db.get(AbsenceType, type_id)
        if at is None or at.company_id != company_id:
            raise HTTPException(status_code=404, detail="Abwesenheitstyp nicht gefunden.")
        return at

    async def _get_or_create_balance(
        self, user_id: UUID, year: int, db: AsyncSession
    ) -> VacationBalance:
        balance = await db.scalar(
            select(VacationBalance).where(
                VacationBalance.user_id == user_id, VacationBalance.year == year
            )
        )
        if balance is None:
            # Automatischer Übertrag: Resturlaub aus dem Vorjahr übernehmen
            prev = await db.scalar(
                select(VacationBalance).where(
                    VacationBalance.user_id == user_id, VacationBalance.year == year - 1
                )
            )
            carried = max(0, prev.remaining_days) if prev else 0
            # Grundanspruch frisch berechnen (Firmen-Default · Teilzeit · Pro-rata).
            # Fallback auf Vorjahres-Anspruch bzw. 30, falls keine Konfig greift.
            user = await db.get(User, user_id)
            company = await db.get(Company, user.company_id) if user and user.company_id else None
            entitled = await self._compute_entitlement(user, company, year, db)
            if entitled is None:
                entitled = prev.entitled_days if prev else 30
            balance = VacationBalance(
                user_id=user_id,
                year=year,
                entitled_days=entitled,
                carried_over=carried,
            )
            db.add(balance)
            await db.flush()
        return balance

    @staticmethod
    def _working_days_per_week(schedule: WorkSchedule) -> int:
        return sum(
            1 for h in [schedule.mon_h, schedule.tue_h, schedule.wed_h,
                        schedule.thu_h, schedule.fri_h, schedule.sat_h, schedule.sun_h]
            if h and h > 0
        )

    async def _compute_entitlement(
        self, user: "User | None", company: "Company | None", year: int, db: AsyncSession
    ) -> int | None:
        """Grundurlaub für ein neues Konto: Firmen-Default, optional Teilzeit-Skalierung
        (Arbeitstage/Woche aus WorkSchedule) und anteilig im Ein-/Austrittsjahr (Zwölftel).
        Gibt None zurück, wenn keine Berechnung möglich ist (→ Aufrufer nutzt Fallback)."""
        if company is None:
            return None
        days = float(company.vacation_default_days)

        # Teilzeit: Anspruch proportional zu den Arbeitstagen/Woche (5 = Vollzeit)
        if company.vacation_from_schedule and user and user.work_schedule_id:
            schedule = await db.get(WorkSchedule, user.work_schedule_id)
            if schedule:
                wd = self._working_days_per_week(schedule)
                days = days * wd / 5.0

        # Anteilig im Ein-/Austrittsjahr (Zwölftel ab/bis Monat)
        if company.vacation_prorate_first_year and user:
            months = 12
            if user.entry_date:
                if user.entry_date.year > year:
                    months = 0
                elif user.entry_date.year == year:
                    months = min(months, 12 - user.entry_date.month + 1)
            if user.exit_date:
                if user.exit_date.year < year:
                    months = 0
                elif user.exit_date.year == year:
                    months = min(months, user.exit_date.month)
            days = days * max(0, months) / 12.0

        # Kaufmännisch runden
        return int(math.floor(days + 0.5))

    @staticmethod
    def _carryover_expired(company: "Company | None", year: int) -> tuple[date | None, bool]:
        """Verfallsdatum des Resturlaubs aus company.settings. (expires_at, is_expired)."""
        s = (company.settings or {}) if company else {}
        month, day = s.get("carryover_expires_month"), s.get("carryover_expires_day")
        if not month or not day:
            return None, False
        try:
            expires_at = date(year, int(month), int(day))
            return expires_at, date.today() > expires_at
        except (ValueError, TypeError):
            return None, False

    @staticmethod
    def effective_available(balance: VacationBalance, expired: bool) -> int:
        """Effektiv verfügbare Urlaubstage. Bei verfallenem Resturlaub zählt der noch
        nicht verbrauchte Teil des Übertrags nicht mehr mit."""
        if expired:
            return balance.entitled_days + balance.special_days - max(0, balance.used_days - balance.carried_over)
        return balance.remaining_days

    async def _deduct_vacation(
        self, user_id: UUID, year: int, days: int, db: AsyncSession
    ) -> None:
        balance = await self._get_or_create_balance(user_id, year, db)
        balance.used_days += days

    async def _calc_daily_hours(self, user_id: UUID, db: AsyncSession) -> Decimal:
        """Tägliche Soll-Stunden aus Arbeitsplan ermitteln (Fallback: 8h)."""
        user = await db.get(User, user_id)
        daily_hours = Decimal("8.00")
        if user and user.work_schedule_id:
            schedule = await db.get(WorkSchedule, user.work_schedule_id)
            if schedule:
                working_days_in_week = sum(
                    1 for h in [schedule.mon_h, schedule.tue_h, schedule.wed_h,
                                schedule.thu_h, schedule.fri_h, schedule.sat_h, schedule.sun_h]
                    if h > 0
                )
                if working_days_in_week > 0:
                    daily_hours = schedule.weekly_hours / Decimal(working_days_in_week)
        return daily_hours

    async def _deduct_overtime(
        self, user_id: UUID, working_days: float, db: AsyncSession,
        fza_hours: "Decimal | None" = None,
    ) -> list[str]:
        """Zieht working_days × tägliche Stunden (oder direkt fza_hours) vom Überstundenkonto ab.
        Gibt Warnungen zurück. Wirft HTTPException wenn Überziehen verboten ist."""
        user = await db.get(User, user_id)
        if fza_hours is not None:
            hours_to_deduct = Decimal(str(fza_hours))
        else:
            daily_hours = await self._calc_daily_hours(user_id, db)
            hours_to_deduct = Decimal(str(working_days)) * daily_hours

        ob = await db.scalar(select(OvertimeBalance).where(OvertimeBalance.user_id == user_id))
        if ob is None:
            company_id = user.company_id if user else None
            if not company_id:
                return []
            ob = OvertimeBalance(user_id=user_id, company_id=company_id)
            db.add(ob)
            await db.flush()

        # Firmen-Konfiguration für Überziehen laden
        company = await db.get(Company, ob.company_id)
        overdraft_allowed = company.overtime_overdraft_allowed if company else True
        warning_threshold = Decimal(str(company.overtime_warning_threshold_hours if company else 0))

        available = ob.available_hours
        warnings: list[str] = []

        if available < hours_to_deduct and not overdraft_allowed:
            raise HTTPException(
                status_code=422,
                detail=(
                    f"Nicht genug Überstunden für Freizeitausgleich. "
                    f"Verfügbar: {float(available):.1f}h, benötigt: {float(hours_to_deduct):.1f}h."
                ),
            )

        after_deduction = available - hours_to_deduct
        if warning_threshold > 0 and after_deduction < warning_threshold:
            sign = "-" if after_deduction < 0 else ""
            warnings.append(
                f"Überstundenkonto sinkt unter die Warnschwelle "
                f"({float(warning_threshold):.0f}h). Verbleibend: {sign}{abs(float(after_deduction)):.1f}h."
            )

        ob.taken_hours += hours_to_deduct
        return warnings

    async def _refund_overtime(
        self, user_id: UUID, working_days: float, db: AsyncSession,
        fza_hours: "Decimal | None" = None,
    ) -> None:
        """Rückbuchung von Freizeitausgleichs-Stunden (z.B. bei Stornierung)."""
        if fza_hours is not None:
            hours_to_refund = Decimal(str(fza_hours))
        else:
            daily_hours = await self._calc_daily_hours(user_id, db)
            hours_to_refund = Decimal(str(working_days)) * daily_hours

        ob = await db.scalar(select(OvertimeBalance).where(OvertimeBalance.user_id == user_id))
        if ob is not None:
            ob.taken_hours = max(Decimal("0"), ob.taken_hours - hours_to_refund)

    async def _get_holiday_dates(
        self, company_id: UUID, year: int, db: AsyncSession
    ) -> set[date]:
        """Feiertage für die Company-Country holen."""
        from app.models.company import Company
        from sqlalchemy import or_

        company = await db.get(Company, company_id)
        country = company.country if company else "DE"
        state = company.state if company else None

        q = select(PublicHoliday.date).where(
            PublicHoliday.year == year,
            PublicHoliday.country == country,
        )
        if state:
            q = q.where(or_(PublicHoliday.state == state, PublicHoliday.state.is_(None)))

        result = await db.scalars(q)
        return set(result.all())

    @staticmethod
    def _calc_working_days(
        start: date,
        end: date,
        holidays: set[date],
        half_day_start: bool,
        half_day_end: bool,
    ) -> float:
        count = 0.0
        current = start
        while current <= end:
            if current.weekday() < 5 and current not in holidays:
                count += 1.0
            current += timedelta(days=1)
        # Halbtage abziehen
        if half_day_start and start.weekday() < 5 and start not in holidays:
            count -= 0.5
        if half_day_end and end.weekday() < 5 and end not in holidays and end != start:
            count -= 0.5
        return max(0.0, count)

    # ── Krankmeldung ──────────────────────────────────────────────────────────

    async def quick_sick(
        self,
        start: date,
        end: date,
        current_user: User,
        db: AsyncSession,
    ) -> tuple[Absence, list[str]]:
        """Sofort-Krankmeldung: nutzt den ersten aktiven SICK-Typ der Firma."""
        sick_type = await db.scalar(
            select(AbsenceType)
            .where(
                AbsenceType.company_id == current_user.company_id,
                AbsenceType.category == AbsenceCategory.SICK,
                AbsenceType.is_active == True,
            )
            .order_by(AbsenceType.name)
            .limit(1)
        )
        if sick_type is None:
            raise HTTPException(status_code=404, detail="Kein aktiver Krankheits-Typ konfiguriert.")
        if end < start:
            raise HTTPException(status_code=400, detail="Enddatum darf nicht vor dem Startdatum liegen.")

        create_data = AbsenceCreate(
            type_id=sick_type.id,
            start_date=start,
            end_date=end,
        )
        return await self.create_absence(create_data, current_user, db)

    async def mark_certificate_received(
        self,
        absence_id: UUID,
        received_at: date | None,
        current_user: User,
        db: AsyncSession,
    ) -> Absence:
        """HR/Admin: AU-Bescheinigung als eingegangen markieren."""
        if current_user.role not in (UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN):
            raise HTTPException(status_code=403, detail="Nur HR/Admin darf den Attest-Eingang markieren.")

        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        owner = await db.get(User, absence.user_id)
        if owner is None or owner.company_id != current_user.company_id:
            raise HTTPException(status_code=403, detail="Zugriff verweigert.")

        absence_type = await db.get(AbsenceType, absence.type_id)
        if absence_type is None or absence_type.category != AbsenceCategory.SICK:
            raise HTTPException(status_code=409, detail="Nur für Krankmeldungen verfügbar.")

        old_value = str(absence.certificate_received_at) if absence.certificate_received_at else None
        absence.certificate_received_at = received_at or date.today()

        db.add(AuditLog(
            company_id=current_user.company_id,
            user_id=current_user.id,
            action="absence_certificate_received",
            entity_type="absence",
            entity_id=absence.id,
            old_value={"certificate_received_at": old_value},
            new_value={
                "certificate_received_at": str(absence.certificate_received_at),
                "absence_user_id": str(absence.user_id),
                "marked_by": str(current_user.id),
                "marked_by_name": current_user.full_name,
            },
        ))
        return absence

    async def get_sick_stats(
        self,
        company_id: UUID,
        current_user: User,
        ref_date: date,
        db: AsyncSession,
        user_id: UUID | None = None,
    ) -> list[dict]:
        """Krankheitsstatistik für rolling 12 Monate ab ref_date.

        Bradford-Faktor: S² × D mit S = Anzahl Episoden, D = Summe Kranktage.
        """
        window_start = ref_date - timedelta(days=365)

        q = (
            select(Absence, User)
            .join(User, Absence.user_id == User.id)
            .join(AbsenceType, Absence.type_id == AbsenceType.id)
            .where(
                User.company_id == company_id,
                AbsenceType.category == AbsenceCategory.SICK,
                Absence.status == AbsenceStatus.APPROVED,
                Absence.start_date <= ref_date,
                Absence.end_date >= window_start,
            )
            .order_by(User.last_name, User.first_name, Absence.start_date)
        )
        if user_id:
            q = q.where(Absence.user_id == user_id)
        # MANAGER sieht nur sein Department
        if current_user.role == UserRole.MANAGER and current_user.department_id:
            q = q.where(User.department_id == current_user.department_id)

        result = await db.execute(q)
        rows = result.all()

        by_user: dict[UUID, dict] = {}
        for absence, user in rows:
            entry = by_user.setdefault(user.id, {
                "user_id": user.id,
                "user_name": user.full_name,
                "personnel_number": user.personnel_number,
                "episodes": 0,
                "total_days": 0.0,
                "certificates_overdue": 0,
            })
            entry["episodes"] += 1
            entry["total_days"] += float(absence.working_days or 0)
            if (
                absence.certificate_required_by
                and absence.certificate_required_by < ref_date
                and absence.certificate_received_at is None
            ):
                entry["certificates_overdue"] += 1

        for entry in by_user.values():
            entry["bradford_factor"] = float(entry["episodes"]) ** 2 * entry["total_days"]

        return list(by_user.values())

    # ── Stornierung mit Re-Genehmigung ──────────────────────────────────────────

    async def request_cancellation(
        self, absence_id: UUID, reason: str | None, current_user: User, db: AsyncSession
    ) -> Absence:
        """Mitarbeiter beantragt die Stornierung eines bereits GENEHMIGTEN Antrags.

        HR/Admin storniert weiterhin direkt (über cancel_absence). Diese Anfrage
        setzt den Status auf CANCELLATION_REQUESTED → Manager muss zustimmen.
        """
        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        if absence.user_id != current_user.id:
            raise HTTPException(status_code=403, detail="Nur eigene Anträge können storniert werden.")
        if absence.status != AbsenceStatus.APPROVED:
            raise HTTPException(
                status_code=409,
                detail="Nur genehmigte Anträge können zur Stornierung eingereicht werden.",
            )

        absence.status = AbsenceStatus.CANCELLATION_REQUESTED

        db.add(AuditLog(
            company_id=current_user.company_id,
            user_id=current_user.id,
            action="absence_cancellation_requested",
            entity_type="absence",
            entity_id=absence.id,
            old_value={"status": "approved"},
            new_value={"status": "cancellation_requested", "reason": reason,
                       "absence_user_id": str(absence.user_id)},
        ))
        body = "Stornierung beantragt" + (f": {reason}" if reason else ".")
        await self._add_system_comment(absence, current_user.company_id, current_user.id, body, db)
        return absence

    async def _apply_cancellation(
        self, absence: Absence, actor: User, db: AsyncSession, from_request: bool,
    ) -> None:
        """Genehmigten Antrag tatsächlich stornieren inkl. Rückbuchung (Urlaub + FZA)."""
        absence_type = await db.get(AbsenceType, absence.type_id)
        if absence_type and absence_type.deducts_vacation:
            await self._refund_vacation(
                absence.user_id, absence.start_date.year, int(absence.working_days), db
            )
        if absence_type and absence_type.affects_overtime_balance:
            await self._refund_overtime(
                absence.user_id, absence.working_days, db, fza_hours=absence.fza_hours
            )

        absence.status = AbsenceStatus.CANCELLED

        db.add(AuditLog(
            company_id=actor.company_id,
            user_id=actor.id,
            action="absence_cancellation_approved" if from_request else "absence_cancelled",
            entity_type="absence",
            entity_id=absence.id,
            old_value={"status": "cancellation_requested" if from_request else "approved"},
            new_value={
                "status": "cancelled",
                "cancelled_by": str(actor.id),
                "cancelled_by_name": actor.full_name,
                "absence_user_id": str(absence.user_id),
                "working_days": float(absence.working_days),
            },
        ))
        await self._add_system_comment(
            absence, actor.company_id, actor.id,
            f"Stornierung genehmigt von {actor.full_name}.", db,
        )

        from app.services.caldav_service import caldav_service
        asyncio.create_task(caldav_service.sync_removed_bg(absence.id))

    async def _refund_vacation(
        self, user_id: UUID, year: int, days: int, db: AsyncSession
    ) -> None:
        balance = await db.scalar(
            select(VacationBalance).where(
                VacationBalance.user_id == user_id, VacationBalance.year == year
            )
        )
        if balance is not None:
            balance.used_days = max(0, balance.used_days - days)

    async def _notify_substitute(self, absence: Absence, db: AsyncSession) -> None:
        """Eingetragene Vertretung über die genehmigte Abwesenheit informieren."""
        if not absence.substitute_id:
            return
        substitute = await db.get(User, absence.substitute_id)
        requester = await db.get(User, absence.user_id)
        if substitute is None or requester is None or not substitute.email:
            return
        from app.core.notifications import pref_enabled
        if not pref_enabled(substitute, "substitute_assigned"):
            return
        from app.services.email_service import email_service
        try:
            await email_service.send_substitute_notification(substitute, requester, absence, db)
        except Exception as exc:  # Mailfehler dürfen die Genehmigung nicht blockieren
            print(f"Vertreter-Benachrichtigung fehlgeschlagen: {exc}")

    # ── Kommentare ──────────────────────────────────────────────────────────────

    async def _add_system_comment(
        self, absence: Absence, company_id: UUID, author_id: UUID | None, body: str, db: AsyncSession
    ) -> None:
        from app.models.absence_comment import AbsenceComment
        db.add(AbsenceComment(
            absence_id=absence.id, company_id=company_id,
            author_id=author_id, body=body, is_system=True,
        ))

    async def _assert_comment_access(
        self, absence: Absence, current_user: User, db: AsyncSession
    ) -> None:
        """Sichtbar für: Antragsteller, eingetragene Vertretung, Manager-Rollen der Firma."""
        if current_user.role in _manager_roles:
            owner = await db.get(User, absence.user_id)
            if owner is None or owner.company_id != current_user.company_id:
                raise HTTPException(status_code=403, detail="Zugriff verweigert.")
            return
        if current_user.id in (absence.user_id, absence.substitute_id):
            return
        raise HTTPException(status_code=403, detail="Keine Berechtigung.")

    async def list_comments(
        self, absence_id: UUID, current_user: User, db: AsyncSession
    ) -> list:
        from app.models.absence_comment import AbsenceComment
        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        await self._assert_comment_access(absence, current_user, db)
        rows = (await db.execute(
            select(AbsenceComment, User)
            .outerjoin(User, AbsenceComment.author_id == User.id)
            .where(AbsenceComment.absence_id == absence_id)
            .order_by(AbsenceComment.created_at)
        )).all()
        result = []
        for comment, author in rows:
            result.append({
                "id": comment.id, "absence_id": comment.absence_id,
                "author_id": comment.author_id,
                "author_name": author.full_name if author else None,
                "body": comment.body, "is_system": comment.is_system,
                "created_at": comment.created_at,
            })
        return result

    async def add_comment(
        self, absence_id: UUID, body: str, current_user: User, db: AsyncSession
    ) -> dict:
        from app.models.absence_comment import AbsenceComment
        absence = await db.get(Absence, absence_id)
        if absence is None:
            raise HTTPException(status_code=404, detail="Abwesenheit nicht gefunden.")
        await self._assert_comment_access(absence, current_user, db)
        comment = AbsenceComment(
            absence_id=absence_id, company_id=current_user.company_id,
            author_id=current_user.id, body=body.strip(), is_system=False,
        )
        db.add(comment)
        await db.flush()
        return {
            "id": comment.id, "absence_id": comment.absence_id,
            "author_id": comment.author_id, "author_name": current_user.full_name,
            "body": comment.body, "is_system": comment.is_system,
            "created_at": comment.created_at,
        }


absence_service = AbsenceService()
