from datetime import date, datetime, time, timezone
from uuid import UUID

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

from app.models.audit_log import AuditLog
from app.models.time_entry import EntrySource, EntryStatus, TimeEntry
from app.models.user import User, UserRole
from app.models.work_schedule import WorkSchedule
from app.schemas.time_entry import (
    BalanceResponse,
    ManualEntryCreate,
    StampInRequest,
    TimeEntryUpdate,
)


def _check_arbzg(start: time, end: time, break_minutes: int) -> list[str]:
    """ArbZG §3 und §4 Prüfung. Gibt Warnungen zurück, blockiert nicht."""
    start_secs = start.hour * 3600 + start.minute * 60 + start.second
    end_secs = end.hour * 3600 + end.minute * 60 + end.second
    if end_secs < start_secs:
        end_secs += 24 * 3600  # Nachtschicht
    total_mins = (end_secs - start_secs) // 60
    worked_mins = total_mins - break_minutes
    worked_hours = worked_mins / 60

    warnings: list[str] = []

    if worked_hours > 10:
        warnings.append(
            f"Maximale Arbeitszeit von 10 Stunden überschritten "
            f"({worked_hours:.1f}h gearbeitet) – ArbZG §3"
        )
    if total_mins >= 9 * 60 and break_minutes < 45:
        warnings.append(
            "Bei mehr als 9h Anwesenheit sind mind. 45 min Pause vorgeschrieben – ArbZG §4"
        )
    elif total_mins >= 6 * 60 and break_minutes < 30:
        warnings.append(
            "Bei mehr als 6h Anwesenheit sind mind. 30 min Pause vorgeschrieben – ArbZG §4"
        )
    return warnings


def _check_rest_period(prev_end: time | None, prev_date: date | None,
                       new_start: time, new_date: date) -> list[str]:
    """Mindestruhezeit 11h zwischen Schichten – ArbZG §5.
    Nur relevant bei Schichtwechsel über Tagesgrenzen, nicht bei mehrfachen
    Stempelungen am gleichen Tag (z.B. Korrektur oder Pause).
    """
    if prev_end is None or prev_date is None:
        return []
    # Gleicher Tag → kein Schichtwechsel, §5 nicht anwendbar
    if prev_date == new_date:
        return []
    prev_end_dt = datetime.combine(prev_date, prev_end, tzinfo=None)
    new_start_dt = datetime.combine(new_date, new_start, tzinfo=None)
    rest_hours = (new_start_dt - prev_end_dt).total_seconds() / 3600
    # Nur warnen wenn tatsächlich weniger als 11h Ruhe zwischen zwei verschiedenen Tagen
    if 0 < rest_hours < 11:
        return [
            f"Mindestruhezeit von 11h unterschritten "
            f"({rest_hours:.1f}h seit letzter Schicht) – ArbZG §5"
        ]
    return []


