#!/usr/bin/env node
'use strict'
/**
 * send_gift_campaign.js — Personalized French email sender for Giftbit campaigns.
 *
 * The Giftbit UI/API generates one gift_url per recipient. Their built-in
 * delivery emails are English-only and lack tracking. This script bridges
 * the gap: take a CSV of contacts (firstname, lastname, email, description)
 * and a CSV of gifts from Giftbit (one column with the gift URL), match
 * them row-by-row, and send a branded French email via Mailjet (the SMTP
 * already configured on the hub).
 *
 * Usage:
 *   node send_gift_campaign.js \
 *     --gifts ./giftbit-gifts.csv \
 *     --contacts ./giftbit-contacts-A-first-email.csv \
 *     --template ./templates/gift-email-fr.html \
 *     --subject "Votre cadeau Gigafibre" \
 *     --amount "50 $" \
 *     --expiry "31 décembre 2026" \
 *     --commitment-months 3 \
 *     --from "Gigafibre <noreply@gigafibre.ca>" \
 *     --smtp-host in-v3.mailjet.com --smtp-port 587 \
 *     --smtp-user $SMTP_USER --smtp-pass $SMTP_PASS \
 *     --throttle-ms 600 \
 *     --dry-run
 *
 * Remove --dry-run to actually send. With --dry-run the script writes
 * the rendered HTML for each recipient to ./preview/ for visual review.
 *
 * Output: ./results-YYYYMMDD-HHMM.csv with columns
 *   firstname,lastname,email,gift_url,status,error,timestamp
 *
 * Match strategy is row-order by default (line N of gifts pairs with line
 * N of contacts). Override with --match-by email if your gifts CSV has
 * an `email` column to use as the join key (more robust against ordering
 * mistakes, but requires Giftbit to include emails in their export).
 */

const fs       = require('fs')
const path     = require('path')
const readline = require('readline')

// Light deps — kept zero so this script runs anywhere with just node.
// (nodemailer is the only required external; install at use time.)

function parseArgs (argv) {
  const out = {}
  for (let i = 2; i < argv.length; i++) {
    const a = argv[i]
    if (a.startsWith('--')) {
      const k = a.slice(2)
      const next = argv[i + 1]
      if (!next || next.startsWith('--')) { out[k] = true } // boolean flag
      else { out[k] = next; i++ }
    }
  }
  return out
}

const args = parseArgs(process.argv)

const REQUIRED = ['gifts', 'contacts', 'template', 'subject', 'from']
for (const k of REQUIRED) {
  if (!args[k]) {
    console.error(`Missing required --${k}`)
    console.error('See header comment for usage.')
    process.exit(1)
  }
}

const DRY_RUN     = !!args['dry-run']
const MATCH_BY    = args['match-by'] || 'row'   // 'row' | 'email'
const THROTTLE_MS = parseInt(args['throttle-ms'] || '600', 10)
const AMOUNT      = args.amount  || '50 $'
const EXPIRY      = args.expiry  || ''
const SUBJECT     = args.subject
const FROM        = args.from
// Retention commitment used in the template's "Condition" pill and prorata
// disclaimer. Default 3 months. Override with --commitment-months 6 etc.
const COMMITMENT_MONTHS = args['commitment-months'] || '3'

// ── CSV parsing ────────────────────────────────────────────────────────────
// Minimal RFC-4180-ish parser. Handles quoted fields with embedded commas
// and escaped double-quotes. Falls back to comma delimiter; auto-detect for
// tab/pipe if the header doesn't contain a comma. Good enough for the
// Giftbit + contacts CSVs we're feeding it.
function parseCsv (text) {
  const sample = text.split(/\r?\n/, 1)[0] || ''
  const delim = sample.includes(',') ? ',' : (sample.includes('\t') ? '\t' : '|')
  const rows = []
  let row = [], field = '', inQuotes = false
  for (let i = 0; i < text.length; i++) {
    const c = text[i]
    if (inQuotes) {
      if (c === '"' && text[i + 1] === '"') { field += '"'; i++ }
      else if (c === '"') inQuotes = false
      else field += c
    } else {
      if (c === '"') inQuotes = true
      else if (c === delim) { row.push(field); field = '' }
      else if (c === '\n' || c === '\r') {
        if (field !== '' || row.length) { row.push(field); rows.push(row); row = []; field = '' }
        if (c === '\r' && text[i + 1] === '\n') i++
      } else field += c
    }
  }
  if (field !== '' || row.length) { row.push(field); rows.push(row) }
  if (!rows.length) return []
  const header = rows[0].map(h => h.trim())
  return rows.slice(1).filter(r => r.some(c => c !== '')).map(r => {
    const obj = {}
    header.forEach((h, i) => { obj[h] = (r[i] || '').trim() })
    return obj
  })
}

