<template>
  <q-page padding>
    <div class="row items-center q-mb-md q-gutter-sm">
      <div class="text-h6 text-weight-bold">Rendez-vous clients</div>
      <q-space />
      <q-btn-toggle v-model="view" dense unelevated toggle-color="primary" :options="[
        { label: 'À planifier', value: 'pending' },
        { label: 'À recontacter', value: 'reschedule' },
        { label: 'Tous', value: 'all' },
      ]" @update:model-value="onViewChange" />
      <q-btn flat dense round icon="refresh" :loading="loadingJobs" @click="loadJobs" />
    </div>

    <div class="row q-col-gutter-md">
      <!-- Worklist -->
      <div class="col-12 col-md-4">
        <DynamicFilter v-model="filterState" dense search-placeholder="Job, client, lieu…" :chip-groups="chipGroups"
          preset-key="filter.rdv" :filtered-count="filteredJobs.length" class="q-mb-sm" />
        <q-list bordered separator class="rounded-borders">
          <q-item-label header>
            {{ view === 'reschedule' ? 'À recontacter' : 'Jobs' }} ({{ filteredJobs.length }})
          </q-item-label>
          <q-item v-for="j in filteredJobs" :key="j.name" clickable :active="sel && sel.name === j.name" active-class="bg-blue-1" @click="selectJob(j)">
            <q-item-section>
              <q-item-label class="text-weight-medium">{{ jobTitle(j) }}</q-item-label>
              <q-item-label caption lines="1"><MapPin :size="12" class="q-mr-xs" style="vertical-align:middle;color:#546e7a" />{{ jobLoc(j) }} · {{ fmtDur(j.duration_h) }}</q-item-label>
              <q-item-label caption class="text-grey-5" style="font-size:10px">{{ j.name }}</q-item-label>
            </q-item-section>
            <q-item-section side>
              <q-badge v-if="view === 'reschedule' || j.booking_status === 'À reporter'" color="red" label="à reporter" />
              <q-badge v-else :color="j.scheduled_date ? 'green' : 'orange'" :label="j.scheduled_date ? (j.scheduled_date.slice(5) + (j.start_time ? ' ' + j.start_time.slice(0,5) : '')) : 'à planifier'" />
            </q-item-section>
          </q-item>
          <q-item v-if="!filteredJobs.length"><q-item-section class="text-grey-6">Aucun job.</q-item-section></q-item>
        </q-list>
      </div>

      <!-- Booking panel -->
      <div class="col-12 col-md-8">
        <q-banner v-if="!sel" class="bg-grey-2">Sélectionne un job à gauche pour prendre rendez-vous.</q-banner>
        <q-card v-else flat bordered>
          <q-card-section class="q-pb-none row items-start">
            <div class="col">
              <div class="text-subtitle1 text-weight-bold">{{ jobTitle(sel) }}</div>
              <div class="text-caption text-grey-7"><MapPin :size="13" style="vertical-align:middle;color:#546e7a" /> {{ jobLoc(sel) }} · durée {{ fmtDur(params.duration) }} · tech actuel : {{ sel.assigned_tech || '—' }}</div>
              <div class="text-caption text-grey-5" style="font-size:10px">{{ sel.name }}</div>
            </div>
            <q-btn flat dense no-caps icon="link" label="Lien client" color="primary" @click="genLink(sel)" />
          </q-card-section>

          <!-- Bandeau À reporter : actions superviseur -->
          <q-card-section v-if="isReschedule" class="q-pt-sm">
            <q-banner dense rounded class="bg-red-1 text-negative">
              ⚠️ Client à recontacter. Propose un créneau ci-dessous (réassignation directe), ou laisse le client choisir :
              <q-btn dense unelevated color="negative" icon="sms" label="Aviser par SMS (lien de reprise)" class="q-ml-sm" :loading="smsing" @click="smsReport(sel)" />
            </q-banner>
          </q-card-section>

          <q-card-section class="row q-col-gutter-sm items-end">
            <q-input dense outlined v-model="params.skill" label="Compétence requise" style="width:160px" :hint="sel.service_type ? ('auto : ' + sel.service_type) : 'tag (vide = tous)'"><template v-if="params.skill" #append><q-icon name="sell" size="16px" color="teal" /></template></q-input>
            <q-input dense outlined v-model="params.zone" label="Zone" style="width:140px" />
            <q-input dense outlined type="number" step="0.5" v-model.number="params.duration" label="Durée (h)" style="width:100px" />
            <q-input dense outlined type="date" v-model="params.start" label="À partir du" style="width:160px" />
            <q-select dense outlined v-model="params.days" :options="[7,14,21]" label="Jours" emit-value map-options style="width:90px" />
          </q-card-section>

          <q-tabs v-model="mode" dense align="left" class="text-primary">
            <q-tab name="propose" label="Proposer des créneaux" />
            <q-tab name="client" label="3 dispos du client" />
          </q-tabs>
          <q-separator />

          <!-- Proposer : créneaux groupés Semaine → Jour, hold à la sélection -->
          <q-card-section v-if="mode === 'propose'">
            <q-btn unelevated color="primary" icon="search" label="Trouver des créneaux" :loading="finding" @click="findSlots" />
            <div v-if="slots.length" class="q-mt-md">
              <div class="text-caption text-grey-7 q-mb-xs">{{ slots.length }} créneaux — clique pour réserver (bloqué {{ holdMinutes }} min pour les autres) :</div>
              <div v-for="wk in slotWeeks" :key="wk.mon">
                <div class="wk-head">{{ wk.label }}</div>
                <div v-for="day in wk.days" :key="day.date" class="q-mb-sm">
                  <div class="day-head">{{ dayLabel(day.date) }}</div>
                  <div class="slot-grid">
                    <div v-for="(s, k) in day.slots" :key="k" class="slot" :class="{ on: chosen === s }" @click="chooseSlot(s)">
                      <div class="text-weight-medium">{{ s.start }}–{{ s.end }}</div>
                      <div class="text-caption text-grey-7">{{ s.tech_name }}</div>
                    </div>
                  </div>
                </div>
              </div>
              <q-btn v-if="chosen" unelevated color="positive" icon="check" :label="`Confirmer : ${dayLabel(chosen.date)} ${chosen.start} · ${chosen.tech_name}`" class="q-mt-md" :loading="confirming" @click="confirm(chosen)" />
            </div>
            <div v-else-if="searched" class="text-grey-6 q-mt-md">Aucun créneau — élargis la période, la zone ou la compétence (le roster doit être publié, et la politique de créneaux peut filtrer).</div>
          </q-card-section>

          <!-- 3 dispos client -->
          <q-card-section v-else>
            <div class="text-caption text-grey-7 q-mb-sm">Saisis les 3 disponibilités du client, en ordre de préférence. On place dans le 1er tenable.</div>
            <div v-for="(p, i) in prefs" :key="i" class="row items-center q-gutter-sm q-mb-xs">
              <q-badge color="grey-7">{{ i + 1 }}</q-badge>
              <q-input dense outlined type="date" v-model="p.date" style="width:160px" />
              <q-btn-toggle v-model="p.ampm" @update:model-value="v => setPrefAmpm(p, v)" dense no-caps unelevated
                toggle-color="primary" color="grey-3" text-color="grey-8"
                :options="[{ label: 'AM', value: 'am' }, { label: 'PM', value: 'pm' }]" />
              <q-input dense outlined type="time" v-model="p.start" style="width:110px">
                <template #append><q-icon name="schedule" size="15px" color="grey-5"><q-tooltip>Heure exacte (optionnel) — AM = avant-midi, PM = après-midi</q-tooltip></q-icon></template>
              </q-input>
            </div>
            <q-btn unelevated color="primary" icon="search" label="Vérifier" :loading="fitting" class="q-mt-sm" @click="doFit" />

            <div v-if="fit" class="q-mt-md">
              <q-banner v-if="fit.chosen" dense rounded class="bg-green-1 text-green-9">
                ✅ Choix <b>#{{ fit.chosen.rank }}</b> retenu : {{ dayLabel(fit.chosen.date) }} {{ fit.chosen.start }}–{{ fit.chosen.end }} · {{ fit.chosen.tech_name }}
                <q-btn unelevated color="positive" icon="check" label="Confirmer" class="q-ml-md" :loading="confirming" @click="confirm(fit.chosen)" />
              </q-banner>
              <q-banner v-else dense rounded class="bg-orange-1 text-warning">
                ⚠️ Aucune des 3 dispos n'est tenable. Créneaux proposés :
                <div class="q-mt-xs">
                  <q-chip v-for="(s, k) in fit.proposed" :key="k" dense>{{ frDate(s.date) }} {{ s.start }} ({{ s.available }} dispo)</q-chip>
                  <span v-if="!fit.proposed.length" class="text-grey-7">aucun — élargis la période ou la compétence.</span>
                </div>
                <div class="text-caption q-mt-xs">Passe à « Proposer des créneaux » pour en assigner un.</div>
              </q-banner>
            </div>
          </q-card-section>
        </q-card>
      </div>
    </div>
  </q-page>
