<template>
  <q-page padding>
    <div class="row items-center q-mb-md">
      <q-btn flat dense round icon="arrow_back" :to="'/campaigns'" class="q-mr-sm" />
      <div class="text-h5">Nouvelle campagne</div>
      <q-space />
      <!-- Always-accessible jump-to-editor link. Opens in new tab so the
           wizard's uploaded files + parsed recipients stay intact. Useful
           when the user wants to tweak the template mid-import. -->
      <q-btn flat color="primary" icon="palette" label="Éditer le template (nouvel onglet)"
             type="a" href="/ops/#/campaigns/templates/gift-email-fr" target="_blank">
        <q-tooltip>S'ouvre dans un nouvel onglet — tes fichiers uploadés restent intacts ici</q-tooltip>
      </q-btn>
    </div>

    <q-stepper v-model="step" header-nav ref="stepper" color="primary" animated flat bordered class="bg-white">

      <!-- Step 1 — Upload + parameters ─────────────────────────────────── -->
      <q-step :name="1" title="Fichiers + paramètres" icon="upload_file" :done="step > 1" :header-nav="step > 1">
        <div class="row q-col-gutter-md">
          <div class="col-12 col-md-6">
            <q-card flat bordered>
              <q-card-section>
                <div class="text-subtitle1 q-mb-sm">1. Export Map CSV (brut)</div>
                <div class="text-caption text-grey-7 q-mb-md">
                  Le fichier <code>selectionAdressesMap*.csv</code> tel qu'exporté de la sélection
                  d'adresses (pipe-delimited, préambule de 1 ligne accepté).
                </div>
                <q-file v-model="mapFile" label="Sélectionner le fichier" accept=".csv" outlined dense @update:model-value="readMapFile">
                  <template v-slot:prepend><q-icon name="attach_file" /></template>
                </q-file>
                <div v-if="mapPreview" class="text-caption q-mt-sm text-grey-7">
                  ✓ {{ countMapRows(mapPreview) }} lignes de contacts (préambule + header retirés)
                </div>
              </q-card-section>
            </q-card>
          </div>
          <div class="col-12 col-md-6">
            <q-card flat bordered>
              <q-card-section>
                <div class="text-subtitle1 q-mb-sm">2. Shortlinks Giftbit CSV</div>
                <div class="text-caption text-grey-7 q-mb-md">
                  Le fichier <code>giftbit-gifts-&lt;id&gt;.csv</code> retourné par
                  <code>create_giftbit_campaign.js</code> (colonnes: firstname, lastname, email,
                  gift_url, giftbit_uuid, gift_value_cents).
                </div>
                <q-file v-model="giftFile" label="Sélectionner le fichier" accept=".csv" outlined dense @update:model-value="readGiftFile">
                  <template v-slot:prepend><q-icon name="attach_file" /></template>
                </q-file>
                <div v-if="giftPreview" class="text-caption q-mt-sm text-grey-7">
                  ✓ {{ countGiftRows(giftPreview) }} cartes-cadeaux
                  <span v-if="giftFormatHint" class="text-grey-6">(format: {{ giftFormatHint }})</span>
                </div>
              </q-card-section>
            </q-card>
          </div>
        </div>

        <q-card flat bordered class="q-mt-md">
          <q-card-section>
            <div class="text-subtitle1 q-mb-sm">Paramètres de la campagne</div>
            <div class="row q-col-gutter-md">
              <q-input v-model="params.name" label="Nom interne" outlined dense class="col-12 col-md-6" />
              <q-input v-model="params.amount" label="Montant (affiché)" outlined dense class="col-6 col-md-3" placeholder="60 $" />
              <q-input v-model.number="params.commitment_months" type="number" label="Engagement (mois)" outlined dense class="col-6 col-md-3" />
              <q-input v-model="params.subject" label="Sujet du courriel" outlined dense class="col-12 col-md-6" />
              <q-input v-model="params.from" label="Expéditeur (From)" outlined dense class="col-12 col-md-6" placeholder="TARGO <support@targointernet.com>" />
              <q-input v-model="params.expiry" label="Expiration (texte affiché)" outlined dense class="col-12 col-md-6" placeholder="31 décembre 2026" />
              <!-- Internal-only expiry on our /g/<token> wrapper. Preset
                   buttons cover the typical cases; "Custom" reveals a
                   number input for any value between 1-365 days. -->
              <div class="col-12 col-md-6">
                <div class="text-caption text-grey-7 q-mb-xs">
                  Expiration interne du lien (avant réassignation possible)
                  <q-icon name="info" size="14px" class="q-ml-xs">
                    <q-tooltip max-width="380px">Date après laquelle notre lien intermédiaire <code>/g/&lt;token&gt;</code> cesse de rediriger. Le lien Giftbit sous-jacent reste valide chez Giftbit — utile pour réassigner un cadeau non utilisé. Tu peux choisir une <strong>date précise</strong> (tous les destinataires auront cette date affichée) ou un <strong>nombre de jours après l'envoi</strong> (relatif, glisse si l'envoi est différé).</q-tooltip>
                  </q-icon>
                </div>
                <!-- Primary: explicit date picker (writes params.gift_expires_at).
                     Secondary: preset toggle in days (writes gift_expiry_days,
                     clears the explicit date). One or the other — never both. -->
                <q-input v-model="params.gift_expires_at_display" outlined dense readonly
                         placeholder="Choisir une date explicite (sinon, presets ci-dessous)"
                         class="q-mb-xs">
                  <template v-slot:append>
                    <q-icon name="event" class="cursor-pointer">
                      <q-popup-proxy cover transition-show="scale" transition-hide="scale">
                        <q-date :model-value="params.gift_expires_at_display"
                                @update:model-value="onExpiryDatePicked"
                                mask="YYYY-MM-DD" :options="dateAfterToday"
                                color="primary" minimal>
                          <div class="row items-center justify-end">
                            <q-btn v-close-popup label="OK" color="primary" flat />
                          </div>
                        </q-date>
                      </q-popup-proxy>
                    </q-icon>
                    <q-icon v-if="params.gift_expires_at_display" name="close" class="cursor-pointer q-ml-xs"
                            @click="clearExpiryDate">
                      <q-tooltip>Effacer la date explicite (retour aux presets)</q-tooltip>
                    </q-icon>
                  </template>
                </q-input>
                <div class="row items-center q-gutter-xs">
                  <q-btn-toggle
                    :model-value="params.gift_expires_at_display ? null : expiryPreset"
                    :disable="!!params.gift_expires_at_display"
                    @update:model-value="setExpiryPreset"
                    :options="[
                      { label: '15j', value: 15 },
                      { label: '30j', value: 30 },
                      { label: '60j', value: 60 },
                      { label: '90j', value: 90 },
                      { label: '180j', value: 180 },
                      { label: 'Custom', value: 'custom' },
                    ]"
                    unelevated dense color="grey-3" text-color="grey-9"
                    toggle-color="primary" toggle-text-color="white"
                    no-caps
                  />
                  <q-input v-if="!params.gift_expires_at_display && expiryPreset === 'custom'"
                           v-model.number="params.gift_expiry_days"
                           type="number" min="1" max="365"
                           dense outlined style="width: 120px"
                           suffix="jours" />
                  <span v-else-if="!params.gift_expires_at_display" class="text-caption text-grey-6 q-ml-sm">
                    = {{ params.gift_expiry_days }} jour{{ params.gift_expiry_days > 1 ? 's' : '' }}
                  </span>
                  <span v-else class="text-caption text-grey-6 q-ml-sm">
                    ≈ {{ daysUntilExplicitDate }} jour{{ daysUntilExplicitDate > 1 ? 's' : '' }} à partir d'aujourd'hui
                  </span>
                </div>
              </div>
              <q-input v-model.number="params.throttle_ms" type="number" label="Throttle (ms entre envois)" outlined dense class="col-12 col-md-6" />
              <q-select v-model="params.multi" :options="multiOptions" emit-value map-options label="Emails multiples (couples)" outlined dense class="col-12 col-md-6" />
              <!-- Per-language template override. Defaults to gift-email-fr /
                   gift-email-en. Lets the operator pick seasonal or A/B
                   variants without touching the code. -->
              <q-select v-model="params.template_fr" :options="frTemplateOptions"
                        label="🇫🇷 Template français" outlined dense
                        class="col-12 col-md-6" emit-value map-options
                        :loading="loadingTemplates"
                        @popup-show="loadTemplateLists">
                <template v-slot:append>
                  <q-btn flat dense round icon="refresh" size="sm" @click.stop="loadTemplateLists">
                    <q-tooltip>Rafraîchir la liste des templates</q-tooltip>
                  </q-btn>
                </template>
                <q-tooltip>Template envoyé aux destinataires marqués FR. Les templates -fr apparaissent en premier ; tu peux quand même choisir n'importe quel autre template.</q-tooltip>
              </q-select>
              <q-select v-model="params.template_en" :options="enTemplateOptions"
                        label="🇺🇸 Template anglais" outlined dense
                        class="col-12 col-md-6" emit-value map-options
                        :loading="loadingTemplates"
                        @popup-show="loadTemplateLists">
                <template v-slot:append>
                  <q-btn flat dense round icon="refresh" size="sm" @click.stop="loadTemplateLists">
                    <q-tooltip>Rafraîchir la liste des templates</q-tooltip>
                  </q-btn>
                </template>
                <q-tooltip>Template envoyé aux destinataires marqués EN.</q-tooltip>
              </q-select>
            </div>
          </q-card-section>
        </q-card>

        <q-stepper-navigation>
          <q-btn flat color="primary" icon="person_add" label="Saisie manuelle (sans CSV)" @click="goManual" class="q-mr-sm">
            <q-tooltip>Sauter l'import CSV et ajouter les destinataires un par un à l'étape suivante</q-tooltip>
          </q-btn>
          <q-btn unelevated color="primary" label="Suivant — Aperçu" icon-right="arrow_forward"
                 :disable="!mapPreview || !giftPreview || parsing"
                 :loading="parsing" @click="goPreview" />
        </q-stepper-navigation>
      </q-step>

      <!-- Step 2 — Preview matched send list ─────────────────────────── -->
      <q-step :name="2" title="Aperçu et matching" icon="preview" :done="step > 2" :header-nav="step > 2">
        <!-- Counter strip — at a glance: total / paired / sendable / unmatched -->
        <div class="row q-col-gutter-sm q-mb-md">
          <q-card flat bordered class="col-6 col-md">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5">{{ recipients.length }}</div>
              <div class="text-caption text-grey-7">Paires contact ↔ lien</div>
            </q-card-section>
          </q-card>
          <q-card flat bordered class="col-6 col-md" :class="{ 'bg-positive-1': sendableCount > 0 }">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5 text-positive">{{ sendableCount }}</div>
              <div class="text-caption text-grey-7">À envoyer</div>
            </q-card-section>
          </q-card>
          <q-card flat bordered class="col-6 col-md">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5 text-positive">{{ matchedCount }}</div>
              <div class="text-caption text-grey-7">Client lié</div>
            </q-card-section>
          </q-card>
          <q-card flat bordered class="col-6 col-md">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5 text-warning">{{ unmatchedCount }}</div>
              <div class="text-caption text-grey-7">Sans client</div>
            </q-card-section>
          </q-card>
          <q-card flat bordered class="col-6 col-md" v-if="unpairedContacts.length">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5 text-negative">{{ unpairedContacts.length }}</div>
              <div class="text-caption text-grey-7">Sans lien</div>
            </q-card-section>
          </q-card>
          <q-card flat bordered class="col-6 col-md" v-if="unusedGifts.length">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5 text-orange">{{ unusedGifts.length }}</div>
              <div class="text-caption text-grey-7">Liens en surplus</div>
            </q-card-section>
          </q-card>
          <q-card flat bordered class="col-6 col-md" v-if="namesNeedingReview">
            <q-card-section class="text-center q-pa-sm">
              <div class="text-h5 text-warning">{{ namesNeedingReview }}</div>
              <div class="text-caption text-grey-7">Noms à vérifier</div>
            </q-card-section>
          </q-card>
        </div>

        <!-- Auto-clean summary — informational, not blocking -->
        <q-banner v-if="namesAutoCorrected || namesNeedingReview" class="bg-blue-1 text-info q-mb-sm" rounded dense>
          <template v-slot:avatar><q-icon name="auto_fix_high" /></template>
          <span v-if="namesAutoCorrected">
            <strong>{{ namesAutoCorrected }} nom(s) auto-corrigés</strong> (Title Case, accents québécois,
            prénoms composés séparés). L'icône ✨ verte dans le tableau indique les changements.
          </span>
          <span v-if="namesNeedingReview" :class="namesAutoCorrected ? 'q-ml-md' : ''">
            <strong>{{ namesNeedingReview }} nom(s) suspects</strong> — icône ⚠ amber : cliquer la cellule
            pour éditer en place.
          </span>
        </q-banner>

        <!-- Imbalance banner: explicit explanation of what the imbalance means -->
        <q-banner v-if="unpairedContacts.length || unusedGifts.length" class="bg-orange-1 text-warning q-mb-md" rounded>
          <template v-slot:avatar><q-icon name="warning" /></template>
          <div v-if="unpairedContacts.length">
            <strong>{{ unpairedContacts.length }} contact(s) sans lien-cadeau</strong> —
            ils n'apparaissent PAS dans la liste d'envoi ci-dessous et ne recevront rien.
            Pour les inclure, acquérir {{ unpairedContacts.length }} liens supplémentaires
            chez Giftbit et re-uploader le fichier.
          </div>
          <div v-if="unusedGifts.length" :class="unpairedContacts.length ? 'q-mt-xs' : ''">
            <strong>{{ unusedGifts.length }} lien(s) Giftbit non utilisés</strong> —
            il y a plus de cartes-cadeaux que de contacts. Le surplus sera perdu si
            la campagne est envoyée tel quel.
          </div>
          <!-- Always-on parse summary: explains the imbalance precisely even
               when no Map rows were dropped (= the Giftbit CSV simply had
               more entries than the Map CSV upstream). -->
          <div v-if="parseSkipped" class="q-mt-sm text-caption" style="color:#7C3F00">
            <q-icon name="info" size="14px" />
            <strong>Map CSV :</strong> {{ parseSkipped.total_rows }} ligne(s) brute(s) →
            <strong>{{ recipients.length }} contact(s)</strong> pairé(s) avec un cadeau.
            <span v-if="(parseSkipped.no_email + parseSkipped.duplicate + parseSkipped.multi_skip) === 0">
              Aucun contact n'a été droppé au parsing — l'imbalance vient probablement du fait
              que le CSV Giftbit a {{ unusedGifts.length }} ligne(s) de plus que le CSV Map.
              Vérifie que le nombre de cartes-cadeaux générées chez Giftbit correspond bien au
              fichier Map utilisé.
            </span>
            <span v-else>
              Ventilation des droppés :
              <span v-if="parseSkipped.no_email"> <strong>{{ parseSkipped.no_email }}</strong> sans email valide</span><span v-if="parseSkipped.no_email && (parseSkipped.duplicate || parseSkipped.multi_skip)"> ·</span>
              <span v-if="parseSkipped.duplicate"> <strong>{{ parseSkipped.duplicate }}</strong> emails en double</span><span v-if="parseSkipped.duplicate && parseSkipped.multi_skip"> ·</span>
              <span v-if="parseSkipped.multi_skip"> <strong>{{ parseSkipped.multi_skip }}</strong> couples ignorés (réglage "Emails multiples")</span>.
            </span>
            <span v-if="parseSkipped.no_name">
              ({{ parseSkipped.no_name }} ligne(s) sans nom ont été <em>gardées</em> et utiliseront "cher client" à l'envoi.)
            </span>
          </div>
        </q-banner>

        <!-- ✓ Paired recipients (will be sent) ────────────────────────── -->
        <div class="row items-center q-mb-xs">
          <div class="text-subtitle1">
            <q-icon name="link" /> Association contact ↔ lien-cadeau
            <span class="text-caption text-grey-7">— vérifier avant d'approuver</span>
          </div>
          <q-space />
          <q-btn unelevated dense color="primary" icon="person_add" label="Ajouter manuellement" @click="openManualDialog">
            <q-tooltip>Ajouter un destinataire en saisissant les champs (sans passer par CSV)</q-tooltip>
          </q-btn>
        </div>
        <q-table
          :rows="recipients" :columns="recipientColumns" row-key="row_index"
          flat bordered dense :pagination="{ rowsPerPage: 25 }"
          :rows-per-page-options="[10, 25, 50, 100, 0]"
          class="q-mb-md"
        >
          <template v-slot:body-cell-row_index="props">
            <q-td :props="props" class="text-grey-6">#{{ props.row.row_index }}</q-td>
          </template>
          <!-- Editable firstname cell with auto-clean indicators.
               Click the cell → q-popup-edit opens, type new value, Enter saves.
               Icons (left of name):
                 ⚠ amber = nameWarning() heuristic flagged this (e.g. "deux prénoms collés")
                 ✨ green = auto-cleaner changed something at parse-time
                            (Title Case, accent restoration, compound split) -->
          <template v-slot:body-cell-firstname="props">
            <q-td :props="props" style="cursor:pointer">
              <q-icon v-if="props.row.name_warnings?.firstname" name="warning" color="warning" size="14px" class="q-mr-xs">
                <q-tooltip>{{ props.row.name_warnings.firstname }}</q-tooltip>
              </q-icon>
              <q-icon v-else-if="props.row.cleaned_changed && props.row.firstname !== props.row.firstname_raw"
                      name="auto_fix_high" color="positive" size="14px" class="q-mr-xs">
                <q-tooltip>Auto-corrigé depuis "<strong>{{ props.row.firstname_raw }}</strong>"</q-tooltip>
              </q-icon>
              {{ props.row.firstname }}
              <q-popup-edit v-model="props.row.firstname" auto-save v-slot="scope" buttons label-set="OK" label-cancel="Annuler">
                <q-input v-model="scope.value" dense autofocus :model-value="scope.value"
                         @keyup.enter="scope.set" @keyup.esc="scope.cancel" />
              </q-popup-edit>
            </q-td>
          </template>
          <template v-slot:body-cell-lastname="props">
            <q-td :props="props" style="cursor:pointer">
              <q-icon v-if="props.row.name_warnings?.lastname" name="warning" color="warning" size="14px" class="q-mr-xs">
                <q-tooltip>{{ props.row.name_warnings.lastname }}</q-tooltip>
              </q-icon>
              <q-icon v-else-if="props.row.cleaned_changed && props.row.lastname !== props.row.lastname_raw"
                      name="auto_fix_high" color="positive" size="14px" class="q-mr-xs">
                <q-tooltip>Auto-corrigé depuis "<strong>{{ props.row.lastname_raw }}</strong>"</q-tooltip>
              </q-icon>
              {{ props.row.lastname }}
              <q-popup-edit v-model="props.row.lastname" auto-save v-slot="scope" buttons label-set="OK" label-cancel="Annuler">
                <q-input v-model="scope.value" dense autofocus
                         @keyup.enter="scope.set" @keyup.esc="scope.cancel" />
              </q-popup-edit>
            </q-td>
          </template>
          <template v-slot:body-cell-gift_url="props">
            <q-td :props="props">
              <a :href="props.row.gift_url" target="_blank" style="color:var(--q-primary); font-family:monospace; font-size:0.78rem">
                {{ shortenUrl(props.row.gift_url) }}
              </a>
            </q-td>
          </template>
          <template v-slot:body-cell-language="props">
            <q-td :props="props">
              <!-- Clickable chip — toggles FR ↔ EN inline. Default value comes
                   from the matched Customer.language (or 'fr' for unmatched).
                   The send-worker picks the template by this value at send
                   time, so flipping here changes which template gets used. -->
              <q-chip dense clickable size="sm"
                      :color="props.row.language === 'en' ? 'blue-grey-6' : 'primary'"
                      text-color="white"
                      :label="(props.row.language || 'fr').toUpperCase()"
                      @click="props.row.language = props.row.language === 'en' ? 'fr' : 'en'">
                <q-tooltip>Cliquer pour basculer FR ↔ EN</q-tooltip>
              </q-chip>
            </q-td>
          </template>
          <template v-slot:body-cell-match="props">
            <q-td :props="props">
              <q-chip v-if="props.row.manual" dense color="green-5" text-color="white" size="sm" icon="edit" label="manuel" />
              <q-chip v-else-if="props.row.customer_id" dense color="positive" text-color="white" size="sm" :label="props.row.match_method" />
              <q-chip v-else dense color="warning" text-color="white" size="sm" label="non lié" />
            </q-td>
          </template>
          <template v-slot:body-cell-actions="props">
            <q-td :props="props" class="text-right">
              <q-btn flat dense size="sm" :icon="props.row.excluded ? 'add_circle' : 'block'"
                     :color="props.row.excluded ? 'positive' : 'negative'"
                     @click="props.row.excluded = !props.row.excluded">
                <q-tooltip>{{ props.row.excluded ? 'Inclure' : 'Exclure' }}</q-tooltip>
              </q-btn>
            </q-td>
          </template>
        </q-table>

        <!-- ⚠ Contacts that have NO gift-url (won't be sent) ─────────── -->
        <q-expansion-item v-if="unpairedContacts.length" expand-separator
                          icon="person_off" :label="`${unpairedContacts.length} contact(s) sans lien-cadeau (ne recevront pas)`"
                          header-class="bg-red-1 text-negative">
          <q-table
            :rows="unpairedContacts" :columns="unpairedColumns" row-key="row_index"
            flat dense hide-bottom :pagination="{ rowsPerPage: 0 }"
          >
            <template v-slot:body-cell-row_index="props">
              <q-td :props="props" class="text-grey-6">#{{ props.row.row_index }}</q-td>
            </template>
          </q-table>
        </q-expansion-item>

        <!-- ⓘ Unused gift URLs (extra capacity) ────────────────────────── -->
        <q-expansion-item v-if="unusedGifts.length" expand-separator
                          icon="card_giftcard"
                          :label="`${unusedGifts.length} lien(s) Giftbit non utilisés`"
                          header-class="bg-orange-1 text-warning"
                          class="q-mt-sm">
          <q-list dense>
            <q-item v-for="g in unusedGifts" :key="g.gift_url">
              <q-item-section side class="text-grey-6">#{{ g.row_index }}</q-item-section>
              <q-item-section>
                <a :href="g.gift_url" target="_blank" style="color:var(--q-primary); font-family:monospace; font-size:0.85rem">{{ g.gift_url }}</a>
              </q-item-section>
            </q-item>
          </q-list>
        </q-expansion-item>

        <!-- ⚠ Lignes du Map CSV qui n'ont pas passé le parsing ─────────── -->
        <q-expansion-item v-if="parseSkippedRows.length" expand-separator
                          icon="filter_alt_off"
                          :label="`${parseSkippedRows.length} ligne(s) du Map CSV non importée(s)`"
                          header-class="bg-red-1 text-negative"
                          class="q-mt-sm">
          <div class="q-pa-sm text-caption text-grey-7">
            Chaque ligne ci-dessous a été <strong>droppée au parsing</strong> et n'apparaît pas
            dans la liste d'envoi. Le numéro de ligne (#) correspond à la position dans le fichier
            Excel original (header inclus).
          </div>
          <q-table
            :rows="parseSkippedRows" :columns="skippedRowsColumns" row-key="source_row"
            flat dense hide-bottom :pagination="{ rowsPerPage: 0 }"
          >
            <template v-slot:body-cell-source_row="props">
              <q-td :props="props" class="text-grey-6">#{{ props.row.source_row }}</q-td>
            </template>
            <template v-slot:body-cell-reason="props">
              <q-td :props="props">
                <q-chip dense size="sm" :color="reasonColor(props.row.reason)" text-color="white"
                        :label="reasonLabel(props.row.reason)" />
              </q-td>
            </template>
            <template v-slot:body-cell-actions="props">
              <q-td :props="props" class="text-right">
                <q-btn v-if="props.row.reason !== 'no_email'" flat dense size="sm" color="primary"
                       icon="person_add" @click="prefillManualFromSkipped(props.row)">
                  <q-tooltip>Ouvrir la dialog "Ajouter manuellement" avec ces champs pré-remplis</q-tooltip>
                </q-btn>
              </q-td>
            </template>
          </q-table>
        </q-expansion-item>

        <!-- Action row: preview + edit template are quick-access utilities,
             both non-destructive. The primary action is "Continuer" which
             moves to Step 3 (still NOT the send — Step 3 has its own
             explicit launch button). Icon changed from 'send' (confusing,
             looked like it fired) to 'arrow_forward'. -->
        <q-stepper-navigation>
          <q-btn flat label="Retour" @click="step = 1" />
          <q-space />
          <q-btn flat color="primary" icon="visibility" label="Aperçu du courriel"
                 :disable="!firstPreviewable" @click="openPreview" class="q-mr-sm">
            <q-tooltip>Voir le rendu du courriel avec les vraies données du destinataire #1 (n'envoie rien)</q-tooltip>
          </q-btn>
          <q-btn flat color="primary" icon="palette" label="Éditer le template"
                 type="a" :href="editorHref" target="_blank" class="q-mr-sm">
            <q-tooltip>Ouvre l'éditeur dans un nouvel onglet — ton import reste ici intact</q-tooltip>
          </q-btn>
          <q-btn unelevated color="primary" :label="`Continuer — ${sendableCount} prêts`"
                 icon-right="arrow_forward"
                 @click="step = 3" :disable="sendableCount === 0">
            <q-tooltip>Va à l'étape de confirmation finale. L'envoi ne démarre qu'au clic sur "Lancer l'envoi" de l'étape 3.</q-tooltip>
          </q-btn>
        </q-stepper-navigation>
      </q-step>

      <!-- Step 3 — Confirm + send ──────────────────────────────────────── -->
      <q-step :name="3" title="Confirmation" icon="send">
        <q-card flat bordered>
          <q-card-section>
            <div class="text-subtitle1 q-mb-sm">Récapitulatif</div>
            <q-list dense>
              <q-item><q-item-section side>Nom</q-item-section><q-item-section>{{ params.name }}</q-item-section></q-item>
              <q-item><q-item-section side>Destinataires actifs</q-item-section><q-item-section>{{ sendableCount }} (sur {{ recipients.length }} paires ; {{ excludedCount }} exclus, {{ unpairedContacts.length }} sans lien)</q-item-section></q-item>
              <q-item><q-item-section side>Répartition par langue</q-item-section><q-item-section>{{ langBreakdown }}</q-item-section></q-item>
              <q-item><q-item-section side>Montant affiché</q-item-section><q-item-section>{{ params.amount }}</q-item-section></q-item>
              <q-item><q-item-section side>Engagement</q-item-section><q-item-section>{{ params.commitment_months }} mois</q-item-section></q-item>
              <q-item><q-item-section side>Sujet</q-item-section><q-item-section>{{ params.subject }}</q-item-section></q-item>
              <q-item><q-item-section side>Expéditeur</q-item-section><q-item-section>{{ params.from }}</q-item-section></q-item>
              <q-item><q-item-section side>Template FR</q-item-section><q-item-section><code>{{ params.template_fr }}</code></q-item-section></q-item>
              <q-item><q-item-section side>Template EN</q-item-section><q-item-section><code>{{ params.template_en }}</code></q-item-section></q-item>
              <q-item><q-item-section side>Throttle</q-item-section><q-item-section>{{ params.throttle_ms }} ms entre envois (≈ {{ Math.round((60 / (params.throttle_ms / 1000)) || 0) }} emails/min)</q-item-section></q-item>
              <q-item><q-item-section side>Durée estimée</q-item-section><q-item-section>≈ {{ estimatedMinutes }} min</q-item-section></q-item>
            </q-list>
          </q-card-section>
          <q-card-section class="bg-red-1 text-negative">
            <q-icon name="warning" /> <strong>Confirmation finale.</strong>
            L'envoi démarre dès le clic sur <em>"Lancer l'envoi maintenant"</em>.
            Tu seras redirigé vers la page de progression temps réel.
          </q-card-section>
        </q-card>
        <q-stepper-navigation>
          <q-btn flat label="Retour modifier" icon="arrow_back" @click="step = 2" />
          <q-space />
          <q-btn flat color="primary" icon="visibility" label="Aperçu courriel" :disable="!firstPreviewable" @click="openPreview" class="q-mr-sm" />
          <q-btn unelevated color="negative" label="Lancer l'envoi maintenant" icon-right="send"
                 :loading="sending" @click="confirmAndLaunch" />
        </q-stepper-navigation>
      </q-step>

    </q-stepper>

    <!-- Manual-add dialog — push a recipient into recipients[] without going
         through CSV parsing. Useful for ad-hoc gifts, retroactive top-ups,
         or test sends to internal stakeholders. The matchCustomer() lookup
         that the CSV path does is skipped here — customer_id stays empty
         unless the user manually pastes it. -->
    <q-dialog v-model="manualOpen" persistent>
      <q-card style="min-width: 480px; max-width: 640px;">
        <q-card-section class="row items-center q-pb-none">
          <div class="text-h6"><q-icon name="person_add" class="q-mr-sm" />Ajouter un destinataire</div>
          <q-space />
          <q-btn flat dense round icon="close" v-close-popup />
        </q-card-section>
        <q-card-section>
          <q-form @submit="submitManualRow" class="q-gutter-sm">
            <div class="row q-col-gutter-sm">
              <q-input v-model="manualRow.firstname" label="Prénom *" outlined dense
                       class="col-12 col-sm-6" :rules="[v => !!v || 'requis']" autofocus />
              <q-input v-model="manualRow.lastname" label="Nom" outlined dense
                       class="col-12 col-sm-6" />
            </div>
            <q-input v-model="manualRow.email" label="Email *" outlined dense type="email"
                     :rules="[v => !!v || 'requis', v => /.+@.+\..+/.test(v) || 'email invalide']" />
            <q-input v-model="manualRow.gift_url" label="Lien-cadeau Giftbit *" outlined dense
                     placeholder="https://gft.link/..."
                     :rules="[v => !!v || 'requis', v => /^https?:\/\//.test(v) || 'doit commencer par http:// ou https://']" />
            <q-input v-model="manualRow.civic_address" label="Adresse civique (pour {{description}})" outlined dense
                     placeholder="25 Rue des Hirondelles" />
            <div class="row q-col-gutter-sm">
              <q-input v-model="manualRow.city" label="Ville" outlined dense
                       placeholder="Ste-Clotilde" class="col-12 col-sm-7" />
              <q-input v-model="manualRow.postal_code" label="Code postal" outlined dense
                       placeholder="J0L 1W0" class="col-12 col-sm-5" />
            </div>
            <div class="row q-col-gutter-sm">
              <q-select v-model="manualRow.language" :options="languageOptions" emit-value map-options
                        label="Langue du template" outlined dense class="col-12 col-sm-6" />
              <q-input v-model="manualRow.phone" label="Téléphone (optionnel)" outlined dense class="col-12 col-sm-6" />
            </div>
            <div class="row q-col-gutter-sm">
              <q-input v-model="manualRow.amount" label="Montant affiché dans le courriel" outlined dense
                       :placeholder="`défaut: ${params.amount}`" class="col-12 col-sm-6">
                <q-tooltip><span v-pre>Texte qui apparaîtra à la place de la variable {{amount}}. Laisse vide pour utiliser le montant de la campagne.</span></q-tooltip>
              </q-input>
              <q-input v-model.number="manualRow.gift_value_cents" type="number"
                       label="Valeur en cents (rapport)" outlined dense placeholder="5000"
                       class="col-12 col-sm-6" /></div>
            <div class="text-caption text-grey-7 q-mt-xs">
              <q-icon name="info" size="14px" /> Ville et code postal ne s'affichent pas dans le courriel —
              ils servent à éviter une confusion entre deux clients du même nom dans le tableau ci-dessous
              et dans le rapport CSV.
            </div>
            <div class="text-caption text-grey-7 q-mt-xs">
              <q-icon name="info" size="14px" /> Aucun match ERPNext n'est tenté.
              <span v-pre>Seule l'adresse civique apparaît dans le courriel (variable <code>{{description}}</code>).</span>
            </div>
            <div class="row q-mt-md">
              <q-space />
              <q-btn flat label="Annuler" v-close-popup class="q-mr-sm" />
              <q-btn unelevated color="primary" type="submit" icon="add" label="Ajouter" />
            </div>
          </q-form>
        </q-card-section>
      </q-card>
    </q-dialog>

    <!-- Preview dialog — renders the actual template with the first sendable
         recipient's data + the campaign params. Lets the user verify the
         visual + content WITHOUT firing any emails. Toggleable FR/EN since
         a mixed-language campaign would send both templates. -->
    <q-dialog v-model="previewOpen" maximized persistent>
      <q-card class="bg-grey-2">
        <q-toolbar class="bg-white" style="border-bottom:1px solid #e5e7eb">
          <q-icon name="visibility" class="q-mr-sm" />
          <q-toolbar-title>
            Aperçu du courriel
            <span v-if="previewRecipient" class="text-caption text-grey-7">
              · destinataire #{{ previewRecipient.row_index }} {{ previewRecipient.firstname }} {{ previewRecipient.lastname }}
            </span>
          </q-toolbar-title>
          <q-btn-toggle v-model="previewLang" :options="[{label:'🇫🇷 FR', value:'fr'},{label:'🇺🇸 EN', value:'en'}]"
                        dense unelevated toggle-color="primary" @update:model-value="renderPreview" />
          <q-btn flat icon="open_in_new" :href="editorHref" target="_blank" class="q-mx-sm">
            <q-tooltip>Éditer dans un nouvel onglet</q-tooltip>
          </q-btn>
          <q-btn flat dense round icon="close" @click="previewOpen = false" />
        </q-toolbar>
        <q-banner v-if="previewLoading" class="bg-blue-1 text-info">
          <q-spinner size="sm" /> Rendu en cours…
        </q-banner>
        <q-card-section class="q-pa-md" style="height: calc(100vh - 60px); overflow:hidden">
          <iframe :srcdoc="previewHtmlContent" style="width:100%; height:100%; border:1px solid #e5e7eb; background:#fff;"></iframe>
        </q-card-section>
      </q-card>
    </q-dialog>
  </q-page>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { parseCsvs, createCampaign, sendCampaign, previewTemplate, listTemplates } from 'src/api/campaigns'

const $q = useQuasar()
const router = useRouter()
const step = ref(1)

const mapFile = ref(null)
const giftFile = ref(null)
const mapPreview = ref('')
const giftPreview = ref('')

const params = ref({
  name: `Campagne ${new Date().toISOString().slice(0,10)}`,
  amount: '60 $',
  commitment_months: 3,
  subject: '🎁 Un cadeau pour toi, de la part de TARGO',
  from: 'TARGO <support@targointernet.com>',
  expiry: '',
  throttle_ms: 600,
  multi: 'first',
  // Internal wrapper-URL expiry. Independent of Giftbit's own expiry —
  // after this many days the /g/<token> link returns the expired page,
  // freeing the underlying Giftbit gift_url for reassignment to another
  // customer in a new campaign.
  gift_expiry_days: 90,
  // Optional explicit cutoff (ISO date). When set, overrides the days
  // calculation — every recipient gets THIS exact date in their email,
  // regardless of when the worker actually fires the send. Useful when
  // you want "valid until June 15" rather than "30 days from now".
  // The hub stores .gift_expires_at on each row at send time from this value.
  gift_expires_at_display: '',  // YYYY-MM-DD picked in the date picker
  // Per-language template selection. Defaults match the canonical templates;
  // operator can switch to a variant (e.g. seasonal) per campaign.
  template_fr: 'gift-email-fr',
  template_en: 'gift-email-en',
})

// Date picker constraints + computed helpers for the expiry picker.
function dateAfterToday (date) {
  // q-date passes ISO string YYYY/MM/DD — must be > today
  const d = new Date(date.replace(/\//g, '-'))
  d.setHours(23, 59, 59, 999)
  return d.getTime() > Date.now()
}
// Mirror of CampaignDetailPage's helper — see that file for the algorithm.
// Anchors at end-of-day America/Montreal regardless of the operator's TZ.
function endOfDayMontreal (yyyyMmDd) {
  const [y, m, d] = yyyyMmDd.split('-').map(Number)
  const probe = new Date(Date.UTC(y, m - 1, d, 12, 0, 0))
  const mtlHour = parseInt(new Intl.DateTimeFormat('en-US', {
    hour: '2-digit', hour12: false, timeZone: 'America/Montreal',
  }).format(probe), 10)
  const offsetHours = 12 - mtlHour
  return new Date(Date.UTC(y, m - 1, d, 23 + offsetHours, 59, 59, 999)).toISOString()
}
const daysUntilExplicitDate = computed(() => {
  if (!params.value.gift_expires_at_display) return 0
  const target = new Date(params.value.gift_expires_at_display + 'T23:59:59')
  return Math.max(1, Math.ceil((target - new Date()) / 86400000))
})
function onExpiryDatePicked (dateStr) {
  // dateStr from q-date with mask "YYYY-MM-DD"
  params.value.gift_expires_at_display = dateStr || ''
  // Sync gift_expiry_days for legacy callers, and so the recap shows ~N days
  if (dateStr) {
    const target = new Date(dateStr + 'T23:59:59')
    params.value.gift_expiry_days = Math.max(1, Math.ceil((target - new Date()) / 86400000))
  }
}
function clearExpiryDate () {
  params.value.gift_expires_at_display = ''
  // Restore default 90 days if user cleared
  if (!EXPIRY_PRESETS.includes(params.value.gift_expiry_days)) {
    params.value.gift_expiry_days = 90
  }
}

// Template dropdowns. Lists EVERY editable gift-email-* template in both
// dropdowns, but sorts so the language-matching ones (-fr / -en suffix)
// appear at the top — operator instinct will pick those first while still
// allowing a non-conventionally-named template to be chosen for testing
// or one-off campaigns.
//
// Refreshed on mount AND every time the dropdown popup opens (q-select
// @popup-show), so creating a new template in another tab and switching
// back picks it up without needing a page reload.
const loadingTemplates = ref(false)
const frTemplateOptions = ref([{ label: 'gift-email-fr', value: 'gift-email-fr' }])
const enTemplateOptions = ref([{ label: 'gift-email-en', value: 'gift-email-en' }])
async function loadTemplateLists () {
  loadingTemplates.value = true
  try {
    const tpls = await listTemplates()
    const buildOption = (t, preferred) => ({
      label: preferred ? `${t.name}` : `${t.name} · sans suffixe de langue`,
      value: t.name,
      size: t.size || 0,
    })
    const fr = [], en = []
    for (const t of tpls) {
      if (!t?.name || typeof t.name !== 'string') continue
      if (t.name.endsWith('-fr')) fr.push(buildOption(t, true))
      else if (t.name.endsWith('-en')) en.push(buildOption(t, true))
      // Templates without -fr/-en suffix (e.g. gift-email-test) are pushed
      // to BOTH lists as fallback choices — the operator can still pick
      // them for either language if they're testing a draft.
      else {
        fr.push(buildOption(t, false))
        en.push(buildOption(t, false))
      }
    }
    // Sort: preferred (no "sans suffixe" tag) first, then alphabetical
    const byPriority = (a, b) => {
      const aPref = !a.label.includes('sans suffixe')
      const bPref = !b.label.includes('sans suffixe')
      if (aPref !== bPref) return aPref ? -1 : 1
      return a.value.localeCompare(b.value)
    }
    fr.sort(byPriority); en.sort(byPriority)
    if (fr.length) frTemplateOptions.value = fr
    if (en.length) enTemplateOptions.value = en
  } catch (e) {
    $q.notify({ type: 'warning', message: 'Liste des templates non chargée: ' + e.message })
  } finally {
    loadingTemplates.value = false
  }
}
onMounted(loadTemplateLists)
const multiOptions = [
  { label: '1er email seulement (1 cadeau/foyer)', value: 'first' },
  { label: 'Séparer en 2 rangées (1 cadeau/personne)', value: 'split' },
  { label: 'Ignorer les couples', value: 'skip' },
]

// expiryPreset reflects the current params.gift_expiry_days as one of the
// preset buttons, or 'custom' when the value is non-standard. Two-way:
// clicking a preset writes back to params.gift_expiry_days.
const EXPIRY_PRESETS = [15, 30, 60, 90, 180]
const expiryPreset = computed(() => {
  const v = Number(params.value.gift_expiry_days)
  return EXPIRY_PRESETS.includes(v) ? v : 'custom'
})
function setExpiryPreset (val) {
  if (val === 'custom') {
    // Don't reset — let the operator continue typing in the custom input.
    // If they were on a preset and switched to custom, keep that value as starting point.
    return
  }
  params.value.gift_expiry_days = val
}

const parsing = ref(false)
const sending = ref(false)
const recipients = ref([])
const unpairedContacts = ref([])
const unusedGifts = ref([])
// Map CSV parse skip counters surfaced from the hub so the operator can see
// exactly which rows didn't make it into the pairing (no email, duplicates,
// multi-skip).
const parseSkipped = ref(null)
// The actual rows dropped (up to 200) — each with the raw source columns
// + reason — for the inspectable expansion-item below the imbalance banner.
const parseSkippedRows = ref([])

// row_index (#1, #2, ...) is the source-CSV position — invaluable for the
// user to cross-reference what they see here against the file they uploaded.
// gift_url is rendered as a clickable short label so contact↔link pairing
// can be eyeballed at a glance and the user can click through to verify
// the shortlink works.
const recipientColumns = [
  { name: 'row_index', label: '#', field: 'row_index', align: 'left' },
  { name: 'firstname', label: 'Prénom', field: 'firstname', align: 'left' },
  { name: 'lastname', label: 'Nom', field: 'lastname', align: 'left' },
  { name: 'email', label: 'Email', field: 'email', align: 'left' },
  { name: 'civic_address', label: 'Adresse', field: 'civic_address', align: 'left' },
  { name: 'city', label: 'Ville', field: r => r.city || r.postal_code || '', align: 'left' },
  { name: 'gift_url', label: 'Lien-cadeau', field: 'gift_url', align: 'left' },
  { name: 'language', label: 'Langue', field: 'language', align: 'left' },
  { name: 'match', label: 'Match', field: 'match_method', align: 'left' },
  { name: 'customer_name', label: 'Client ERPNext', field: 'customer_name', align: 'left' },
  { name: 'actions', label: '', field: '', align: 'right' },
]

const unpairedColumns = [
  { name: 'row_index', label: '#', field: 'row_index', align: 'left' },
  { name: 'firstname', label: 'Prénom', field: 'firstname', align: 'left' },
  { name: 'lastname', label: 'Nom', field: 'lastname', align: 'left' },
  { name: 'email', label: 'Email', field: 'email', align: 'left' },
  { name: 'civic_address', label: 'Adresse', field: 'civic_address', align: 'left' },
  { name: 'postal_code', label: 'Code postal', field: 'postal_code', align: 'left' },
]

// Columns for the "dropped at parsing" expansion table. Reads from the raw
// CSV columns (raw_full_name, raw_email, etc.) since the row never made it
// through the contact-building pipeline.
const skippedRowsColumns = [
  { name: 'source_row', label: 'Ligne #', field: 'source_row', align: 'left' },
  { name: 'reason',     label: 'Raison',  field: 'reason',     align: 'left' },
  { name: 'raw_full_name', label: 'Nom au CSV', field: 'raw_full_name', align: 'left' },
  { name: 'raw_email',     label: 'Email au CSV', field: 'raw_email',  align: 'left' },
  { name: 'raw_address',   label: 'Adresse',      field: 'raw_address', align: 'left' },
  { name: 'raw_postal',    label: 'Code postal',  field: 'raw_postal',  align: 'left' },
  { name: 'actions',       label: '',             field: '',            align: 'right' },
]

function reasonColor (reason) {
  return {
    no_email:   'red-7',
    duplicate:  'orange-7',
    multi_skip: 'amber-8',
  }[reason] || 'grey-7'
}
function reasonLabel (reason) {
  return {
    no_email:   'Sans email valide',
    duplicate:  'Email en double',
    multi_skip: 'Couple ignoré (réglage multi)',
  }[reason] || reason
}

// Open the manual-add dialog pre-filled with the dropped row's data so the
// operator can recover it in one click. The duplicate/multi_skip cases are
// the ones worth recovering (the email IS there — we just intentionally
// skipped it); no_email cases can't be recovered without an email address.
function prefillManualFromSkipped (row) {
  const [first, ...rest] = (row.raw_full_name || '').trim().split(/\s+/)
  manualRow.value = {
    ...emptyManualRow(),
    firstname:     first || '',
    lastname:      rest.join(' ') || '',
    email:         (row.raw_email || '').split(/[;,\s]+/).filter(e => e.includes('@'))[0] || '',
    phone:         row.raw_phone || '',
    civic_address: row.raw_address || '',
    postal_code:   row.raw_postal || '',
  }
  manualOpen.value = true
}

const matchedCount = computed(() => recipients.value.filter(r => r.customer_id).length)
const unmatchedCount = computed(() => recipients.value.filter(r => !r.customer_id).length)
const excludedCount = computed(() => recipients.value.filter(r => r.excluded).length)
// Net number of emails that will actually be fired off (paired AND not excluded)
const sendableCount = computed(() => recipients.value.filter(r => !r.excluded && r.gift_url).length)

// Names that the auto-cleaner couldn't confidently fix. Heuristic warnings
// from the backend (digit in name, two names possibly stuck together, etc.).
// User should glance at these before sending.
const namesNeedingReview = computed(() =>
  recipients.value.filter(r => r.name_warnings?.firstname || r.name_warnings?.lastname).length
)
const namesAutoCorrected = computed(() =>
  recipients.value.filter(r => r.cleaned_changed).length
)

// FR / EN breakdown of the sendable recipients — useful preview before launch
// so the user knows which template will actually be used and how many.
const langBreakdown = computed(() => {
  const counts = {}
  for (const r of recipients.value) {
    if (r.excluded || !r.gift_url) continue
    const lang = (r.language || 'fr').toLowerCase()
    counts[lang] = (counts[lang] || 0) + 1
  }
  const parts = Object.entries(counts).map(([l, n]) => `${n} × ${l.toUpperCase()}`)
  return parts.length ? parts.join(', ') : '—'
})
const estimatedMinutes = computed(() => {
  const per = (params.value.throttle_ms || 600) / 1000
  return Math.max(1, Math.round((sendableCount.value * per) / 60))
})

function shortenUrl (u) {
  if (!u) return ''
  return u.replace(/^https?:\/\//, '').slice(0, 28) + (u.length > 35 ? '…' : '')
}

// ── Preview dialog ──────────────────────────────────────────────────────────
// Renders the email through the hub's /campaigns/templates/:name/preview
// endpoint, using the first sendable recipient's data + the current campaign
// params. Non-destructive — no emails are fired by this action.
const previewOpen = ref(false)
const previewLoading = ref(false)
const previewHtmlContent = ref('')
const previewLang = ref('fr')
const previewRecipient = ref(null)

// First recipient that's actually going to be sent — used as the preview
// sample so the user sees real data, not synthetic placeholders.
const firstPreviewable = computed(() =>
  recipients.value.find(r => !r.excluded && r.gift_url) || null
)

// Link to the template editor for the relevant language. Always opens
// in a new tab so the user's in-progress wizard state is preserved.
const editorHref = computed(() =>
  `/ops/#/campaigns/templates/gift-email-${previewLang.value}`
)

async function openPreview () {
  const r = firstPreviewable.value
  if (!r) return
  previewRecipient.value = r
  // Default preview language to the recipient's actual language so the user
  // first sees what THIS recipient will receive
  previewLang.value = (r.language || 'fr').toLowerCase().split('-')[0]
  previewOpen.value = true
  await renderPreview()
}

async function renderPreview () {
  if (!previewRecipient.value) return
  previewLoading.value = true
  try {
    const r = previewRecipient.value
    const vars = {
      firstname:         r.firstname || (previewLang.value === 'en' ? 'dear customer' : 'cher client'),
      lastname:          r.lastname  || '',
      email:             r.email,
      description:       r.civic_address || '',
      gift_url:          r.gift_url,
      amount:            params.value.amount,
      expiry:            params.value.expiry,
      commitment_months: params.value.commitment_months,
      year:              new Date().getFullYear(),
    }
    const res = await previewTemplate(`gift-email-${previewLang.value}`, { vars })
    previewHtmlContent.value = res.rendered || ''
  } catch (e) {
    $q.notify({ type: 'negative', message: 'Aperçu impossible: ' + e.message })
  } finally {
    previewLoading.value = false
  }
}

// Wrapper around launchSend that confirms one last time before firing. The
// Step 3 page is already a "confirmation step", but this dialog adds one
// final friction so accidental clicks don't fire 200 emails.
function confirmAndLaunch () {
  $q.dialog({
    title: 'Envoyer maintenant ?',
    message: `Cette action enverra <strong>${sendableCount.value} courriel(s)</strong>
              via Mailjet immédiatement. Pas annulable une fois démarré.`,
    html: true,
    persistent: true,
    ok: { label: 'Oui, envoyer', color: 'negative', icon: 'send', unelevated: true },
    cancel: { label: 'Annuler', flat: true },
  }).onOk(() => {
    launchSend()
  })
}

function readFile (file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader()
    r.onload = () => resolve(r.result)
    r.onerror = reject
    r.readAsText(file, 'utf-8')
  })
}
async function readMapFile (file) { if (file) mapPreview.value = await readFile(file) }
async function readGiftFile (file) { if (file) giftPreview.value = await readFile(file) }

// Counts must mirror the backend parser exactly so the user sees the same
// numbers in the preview as what Step 2 will receive.

// Map CSV format: 1-line title preamble + header row + N data rows.
// Returns N (the # of contact lines, excluding the preamble and header).
function countMapRows (text) {
  if (!text) return 0
  const lines = text.split(/\r?\n/).filter(l => l.trim())
  // -1 for preamble, -1 for header
  return Math.max(0, lines.length - 2)
}

// Giftbit CSV: TWO formats
//   1. "Link Order" — headerless, one URL per line (each URL = 1 gift)
//   2. "Campaign export" — header row + N data rows (-1 for header)
// Detect like the backend: first non-empty line is a bare URL with no
// separator → no header.
function countGiftRows (text) {
  if (!text) return 0
  const cleaned = text.replace(/^﻿/, '').trim()
  if (!cleaned) return 0
  const firstLine = cleaned.split(/\r?\n/, 1)[0].trim()
  const isLinkOrder = /^https?:\/\/\S+$/.test(firstLine) &&
                      !firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|')
  const lines = cleaned.split(/\r?\n/).filter(l => l.trim())
  return isLinkOrder ? lines.length : Math.max(0, lines.length - 1)
}

// Show "Link Order" or "Campaign export" hint next to the gift count
const giftFormatHint = computed(() => {
  if (!giftPreview.value) return ''
  const firstLine = giftPreview.value.replace(/^﻿/, '').trim().split(/\r?\n/, 1)[0].trim()
  if (/^https?:\/\/\S+$/.test(firstLine) && !firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|')) {
    return 'Link Order'
  }
  return 'Campaign export'
})

async function goPreview () {
  if (!mapPreview.value || !giftPreview.value) return
  parsing.value = true
  try {
    const r = await parseCsvs({ map_csv: mapPreview.value, giftbit_csv: giftPreview.value, multi: params.value.multi })
    recipients.value      = r.recipients || []
    unpairedContacts.value = r.unpaired_contacts || []
    unusedGifts.value      = r.unused_gifts || []
    parseSkipped.value     = r.skipped || null
    parseSkippedRows.value = r.skipped_rows || []
    step.value = 2
  } catch (e) {
    $q.notify({ type: 'negative', message: 'Erreur de parsing: ' + e.message })
  } finally {
    parsing.value = false
  }
}

// Skip CSV parsing entirely — empty recipients list, user adds rows manually
// via the "Ajouter manuellement" button on Step 2. The Suivant button is
// disabled if no rows, so the user can't accidentally proceed with nothing.
function goManual () {
  recipients.value = []
  unpairedContacts.value = []
  unusedGifts.value = []
  step.value = 2
  // Open the dialog immediately — the most likely next action is adding a row
  openManualDialog()
}

// ── Manual recipient entry ────────────────────────────────────────────────
// Lets the user add a recipient by typing the fields directly instead of
// importing from CSV. Useful for: small ad-hoc gifts, replacement sends after
// a bounce, internal QA test sends, or topping up an existing CSV import
// with a missed contact. No ERPNext matching is attempted on these rows.
const manualOpen = ref(false)
const languageOptions = [
  { label: '🇫🇷 Français', value: 'fr' },
  { label: '🇺🇸 English',  value: 'en' },
]
function emptyManualRow () {
  return {
    firstname: '', lastname: '', email: '', phone: '',
    civic_address: '', city: '', postal_code: '', language: 'fr',
    gift_url: '', gift_value_cents: null, amount: '',
  }
}
const manualRow = ref(emptyManualRow())

function openManualDialog () {
  manualRow.value = emptyManualRow()
  manualOpen.value = true
}

function submitManualRow () {
  // Determine next row_index. CSV-imported rows start at 1; manuals continue
  // the sequence so #N reads naturally regardless of source.
  const maxIdx = recipients.value.reduce((m, r) => Math.max(m, r.row_index || 0), 0)
  recipients.value.push({
    row_index:        maxIdx + 1,
    firstname:        (manualRow.value.firstname || '').trim(),
    lastname:         (manualRow.value.lastname  || '').trim(),
    email:            (manualRow.value.email     || '').trim().toLowerCase(),
    phone:            (manualRow.value.phone     || '').trim(),
    civic_address:    (manualRow.value.civic_address || '').trim(),
    city:             (manualRow.value.city || '').trim(),
    postal_code:      (manualRow.value.postal_code || '').trim().toUpperCase(),
    language:         manualRow.value.language || 'fr',
    gift_url:         (manualRow.value.gift_url || '').trim(),
    gift_value_cents: manualRow.value.gift_value_cents || null,
    // Per-recipient amount override. Empty string → falls back to campaign
    // params.amount in the worker. Useful for manuals on a mixed-amount campaign.
    amount:           (manualRow.value.amount || '').trim() || null,
    giftbit_uuid:     null,
    // Flag for downstream code + table display so a "manuel" chip can be shown
    // and the match-method column doesn't read "non lié" misleadingly.
    manual:           true,
    match_method:     'manuel',
    customer_id:      null,
    status:           'pending',
    excluded:         false,
  })
  manualOpen.value = false
  $q.notify({ type: 'positive', message: 'Destinataire ajouté' })
}

async function launchSend () {
  sending.value = true
  try {
    // The hub expects params.gift_expires_at as a full ISO datetime when the
    // user picked an explicit cutoff. The UI carries the YYYY-MM-DD shape
    // in gift_expires_at_display — translate here, anchored at end-of-day
    // local time so "until June 15" really means through all of June 15.
    const apiParams = { ...params.value }
    if (apiParams.gift_expires_at_display) {
      // Anchor at 23:59:59 America/Montreal — handles EDT/EST automatically.
      apiParams.gift_expires_at = endOfDayMontreal(apiParams.gift_expires_at_display)
    } else {
      delete apiParams.gift_expires_at
    }
    delete apiParams.gift_expires_at_display   // UI-only field, drop before save
    const saved = await createCampaign({
      name: params.value.name,
      params: apiParams,
      recipients: recipients.value,
    })
    await sendCampaign(saved.id)
    router.push(`/campaigns/${saved.id}`)
  } catch (e) {
    $q.notify({ type: 'negative', message: 'Erreur: ' + e.message })
  } finally {
    sending.value = false
  }
}
</script>