function readCsv (filepath) {
  const text = fs.readFileSync(filepath, 'utf8').replace(/^﻿/, '')
  return parseCsv(text)
}

// Find the "gift URL" column in the Giftbit export. Giftbit's column
// naming varies between exports; check the common spellings.
function findUrlColumn (row) {
  const keys = Object.keys(row)
  const candidates = ['gift_url', 'gift link', 'gift_link', 'url', 'link',
    'redemption_url', 'gift', 'giftbit_link', 'campaign_link']
  for (const c of candidates) {
    const hit = keys.find(k => k.toLowerCase().replace(/\s+/g, '_') === c)
    if (hit) return hit
  }
  // Fallback: first column that looks like a URL in the first row
  for (const k of keys) {
    if ((row[k] || '').match(/^https?:\/\//)) return k
  }
  return null
}

// ── Matching ───────────────────────────────────────────────────────────────
function matchRowOrder (gifts, contacts, urlCol) {
  const matched = []
  const n = Math.min(gifts.length, contacts.length)
  for (let i = 0; i < n; i++) {
    matched.push({ contact: contacts[i], gift_url: gifts[i][urlCol] })
  }
  return {
    matched,
    leftover_gifts:    gifts.length    - n,
    leftover_contacts: contacts.length - n,
  }
}

function matchByEmail (gifts, contacts, urlCol) {
  const emailCol = Object.keys(gifts[0] || {}).find(k => k.toLowerCase().includes('email'))
  if (!emailCol) {
    throw new Error('--match-by email requested but no email column found in gifts CSV')
  }
  const giftByEmail = new Map(gifts.map(g => [(g[emailCol] || '').trim().toLowerCase(), g]))
  const matched = []
  let unmatched = 0
  for (const c of contacts) {
    const g = giftByEmail.get((c.email || '').trim().toLowerCase())
    if (g) matched.push({ contact: c, gift_url: g[urlCol] })
    else { unmatched++ }
  }
  return { matched, leftover_gifts: gifts.length - matched.length, leftover_contacts: unmatched }
}

// ── Template rendering ─────────────────────────────────────────────────────
// Supports two constructs:
//   {{var}}                 simple substitution
//   {{#var}}...{{/var}}     section block: kept if var is truthy, dropped otherwise
//
// The section pass runs FIRST so that variable expansion can fill in the
// kept body. Non-greedy match with [\s\S] handles multi-line blocks (HTML
// templates span many lines between the open/close tags).
function render (tpl, vars) {
  // Pass 1: section blocks (truthy → keep body, falsy → drop everything)
  tpl = tpl.replace(/\{\{\s*#\s*(\w+)\s*\}\}([\s\S]*?)\{\{\s*\/\s*\1\s*\}\}/g,
                    (_, k, body) => (vars[k] ? body : ''))
  // Pass 2: simple variable substitution
  return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => {
    const v = vars[k]
    return v == null ? '' : String(v)
  })
}

// ── Mailer (lazy require so dry-run works without nodemailer installed) ───
function makeTransporter () {
  let nodemailer
  try { nodemailer = require('nodemailer') } catch (e) {
    console.error('ERROR: nodemailer not installed. Run:')
    console.error('  npm install nodemailer')
    process.exit(1)
  }
  return nodemailer.createTransport({
    host: args['smtp-host'] || 'in-v3.mailjet.com',
    port: parseInt(args['smtp-port'] || '587', 10),
    secure: false,
    auth: {
      user: args['smtp-user'] || process.env.SMTP_USER,
      pass: args['smtp-pass'] || process.env.SMTP_PASS,
    },
  })
}

function sleep (ms) { return new Promise(r => setTimeout(r, ms)) }

// ── Main flow ──────────────────────────────────────────────────────────────
async function main () {
  console.log(`\n── Gift campaign send — ${DRY_RUN ? 'DRY RUN' : 'LIVE'} ──`)
  console.log(`  gifts:    ${args.gifts}`)
  console.log(`  contacts: ${args.contacts}`)
  console.log(`  template: ${args.template}`)
  console.log(`  subject:  "${SUBJECT}"`)
  console.log(`  amount:   ${AMOUNT}`)
  console.log(`  match:    ${MATCH_BY}`)

  const gifts    = readCsv(args.gifts)
  const contacts = readCsv(args.contacts)
  const tpl      = fs.readFileSync(args.template, 'utf8')

  console.log(`\n  loaded ${gifts.length} gifts, ${contacts.length} contacts`)

  if (!gifts.length) { console.error('No gifts loaded.'); process.exit(1) }
  if (!contacts.length) { console.error('No contacts loaded.'); process.exit(1) }

  const urlCol = findUrlColumn(gifts[0])
  if (!urlCol) {
    console.error('Cannot detect gift URL column. Available columns:', Object.keys(gifts[0]))
    process.exit(1)
  }
  console.log(`  gift URL column: "${urlCol}"`)

  const { matched, leftover_gifts, leftover_contacts } = MATCH_BY === 'email'
    ? matchByEmail(gifts, contacts, urlCol)
    : matchRowOrder(gifts, contacts, urlCol)

  console.log(`  matched: ${matched.length} (gifts left over: ${leftover_gifts}, contacts skipped: ${leftover_contacts})`)
  if (!matched.length) { console.error('No matches.'); process.exit(1) }

  // Prepare output paths
  const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 16)
  const resultsPath = path.resolve(`results-${stamp}.csv`)
  const previewDir  = path.resolve(`preview-${stamp}`)
  if (DRY_RUN) fs.mkdirSync(previewDir, { recursive: true })

  const transporter = DRY_RUN ? null : makeTransporter()
  if (transporter) {
    await transporter.verify().catch(e => {
      console.error('SMTP verify failed:', e.message); process.exit(1)
    })
    console.log('  SMTP connection OK')
  }

  // Open results CSV (write header first)
  const out = fs.createWriteStream(resultsPath, { encoding: 'utf8' })
  out.write('firstname,lastname,email,gift_url,status,error,timestamp\n')

  let sent = 0, failed = 0
  for (let i = 0; i < matched.length; i++) {
    const { contact, gift_url } = matched[i]
    const vars = {
      firstname:         contact.firstname || 'cher client',
      lastname:          contact.lastname  || '',
      email:             contact.email,
      description:       contact.description || '',
      gift_url,
      amount:            AMOUNT,
      expiry:            EXPIRY,
      commitment_months: COMMITMENT_MONTHS,
    }
    const html = render(tpl, vars)
    const ts = new Date().toISOString()

    try {
      if (DRY_RUN) {
        const safeEmail = contact.email.replace(/[^a-z0-9._@-]/gi, '_')
        fs.writeFileSync(path.join(previewDir, `${String(i + 1).padStart(3, '0')}-${safeEmail}.html`), html)
        out.write(`${csvCell(contact.firstname)},${csvCell(contact.lastname)},${csvCell(contact.email)},${csvCell(gift_url)},dry-run,,${ts}\n`)
        sent++
      } else {
        await transporter.sendMail({
          from: FROM,
          to:   `"${contact.firstname} ${contact.lastname}".trim() <${contact.email}>`.replace('""', '"'),
          subject: SUBJECT,
          html,
        })
        out.write(`${csvCell(contact.firstname)},${csvCell(contact.lastname)},${csvCell(contact.email)},${csvCell(gift_url)},sent,,${ts}\n`)
        sent++
        await sleep(THROTTLE_MS)
      }
      if ((i + 1) % 25 === 0) process.stdout.write(`  ${i + 1}/${matched.length} done\n`)
    } catch (e) {
      out.write(`${csvCell(contact.firstname)},${csvCell(contact.lastname)},${csvCell(contact.email)},${csvCell(gift_url)},failed,${csvCell(e.message)},${ts}\n`)
      failed++
      console.error(`  ✗ ${contact.email}: ${e.message}`)
    }
  }

  out.end()
  console.log(`\n  ✓ ${sent} ${DRY_RUN ? 'previewed' : 'sent'}, ${failed} failed`)
  console.log(`  → results: ${resultsPath}`)
  if (DRY_RUN) console.log(`  → previews: ${previewDir}/`)
}

function csvCell (s) {
  s = String(s == null ? '' : s)
  if (s.includes(',') || s.includes('"') || s.includes('\n')) {
    return '"' + s.replace(/"/g, '""') + '"'
  }
  return s
}

main().catch(e => { console.error('Fatal:', e); process.exit(1) })