</template>

<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { fmtDur } from 'src/composables/useHelpers' // durée canonique (source unique : «30m»/«1h»/«1h30»)
import DynamicFilter from 'src/components/shared/DynamicFilter.vue'
import { MapPin } from 'lucide-vue-next' // pin monochrome outline (style menu Dispatch)
import { useQuasar } from 'quasar'
import * as roster from 'src/api/roster'

const $q = useQuasar()
const jobs = ref([])
const rescheduleJobs = ref([])
const view = ref('pending')
const loadingJobs = ref(false)
const sel = ref(null)
const mode = ref('propose')
const params = reactive({ skill: '', zone: '', duration: 1, start: todayISO(), days: 14 })
// Les 3 dispos sont PRÉ-REMPLIES avec des valeurs réelles (et non des défauts navigateur vides) →
// « Vérifier » fonctionne d'emblée ; l'agent ajuste selon ce que dit le client.
function plusDaysISO (iso, n) { const a = iso.split('-').map(Number); const d = new Date(Date.UTC(a[0], a[1] - 1, a[2])); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10) }
function defaultPrefs () { const d1 = plusDaysISO(todayISO(), 1); const d2x = plusDaysISO(todayISO(), 2); return [{ date: d1, start: '08:00', ampm: 'am' }, { date: d1, start: '13:00', ampm: 'pm' }, { date: d2x, start: '08:00', ampm: 'am' }] }
// Choix rapide AM/PM (avant-midi / après-midi) → règle l'heure de départ représentative (fenêtre 8-12 / 12-16) ;
// l'heure exacte reste ajustable. Aligné sur la contrainte AM/PM du dispatch.
function setPrefAmpm (p, v) { p.ampm = v; if (v === 'am') p.start = '08:00'; else if (v === 'pm') p.start = '13:00' }
const prefs = ref(defaultPrefs())
const fit = ref(null); const fitting = ref(false)
const slots = ref([]); const chosen = ref(null); const finding = ref(false); const searched = ref(false)
const confirming = ref(false); const smsing = ref(false)
const holdMinutes = 10