class TimeService:

    # ── Stempeluhr ────────────────────────────────────────────────────────────

    async def stamp_in(
        self,
        user: User,
        data: StampInRequest,
        db: AsyncSession,
    ) -> tuple[TimeEntry, list[str]]:
        today = datetime.now(timezone.utc).date()
        now_time = datetime.now(timezone.utc).time().replace(tzinfo=None)

        # Offenen Eintrag für heute prüfen
        open_entry = await self._get_open_entry(user.id, db)
        if open_entry is not None:
            raise HTTPException(status_code=409, detail="Bereits eingestempelt. Bitte zuerst ausstempeln.")

        # Letzten abgeschlossenen Eintrag für Ruhezeit-Check holen
        last_entry = await db.scalar(
            select(TimeEntry)
            .where(TimeEntry.user_id == user.id, TimeEntry.end_time.isnot(None))
            .order_by(TimeEntry.date.desc(), TimeEntry.end_time.desc())
            .limit(1)
        )

        warnings = _check_rest_period(
            last_entry.end_time if last_entry else None,
            last_entry.date if last_entry else None,
            now_time,
            today,
        )

        entry = TimeEntry(
            user_id=user.id,
            date=today,
            start_time=now_time,
            break_minutes=0,
            source=data.source,
            project_id=data.project_id,
            note=data.note,
            status=EntryStatus.PENDING,
        )
        db.add(entry)
        await db.flush()
        return entry, warnings

    async def stamp_out(
        self,
        user: User,
        note: str | None,
        db: AsyncSession,
    ) -> tuple[TimeEntry, list[str]]:
        entry = await self._get_open_entry(user.id, db)
        if entry is None:
            raise HTTPException(status_code=404, detail="Kein offener Zeitstempel gefunden.")

        now_time = datetime.now(timezone.utc).time().replace(tzinfo=None)

        # Aktive Pause beenden falls vergessen
        if entry.break_start is not None:
            extra_break = self._calc_break_minutes(entry.break_start, now_time)
            entry.break_minutes += extra_break
            entry.break_start = None

        entry.end_time = now_time
        entry.updated_at = datetime.now(timezone.utc)
        if note:
            entry.note = note

        warnings = _check_arbzg(entry.start_time, entry.end_time, entry.break_minutes)
        return entry, warnings

    async def break_start(self, user: User, db: AsyncSession) -> TimeEntry:
        entry = await self._get_open_entry(user.id, db)
        if entry is None:
            raise HTTPException(status_code=404, detail="Kein offener Zeitstempel gefunden.")
        if entry.break_start is not None:
            raise HTTPException(status_code=409, detail="Pause bereits aktiv.")

        now_time = datetime.now(timezone.utc).time().replace(tzinfo=None)
        entry.break_start = now_time
        entry.updated_at = datetime.now(timezone.utc)
        return entry

    async def break_end(self, user: User, db: AsyncSession) -> TimeEntry:
        entry = await self._get_open_entry(user.id, db)
        if entry is None:
            raise HTTPException(status_code=404, detail="Kein offener Zeitstempel gefunden.")
        if entry.break_start is None:
            raise HTTPException(status_code=409, detail="Keine aktive Pause.")

        now_time = datetime.now(timezone.utc).time().replace(tzinfo=None)
        extra = self._calc_break_minutes(entry.break_start, now_time)
        entry.break_minutes += extra
        entry.break_start = None
        entry.updated_at = datetime.now(timezone.utc)
        return entry

    # ── Einträge ──────────────────────────────────────────────────────────────

    async def get_today(self, user: User, db: AsyncSession) -> list[TimeEntry]:
        today = datetime.now(timezone.utc).date()
        result = await db.scalars(
            select(TimeEntry)
            .where(TimeEntry.user_id == user.id, TimeEntry.date == today)
            .order_by(TimeEntry.start_time)
        )
        return list(result.all())

    async def list_entries(
        self,
        company_id: UUID,
        current_user: User,
        db: AsyncSession,
        user_id: UUID | None = None,
        date_from: date | None = None,
        date_to: date | None = None,
        status: EntryStatus | None = None,
        skip: int = 0,
        limit: int = 50,
    ) -> tuple[int, list[TimeEntry]]:
        # Basis: nur Einträge der eigenen Company
        # Subquery: JOIN user für company_id Filter
        q = (
            select(TimeEntry)
            .join(User, TimeEntry.user_id == User.id)
            .where(User.company_id == company_id)
        )

        # EMPLOYEE sieht nur eigene Einträge
        if current_user.role == UserRole.EMPLOYEE:
            q = q.where(TimeEntry.user_id == current_user.id)
        elif user_id:
            q = q.where(TimeEntry.user_id == user_id)

        if date_from:
            q = q.where(TimeEntry.date >= date_from)
        if date_to:
            q = q.where(TimeEntry.date <= date_to)
        if status:
            q = q.where(TimeEntry.status == status)

        total = await db.scalar(select(func.count()).select_from(q.subquery()))
        entries = await db.scalars(q.order_by(TimeEntry.date.desc(), TimeEntry.start_time.desc()).offset(skip).limit(limit))
        return total or 0, list(entries.all())

    async def create_manual(
        self,
        data: ManualEntryCreate,
        current_user: User,
        db: AsyncSession,
    ) -> tuple[TimeEntry, list[str]]:
        target_user_id = current_user.id

        # Employees need explicit permission to create manual entries
        _elevated = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
        if current_user.role == UserRole.EMPLOYEE and not current_user.can_manual_time_entry:
            raise HTTPException(status_code=403, detail="Manuelle Zeiterfassung ist für Ihr Konto nicht freigeschaltet.")

        if data.user_id and data.user_id != current_user.id:
            if current_user.role not in _elevated:
                raise HTTPException(status_code=403, detail="Keine Berechtigung für andere Benutzer.")
            target = await db.get(User, data.user_id)
            if target is None or target.company_id != current_user.company_id:
                raise HTTPException(status_code=404, detail="Mitarbeiter nicht gefunden.")
            target_user_id = data.user_id

        entry = TimeEntry(
            user_id=target_user_id,
            date=data.date,
            start_time=data.start_time,
            end_time=data.end_time,
            break_minutes=data.break_minutes,
            project_id=data.project_id,
            note=data.note,
            source=data.source,
            status=EntryStatus.PENDING,
        )
        db.add(entry)
        await db.flush()

        warnings = _check_arbzg(data.start_time, data.end_time, data.break_minutes)
        return entry, warnings

    async def update_entry(
        self,
        entry_id: UUID,
        data: TimeEntryUpdate,
        current_user: User,
        db: AsyncSession,
    ) -> TimeEntry:
        _manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
        entry = await self._get_entry_or_404(entry_id, db)
        await self._assert_access(entry, current_user, db)

        if entry.status == EntryStatus.APPROVED:
            if current_user.role not in _manager_roles:
                raise HTTPException(status_code=403, detail="Genehmigte Einträge können nur von Vorgesetzten geändert werden.")
            if not data.correction_note:
                raise HTTPException(status_code=422, detail="Änderungsgrund (correction_note) ist bei genehmigten Einträgen Pflicht.")

            # Vorherigen Zustand für AuditLog sichern
            old_snapshot = {
                "started_at":    entry.started_at.isoformat() if entry.started_at else None,
                "ended_at":      entry.ended_at.isoformat() if entry.ended_at else None,
                "break_minutes": entry.break_minutes,
                "note":          entry.note,
                "correction_note": entry.correction_note,
            }

        changes = data.model_dump(exclude_none=True)
        for field, value in changes.items():
            setattr(entry, field, value)
        entry.updated_at = datetime.now(timezone.utc)

        if entry.status == EntryStatus.APPROVED:
            new_snapshot = {
                "started_at":    entry.started_at.isoformat() if entry.started_at else None,
                "ended_at":      entry.ended_at.isoformat() if entry.ended_at else None,
                "break_minutes": entry.break_minutes,
                "note":          entry.note,
                "correction_note": entry.correction_note,
            }
            user_obj = await db.get(User, entry.user_id)
            db.add(AuditLog(
                company_id=current_user.company_id,
                user_id=current_user.id,
                action="time_entry_approved_edit",
                entity_type="time_entry",
                entity_id=entry.id,
                old_value=old_snapshot,
                new_value={**new_snapshot, "changed_by": str(current_user.id),
                           "target_user": str(entry.user_id),
                           "target_user_name": user_obj.full_name if user_obj else None},
            ))

        return entry

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

        entry = await self._get_entry_or_404(entry_id, db)

        # Cross-Tenant-Schutz
        entry_user = await db.get(User, entry.user_id)
        if entry_user is None or entry_user.company_id != current_user.company_id:
            raise HTTPException(status_code=403, detail="Zugriff verweigert.")

        # Self-Approval-Schutz (L-03)
        if entry.user_id == current_user.id:
            raise HTTPException(
                status_code=409,
                detail="Eigene Zeiteinträge können nicht selbst genehmigt werden."
            )

        if entry.status != EntryStatus.PENDING:
            raise HTTPException(status_code=409, detail="Nur ausstehende Einträge können genehmigt werden.")

        entry.status = EntryStatus.APPROVED
        entry.approved_by = current_user.id
        entry.updated_at = datetime.now(timezone.utc)

        # Überstundenkonto neuberechnen (Gap-2-Fix)
        from app.services.report_service import _recalculate_overtime_balance
        schedule = await db.scalar(
            select(WorkSchedule)
            .where(WorkSchedule.company_id == entry_user.company_id)
            .order_by(WorkSchedule.valid_from.desc())
            .limit(1)
        )
        await _recalculate_overtime_balance(entry_user, schedule, db)

        return entry

    async def reject_entry(
        self,
        entry_id: UUID,
        current_user: User,
        correction_note: str | None,
        db: AsyncSession,
    ) -> TimeEntry:
        if current_user.role not in (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN):
            raise HTTPException(status_code=403, detail="Keine Berechtigung zum Ablehnen.")

        entry = await self._get_entry_or_404(entry_id, db)
        if entry.status != EntryStatus.PENDING:
            raise HTTPException(status_code=409, detail="Nur ausstehende Einträge können abgelehnt werden.")

        entry.status = EntryStatus.REJECTED
        entry.approved_by = current_user.id
        if correction_note:
            entry.correction_note = correction_note
        entry.updated_at = datetime.now(timezone.utc)
        return entry

    async def delete_entry(
        self,
        entry_id: UUID,
        current_user: User,
        db: AsyncSession,
    ) -> None:
        entry = await self._get_entry_or_404(entry_id, db)
        await self._assert_access(entry, current_user, db)

        # Genehmigte Einträge dürfen nur von HR/Admin gelöscht werden
        if entry.status == EntryStatus.APPROVED:
            if current_user.role not in (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN):
                raise HTTPException(
                    status_code=403,
                    detail="Genehmigte Einträge können nur von Vorgesetzten gelöscht werden."
                )

        await db.delete(entry)

    async def get_balance(
        self,
        user_id: UUID,
        current_user: User,
        db: AsyncSession,
        period_start: date | None = None,
        period_end: date | None = None,
    ) -> BalanceResponse:
        # Zugriff prüfen
        if user_id != current_user.id and current_user.role == UserRole.EMPLOYEE:
            raise HTTPException(status_code=403, detail="Keine Berechtigung.")

        today = datetime.now(timezone.utc).date()
        if period_start is None:
            period_start = today.replace(day=1)
        if period_end is None:
            period_end = today

        # Genehmigte Einträge summieren
        approved_entries = await db.scalars(
            select(TimeEntry).where(
                and_(
                    TimeEntry.user_id == user_id,
                    TimeEntry.date >= period_start,
                    TimeEntry.date <= period_end,
                    TimeEntry.status == EntryStatus.APPROVED,
                    TimeEntry.end_time.isnot(None),
                )
            )
        )
        approved_list = list(approved_entries.all())

        total_worked = sum(e.worked_hours or 0.0 for e in approved_list)

        # Ausstehende Einträge zählen
        pending_count = await db.scalar(
            select(func.count(TimeEntry.id)).where(
                and_(
                    TimeEntry.user_id == user_id,
                    TimeEntry.date >= period_start,
                    TimeEntry.date <= period_end,
                    TimeEntry.status == EntryStatus.PENDING,
                )
            )
        ) or 0

        # Soll-Stunden aus Arbeitsplan ermitteln (neuester gültiger Plan)
        schedule = await db.scalar(
            select(WorkSchedule)
            .join(User, WorkSchedule.company_id == User.company_id)
            .where(
                User.id == user_id,
                WorkSchedule.valid_from <= period_start,
            )
            .order_by(WorkSchedule.valid_from.desc())
            .limit(1)
        )

        expected = self._calc_expected_hours(period_start, period_end, schedule)

        return BalanceResponse(
            user_id=user_id,
            period_start=period_start,
            period_end=period_end,
            total_hours_worked=round(total_worked, 2),
            expected_hours=round(expected, 2),
            overtime_hours=round(total_worked - expected, 2),
            approved_entries=len(approved_list),
            pending_entries=pending_count,
        )

    # ── Arbeitspläne ──────────────────────────────────────────────────────────

    async def create_work_schedule(
        self,
        company_id: UUID,
        data,
        db: AsyncSession,
    ) -> WorkSchedule:
        schedule = WorkSchedule(company_id=company_id, **data.model_dump())
        db.add(schedule)
        await db.flush()
        return schedule

    async def list_work_schedules(self, company_id: UUID, db: AsyncSession) -> list[WorkSchedule]:
        result = await db.scalars(
            select(WorkSchedule)
            .where(WorkSchedule.company_id == company_id)
            .order_by(WorkSchedule.valid_from.desc())
        )
        return list(result.all())

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

    async def _get_open_entry(self, user_id: UUID, db: AsyncSession) -> TimeEntry | None:
        return await db.scalar(
            select(TimeEntry).where(
                TimeEntry.user_id == user_id,
                TimeEntry.end_time.is_(None),
            ).order_by(TimeEntry.date.desc(), TimeEntry.start_time.desc()).limit(1)
        )

    async def _get_entry_or_404(self, entry_id: UUID, db: AsyncSession) -> TimeEntry:
        entry = await db.get(TimeEntry, entry_id)
        if entry is None:
            raise HTTPException(status_code=404, detail="Zeiterfassungseintrag nicht gefunden.")
        return entry

    async def _assert_access(self, entry: TimeEntry, user: User, db: AsyncSession) -> None:
        if entry.user_id != user.id and user.role not in (
            UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN
        ):
            raise HTTPException(status_code=403, detail="Keine Berechtigung.")
        entry_user = await db.get(User, entry.user_id)
        if entry_user is None or entry_user.company_id != user.company_id:
            raise HTTPException(status_code=403, detail="Zugriff verweigert.")

    @staticmethod
    def _calc_break_minutes(start: time, end: time) -> int:
        s = start.hour * 60 + start.minute
        e = end.hour * 60 + end.minute
        if e < s:
            e += 24 * 60
        return max(0, e - s)

    @staticmethod
    def _calc_expected_hours(period_start: date, period_end: date, schedule: WorkSchedule | None) -> float:
        """Soll-Stunden für den Zeitraum berechnen."""
        from datetime import timedelta
        total = 0.0
        current = period_start
        while current <= period_end:
            wd = current.weekday()  # 0=Mon
            if schedule:
                total += float(schedule.hours_for_weekday(wd))
            else:
                # Fallback: 8h Mo-Fr
                if wd < 5:
                    total += 8.0
            current += timedelta(days=1)
        return total


time_service = TimeService()
