// ── Scheduling logic: timeline computation, route cache, job placement ───────
import { ref, computed } from 'vue'
import { localDateStr, timeToH, hToTime, sortJobsByTime, jobSpansDate, techDaySchedule, techDayCapacityH, expandRRule } from './useHelpers'
import { ABSENCE_REASONS } from './useTechManagement'

export function useScheduler (store, currentView, periodStart, periodDays, dayColumns, pxPerHr, getJobDate, getJobTime, jobColorFn) {
  // Day view: 6AM–6PM. Week view: 7AM–8PM.
  const H_START = computed(() => currentView.value === 'day' ? 6 : 7)
  const H_END   = computed(() => currentView.value === 'day' ? 18 : 20)

  // ── Route cache ────────────────────────────────────────────────────────────
  const routeLegs     = ref({})
  const routeGeometry = ref({})

  // ── Ghost occurrences from recurring templates ─────────────────────────────
  function ghostOccurrencesForDate (tech, dateStr) {
    const templates = tech.queue.filter(j => j.isRecurring && j.recurrenceRule)
    if (!templates.length) return []
    const ghosts = []
    for (const tpl of templates) {
      const rangeEnd = tpl.recurrenceEnd || localDateStr((() => { const d = new Date(); d.setDate(d.getDate() + 90); return d })())
      const dates = expandRRule(tpl.recurrenceRule, tpl.scheduledDate, dateStr, dateStr, tpl.pausePeriods || [])
      if (!dates.includes(dateStr)) continue
      // Skip if dateStr is the template's own scheduledDate (already rendered as real job)
      if (dateStr === tpl.scheduledDate) continue
      // Skip if a materialized instance already exists for this date
      const hasMaterialized = tech.queue.some(j => j.templateId === tpl.id && j.scheduledDate === dateStr)
      if (hasMaterialized) continue
      // Skip tech off-days (unless continuous)
      if (!tpl.continuous && !techDaySchedule(tech, dateStr)) continue
      ghosts.push({
        ...tpl,
        id: `ghost-${tpl.id}-${dateStr}`,
        _realId: tpl.id,
        scheduledDate: dateStr,
        endDate: null,
        _isGhost: true,
        _templateJob: tpl,
      })
    }
    return ghosts
  }

  // ── Parent start position cache ────────────────────────────────────────────
  let _parentStartCache = {}

  function getParentStartH (job) {
    if (!store.technicians.length) return job.startHour ?? 8
    const key = `${job.assignedTech}||${job.id}`
    if (_parentStartCache[key] !== undefined) return _parentStartCache[key]
    const leadTech = store.technicians.find(t => t.id === job.assignedTech)
    if (!leadTech) return job.startHour ?? 8
    const dayStr = localDateStr(periodStart.value)
    const leadJobs = sortJobsByTime(leadTech.queue.filter(j => getJobDate(j.id) === dayStr))
    const cacheKey = `${leadTech.id}||${dayStr}`
    const legMins  = routeLegs.value[cacheKey]
    const hasHome  = !!(leadTech.coords?.[0] && leadTech.coords?.[1])
    const _parentSched = techDaySchedule(leadTech, dayStr)
    let cursor = _parentSched ? _parentSched.startH : 8, result = job.startHour ?? cursor
    leadJobs.forEach((j, idx) => {
      const showTravel = idx > 0 || (idx === 0 && hasHome)
      if (showTravel) {
        const legIdx = hasHome ? idx : idx - 1
        const routeMin = legMins?.[legIdx]
        cursor += (routeMin != null ? routeMin : (parseFloat(j.legDur) > 0 ? parseFloat(j.legDur) : 20)) / 60
      }
      const pinnedH = j.startTime ? timeToH(j.startTime) : null
      const startH = pinnedH ?? cursor
      if (j.id === job.id) result = startH
      cursor = startH + (parseFloat(j.duration) || 1)
    })
    _parentStartCache[key] = result
    return result
  }

  // ── All jobs for a tech on a date (primary + assists) ──────────────────────
  function techAllJobsForDate (tech, dateStr) {
    _parentStartCache = {}
    const primary = tech.queue.filter(j => jobSpansDate(j, dateStr, tech))
    const assists = (tech.assistJobs || [])
      .filter(j => jobSpansDate(j, dateStr, tech))
      .map(j => {
        const a = j.assistants.find(x => x.techId === tech.id)
        const parentH = getParentStartH(j)
        return {
          ...j,
          duration: a?.duration || j.duration,
          startTime: hToTime(parentH),
          startHour: parentH,
          _isAssist: true,
          _assistPinned: !!a?.pinned,
          _assistNote: a?.note || '',
          _parentJob: j,
        }
      })
    const ghosts = ghostOccurrencesForDate(tech, dateStr)
    return sortJobsByTime([...primary, ...assists, ...ghosts])
  }

  // ── Absence / schedule-off segments for a tech on a given date ───────────────
  function absenceSegmentsForDate (tech, dateStr) {
    const segs = []

    // 1. Explicit absence (vacation, sick, etc.)
    if (tech.status === 'off' && tech.absenceFrom) {
      const from = tech.absenceFrom
      const until = tech.absenceUntil || from
      if (dateStr >= from && dateStr <= until) {
        const startH = tech.absenceStartTime ? timeToH(tech.absenceStartTime) : H_START.value
        const endH = tech.absenceEndTime ? timeToH(tech.absenceEndTime) : H_END.value
        const reasonObj = ABSENCE_REASONS.find(r => r.value === tech.absenceReason) || { label: 'Absent', icon: '⏸' }
        const left = (startH - H_START.value) * pxPerHr.value
        const width = (endH - startH) * pxPerHr.value
        segs.push({
          type: 'absence', startH, endH,
          reason: tech.absenceReason, reasonLabel: reasonObj.label, reasonIcon: reasonObj.icon,
          from: tech.absenceFrom, until: tech.absenceUntil, techId: tech.id,
          style: { left: left + 'px', width: Math.max(18, width) + 'px', top: '4px', bottom: '4px', position: 'absolute' },
          job: { id: `absence-${tech.id}-${dateStr}` },
        })
        return segs // absence covers the day — skip schedule check
      }
    }

    // 2. Weekly schedule off-day (regular day off like Fridays for 4×10 schedule)
    //    Marked as _isDayOff so TimelineRow can hide it when planning mode is off
    const daySched = techDaySchedule(tech, dateStr)
    if (!daySched) {
      const left = 0
      const width = (H_END.value - H_START.value) * pxPerHr.value
      segs.push({
        type: 'absence', startH: H_START.value, endH: H_END.value,
        reason: 'day_off', reasonLabel: 'Jour de repos', reasonIcon: '📅',
        _isDayOff: true,
        from: null, until: null, techId: tech.id,
        style: { left: left + 'px', width: Math.max(18, width) + 'px', top: '4px', bottom: '4px', position: 'absolute' },
        job: { id: `schedoff-${tech.id}-${dateStr}` },
      })
    }

    return segs
  }

  // ── Day view: schedule blocks with pinned anchors + auto-flow ──────────────
  function techDayJobsWithTravel (tech) {
    _checkCacheInvalidation()
    const dayStr   = localDateStr(periodStart.value)
    const segKey = `${tech.id}||${dayStr}||${tech.queue.length}||${(tech.assistJobs||[]).length}`
    if (_daySegmentsCache.has(segKey)) return _daySegmentsCache.get(segKey)
    const cacheKey = `${tech.id}||${dayStr}`
    const legMins  = routeLegs.value[cacheKey]
    const hasHome  = !!(tech.coords?.[0] && tech.coords?.[1])
    const allJobs  = techAllJobsForDate(tech, dayStr)

    const flowEntries = []
    const floatingEntries = []
    allJobs.forEach(job => {
      const isAssist = !!job._isAssist
      const dur = parseFloat(job.duration) || 1
      const isPinned = isAssist ? !!job._assistPinned : !!getJobTime(job.id)
      const pinH = isAssist ? job.startHour : (getJobTime(job.id) ? timeToH(getJobTime(job.id)) : null)
      const entry = { job, dur, isAssist, isPinned, pinH }
      if (isAssist && !job._assistPinned) floatingEntries.push(entry)
      else flowEntries.push(entry)
    })

    // Absence blocks act as occupied time (prevent auto-placing jobs there)
    const absSegs  = absenceSegmentsForDate(tech, dayStr)
    const absOccupied = absSegs.map(s => ({ start: s.startH, end: s.endH }))

    const pinnedAnchors = flowEntries.filter(e => e.isPinned).map(e => ({ start: e.pinH, end: e.pinH + e.dur }))
    const placed = []
    const occupied = [...pinnedAnchors.map(a => ({ ...a })), ...absOccupied]
    const sortedFlow = [...flowEntries].sort((a, b) => {
      if (a.isPinned && b.isPinned) return a.pinH - b.pinH
      if (a.isPinned) return -1
      if (b.isPinned) return 1
      return 0
    })

    sortedFlow.filter(e => e.isPinned).forEach(e => placed.push({ entry: e, startH: e.pinH }))

    const daySched = techDaySchedule(tech, dayStr)
    let cursor = daySched ? daySched.startH : 8, flowIdx = 0
    sortedFlow.filter(e => !e.isPinned).forEach(e => {
      const legIdx = hasHome ? flowIdx : flowIdx - 1
      const routeMin = legMins?.[legIdx >= 0 ? legIdx : 0]
      const travelH = (routeMin != null ? routeMin : (parseFloat(e.job.legDur) > 0 ? parseFloat(e.job.legDur) : 20)) / 60
      let startH = cursor + (flowIdx > 0 || hasHome ? travelH : 0)
      let safe = false
      while (!safe) {
        const endH = startH + e.dur
        const overlap = occupied.find(o => startH < o.end && endH > o.start)
        if (overlap) startH = overlap.end + travelH
        else safe = true
      }
      placed.push({ entry: e, startH })
      occupied.push({ start: startH, end: startH + e.dur })
      cursor = startH + e.dur
      flowIdx++
    })

    // Inject absence blocks as pseudo-placed entries so travel chains around them
    absSegs.forEach(s => {
      placed.push({ entry: { job: s.job, dur: s.endH - s.startH, isAssist: false, isPinned: true, pinH: s.startH, _isAbsence: true, _absSeg: s }, startH: s.startH })
    })

    placed.sort((a, b) => a.startH - b.startH)

    const result = []

    // Shift availability background block (renders behind jobs)
    if (daySched) {
      const shiftLeft = (daySched.startH - H_START.value) * pxPerHr.value
      const shiftWidth = (daySched.endH - daySched.startH) * pxPerHr.value
      result.push({
        type: 'shift', startH: daySched.startH, endH: daySched.endH,
        label: `${daySched.start} – ${daySched.end}`,
        style: { left: shiftLeft + 'px', width: Math.max(18, shiftWidth) + 'px', top: '0', bottom: '0', position: 'absolute' },
      })
    }

    // Extra shifts (on-call, garde) from tech.extraShifts
    const extras = (tech.extraShifts || []).filter(s => {
      if (!s.rrule || !s.startTime || !s.endTime) return false
      const dates = expandRRule(s.rrule, s.from || tech.scheduledDate || dayStr, dayStr, dayStr, [])
      return dates.includes(dayStr)
    })
    extras.forEach(s => {
      const sH = timeToH(s.startTime), eH = timeToH(s.endTime)
      result.push({
        type: 'shift', startH: sH, endH: eH,
        label: `${s.label || 'Garde'}: ${s.startTime} – ${s.endTime}`,
        isOnCall: true,
        style: { left: (sH - H_START.value) * pxPerHr.value + 'px', width: Math.max(18, (eH - sH) * pxPerHr.value) + 'px', top: '0', bottom: '0', position: 'absolute' },
      })
    })

    let prevEndH = null
    let legCounter = 0
    placed.forEach((p) => {
      const { entry, startH } = p
      const { job, dur, isAssist, isPinned, _isAbsence, _absSeg } = entry

      // Travel gap between previous block end and this block start
      const travelStart = prevEndH ?? (hasHome ? 8 : null)
      if (travelStart != null && startH > travelStart + 0.01 && !_isAbsence) {
        const gapH = startH - travelStart
        const realJob = isAssist ? job._parentJob : job
        const legIdx = hasHome ? legCounter : legCounter - 1
        const routeMin = legMins?.[legIdx >= 0 ? legIdx : 0]
        const fromRoute = routeMin != null
        result.push({
          type: 'travel', job: realJob, travelMin: fromRoute ? routeMin : Math.round(gapH * 60), fromRoute, isAssist: false,
          style: { left: (travelStart - H_START.value) * pxPerHr.value + 'px', width: Math.max(8, gapH * pxPerHr.value) + 'px', top: '10px', bottom: '10px', position: 'absolute' },
          color: jobColorFn(realJob),
        })
        legCounter++
      }

      if (_isAbsence) {
        // Render as absence block
        result.push(_absSeg)
      } else {
        const realJob = isAssist ? job._parentJob : job
        const jLeft = (startH - H_START.value) * pxPerHr.value
        const jWidth = Math.max(18, dur * pxPerHr.value)
        result.push({
          type: isAssist ? 'assist' : 'job', job: realJob,
          pinned: isPinned, pinnedTime: isPinned ? hToTime(startH) : null, isAssist,
          assistPinned: isAssist ? isPinned : false, assistDur: isAssist ? dur : null,
          assistNote: isAssist ? job._assistNote : null, assistTechId: isAssist ? tech.id : null,
          _isGhost: !!job._isGhost, _templateJob: job._templateJob || null,
          style: { left: jLeft + 'px', width: jWidth + 'px', top: '6px', bottom: '6px', position: 'absolute' },
        })
      }
      prevEndH = startH + dur
    })

    floatingEntries.forEach(entry => {
      const { job, dur } = entry
      const startH = job.startHour ?? 8
      result.push({
        type: 'assist', job: job._parentJob, pinned: false, pinnedTime: null, isAssist: true,
        assistPinned: false, assistDur: dur, assistNote: job._assistNote, assistTechId: tech.id,
        style: { left: (startH - H_START.value) * pxPerHr.value + 'px', width: Math.max(18, dur * pxPerHr.value) + 'px', top: '52%', bottom: '4px', position: 'absolute' },
      })
    })

    _daySegmentsCache.set(segKey, result)
    return result
  }

  // ── Week view helpers ──────────────────────────────────────────────────────
  function techBookingsByDay (tech) {
    return dayColumns.value.map(d => {
      const ds = localDateStr(d)
      const primary = tech.queue.filter(j => jobSpansDate(j, ds, tech))
      const assists = (tech.assistJobs || [])
        .filter(j => jobSpansDate(j, ds, tech) && j.assistants.find(a => a.techId === tech.id)?.pinned)
        .map(j => ({ ...j, _isAssistChip: true, _assistDur: j.assistants.find(a => a.techId === tech.id)?.duration || j.duration }))
      const ghosts = ghostOccurrencesForDate(tech, ds)
      const absSegs = absenceSegmentsForDate(tech, ds)
      return { day: d, dateStr: ds, jobs: [...primary, ...assists, ...ghosts], absent: absSegs.length > 0, absenceInfo: absSegs[0] || null }
    })
  }

  // ── Memoization caches (cleared on reactive dependency changes) ──────────
  const _periodLoadCache = new Map()
  const _periodCapCache = new Map()
  const _daySegmentsCache = new Map()

  // Invalidate caches when period/view changes
  let _lastCacheKey = ''
  function _checkCacheInvalidation () {
    const key = `${currentView.value}||${periodStart.value}||${dayColumns.value.length}||${store.jobs.length}||${store.jobVersion}`
    if (key !== _lastCacheKey) {
      _lastCacheKey = key
      _periodLoadCache.clear()
      _periodCapCache.clear()
      _daySegmentsCache.clear()
    }
  }

  function periodLoadH (tech) {
    _checkCacheInvalidation()
    if (_periodLoadCache.has(tech.id)) return _periodLoadCache.get(tech.id)
    const dateSet = new Set(dayColumns.value.map(d => localDateStr(d)))
    let total = tech.queue.reduce((sum, j) => {
      const ds = getJobDate(j.id)
      return ds && dateSet.has(ds) ? sum + (parseFloat(j.duration) || 0) : sum
    }, 0)
    ;(tech.assistJobs || []).forEach(j => {
      const ds = getJobDate(j.id)
      if (ds && dateSet.has(ds)) {
        const a = j.assistants.find(x => x.techId === tech.id)
        if (a?.pinned) total += parseFloat(a?.duration || j.duration) || 0
      }
    })
    // Include absence hours as occupied time
    dayColumns.value.forEach(d => {
      const ds = localDateStr(d)
      const absSegs = absenceSegmentsForDate(tech, ds)
      absSegs.forEach(s => { total += s.endH - s.startH })
    })
    _periodLoadCache.set(tech.id, total)
    return total
  }

  // Per-tech capacity for the visible period (for utilization bar denominator)
  function techPeriodCapacityH (tech) {
    _checkCacheInvalidation()
    if (_periodCapCache.has(tech.id)) return _periodCapCache.get(tech.id)
    let total = 0
    dayColumns.value.forEach(d => {
      total += techDayCapacityH(tech, localDateStr(d))
    })
    const result = total || 8 // fallback to avoid /0
    _periodCapCache.set(tech.id, result)
    return result
  }

  // Per-tech end hour for current day (for capacity line position)
  function techDayEndH (tech) {
    const dayStr = localDateStr(periodStart.value)
    const sched = techDaySchedule(tech, dayStr)
    return sched ? sched.endH : 16
  }

  function techsActiveOnDay (dateStr, resources) {
    return resources.filter(tech =>
      tech.queue.some(j => jobSpansDate(j, dateStr, tech)) ||
      (tech.assistJobs || []).some(j => jobSpansDate(j, dateStr, tech) && j.assistants.find(a => a.techId === tech.id)?.pinned)
    )
  }

  function dayJobCount (dateStr, resources) {
    const jobIds = new Set()
    resources.forEach(t => t.queue.filter(j => jobSpansDate(j, dateStr, t)).forEach(j => jobIds.add(j.id)))
    return jobIds.size
  }

  return {
    H_START, H_END, routeLegs, routeGeometry,
    techAllJobsForDate, techDayJobsWithTravel, absenceSegmentsForDate, ghostOccurrencesForDate,
    techBookingsByDay, periodLoadH, techPeriodCapacityH, techDayEndH,
    techsActiveOnDay, dayJobCount,
  }
}