function todayISO () { return new Date().toLocaleDateString('en-CA', { timeZone: 'America/Toronto' }) }
const FR_DOW = ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam']
const FR_DOW_FULL = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']
const MO = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.']
function d2 (iso) { const a = iso.split('-').map(Number); return new Date(Date.UTC(a[0], a[1] - 1, a[2])) }
function frDate (iso) { if (!iso) return ''; return FR_DOW[d2(iso).getUTCDay()] + ' ' + iso.slice(8) + '/' + iso.slice(5, 7) }
function dayLabel (iso) { const dt = d2(iso); return FR_DOW_FULL[dt.getUTCDay()] + ' ' + dt.getUTCDate() + ' ' + MO[dt.getUTCMonth()] }
// Titre LISIBLE d'un job : type de service + client (l'ID DJ-… est secondaire). Lieu = adresse lisible si dispo.
function jobTitle (j) { if (!j) return ''; const t = (j.service_type || '').trim(); const c = (j.customer_name || '').trim(); if (t) return c ? (t + ' — ' + c) : t; return c || j.location_label || j.name }
function jobLoc (j) { return (j && (j.location_label || j.service_location)) || '—' }
// fmtDur → useHelpers.fmtDur (source unique)
function wkMon (iso) { const dt = d2(iso); const off = (dt.getUTCDay() + 6) % 7; dt.setUTCDate(dt.getUTCDate() - off); return dt.toISOString().slice(0, 10) }
function wkLabel (m) { const a = d2(m); const b = new Date(a); b.setUTCDate(b.getUTCDate() + 6); return 'Semaine du ' + a.getUTCDate() + ' ' + MO[a.getUTCMonth()] + ' – ' + b.getUTCDate() + ' ' + MO[b.getUTCMonth()] }

