import { computed, onMounted, ref, watch } from 'vue'
import type { Employee } from '~/services/dto/employee'
import type { Site } from '~/services/dto/site'
import type { WorkHour, WorkHourDayContext, WeeklyWorkHourSummary } from '~/services/dto/work-hour'
import type { AbsenceType } from '~/services/dto/absence-type'
import type { Absence } from '~/services/dto/absence'
import type { HalfDay } from '~/services/dto/half-day'
import { CONTRACT_TYPES, TRACKING_MODES } from '~/services/dto/contract'
import type { HourRow } from '~/components/hours/types'
import { listScopedEmployees } from '~/services/employees'
import { listAbsenceTypes } from '~/services/absence-types'
import { createAbsence, deleteAbsence, listAbsences, updateAbsence } from '~/services/absences'
import { listPublicHolidays } from '~/services/public-holidays'
import {
  bulkUpdateWorkHourSiteValidation,
  bulkUpdateWorkHourValidation,
  bulkUpsertWorkHours,
  getWorkHourDayContext,
  getWorkHourValidationStatus,
  getWeeklyWorkHourSummary,
  listWorkHoursByDate,
  updateWorkHourSiteValidation,
  updateWorkHourValidation
} from '~/services/work-hours'
import {
  formatDateLongFr,
  formatWeekDayHeaderFr,
  formatWeekRangeFr,
  getIsoWeekNumber,
  getOffsetFromTodayYmd,
  getWeekStartDate,
  getTodayYmd,
  parseYmd,
  shiftYmd,
  toYmd
} from '~/utils/date'
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'

