'use strict'
const cfg = require('./config')
const { log, json, parseBody } = require('./helpers')
const erp = require('./erp')
const types = require('./types')
const ui = require('./ui')
const { signJwt, verifyJwt } = require('./magic-link')

// ── Acceptance Links ─────────────────────────────────────────────────────────
// Two modes:
//   1. Simple JWT acceptance (default) — customer clicks link, sees summary, clicks "J'accepte"
//   2. DocuSeal signature (when contract attached) — redirects to DocuSeal for e-signature

const DOCUSEAL_URL = cfg.DOCUSEAL_URL || ''    // e.g. https://sign.gigafibre.ca
const DOCUSEAL_KEY = cfg.DOCUSEAL_API_KEY || '' // API key from DocuSeal settings

// ── Generate acceptance token ────────────────────────────────────────────────

function generateAcceptanceToken (quotationName, customer, ttlHours = 168) {
  const payload = {
    sub: customer,
    doc: quotationName,
    type: 'acceptance',
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + ttlHours * 3600,
  }
  return signJwt(payload)
}

function generateAcceptanceLink (quotationName, customer, ttlHours = 168) {
  const token = generateAcceptanceToken(quotationName, customer, ttlHours)
  return `${cfg.HUB_PUBLIC_URL || 'https://msg.gigafibre.ca'}/accept/${token}`
}

// ── DocuSeal integration ─────────────────────────────────────────────────────