const isReschedule = computed(() => view.value === 'reschedule' || (sel.value && sel.value.booking_status === 'À reporter'))
// Filtre dynamique (client) de la worklist : recherche (job/client/lieu) + chips Type. La PORTÉE (à planifier/à recontacter/tous) reste le toggle `view`.
const filterState = ref({ search: '', chips: {} })
const serviceTypes = computed(() => [...new Set(jobs.value.map(j => (j.service_type || '').trim()).filter(Boolean))].sort().map(t => ({ value: t, label: t })))
const chipGroups = computed(() => [{ key: 'type', label: 'Type', icon: 'category', options: serviceTypes.value }])
const filteredJobs = computed(() => {
  let base = view.value === 'reschedule' ? rescheduleJobs.value : (view.value === 'all' ? jobs.value : jobs.value.filter(j => !j.scheduled_date || j.booking_status !== 'Confirmé'))
  const t = filterState.value.chips.type
  if (t) base = base.filter(j => (j.service_type || '').trim() === t)
  const q = (filterState.value.search || '').toLowerCase().trim()
  if (q) base = base.filter(j => (jobTitle(j) + ' ' + jobLoc(j) + ' ' + (j.customer_name || '')).toLowerCase().includes(q))
  return base
})
// Créneaux groupés Semaine → Jour
const slotWeeks = computed(() => {
  const byWeek = {}
  for (const s of slots.value) {
    const mon = wkMon(s.date)
    const w = byWeek[mon] || (byWeek[mon] = { mon, label: wkLabel(mon), days: {} })
    ;(w.days[s.date] || (w.days[s.date] = [])).push(s)
  }
  return Object.values(byWeek).sort((a, b) => a.mon.localeCompare(b.mon)).map(w => ({
    mon: w.mon, label: w.label, days: Object.keys(w.days).sort().map(date => ({ date, slots: w.days[date] })),
  }))
})

async function loadJobs () {
  loadingJobs.value = true
  try {
    jobs.value = (await roster.bookJobs()).jobs || []
    rescheduleJobs.value = ((await roster.jobsToReschedule()).jobs || []).map(j => ({ ...j, booking_status: 'À reporter' }))
  } catch (e) { err(e) } finally { loadingJobs.value = false }
}
function onViewChange () { sel.value = null; resetPanel() }
function resetPanel () { fit.value = null; slots.value = []; chosen.value = null; searched.value = false; prefs.value = defaultPrefs() }
async function selectJob (j) { await releaseChosen(); sel.value = j; params.duration = Number(j.duration_h) || 1; params.skill = j.required_skill || ''; resetPanel() } // compétence requise dérivée du type de job