export const useHoursPage = () => {
  const auth = useAuthStore()
  const toast = useToast()
  const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
  const isSelfUser = computed(() => auth.user?.roles?.includes('ROLE_SELF') ?? false)
  const isSiteManager = computed(() => !isAdmin.value && !isSelfUser.value)
  const viewMode = ref<'day' | 'week'>('day')

  const selectedDate = ref(getTodayYmd())
  const employees = ref<Employee[]>([])
  const employeeFilter = ref('')
  const selectedSiteIds = ref<number[]>([])
  const sitesInitialized = ref(false)
  const rows = ref<Record<number, HourRow>>({})
  // Instantané des lignes telles que chargées depuis le serveur (clé = employeeId).
  // Sert à n'envoyer au bulk-upsert que les lignes réellement modifiées par l'utilisateur,
  // afin de ne jamais écraser/supprimer une ligne saisie entre-temps par un autre utilisateur
  // (perte de données par enregistrement « à l'aveugle » d'une grille périmée).
  const loadedRows = ref<Record<number, HourRow>>({})
  const dayContext = ref<WorkHourDayContext | null>(null)
  const weeklySummary = ref<WeeklyWorkHourSummary | null>(null)
  const absenceTypes = ref<AbsenceType[]>([])
  const absences = ref<Absence[]>([])
  const publicHolidaysByYear = ref<Record<number, Record<string, string>>>({})
  const isAbsenceDrawerOpen = ref(false)
  const isAbsenceSubmitting = ref(false)
  const editingAbsence = ref<Absence | null>(null)
  const absenceForm = ref({
    employeeId: '' as number | '',
    typeId: '' as number | '',
    startDate: '',
    startHalf: 'AM' as HalfDay,
    endDate: '',
    endHalf: 'PM' as HalfDay,
    comment: ''
  })
  const isLoading = ref(false)
  const isWeekLoading = ref(false)
  const isSubmitting = ref(false)
  const validatingRowIds = ref<number[]>([])
  const siteValidatingRowIds = ref<number[]>([])
  // Jours entièrement validés (admin) par mois civil affiché dans le calendrier
  // de la vue Jour. Clé = 'YYYY-MM', valeur = liste de dates Y-m-d. Chargé à la
  // volée sur @month-change (jamais préchargé sur plusieurs années).
  const validatedDaysByMonth = ref<Record<string, string[]>>({})

  const dayGridCols = computed(() => {
    const metricCol = '0.4fr'
    const validationCols = isAdmin.value || isSiteManager.value ? `${metricCol}` : `${metricCol} ${metricCol}`
    return `1.2fr 0.6fr repeat(6, 0.8fr) ${metricCol} ${metricCol} ${metricCol} ${validationCols}`
  })

  const weekGridCols = '1.6fr repeat(7, 1fr) repeat(6, 0.6fr) 0.3fr'

  const sites = computed<Site[]>(() => {
    const siteMap = new Map<number, Site>()
    for (const employee of employees.value) {
      if (employee.site) {
        siteMap.set(employee.site.id, employee.site)
      }
    }

    return Array.from(siteMap.values()).sort((siteA, siteB) => {
      const orderA = siteA.displayOrder ?? 0
      const orderB = siteB.displayOrder ?? 0
      if (orderA !== orderB) return orderA - orderB
      return siteA.name.localeCompare(siteB.name, 'fr')
    })
  })

  const visibleEmployees = computed(() => {
    if (selectedSiteIds.value.length === 0) return []
    const filter = employeeFilter.value.trim().toLowerCase()
    return employees.value.filter((employee) => {
      if (employee.isDriver === true) return false
      const siteId = employee.site?.id
      if (!siteId || !selectedSiteIds.value.includes(siteId)) return false
      if (!filter) return true
      const firstName = employee.firstName?.toLowerCase() ?? ''
      const lastName = employee.lastName?.toLowerCase() ?? ''
      return firstName.includes(filter) || lastName.includes(filter)
    })
  })

  const displayedEmployees = computed(() => {
    return visibleEmployees.value.filter((employee) => hasContractAtSelectedDate(employee.id))
  })

  const visibleEmployeeIdSet = computed(() => new Set(visibleEmployees.value.map((employee) => employee.id)))

  const filteredWeeklySummary = computed<WeeklyWorkHourSummary | null>(() => {
    if (!weeklySummary.value) return null
    return {
      ...weeklySummary.value,
      rows: weeklySummary.value.rows.filter((row) =>
        visibleEmployeeIdSet.value.has(row.employeeId) && row.hasContractForWeek !== false
      )
    }
  })

  const saveButtonClass = computed(() => {
    if (isSubmitting.value || employees.value.length === 0) {
      return 'opacity-50 cursor-not-allowed'
    }

    return ''
  })

  const isValidationPending = (employeeId: number) => validatingRowIds.value.includes(employeeId)
  const isSiteValidationPending = (employeeId: number) => siteValidatingRowIds.value.includes(employeeId)
  const canToggleValidation = (employeeId: number) => !!rows.value[employeeId]?.workHourId
  const canToggleSiteValidation = (employeeId: number) => {
    if (!isSiteManager.value) return false
    const row = rows.value[employeeId]
    if (!row?.workHourId) return false
    // Une validation RH fige la ligne côté chef de site.
    if (row.isValid) return false
    return true
  }

  const canCreateEmptyValidationRow = (employeeId: number) => {
    const row = rows.value[employeeId]
    if (row?.workHourId) return false
    if (!hasContractAtSelectedDate(employeeId)) return false
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    return !!dayRow?.absenceLabel || is4hContract(employeeId)
  }

  const canCreateValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)

  const canCreateSiteValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)

  const bulkValidatableEmployeeIds = computed(() => {
    return visibleEmployees.value
      .map((employee) => employee.id)
      .filter((employeeId) => canToggleValidation(employeeId) || canCreateValidationRowFromAbsence(employeeId))
  })

  const isBulkValidationChecked = computed(() => {
    const ids = bulkValidatableEmployeeIds.value
    if (ids.length === 0) return false
    return ids.every((employeeId) => rows.value[employeeId]?.isValid ?? false)
  })

  const isBulkValidationIndeterminate = computed(() => {
    const ids = bulkValidatableEmployeeIds.value
    if (ids.length === 0) return false
    const checkedCount = ids.filter((employeeId) => rows.value[employeeId]?.isValid ?? false).length
    return checkedCount > 0 && checkedCount < ids.length
  })

  const bulkSiteValidatableEmployeeIds = computed(() => {
    if (!isSiteManager.value) return []
    return visibleEmployees.value
      .map((employee) => employee.id)
      .filter((employeeId) => canToggleSiteValidation(employeeId) || canCreateSiteValidationRowFromAbsence(employeeId))
  })

  const isBulkSiteValidationChecked = computed(() => {
    const ids = bulkSiteValidatableEmployeeIds.value
    if (ids.length === 0) return false
    return ids.every((employeeId) => rows.value[employeeId]?.isSiteValid ?? false)
  })

  const isBulkSiteValidationIndeterminate = computed(() => {
    const ids = bulkSiteValidatableEmployeeIds.value
    if (ids.length === 0) return false
    const checkedCount = ids.filter((employeeId) => rows.value[employeeId]?.isSiteValid ?? false).length
    return checkedCount > 0 && checkedCount < ids.length
  })

  const canBulkToggleSiteValidation = computed(() => bulkSiteValidatableEmployeeIds.value.length > 0)

  const dayContextByEmployeeId = computed(() => {
    const map = new Map<number, WorkHourDayContext['rows'][number]>()
    for (const row of dayContext.value?.rows ?? []) {
      map.set(row.employeeId, row)
    }
    return map
  })

  const shortcutButtonClass = (target: 'yesterday' | 'today' | 'tomorrow') => {
    const targetDate = target === 'yesterday'
      ? getOffsetFromTodayYmd(-1)
      : target === 'tomorrow'
        ? getOffsetFromTodayYmd(1)
        : getTodayYmd()

    if (selectedDate.value === targetDate) {
      return 'bg-primary-500 text-white'
    }

    return 'bg-white text-primary-500 hover:bg-tertiary-500'
  }

  const weekShortcutButtonClass = (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => {
    const selected = parseYmd(selectedDate.value)
    if (!selected) {
      return 'bg-white text-primary-500 hover:bg-tertiary-500'
    }

    const today = new Date()
    const targetDate = new Date(today)
    if (target === 'previousWeek') targetDate.setDate(today.getDate() - 7)
    if (target === 'nextWeek') targetDate.setDate(today.getDate() + 7)

    const selectedWeekStart = getWeekStartDate(selected)
    const targetWeekStart = getWeekStartDate(targetDate)
    const isActive = selectedWeekStart.getTime() === targetWeekStart.getTime()

    if (isActive) {
      return 'bg-primary-500 text-white'
    }

    return 'bg-white text-primary-500 hover:bg-tertiary-500'
  }

  const getWeekShortcutLabel = (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => {
    const today = new Date()
    if (target === 'previousWeek') today.setDate(today.getDate() - 7)
    if (target === 'nextWeek') today.setDate(today.getDate() + 7)

    const weekNumber = getIsoWeekNumber(today)
    return `Sem. S${weekNumber}`
  }

  const formattedSelectedDate = computed(() => {
    const parsed = parseYmd(selectedDate.value)
    if (!parsed) return selectedDate.value

    if (viewMode.value === 'week') {
      return formatWeekRangeFr(parsed)
    }

    return formatDateLongFr(parsed)
  })

  const selectedYear = computed(() => {
    const parsed = parseYmd(selectedDate.value)
    return parsed ? parsed.getFullYear() : null
  })

  const selectedHolidayLabel = computed(() => {
    const year = selectedYear.value
    if (!year) return ''
    return publicHolidaysByYear.value[year]?.[selectedDate.value] ?? ''
  })

  const isSelectedDateHoliday = computed(() => selectedHolidayLabel.value !== '')

  const weekDayHeaders = computed(() => {
    const days = weeklySummary.value?.days ?? []
    return days.map((date) => ({ date, label: formatWeekDayHeaderFr(date) }))
  })

  const shiftDate = (steps: number) => {
    const offset = viewMode.value === 'week' ? (steps * 7) : steps
    const next = shiftYmd(selectedDate.value, offset)
    if (!next) return
    selectedDate.value = next
  }

  const setToday = () => {
    selectedDate.value = getTodayYmd()
  }

  const setYesterday = () => {
    setToday()
    shiftDate(-1)
  }

  const setTomorrow = () => {
    setToday()
    shiftDate(1)
  }

  const setThisWeek = () => {
    selectedDate.value = getTodayYmd()
  }

  const setPreviousWeek = () => {
    const previousWeek = shiftYmd(getTodayYmd(), -7)
    if (!previousWeek) return
    selectedDate.value = previousWeek
  }

  const setNextWeek = () => {
    const nextWeek = shiftYmd(getTodayYmd(), 7)
    if (!nextWeek) return
    selectedDate.value = nextWeek
  }

  const resetAbsenceForm = () => {
    absenceForm.value = {
      employeeId: '',
      typeId: '',
      startDate: '',
      startHalf: 'AM',
      endDate: '',
      endHalf: 'PM',
      comment: ''
    }
  }

  const closeAbsenceDrawer = () => {
    isAbsenceDrawerOpen.value = false
    editingAbsence.value = null
    resetAbsenceForm()
  }

  const emptyRow = (): HourRow => ({
    workHourId: null,
    morningFrom: '',
    morningTo: '',
    afternoonFrom: '',
    afternoonTo: '',
    eveningFrom: '',
    eveningTo: '',
    isPresentMorning: false,
    isPresentAfternoon: false,
    isSiteValid: false,
    isValid: false,
    updatedAt: null
  })

  // Résout le contrat à la date affichée (ligne du jour), avec repli sur le contrat courant.
  const resolveDayContract = (employee: Employee) => {
    const dayRow = dayContextByEmployeeId.value.get(employee.id)
    if (dayRow?.hasContractAtDate) {
      return {
        trackingMode: dayRow.trackingMode ?? null,
        weeklyHours: dayRow.weeklyHours ?? null,
        type: dayRow.contractType ?? null,
        name: dayRow.contractName ?? ''
      }
    }
    return {
      trackingMode: employee.contract?.trackingMode ?? null,
      weeklyHours: employee.contract?.weeklyHours ?? null,
      type: employee.contract?.type ?? null,
      name: employee.contract?.name ?? ''
    }
  }

  const isPresenceTracking = (employee: Employee) => resolveDayContract(employee).trackingMode === TRACKING_MODES.PRESENCE
  const isTimeTracking = (employee: Employee) => !isPresenceTracking(employee)
  const is4hContract = (employeeId: number) => {
    const employee = employees.value.find((e) => e.id === employeeId)
    return employee ? resolveDayContract(employee).weeklyHours === 4 : false
  }
  const isRowLocked = (employeeId: number) => {
    const row = rows.value[employeeId]
    if (!row) return false
    if (row.isValid) return true
    if (!isAdmin.value && row.isSiteValid) return true
    return false
  }

  const contractLabel = (employee: Employee) => {
    const contract = resolveDayContract(employee)
    if (!contract.type && !contract.name) return '-'
    if (contract.type === CONTRACT_TYPES.INTERIM) {
      return contract.name
    }
    if (contract.weeklyHours !== null && contract.weeklyHours !== undefined && contract.trackingMode === TRACKING_MODES.TIME) {
      return `${contract.weeklyHours}h`
    }
    return contract.name
  }

  const normalizeTime = (value: string): string | null => {
    const trimmed = value.trim()
    return trimmed === '' ? null : trimmed
  }

  const toMinutes = (time: string | null | undefined): number | null => {
    if (!time) return null
    const [hours, minutes] = time.split(':').map(Number)
    if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return null
    return (hours * 60) + minutes
  }

  const resolveInterval = (from: string | null | undefined, to: string | null | undefined): [number, number] | null => {
    const fromMinutes = toMinutes(from)
    const toMinutesValue = toMinutes(to)
    if (fromMinutes === null || toMinutesValue === null) return null

    const end = toMinutesValue <= fromMinutes ? toMinutesValue + 1440 : toMinutesValue
    return [fromMinutes, end]
  }

  const intervalMinutes = (from: string | null | undefined, to: string | null | undefined): number => {
    const interval = resolveInterval(from, to)
    if (!interval) return 0
    const [start, end] = interval
    return Math.max(0, end - start)
  }

  const overlap = (startA: number, endA: number, startB: number, endB: number): number => {
    const start = Math.max(startA, startB)
    const end = Math.min(endA, endB)
    return Math.max(0, end - start)
  }

  const nightIntervalMinutes = (from: string | null | undefined, to: string | null | undefined): number => {
    const interval = resolveInterval(from, to)
    if (!interval) return 0
    const [start, end] = interval

    const nightWindows: Array<[number, number]> = [
      [0, 360],
      [1260, 1440]
    ]

    let total = 0
    for (let dayOffset = 0; dayOffset <= 1; dayOffset++) {
      const shift = dayOffset * 1440
      for (const [nightStart, nightEnd] of nightWindows) {
        total += overlap(start, end, nightStart + shift, nightEnd + shift)
      }
    }

    return total
  }

  const getRowMetrics = (employeeId: number) => {
    const row = rows.value[employeeId] ?? emptyRow()
    const ranges = [
      [row.morningFrom, row.morningTo],
      [row.afternoonFrom, row.afternoonTo],
      [row.eveningFrom, row.eveningTo]
    ] as const

    let totalMinutes = 0
    let nightMinutes = 0

    for (const [from, to] of ranges) {
      totalMinutes += intervalMinutes(from, to)
      nightMinutes += nightIntervalMinutes(from, to)
    }

    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    const creditedMinutes = dayRow?.creditedMinutes ?? 0
    totalMinutes += creditedMinutes
    let dayMinutes = Math.max(0, totalMinutes - nightMinutes)

    // Virtual holiday credit: the backend already applies the contract-period
    // schedule (workDaysHours) and the absence-override rule, so just use the
    // computed value instead of recomputing on the client.
    const virtualHolidayMinutes = dayRow?.virtualHolidayMinutes ?? 0
    if (virtualHolidayMinutes > totalMinutes) {
      dayMinutes += virtualHolidayMinutes - totalMinutes
      totalMinutes = virtualHolidayMinutes
    }

    return { dayMinutes, nightMinutes, totalMinutes, virtualHolidayMinutes }
  }

  const getRowAbsenceLabel = (employeeId: number) => {
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    if (dayRow && dayRow.hasContractAtDate === false) {
      return 'Contrat non démarré'
    }
    if (!dayRow?.absenceLabel) return ''
    if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') {
      const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.'
      return `${dayRow.absenceLabel} (${halfLabel})`
    }
    return `${dayRow.absenceLabel} (journée)`
  }

  const getRowAbsenceStyle = (employeeId: number) => {
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    if (dayRow && dayRow.hasContractAtDate === false) {
      return { backgroundColor: '#6b7280' }
    }
    if (!dayRow?.absenceLabel) return undefined
    return { backgroundColor: dayRow.absenceColor || '#dc2626' }
  }

  const hasRowFormation = (employeeId: number): boolean => {
    return dayContextByEmployeeId.value.get(employeeId)?.hasFormation === true
  }

  const getRowFormationLabel = (employeeId: number): string => {
    return dayContextByEmployeeId.value.get(employeeId)?.formationLabel ?? ''
  }

  const getRowContractNature = (employeeId: number): 'CDI' | 'CDD' | 'INTERIM' | null => {
    return dayContextByEmployeeId.value.get(employeeId)?.contractNature ?? null
  }

  // Jours travaillés du planning (contrats CUSTOM uniquement), ex. "LU,VE". null sinon.
  const getRowWorkedDaysLabel = (employeeId: number): string | null => {
    return formatWorkedDaysShort(dayContextByEmployeeId.value.get(employeeId)?.workDaysHours)
  }

  const getRowUpdatedAt = (employeeId: number): string => {
    const raw = rows.value[employeeId]?.updatedAt
    if (!raw) return ''
    const date = new Date(raw)
    if (Number.isNaN(date.getTime())) return ''
    return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
  }

  const getPresenceDayValue = (employeeId: number) => {
    const row = rows.value[employeeId]
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    const absentMorning = dayRow?.absentMorning ?? false
    const absentAfternoon = dayRow?.absentAfternoon ?? false
    const basePresence = ((row?.isPresentMorning && !absentMorning) ? 0.5 : 0) + ((row?.isPresentAfternoon && !absentAfternoon) ? 0.5 : 0)
    const creditedPresence = dayRow?.creditedPresenceUnits ?? 0
    const total = Math.min(1, basePresence + creditedPresence)
    return Number.isInteger(total) ? String(total) : total.toFixed(1)
  }

  const hasContractAtSelectedDate = (employeeId: number) => {
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    if (!dayRow) return true
    return dayRow.hasContractAtDate !== false
  }

  const isHalfLockedByAbsence = (employeeId: number, half: 'AM' | 'PM') => {
    if (!hasContractAtSelectedDate(employeeId)) return true
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    if (!dayRow) return false
    return half === 'AM' ? dayRow.absentMorning : dayRow.absentAfternoon
  }

  const isEveningLockedByAbsence = (employeeId: number) => {
    if (!hasContractAtSelectedDate(employeeId)) return true
    const dayRow = dayContextByEmployeeId.value.get(employeeId)
    if (!dayRow) return false
    return dayRow.absentAfternoon
  }

  const formatMinutes = (minutes: number) => {
    const safeMinutes = Math.max(0, minutes)
    const hours = Math.floor(safeMinutes / 60)
    const rest = safeMinutes % 60
    return `${String(hours).padStart(2, '0')}:${String(rest).padStart(2, '0')}`
  }

  const hydrateRows = (workHours: WorkHour[]) => {
    const byEmployeeId = new Map<number, WorkHour>()
    for (const workHour of workHours) {
      byEmployeeId.set(workHour.employee.id, workHour)
    }

    const nextRows: Record<number, HourRow> = {}
    for (const employee of employees.value) {
      const workHour = byEmployeeId.get(employee.id)
      nextRows[employee.id] = {
        workHourId: workHour?.id ?? null,
        morningFrom: workHour?.morningFrom ?? '',
        morningTo: workHour?.morningTo ?? '',
        afternoonFrom: workHour?.afternoonFrom ?? '',
        afternoonTo: workHour?.afternoonTo ?? '',
        eveningFrom: workHour?.eveningFrom ?? '',
        eveningTo: workHour?.eveningTo ?? '',
        isPresentMorning: workHour?.isPresentMorning ?? false,
        isPresentAfternoon: workHour?.isPresentAfternoon ?? false,
        isSiteValid: workHour?.isSiteValid ?? false,
        isValid: workHour?.isValid ?? false,
        updatedAt: workHour?.updatedAt ?? null
      }
    }

    rows.value = nextRows
    // Clone indépendant : les éditions mutent les objets de rows.value, pas ceux-ci.
    loadedRows.value = Object.fromEntries(
      Object.entries(nextRows).map(([employeeId, row]) => [employeeId, { ...row }])
    )
  }

  const loadAbsenceTypes = async () => {
    absenceTypes.value = await listAbsenceTypes()
  }

  const loadPublicHolidaysForSelectedYear = async () => {
    const year = selectedYear.value
    if (!year) return
    if (publicHolidaysByYear.value[year]) return

    const holidays = await listPublicHolidays('metropole', year)
    publicHolidaysByYear.value = {
      ...publicHolidaysByYear.value,
      [year]: holidays
    }
  }

  const loadAbsences = async () => {
    absences.value = await listAbsences({
      from: selectedDate.value,
      to: selectedDate.value,
      siteIds: isAdmin.value ? selectedSiteIds.value : undefined
    })
  }

  const openAbsenceDrawer = (employeeId: number) => {
    if (!hasContractAtSelectedDate(employeeId)) return

    const existing = absences.value.find((absence) => {
      if (absence.employee?.id !== employeeId) return false
      const start = absence.startDate.slice(0, 10)
      const end = absence.endDate.slice(0, 10)
      return selectedDate.value >= start && selectedDate.value <= end
    }) ?? null

    if (existing) {
      editingAbsence.value = existing
      absenceForm.value = {
        employeeId,
        typeId: existing.type?.id ?? '',
        startDate: existing.startDate.slice(0, 10),
        startHalf: existing.startHalf ?? 'AM',
        endDate: existing.endDate.slice(0, 10),
        endHalf: existing.endHalf ?? 'PM',
        comment: existing.comment ?? ''
      }
    } else {
      editingAbsence.value = null
      absenceForm.value = {
        employeeId,
        typeId: '',
        startDate: selectedDate.value,
        startHalf: 'AM',
        endDate: selectedDate.value,
        endHalf: 'PM',
        comment: ''
      }
    }

    isAbsenceDrawerOpen.value = true
  }

  const applyLocalClearFromAbsence = (employeeId: number, startHalf: HalfDay, endHalf: HalfDay) => {
    const row = rows.value[employeeId]
    if (!row) return

    if (startHalf === 'AM' && endHalf === 'AM') {
      row.morningFrom = ''
      row.morningTo = ''
      return
    }

    if (startHalf === 'PM' && endHalf === 'PM') {
      row.afternoonFrom = ''
      row.afternoonTo = ''
      row.eveningFrom = ''
      row.eveningTo = ''
      return
    }

    row.morningFrom = ''
    row.morningTo = ''
    row.afternoonFrom = ''
    row.afternoonTo = ''
    row.eveningFrom = ''
    row.eveningTo = ''
  }

  const refreshAfterAbsenceChange = async () => {
    if (isAdmin.value) {
      await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
    } else {
      weeklySummary.value = null
      await Promise.all([loadDayContext(), loadAbsences()])
    }
    await reloadValidationMonth(selectedDate.value)
  }

  const submitAbsence = async () => {
    const form = absenceForm.value
    if (isAbsenceSubmitting.value || form.employeeId === '' || form.typeId === '') return

    isAbsenceSubmitting.value = true
    try {
      if (editingAbsence.value) {
        await updateAbsence({
          id: editingAbsence.value.id,
          employeeId: Number(form.employeeId),
          typeId: Number(form.typeId),
          startDate: form.startDate,
          startHalf: form.startHalf,
          endDate: form.endDate,
          endHalf: form.endHalf,
          comment: editingAbsence.value.comment ?? ''
        })
      } else {
        await createAbsence({
          employeeId: Number(form.employeeId),
          typeId: Number(form.typeId),
          startDate: form.startDate,
          startHalf: form.startHalf,
          endDate: form.endDate,
          endHalf: form.endHalf,
          comment: ''
        })
      }

      applyLocalClearFromAbsence(Number(form.employeeId), form.startHalf, form.endHalf)
      closeAbsenceDrawer()
      await refreshAfterAbsenceChange()
    } finally {
      isAbsenceSubmitting.value = false
    }
  }

  const deleteAbsenceFromDrawer = async () => {
    if (!editingAbsence.value || isAbsenceSubmitting.value) return

    isAbsenceSubmitting.value = true
    try {
      await deleteAbsence(editingAbsence.value.id)
      closeAbsenceDrawer()
      await refreshAfterAbsenceChange()
    } finally {
      isAbsenceSubmitting.value = false
    }
  }

  const toggleValidation = async (
    employeeId: number,
    checked: boolean,
    options: { toast?: boolean } = {}
  ) => {
    const row = rows.value[employeeId]
    if (!row?.workHourId && checked) {
      if (canCreateEmptyValidationRow(employeeId)) {
        await bulkUpsertWorkHours({
          workDate: selectedDate.value,
          entries: [{
            employeeId,
            morningFrom: null,
            morningTo: null,
            afternoonFrom: null,
            afternoonTo: null,
            eveningFrom: null,
            eveningTo: null,
            isPresentMorning: false,
            isPresentAfternoon: false
          }]
        }, { toast: false })

        await loadWorkHours()
      }
    }

    const updatedRow = rows.value[employeeId]
    if (!updatedRow?.workHourId) {
      if (options.toast !== false) {
        toast.error({
          title: 'Validation impossible',
          message: 'La ligne doit contenir des heures ou une absence.'
        })
      }
      return
    }

    if (isValidationPending(employeeId)) return

    validatingRowIds.value = [...validatingRowIds.value, employeeId]
    try {
      await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
      updatedRow.isValid = checked
      await reloadValidationMonth(selectedDate.value)
    } finally {
      validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
    }
  }

  const toggleSiteValidation = async (
    employeeId: number,
    checked: boolean,
    options: { toast?: boolean } = {}
  ) => {
    const row = rows.value[employeeId]
    if (!row?.workHourId && checked) {
      if (canCreateEmptyValidationRow(employeeId)) {
        await bulkUpsertWorkHours({
          workDate: selectedDate.value,
          entries: [{
            employeeId,
            morningFrom: null,
            morningTo: null,
            afternoonFrom: null,
            afternoonTo: null,
            eveningFrom: null,
            eveningTo: null,
            isPresentMorning: false,
            isPresentAfternoon: false
          }]
        }, { toast: false })

        await loadWorkHours()
      }
    }

    const updatedRow = rows.value[employeeId]
    if (!updatedRow?.workHourId) {
      if (options.toast !== false) {
        toast.error({
          title: 'Validation impossible',
          message: 'La ligne doit contenir des heures ou une absence.'
        })
      }
      return
    }

    if (isSiteValidationPending(employeeId)) return
    if (!canToggleSiteValidation(employeeId)) return

    siteValidatingRowIds.value = [...siteValidatingRowIds.value, employeeId]
    try {
      await updateWorkHourSiteValidation(updatedRow.workHourId, checked, { toast: options.toast })
      updatedRow.isSiteValid = checked
    } finally {
      siteValidatingRowIds.value = siteValidatingRowIds.value.filter((id) => id !== employeeId)
    }
  }

  const toggleValidationBulk = async (checked: boolean) => {
    const employeeIds = bulkValidatableEmployeeIds.value
    if (employeeIds.length === 0) return

    const pendingIds = new Set(validatingRowIds.value)
    const availableEmployeeIds = employeeIds.filter((employeeId) => !pendingIds.has(employeeId))
    if (availableEmployeeIds.length === 0) return

    if (checked) {
      const toCreateIds = availableEmployeeIds.filter((employeeId) => canCreateValidationRowFromAbsence(employeeId))
      if (toCreateIds.length > 0) {
        await bulkUpsertWorkHours({
          workDate: selectedDate.value,
          entries: toCreateIds.map((employeeId) => ({
            employeeId,
            morningFrom: null,
            morningTo: null,
            afternoonFrom: null,
            afternoonTo: null,
            eveningFrom: null,
            eveningTo: null,
            isPresentMorning: false,
            isPresentAfternoon: false
          }))
        }, { toast: false })

        await loadWorkHours()
      }
    }

    const targetEmployeeIds = availableEmployeeIds.filter((employeeId) => canToggleValidation(employeeId))
    if (targetEmployeeIds.length === 0) {
      toast.error({
        title: 'Validation impossible',
        message: 'Aucune ligne ne peut être validée.'
      })
      return
    }

    validatingRowIds.value = Array.from(new Set([...validatingRowIds.value, ...targetEmployeeIds]))

    try {
      const result = await bulkUpdateWorkHourValidation({
        workDate: selectedDate.value,
        isValid: checked,
        employeeIds: targetEmployeeIds
      }, { toast: false })

      await loadWorkHours()
      await reloadValidationMonth(selectedDate.value)

      if (result.updated === 0) {
        toast.error({
          title: 'Erreur',
          message: 'Aucune ligne mise à jour.'
        })
        return
      }

      if (result.skipped > 0) {
        toast.success({
          title: 'Succès partiel',
          message: `${result.updated} mise(s) à jour, ${result.skipped} ignorée(s).`
        })
        return
      }

      toast.success({
        title: 'Succès',
        message: checked
          ? `${result.updated} ligne(s) validée(s).`
          : `${result.updated} validation(s) retirée(s).`
      })
    } catch {
      toast.error({
        title: 'Erreur',
        message: 'Impossible de mettre à jour les validations.'
      })
    } finally {
      validatingRowIds.value = validatingRowIds.value.filter((id) => !targetEmployeeIds.includes(id))
    }
  }

  const toggleSiteValidationBulk = async (checked: boolean) => {
    if (!isSiteManager.value) return

    const employeeIds = bulkSiteValidatableEmployeeIds.value
    if (employeeIds.length === 0) return

    const pendingIds = new Set(siteValidatingRowIds.value)
    const availableEmployeeIds = employeeIds.filter((employeeId) => !pendingIds.has(employeeId))
    if (availableEmployeeIds.length === 0) return

    if (checked) {
      const toCreateIds = availableEmployeeIds.filter((employeeId) => canCreateSiteValidationRowFromAbsence(employeeId))
      if (toCreateIds.length > 0) {
        await bulkUpsertWorkHours({
          workDate: selectedDate.value,
          entries: toCreateIds.map((employeeId) => ({
            employeeId,
            morningFrom: null,
            morningTo: null,
            afternoonFrom: null,
            afternoonTo: null,
            eveningFrom: null,
            eveningTo: null,
            isPresentMorning: false,
            isPresentAfternoon: false
          }))
        }, { toast: false })

        await loadWorkHours()
      }
    }

    const targetEmployeeIds = availableEmployeeIds.filter((employeeId) => canToggleSiteValidation(employeeId))
    if (targetEmployeeIds.length === 0) {
      toast.error({
        title: 'Validation impossible',
        message: 'Aucune ligne ne peut être validée côté site.'
      })
      return
    }

    siteValidatingRowIds.value = Array.from(new Set([...siteValidatingRowIds.value, ...targetEmployeeIds]))

    try {
      const result = await bulkUpdateWorkHourSiteValidation({
        workDate: selectedDate.value,
        isSiteValid: checked,
        employeeIds: targetEmployeeIds
      }, { toast: false })

      await loadWorkHours()

      if (result.updated === 0) {
        toast.error({
          title: 'Erreur',
          message: 'Aucune ligne site mise à jour.'
        })
        return
      }

      if (result.skipped > 0) {
        toast.success({
          title: 'Succès partiel',
          message: `${result.updated} mise(s) à jour, ${result.skipped} ignorée(s).`
        })
        return
      }

      toast.success({
        title: 'Succès',
        message: checked
          ? `${result.updated} validation(s) site enregistrée(s).`
          : `${result.updated} validation(s) site retirée(s).`
      })
    } catch {
      toast.error({
        title: 'Erreur',
        message: 'Impossible de mettre à jour les validations site.'
      })
    } finally {
      siteValidatingRowIds.value = siteValidatingRowIds.value.filter((id) => !targetEmployeeIds.includes(id))
    }
  }

  const loadEmployees = async () => {
    const scopedEmployees = await listScopedEmployees()
    employees.value = sortEmployeesBySiteAndOrder(scopedEmployees)
  }

  const loadWorkHours = async () => {
    const workHours = await listWorkHoursByDate(selectedDate.value)
    hydrateRows(workHours)
  }

  const loadWeeklySummary = async () => {
    isWeekLoading.value = true
    try {
      weeklySummary.value = await getWeeklyWorkHourSummary(selectedDate.value)
    } finally {
      isWeekLoading.value = false
    }
  }

  const loadDayContext = async () => {
    dayContext.value = await getWorkHourDayContext(selectedDate.value)
  }

  // --- Calendrier vue Jour : jours validés en vert ---------------------------
  const monthKey = (year: number, monthIndex: number) => `${year}-${String(monthIndex + 1).padStart(2, '0')}`

  // Fusionne tous les mois chargés en une seule map ISO → 'success' pour MalioDate.
  const markedDates = computed<Record<string, 'success'>>(() => {
    const map: Record<string, 'success'> = {}
    for (const days of Object.values(validatedDaysByMonth.value)) {
      for (const day of days) map[day] = 'success'
    }
    return map
  })

  // Charge le statut du mois affiché. La plage couvre toute la grille visible
  // (lundi avant le 1er → dimanche après le dernier jour) pour colorer aussi les
  // jours débordants des mois adjacents.
  const loadValidationMonth = async (monthIndex: number, year: number, options: { force?: boolean } = {}) => {
    const key = monthKey(year, monthIndex)
    if (!options.force && validatedDaysByMonth.value[key]) return

    const gridStart = getWeekStartDate(new Date(year, monthIndex, 1))
    const gridEnd = getWeekStartDate(new Date(year, monthIndex + 1, 0))
    gridEnd.setDate(gridEnd.getDate() + 6)

    const from = toYmd(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate())
    const to = toYmd(gridEnd.getFullYear(), gridEnd.getMonth(), gridEnd.getDate())
    const days = await getWorkHourValidationStatus(from, to)
    validatedDaysByMonth.value = { ...validatedDaysByMonth.value, [key]: days }
  }

  const onCalendarMonthChange = (payload: { month: number; year: number }) => {
    void loadValidationMonth(payload.month, payload.year)
  }

  // Après une modification qui touche la validation d'un jour (validation,
  // sauvegarde d'heures, absence), recharge le mois concerné s'il est déjà en
  // cache → le calendrier se recolore. Sinon no-op (le prochain affichage fetch).
  const reloadValidationMonth = async (dateYmd: string) => {
    const parsed = parseYmd(dateYmd)
    if (!parsed) return
    const key = monthKey(parsed.getFullYear(), parsed.getMonth())
    if (!validatedDaysByMonth.value[key]) return
    await loadValidationMonth(parsed.getMonth(), parsed.getFullYear(), { force: true })
  }

  const refreshByDate = async () => {
    if (isAdmin.value) {
      await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
      return
    }

    weeklySummary.value = null
    await Promise.all([loadWorkHours(), loadDayContext(), loadAbsences()])
  }

  const loadPage = async () => {
    isLoading.value = true
    try {
      await loadPublicHolidaysForSelectedYear()
      await loadEmployees()
      await loadAbsenceTypes()
      await refreshByDate()
    } finally {
      isLoading.value = false
    }
  }

  onMounted(loadPage)

  watch(sites, (nextSites) => {
    const currentSiteIds = nextSites.map((site) => site.id)

    if (!sitesInitialized.value) {
      if (currentSiteIds.length === 0) return
      selectedSiteIds.value = currentSiteIds
      sitesInitialized.value = true
      return
    }

    selectedSiteIds.value = selectedSiteIds.value.filter((siteId) => currentSiteIds.includes(siteId))
  }, { immediate: true })

  watch(isAdmin, async (admin) => {
    if (!admin) {
      viewMode.value = 'day'
      weeklySummary.value = null
      await Promise.all([loadAbsenceTypes(), loadAbsences()])
      return
    }
    await loadAbsenceTypes()
    await loadAbsences()
  }, { immediate: true })

  watch(selectedDate, async () => {
    await loadPublicHolidaysForSelectedYear()
    await refreshByDate()
  })

  // Construit l'entrée bulk-upsert à partir d'une ligne (état courant OU instantané chargé).
  const buildEntry = (employee: Employee, row: HourRow) => {
    const employeeId = employee.id
    if (isPresenceTracking(employee)) {
      return {
        employeeId,
        morningFrom: null,
        morningTo: null,
        afternoonFrom: null,
        afternoonTo: null,
        eveningFrom: null,
        eveningTo: null,
        isPresentMorning: row.isPresentMorning,
        isPresentAfternoon: row.isPresentAfternoon
      }
    }

    return {
      employeeId,
      morningFrom: normalizeTime(row.morningFrom),
      morningTo: normalizeTime(row.morningTo),
      afternoonFrom: normalizeTime(row.afternoonFrom),
      afternoonTo: normalizeTime(row.afternoonTo),
      eveningFrom: normalizeTime(row.eveningFrom),
      eveningTo: normalizeTime(row.eveningTo),
      isPresentMorning: false,
      isPresentAfternoon: false
    }
  }

  // Une entrée est vide quand aucune plage horaire ni présence n'est renseignée.
  // Sert à marquer une suppression explicite (`delete: true`) côté bulk-upsert.
  const isEntryEmpty = (entry: ReturnType<typeof buildEntry>) =>
    !entry.morningFrom && !entry.morningTo
    && !entry.afternoonFrom && !entry.afternoonTo
    && !entry.eveningFrom && !entry.eveningTo
    && !entry.isPresentMorning && !entry.isPresentAfternoon

  const handleSave = async () => {
    if (isSubmitting.value || employees.value.length === 0) return

    isSubmitting.value = true
    try {
      const entries = employees.value
        .filter((employee) => hasContractAtSelectedDate(employee.id) && !isRowLocked(employee.id))
        .map((employee) => {
          const current = buildEntry(employee, rows.value[employee.id] ?? emptyRow())
          const original = buildEntry(employee, loadedRows.value[employee.id] ?? emptyRow())
          return { current, original }
        })
        // N'envoie que les lignes réellement modifiées : une ligne intouchée n'est jamais
        // transmise, donc jamais supprimée même si un autre utilisateur l'a saisie entre-temps.
        .filter(({ current, original }) => JSON.stringify(current) !== JSON.stringify(original))
        // Une ligne vidée par l'utilisateur (donc différente de l'instantané chargé) porte le
        // flag `delete` : le backend n'autorise la suppression d'une ligne existante que sur
        // intention explicite. Sans ce flag, une grille périmée ne peut rien détruire.
        .map(({ current }) => (isEntryEmpty(current) ? { ...current, delete: true } : current))

      if (entries.length === 0) {
        return
      }

      await bulkUpsertWorkHours({
        workDate: selectedDate.value,
        entries
      })

      await refreshByDate()
      await reloadValidationMonth(selectedDate.value)
    } finally {
      isSubmitting.value = false
    }
  }

  const isWeekCommentDrawerOpen = ref(false)
  const weekCommentContext = ref<{ employeeId: number; employeeLabel: string; weekStart: string; weekEnd: string; content: string; commentId: number | null } | null>(null)
  const openWeekCommentDrawer = (row: { employeeId: number; firstName: string; lastName: string; comment?: string | null; commentId?: number | null }) => {
    if (!weeklySummary.value) return
    weekCommentContext.value = { employeeId: row.employeeId, employeeLabel: `${row.firstName} ${row.lastName}`.trim(), weekStart: weeklySummary.value.weekStart, weekEnd: weeklySummary.value.weekEnd, content: row.comment ?? '', commentId: row.commentId ?? null }
    isWeekCommentDrawerOpen.value = true
  }
  const reloadWeeklySummary = async () => { await loadWeeklySummary() }

  return {
    isAdmin,
    isSelfUser,
    isSiteManager,
    viewMode,
    selectedDate,
    employeeFilter,
    sites,
    selectedSiteIds,
    employees,
    visibleEmployees,
    displayedEmployees,
    rows,
    absenceTypes,
    absenceForm,
    isAbsenceDrawerOpen,
    isAbsenceSubmitting,
    editingAbsence,
    weeklySummary,
    filteredWeeklySummary,
    isLoading,
    isWeekLoading,
    isSubmitting,
    dayGridCols,
    weekGridCols,
    saveButtonClass,
    formattedSelectedDate,
    isSelectedDateHoliday,
    selectedHolidayLabel,
    weekDayHeaders,
    shortcutButtonClass,
    weekShortcutButtonClass,
    getWeekShortcutLabel,
    setToday,
    setYesterday,
    setTomorrow,
    setThisWeek,
    setPreviousWeek,
    setNextWeek,
    shiftDate,
    contractLabel,
    isTimeTracking,
    isPresenceTracking,
    isRowLocked,
    isHalfLockedByAbsence,
    isEveningLockedByAbsence,
    hasContractAtSelectedDate,
    isValidationPending,
    isSiteValidationPending,
    canToggleValidation,
    canToggleSiteValidation,
    canCreateSiteValidationRowFromAbsence,
    isBulkValidationChecked,
    isBulkValidationIndeterminate,
    isBulkSiteValidationChecked,
    isBulkSiteValidationIndeterminate,
    canBulkToggleSiteValidation,
    toggleValidation,
    toggleSiteValidation,
    toggleValidationBulk,
    toggleSiteValidationBulk,
    getRowMetrics,
    getRowAbsenceLabel,
    getRowAbsenceStyle,
    hasRowFormation,
    getRowFormationLabel,
    getRowContractNature,
    getRowWorkedDaysLabel,
    getRowUpdatedAt,
    getPresenceDayValue,
    openAbsenceDrawer,
    submitAbsence,
    deleteAbsenceFromDrawer,
    closeAbsenceDrawer,
    formatMinutes,
    handleSave,
    markedDates,
    onCalendarMonthChange,
    isWeekCommentDrawerOpen,
    weekCommentContext,
    openWeekCommentDrawer,
    reloadWeeklySummary
  }
}