async function createDocuSealSubmission (opts) {
  if (!DOCUSEAL_URL || !DOCUSEAL_KEY) return null

  const { templateId, email, name, phone, values, completedRedirectUrl } = opts

  try {
    const res = await fetch(`${DOCUSEAL_URL}/api/submissions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Auth-Token': DOCUSEAL_KEY,
      },
      body: JSON.stringify({
        template_id: templateId,
        send_email: !!email,
        send_sms: false,
        completed_redirect_url: completedRedirectUrl || '',
        submitters: [{
          role: 'Première partie',
          email: email || '',
          name: name || '',
          phone: phone || '',
          values: values || {},
        }],
      }),
    })

    if (!res.ok) {
      const err = await res.text()
      log(`DocuSeal submission failed: ${res.status} ${err}`)
      return null
    }

    const data = await res.json()
    // Response is array of submitters
    const submitter = Array.isArray(data) ? data[0] : data
    return {
      submissionId: submitter.submission_id,
      submitterId: submitter.id,
      signUrl: submitter.embed_src || `${DOCUSEAL_URL}/s/${submitter.slug}`,
      status: submitter.status,
    }
  } catch (e) {
    log('DocuSeal error:', e.message)
    return null
  }
}

async function checkDocuSealStatus (submissionId) {
  if (!DOCUSEAL_URL || !DOCUSEAL_KEY) return null

  try {
    const res = await fetch(`${DOCUSEAL_URL}/api/submissions/${submissionId}`, {
      headers: { 'X-Auth-Token': DOCUSEAL_KEY },
    })
    if (!res.ok) return null
    return await res.json()
  } catch { return null }
}

// ── ERPNext helpers ──────────────────────────────────────────────────────────

async function fetchQuotation (name) {
  return erp.get('Quotation', name)
}

async function acceptQuotation (name, acceptanceData) {
  // Add acceptance comment with proof
  await erp.create('Comment', {
    comment_type: 'Info',
    reference_doctype: 'Quotation',
    reference_name: name,
    content: `<b>✅ Devis accepté par le client</b><br>
      <b>Horodatage:</b> ${new Date().toISOString()}<br>
      <b>Méthode:</b> ${acceptanceData.method || 'Lien JWT'}<br>
      <b>Contact:</b> ${acceptanceData.contact || 'N/A'}<br>
      <b>IP:</b> ${acceptanceData.ip || 'N/A'}<br>
      <b>User-Agent:</b> ${acceptanceData.userAgent || 'N/A'}<br>
      ${acceptanceData.docusealUrl ? `<b>Document signé:</b> <a href="${acceptanceData.docusealUrl}">${acceptanceData.docusealUrl}</a>` : ''}`,
  })

  // Update quotation status (best-effort — ignore failure)
  await erp.update('Quotation', name, { accepted_by_client: 1 }).catch(() => {})

  // ── Create deferred dispatch jobs if wizard_steps exist ──
  try {
    const quotation = await fetchQuotation(name)
    if (quotation && quotation.wizard_steps) {
      const steps = JSON.parse(quotation.wizard_steps)
      const ctx = quotation.wizard_context ? JSON.parse(quotation.wizard_context) : {}
      if (Array.isArray(steps) && steps.length) {
        const createdJobs = await createDeferredJobs(steps, ctx, name)
        log(`Created ${createdJobs.length} deferred jobs for ${name} upon acceptance`)

        // Clear wizard_steps so they don't get created again
        await erp.update('Quotation', name, { wizard_steps: '', wizard_context: '' })

        // Also create subscriptions for recurring items on the quotation
        await createDeferredSubscriptions(quotation, ctx)
      }
    }
  } catch (e) {
    log('Deferred job creation on acceptance failed:', e.message)
  }
}

// ── Create dispatch jobs from wizard steps stored on quotation ───────────────

async function createDeferredJobs (steps, ctx, quotationName) {
  const createdJobs = []
  const ref = await require('./dispatch').nextJobRef() // numéro standardisé YYYYMMDD-XXX (remplace DJ-<base36>)

  for (let i = 0; i < steps.length; i++) {
    const step = steps[i]
    const ticketId = `${ref}-s${i + 1}`

    let dependsOn = ''
    if (step.depends_on_step !== null && step.depends_on_step !== undefined) {
      const depJob = createdJobs[step.depends_on_step]
      if (depJob) dependsOn = depJob.name
    }

    const parentJob = i > 0 && createdJobs[0] ? createdJobs[0].name : ''

    // Chain gating: only the root job (no depends_on) is born "open".
    // Dependents wait in "On Hold" — dispatch.unblockDependents() flips them
    // to "open" when their parent completes. This keeps the tech's active
    // list uncluttered (only currently-actionable work shows up).
    const payload = {
      ticket_id: ticketId,
      subject: step.subject || 'Tâche',
      address: ctx.address || '',
      duration_h: step.duration_h || 1,
      priority: step.priority || types.JOB_PRIORITY.MEDIUM,
      status: dependsOn ? types.JOB_STATUS.ON_HOLD : types.JOB_STATUS.OPEN,
      job_type: step.job_type || 'Autre',
      source_issue: ctx.issue || '',
      customer: ctx.customer || '',
      service_location: ctx.service_location || '',
      order_source: ctx.order_source || 'Quotation',
      // assigned_group drives group-based subscription in the Tech PWA
      // (techs see "Tâches du groupe" matching their assigned_group and can
      // self-assign). Was previously only in notes which isn't queryable.
      assigned_group: step.assigned_group || '',
      depends_on: dependsOn,
      parent_job: parentJob,
      step_order: step.step_order || (i + 1),
      on_open_webhook: step.on_open_webhook || '',
      on_close_webhook: step.on_close_webhook || '',
      notes: [
        step.assigned_group ? `Groupe: ${step.assigned_group}` : '',
        `Source: ${quotationName}`,
        'Créé automatiquement après acceptation client',
      ].filter(Boolean).join(' | '),
      // scheduled_date precedence: explicit step date > ctx default > today.
      // We default to today because ERPNext list views + dispatcher boards
      // commonly filter "scheduled_date >= today", and null dates make jobs
      // disappear from the dispatch queue (user-visible symptom: "Aucune
      // job disponible pour dispatch"). Dispatcher can always reschedule.
      scheduled_date: step.scheduled_date || ctx.scheduled_date || new Date().toISOString().slice(0, 10),
    }

    const res = await erp.create('Dispatch Job', payload)
    if (res.ok && res.data) {
      createdJobs.push(res.data)
      log(`  + Job ${res.data.name}: ${step.subject}`)
    } else {
      createdJobs.push({ name: ticketId })
      log(`  ! Job creation failed for: ${step.subject} — ${res.error || 'unknown'}`)
    }
  }

  return createdJobs
}

// ── Create Service Subscriptions for recurring items on accepted quotation ──
//
// Writes the *custom* Service Subscription doctype (not Frappe's built-in
// Subscription). Rows are born with status='En attente' and start_date=today
// as a placeholder — dispatch.activateSubscriptionForJob() flips status to
// 'Actif' and rewrites start_date to the real activation day when the final
// job in the install chain completes.
//
// Linkage to the chain is implicit: (customer, service_location, status='En
// attente') uniquely identifies a pending subscription. On terminal job
// completion we look it up and activate + prorate.

function _guessServiceCategory (item) {
  const name = `${item.item_name || ''} ${item.item_code || ''} ${item.description || ''}`.toLowerCase()
  if (/iptv|tv|t[ée]l[ée]|cha[îi]ne/.test(name)) return 'IPTV'
  if (/voip|t[ée]l[ée]phon|ligne|pbx/.test(name)) return 'VoIP'
  if (/bundle|combo|forfait.*combin|duo|trio/.test(name)) return 'Bundle'
  if (/h[ée]bergement|hosting|cloud|email/.test(name)) return 'Hébergement'
  if (/internet|fibre|fiber|dsl|\bmbps\b|\bgbps\b/.test(name)) return 'Internet'
  return 'Autre'
}

function _extractSpeeds (item) {
  const src = `${item.item_name || ''} ${item.description || ''}`
  const both = src.match(/(\d+)\s*\/\s*(\d+)\s*mbps/i)
  if (both) return { down: parseInt(both[1], 10), up: parseInt(both[2], 10) }
  const down = src.match(/(\d+)\s*mbps/i)
  if (down) return { down: parseInt(down[1], 10), up: null }
  return { down: null, up: null }
}

function _extractDurationMonths (item) {
  const m = (item.description || '').match(/×\s*(\d+)\s*mois/i) ||
            (item.description || '').match(/(\d+)\s*mois/i)
  return m ? parseInt(m[1], 10) : null
}

async function createDeferredSubscriptions (quotation, ctx) {
  const customer = ctx.customer || quotation.customer || quotation.party_name || ''
  const serviceLocation = ctx.service_location || ''
  if (!customer) return []
  if (!serviceLocation) {
    log('  ! createDeferredSubscriptions: no service_location in ctx — skipping')
    return []
  }

  const recurringItems = (quotation.items || []).filter(i =>
    (i.description || '').includes('$/mois')
  )
  if (!recurringItems.length) return []

  const today = new Date().toISOString().split('T')[0]
  const created = []

  for (const item of recurringItems) {
    // Extract monthly rate from description like "Service — 49.99$/mois × 12 mois"
    const rateMatch = (item.description || '').match(/([\d.]+)\$\/mois/)
    const monthlyPrice = rateMatch ? parseFloat(rateMatch[1]) : Number(item.rate) || 0
    const speeds = _extractSpeeds(item)
    const durationMonths = _extractDurationMonths(item)

    const payload = {
      customer,
      service_location: serviceLocation,
      status: 'En attente',            // waits for final job completion
      service_category: _guessServiceCategory(item),
      plan_name: item.item_name || item.item_code || 'Abonnement',
      speed_down: speeds.down || 0,
      speed_up: speeds.up || 0,
      monthly_price: monthlyPrice,
      billing_cycle: 'Mensuel',
      contract_duration: durationMonths || 0,
      // start_date is required by the doctype — use today as a placeholder.
      // Real activation date is rewritten by activateSubscriptionForJob().
      start_date: today,
      notes: `Créé automatiquement via acceptation du devis ${quotation.name || ''}`,
    }

    const res = await erp.create('Service Subscription', payload)
    if (res.ok && res.name) {
      created.push(res.name)
      log(`  + Service Subscription ${res.name} (En attente) — ${item.item_name}`)
    } else {
      log(`  ! Service Subscription creation failed for ${item.item_name}: ${res.error || 'unknown'}`)
    }
  }
  return created
}

// ── PDF generation via ERPNext ────────────────────────────────────────────────

async function getQuotationPdfBuffer (quotationName, printFormat) {
  // PDF comes back as raw bytes, not JSON — go around erpFetch.
  const format = printFormat || 'Standard'
  const url = `${cfg.ERP_URL}/api/method/frappe.utils.print_format.download_pdf?doctype=Quotation&name=${encodeURIComponent(quotationName)}&format=${encodeURIComponent(format)}&no_letterhead=0`
  const pdfRes = await fetch(url, {
    headers: {
      'Authorization': `token ${cfg.ERP_TOKEN}`,
      'X-Frappe-Site-Name': cfg.ERP_SITE,
    },
  })
  if (!pdfRes.ok) return null
  return Buffer.from(await pdfRes.arrayBuffer())
}

async function getDocPdfBuffer (doctype, name, printFormat) {
  const format = printFormat || 'Standard'
  const url = `${cfg.ERP_URL}/api/method/frappe.utils.print_format.download_pdf?doctype=${encodeURIComponent(doctype)}&name=${encodeURIComponent(name)}&format=${encodeURIComponent(format)}&no_letterhead=0`
  const pdfRes = await fetch(url, {
    headers: {
      'Authorization': `token ${cfg.ERP_TOKEN}`,
      'X-Frappe-Site-Name': cfg.ERP_SITE,
    },
  })
  if (!pdfRes.ok) return null
  return Buffer.from(await pdfRes.arrayBuffer())
}

// ── Acceptance page HTML ─────────────────────────────────────────────────────

function renderAcceptancePage (quotation, token, accepted = false) {
  const items = (quotation.items || []).map(i =>
    `<tr>
      <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;">${i.item_name || i.item_code}</td>
      <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;text-align:center;">${i.qty}</td>
      <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;text-align:right;">${Number(i.rate).toFixed(2)} $</td>
      <td style="padding:8px 12px;border-bottom:1px solid #e2e8f0;text-align:right;font-weight:700;">${Number(i.amount || i.qty * i.rate).toFixed(2)} $</td>
    </tr>`
  ).join('')

  const total = Number(quotation.grand_total || quotation.total || 0).toFixed(2)
  const terms = quotation.terms || ''

  return `<!DOCTYPE html>
<html lang="fr"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Devis ${quotation.name} — Gigafibre</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;background:#f1f5f9;color:#1e293b;min-height:100vh;display:flex;justify-content:center;padding:1.5rem}
.card{background:white;border-radius:16px;max-width:640px;width:100%;box-shadow:0 4px 24px rgba(0,0,0,0.08);overflow:hidden}
.header{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;padding:1.5rem 2rem}
.header h1{font-size:1.3rem;font-weight:700}
.header p{font-size:0.82rem;opacity:0.85;margin-top:0.25rem}
.body{padding:1.5rem 2rem}
table{width:100%;border-collapse:collapse;margin:1rem 0;font-size:0.85rem}
th{background:#f8fafc;padding:8px 12px;text-align:left;font-size:0.72rem;text-transform:uppercase;letter-spacing:0.05em;color:#64748b;border-bottom:2px solid #e2e8f0}
.total-row{display:flex;justify-content:space-between;align-items:center;background:#f0fdf4;padding:0.75rem 1rem;border-radius:10px;margin:1rem 0}
.total-row .amount{font-size:1.3rem;font-weight:700;color:#16a34a}
.terms{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:1rem;font-size:0.78rem;color:#475569;margin:1rem 0;max-height:200px;overflow-y:auto;white-space:pre-wrap}
.accept-form{text-align:center;padding:1rem 0}
.cb-label{display:flex;align-items:center;justify-content:center;gap:0.5rem;font-size:0.85rem;color:#475569;margin-bottom:1rem;cursor:pointer}
.cb-label input{accent-color:#6366f1;width:18px;height:18px}
.btn-accept{background:#22c55e;color:white;border:none;border-radius:12px;padding:0.85rem 2.5rem;font-size:1rem;font-weight:700;font-family:inherit;cursor:pointer;transition:background 0.15s}
.btn-accept:hover{background:#16a34a}
.btn-accept:disabled{background:#86efac;cursor:not-allowed}
.btn-pdf{display:inline-flex;align-items:center;gap:0.4rem;background:#6366f1;color:white;border:none;border-radius:10px;padding:0.6rem 1.5rem;font-size:0.85rem;font-weight:600;font-family:inherit;cursor:pointer;text-decoration:none;transition:background 0.15s;margin-bottom:1rem}
.btn-pdf:hover{background:#4f46e5}
.accepted-banner{background:#dcfce7;color:#166534;padding:1rem;text-align:center;font-weight:700;font-size:0.92rem;border-radius:10px;margin:1rem 0}
.footer{text-align:center;padding:1rem;font-size:0.72rem;color:#94a3b8}
</style></head><body>
<div class="card">
  <div class="header">
    <h1>Devis ${quotation.name}</h1>
    <p>${quotation.customer_name || quotation.party_name || ''} — ${new Date().toLocaleDateString('fr-CA')}</p>
  </div>
  <div class="body">
    <table>
      <thead><tr><th>Description</th><th style="text-align:center">Qté</th><th style="text-align:right">Prix</th><th style="text-align:right">Total</th></tr></thead>
      <tbody>${items}</tbody>
    </table>
    <div class="total-row">
      <span style="font-weight:600;color:#475569">Total</span>
      <span class="amount">${total} $</span>
    </div>
    <div style="text-align:center">
      <a class="btn-pdf" href="/accept/pdf/${token}" target="_blank">&#128196; Télécharger le PDF</a>
    </div>
    ${terms ? `<div class="terms"><strong>Conditions :</strong>\n${terms}</div>` : ''}
    ${accepted
      ? '<div class="accepted-banner">✅ Ce devis a été accepté. Merci!</div>'
      : `<div class="accept-form">
          <label class="cb-label"><input type="checkbox" id="cb-accept" onchange="document.getElementById('btn-accept').disabled=!this.checked"> J'ai lu et j'accepte les conditions</label>
          <button class="btn-accept" id="btn-accept" disabled onclick="doAccept()">Accepter le devis</button>
          <div id="accept-status" style="margin-top:0.75rem;font-size:0.82rem;color:#64748b"></div>
        </div>
        <script>
        async function doAccept(){
          const btn=document.getElementById('btn-accept');
          const status=document.getElementById('accept-status');
          btn.disabled=true;btn.textContent='Traitement...';
          try{
            const r=await fetch('/accept/confirm',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:'${token}'})});
            const d=await r.json();
            if(d.ok){status.innerHTML='<span style="color:#16a34a;font-weight:700">✅ Devis accepté! Vous recevrez une confirmation.</span>';btn.style.display='none';document.getElementById('cb-accept').parentElement.style.display='none';}
            else{status.innerHTML='<span style="color:#dc2626">Erreur: '+(d.error||'Lien invalide')+'</span>';btn.disabled=false;btn.textContent='Accepter le devis';}
          }catch(e){status.innerHTML='<span style="color:#dc2626">Erreur réseau</span>';btn.disabled=false;btn.textContent='Accepter le devis';}
        }
        <\/script>`
    }
  </div>
  <div class="footer">Gigafibre — Targo Télécommunications</div>
</div>
</body></html>`
}

// ── HTTP handler ─────────────────────────────────────────────────────────────

async function handle (req, res, method, path) {
  // GET /accept/pdf/:token — Download PDF of the quotation
  if (path.startsWith('/accept/pdf/') && method === 'GET') {
    const token = path.replace('/accept/pdf/', '')
    const payload = verifyJwt(token)
    if (!payload || payload.type !== 'acceptance') {
      return json(res, 401, { error: 'Token invalide ou expiré' })
    }
    try {
      const buf = await getDocPdfBuffer('Quotation', payload.doc)
      if (!buf) return json(res, 404, { error: 'PDF non disponible' })
      res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-Disposition': `inline; filename="${payload.doc}.pdf"`,
        'Content-Length': buf.length,
      })
      return res.end(buf)
    } catch (e) {
      log('PDF download error:', e.message)
      return json(res, 500, { error: 'Erreur lors de la génération du PDF' })
    }
  }

  // GET /accept/doc-pdf/:doctype/:name — Download PDF of any document (authenticated)
  if (path.startsWith('/accept/doc-pdf/') && method === 'GET') {
    // Route « PDF de N'IMPORTE QUEL doc » → staff-only. Sous le préfixe PUBLIC
    // /accept, le gate ne la couvre pas : on exige le token hub ici (Ops passe par
    // le proxy /ops/hub qui l'injecte), sinon IDOR anonyme.
    const tok = (req.headers.authorization || '').replace(/^Bearer\s+/i, '')
    if (!process.env.HUB_SERVICE_TOKEN || tok !== process.env.HUB_SERVICE_TOKEN) {
      return json(res, 401, { error: 'Unauthorized' })
    }
    const parts = path.replace('/accept/doc-pdf/', '').split('/')
    const doctype = decodeURIComponent(parts[0] || '')
    const name = decodeURIComponent(parts[1] || '')
    const url = new URL(req.url, `http://localhost:${cfg.PORT}`)
    const format = url.searchParams.get('format') || 'Standard'
    if (!doctype || !name) return json(res, 400, { error: 'doctype and name required' })
    try {
      const buf = await getDocPdfBuffer(doctype, name, format)
      if (!buf) return json(res, 404, { error: 'PDF non disponible' })
      res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-Disposition': `inline; filename="${name}.pdf"`,
        'Content-Length': buf.length,
      })
      return res.end(buf)
    } catch (e) {
      log('Doc PDF error:', e.message)
      return json(res, 500, { error: 'Erreur PDF' })
    }
  }

  // GET /accept/:token — Show acceptance page
  if (path.startsWith('/accept/') && method === 'GET' && !path.includes('/confirm')) {
    const token = path.replace('/accept/', '')
    const payload = verifyJwt(token)

    if (!payload || payload.type !== 'acceptance') {
      res.writeHead(200, ui.htmlHeaders())
      return res.end(ui.pageExpired())
    }

    try {
      const quotation = await fetchQuotation(payload.doc)
      if (!quotation) {
        return json(res, 404, { error: 'Quotation not found' })
      }
      const html = renderAcceptancePage(quotation, token)
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' })
      return res.end(html)
    } catch (e) {
      log('Acceptance page error:', e.message)
      return json(res, 500, { error: 'Server error' })
    }
  }

  // POST /accept/confirm — Record acceptance
  if (path === '/accept/confirm' && method === 'POST') {
    const body = await parseBody(req)
    const payload = verifyJwt(body.token)
    if (!payload || payload.type !== 'acceptance') {
      return json(res, 401, { error: 'Token invalide ou expiré' })
    }

    const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || ''
    const ua = req.headers['user-agent'] || ''

    try {
      await acceptQuotation(payload.doc, {
        method: 'Lien JWT',
        contact: payload.sub,
        ip,
        userAgent: ua,
      })
      log(`Quotation accepted: ${payload.doc} by ${payload.sub} from ${ip}`)

      // Fire flow trigger (on_quotation_accepted). Non-blocking.
      try {
        require('./flow-runtime').dispatchEvent('on_quotation_accepted', {
          doctype: 'Quotation', docname: payload.doc, customer: payload.sub,
          variables: { method: 'JWT', ip, user_agent: ua },
        }).catch(e => log('flow trigger on_quotation_accepted failed:', e.message))
      } catch (e) { log('flow trigger load error:', e.message) }

      return json(res, 200, { ok: true, quotation: payload.doc, message: 'Devis accepté. Le projet a été lancé.' })
    } catch (e) {
      log('Accept confirm error:', e.message)
      return json(res, 500, { error: 'Failed to record acceptance' })
    }
  }

  // POST /accept/generate — Generate acceptance link (called from ops app / wizard)
  if (path === '/accept/generate' && method === 'POST') {
    const body = await parseBody(req)
    const { quotation, customer, ttl_hours, send_sms, phone, send_email, email, use_docuseal, docuseal_template_id } = body

    if (!quotation || !customer) {
      return json(res, 400, { error: 'quotation and customer required' })
    }

    const result = { ok: true }

    // Mode 1: DocuSeal e-signature
    if (use_docuseal && DOCUSEAL_URL && DOCUSEAL_KEY) {
      const acceptLink = generateAcceptanceLink(quotation, customer, ttl_hours || 168)
      const dsResult = await createDocuSealSubmission({
        templateId: docuseal_template_id || cfg.DOCUSEAL_DEFAULT_TEMPLATE_ID || 1,
        email: email || '',
        name: customer,
        phone: phone || '',
        values: { 'Nom': customer, 'Customer': customer, 'Quotation': quotation },
        completedRedirectUrl: acceptLink + '?signed=1',
      })
      if (dsResult) {
        result.method = 'docuseal'
        result.sign_url = dsResult.signUrl
        result.submission_id = dsResult.submissionId

        // Persist signing URL on the Quotation so the print-format QR code is populated
        const up = await erp.update('Quotation', quotation, { custom_docuseal_signing_url: dsResult.signUrl })
        if (!up.ok) log('Failed to save DocuSeal signing URL to Quotation:', up.error)
      } else {
        // Fallback to JWT if DocuSeal fails
        result.method = 'jwt'
        result.link = acceptLink
      }
    } else {
      // Mode 2: Simple JWT acceptance
      result.method = 'jwt'
      result.link = generateAcceptanceLink(quotation, customer, ttl_hours || 168)
    }

    // Send SMS if requested
    if (send_sms && phone) {
      const linkToSend = result.sign_url || result.link
      const msg = `Gigafibre — Votre devis ${quotation} est prêt pour acceptation.\n\n📋 Voir le devis: ${linkToSend}\n\nCe lien expire dans ${Math.round((ttl_hours || 168) / 24)} jours.`
      try {
        const { sendSmsInternal } = require('./twilio')
        await sendSmsInternal(phone, msg)
        result.sms_sent = true
        log(`Acceptance link sent via SMS to ${phone} for ${quotation}`)
      } catch (e) {
        result.sms_sent = false
        log('SMS send failed:', e.message)
      }
    }

    // Send email if requested — via nodemailer with PDF attachment
    if (send_email && email) {
      try {
        const { sendQuotationEmail } = require('./email')
        const linkToSend = result.sign_url || result.link

        // Fetch PDF to attach
        let pdfBuf = null
        try { pdfBuf = await getDocPdfBuffer('Quotation', quotation) } catch {}

        const sent = await sendQuotationEmail({
          to: email,
          quotationName: quotation,
          acceptLink: linkToSend,
          pdfBuffer: pdfBuf,
        })
        result.email_sent = sent
        if (sent) log(`Acceptance email sent to ${email} for ${quotation}`)
      } catch (e) {
        result.email_sent = false
        log('Email send failed:', e.message)
      }
    }

    return json(res, 200, result)
  }

  // POST /accept/send — Send acceptance link via email or SMS (from success screen)
  if (path === '/accept/send' && method === 'POST') {
    const body = await parseBody(req)
    const { quotation, customer, channel, to } = body
    // channel: 'email' | 'sms'

    if (!quotation || !to || !channel) {
      return json(res, 400, { error: 'quotation, channel and to required' })
    }

    const result = { ok: true }
    const link = generateAcceptanceLink(quotation, customer || '', 168)
    result.link = link

    if (channel === 'sms') {
      const msg = `Gigafibre — Votre devis ${quotation} est prêt.\n\nConsultez-le ici: ${link}\n\nValide 7 jours.`
      try {
        const { sendSmsInternal } = require('./twilio')
        await sendSmsInternal(to, msg)
        result.sent = true
        log(`Acceptance SMS sent to ${to} for ${quotation}`)
      } catch (e) {
        result.sent = false
        result.error = e.message
        log('SMS send failed:', e.message)
      }
    } else if (channel === 'email') {
      try {
        const { sendQuotationEmail } = require('./email')
        let pdfBuf = null
        try { pdfBuf = await getDocPdfBuffer('Quotation', quotation) } catch {}
        const sent = await sendQuotationEmail({
          to,
          quotationName: quotation,
          acceptLink: link,
          pdfBuffer: pdfBuf,
        })
        result.sent = sent
        if (sent) log(`Acceptance email sent to ${to} for ${quotation}`)
      } catch (e) {
        result.sent = false
        result.error = e.message
        log('Email send failed:', e.message)
      }
    }

    return json(res, 200, result)
  }

  // POST /accept/docuseal-webhook — DocuSeal completion webhook
  if (path === '/accept/docuseal-webhook' && method === 'POST') {
    const body = await parseBody(req)
    log('DocuSeal webhook:', body.event_type, body.data?.external_id || '')

    if (body.event_type === 'form.completed') {
      const submitter = body.data
      const quotationName = submitter?.values?.find(v => v.field === 'Quotation')?.value
      const customer = submitter?.values?.find(v => v.field === 'Customer')?.value
      const signedDocUrl = submitter?.documents?.[0]?.url || ''

      if (quotationName) {
        await acceptQuotation(quotationName, {
          method: 'DocuSeal e-signature',
          contact: submitter.email || submitter.phone || customer || '',
          ip: req.headers['x-forwarded-for'] || '',
          userAgent: 'DocuSeal Webhook',
          docusealUrl: signedDocUrl,
        })
        log(`Quotation ${quotationName} accepted via DocuSeal signature`)
      }
    }

    return json(res, 200, { ok: true })
  }

  return json(res, 404, { error: 'Acceptance endpoint not found' })
}

module.exports = {
  handle,
  generateAcceptanceToken,
  generateAcceptanceLink,
  createDocuSealSubmission,
  // Exposed so other modules (contracts.js) can build the same chained
  // Dispatch Job structure without duplicating the On-Hold / depends_on logic.
  createDeferredJobs,
}