async function doFit () {
  const valid = prefs.value.filter(p => p.date && p.start)
  if (!valid.length) { $q.notify({ type: 'warning', message: 'Saisis au moins une dispo' }); return }
  fitting.value = true
  try { fit.value = await roster.bookFit({ skill: params.skill, zone: params.zone, duration: params.duration, prefs: valid }) } catch (e) { err(e) } finally { fitting.value = false }
}
async function findSlots () {
  finding.value = true; searched.value = true; await releaseChosen(); chosen.value = null
  try { slots.value = (await roster.bookSlots({ skill: params.skill, zone: params.zone, duration: params.duration, start: params.start, days: params.days, limit: 60 })).slots || [] } catch (e) { err(e) } finally { finding.value = false }
}
// Réserver temporairement la fenêtre choisie (libère la précédente)
async function chooseSlot (s) {
  if (chosen.value && chosen.value !== s) { try { await roster.bookHold({ date: chosen.value.date, start: chosen.value.start, release: true }) } catch (e) {} }
  chosen.value = s
  try { await roster.bookHold({ date: s.date, start: s.start, minutes: holdMinutes }) } catch (e) {}
}
async function releaseChosen () { if (chosen.value) { try { await roster.bookHold({ date: chosen.value.date, start: chosen.value.start, release: true }) } catch (e) {} } }

async function confirm (s) {
  confirming.value = true
  try {
    const prefsUsed = prefs.value.filter(p => p.date && p.start)
    const r = await roster.bookConfirm({ job: sel.value.name, tech: s.tech, date: s.date, start: s.start, duration: params.duration, prefs: prefsUsed })
    if (r.ok === false) { err(new Error(r.error || 'échec')); return }
    $q.notify({ type: 'positive', message: `RDV confirmé : ${dayLabel(s.date)} ${s.start} · ${s.tech_name || s.tech}` })
    chosen.value = null // hold libéré côté serveur à la confirmation
    await loadJobs(); sel.value = (jobs.value.concat(rescheduleJobs.value)).find(j => j.name === sel.value?.name) || null; resetPanel()
  } catch (e) { err(e) } finally { confirming.value = false }
}

async function genLink (j) {
  try {
    const r = await roster.bookLink(j.name)
    if (r.url) { try { await navigator.clipboard.writeText(r.url) } catch (e) {} $q.notify({ type: 'positive', message: 'Lien copié : ' + r.url, timeout: 7000, multiLine: true }) }
  } catch (e) { err(e) }
}
function smsReport (j) {
  $q.dialog({
    title: 'Aviser le client par SMS',
    message: 'Le job sera désassigné (remis au pool) et un SMS avec le lien de reprise de RDV sera envoyé au client. Continuer ?',
    cancel: true, persistent: true,
  }).onOk(async () => {
    smsing.value = true
    try {
      const r = await roster.notifyReschedule({ job: j.name })
      $q.notify({ type: r.sms ? 'positive' : 'warning', message: r.sms ? ('SMS envoyé · ' + (r.phone || '')) : (r.note || r.error || 'Statut « À reporter » posé (pas de SMS).'), timeout: 6000 })
      await loadJobs(); view.value = 'reschedule'; sel.value = null; resetPanel()
    } catch (e) { err(e) } finally { smsing.value = false }
  })
}
function err (e) { $q.notify({ type: 'negative', message: '' + (e.message || e) }) }

onMounted(loadJobs)
onBeforeUnmount(releaseChosen)
</script>

<style scoped>
.slot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px; }
.slot { border: 1px solid #e0e0e0; border-radius: 6px; padding: 6px 8px; cursor: pointer; font-size: 12px; text-align: center; }
.slot:hover { border-color: #1976d2; }
.slot.on { background: #c8e6c9; border-color: #2e7d32; }
.wk-head { background: #1565c0; color: #fff; font-weight: 700; font-size: 13px; padding: 6px 12px; border-radius: 6px; margin: 14px 0 6px; }
.day-head { font-weight: 600; font-size: 12px; color: #444; margin: 6px 0 4px; }
</style>
