// YJ Electric — Booking & Quote Flow v2: confirm step, shell, page
// Load after schedule-shared.jsx, schedule-v2-shared.jsx, schedule-v2-steps.jsx.

// ── CALENDAR INVITE (.ics) ─────────────────────────────────
function v2ToICSDate(d) {
  return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
}
function v2IcsEscape(str) {
  return String(str || '').replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
}
function v2DownloadInvite(data, refNum, booking) {
  // Prefer Volt's authoritative window instants (booking.start/end are already
  // America/Chicago-anchored UTC ISO strings) so the calendar time is correct on
  // ANY device timezone. setHours() on a local-midnight Date uses the BROWSER's
  // tz, which lands a non-Central booker at the wrong absolute time — only used
  // as a fallback when Volt didn't return the instants.
  let start, end;
  if (booking && booking.start && booking.end) {
    start = new Date(booking.start);
    end = new Date(booking.end);
  } else {
    if (!data.date || data.slotStart == null) return;
    start = new Date(data.date); start.setHours(data.slotStart, 0, 0, 0);
    end = new Date(data.date); end.setHours(data.slotEnd, 0, 0, 0);
  }
  const summary = v2SelectedServices(data).map(s => s.label).join(', ') || 'Electrical service';
  const address = [data.address, [data.city, data.state].filter(Boolean).join(', '), data.zip].filter(Boolean).join(', ');
  const lines = [
    'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//YJ Electric Group//Schedule//EN', 'CALSCALE:GREGORIAN',
    'BEGIN:VEVENT',
    `UID:${refNum || Date.now()}@yjelectricgroup.com`,
    `DTSTAMP:${v2ToICSDate(new Date())}`,
    `DTSTART:${v2ToICSDate(start)}`,
    `DTEND:${v2ToICSDate(end)}`,
    `SUMMARY:${v2IcsEscape(`YJ Electric — ${summary}`)}`,
    `DESCRIPTION:${v2IcsEscape(`Work Order ${refNum || ''}. Crew will arrive within your scheduled window.`)}`,
    address ? `LOCATION:${v2IcsEscape(address)}` : null,
    'END:VEVENT', 'END:VCALENDAR',
  ].filter(Boolean);
  const blob = new Blob([lines.join('\r\n')], { type: 'text/calendar;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = `YJ-Electric-${refNum || 'appointment'}.ics`;
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

// ── REVIEW GRID ────────────────────────────────────────────
function V2Field({ k, v }) {
  return (
    <div>
      <div style={{ fontFamily: 'Barlow Condensed', fontSize: 10, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: SG.gray, marginBottom: 2 }}>{k}</div>
      <div style={{ fontFamily: 'Barlow Condensed', fontSize: 15, fontWeight: 700, color: SG.navy, lineHeight: 1.35 }}>{v || '—'}</div>
    </div>
  );
}

function V2ReviewGrid({ data, instant = true }) {
  const { work, isProject, selected, totalHours, bookable } = v2BookingInfo(data);
  const customer = V2_CUSTOMERS.find(c => c.key === data.customerType);
  const prop = V2_PROPS.find(p => p.key === data.propertyType);
  const scope = isProject
    ? ((data.projectDetails || '').trim().length > 90 ? data.projectDetails.trim().slice(0, 87) + '…' : (data.projectDetails || '').trim())
    : selected.map(s => s.label).join(' · ');
  const booked = data.path === 'book' && data.date && data.slot;
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px 18px' }}>
      <V2Field k="Requested By" v={customer?.label} />
      <V2Field k="Property" v={prop?.label} />
      <V2Field k="Work Type" v={work?.label} />
      <div style={{ gridColumn: '1 / -1' }}>
        <V2Field k={isProject ? 'Project Scope' : 'Services'} v={scope} />
      </div>
      {bookable && <V2Field k="Est. Time On Site" v={`~${totalHours} hrs`} />}
      {/* "Scheduled" only when the slot books instantly; a confirm-mode pick is a
          "Requested" time until a dispatcher confirms it. */}
      <V2Field k={booked ? (instant ? 'Scheduled' : 'Requested') : 'Next Step'} v={booked
        ? `${fmtDay(data.date, 'short')} · ${data.slot}`
        : 'Written quote within 24 hours'} />
      <div style={{ gridColumn: '1 / -1' }}>
        <V2Field k="Location" v={[data.address, [data.city, data.state].filter(Boolean).join(', '), data.zip].filter(Boolean).join(' · ')} />
      </div>
      <V2Field k="Contact" v={`${data.firstName} ${data.lastName}`.trim()} />
      <V2Field k="Phone" v={data.phone} />
      <V2Field k="Email" v={data.email || '—'} />
      <V2Field k="Files" v={data.files.length ? `${data.files.length} attached` : 'None'} />
    </div>
  );
}

// ── STEP N — CONFIRM ───────────────────────────────────────
function V2StepConfirm({ data, setData, refNum, submitted, outcome, booking, submitError, submitting, captchaToken, setCaptchaToken, turnstileNonce, turnstileFailed, onTurnstileUnavailable, photoNote, quoteOnly, instant, afterHours, slotConflict, onPickNewTime, stepNum, goToStep }) {
  const intentBooked = data.path === 'book' && !quoteOnly;
  // Pre-submit reflects the client's intent; post-submit keys on the SERVER
  // outcome ('booked' | 'requested' | 'quote') — a 'book' attempt can land as a
  // request (confirm-mode / same-day / edge ZIP) or a quote (far ZIP).
  const booked = submitted ? outcome === 'booked' : intentBooked;
  const isQuote = submitted && outcome === 'quote';
  // Pre-submit, a booked-path attempt is only truly "locked in" when the feed
  // says the slot books instantly (auto mode, covered ZIP). In confirm mode it
  // lands as a request — so word the review + CTA honestly ("request your time").
  const instantBook = intentBooked && !!instant;

  if (!submitted) {
    return (
      <div style={{ maxWidth: 640, margin: '0 auto' }}>
        <V2StepHeader num={stepNum || '05'} eyebrow="Confirm" title={booked ? (instantBook ? 'Review & lock it in' : 'Review & request your time') : 'Review your quote request'} />
        <div style={{ background: '#fff', border: `1.5px solid ${SG.grayLight}`, borderRadius: 4, padding: 20 }}>
          <V2ReviewGrid data={data} instant={instantBook} />
        </div>
        {quoteOnly && <V2QuoteInfoPanel data={data} goToStep={goToStep} />}
        <div style={{ marginTop: 14, padding: '14px 18px', background: '#fff', border: `1.5px solid ${SG.grayLight}`, borderRadius: 4 }}>
          <label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', cursor: 'pointer' }}>
            <input type="checkbox" checked={data.textOptIn}
              onChange={e => setData(d => ({ ...d, textOptIn: e.target.checked }))}
              style={{ width: 20, height: 20, marginTop: 1, accentColor: SG.gold, cursor: 'pointer' }} />
            <div style={{ fontFamily: 'Barlow Condensed' }}>
              <div style={{ fontWeight: 700, fontSize: 14, color: SG.navy, letterSpacing: '0.02em' }}>Text me my appointment updates</div>
              <div style={{ fontSize: 12, color: SG.gray, marginTop: 2 }}>{booked ? 'Confirmation, arrival window, and job status by text.' : 'Confirmation and your quote by text the moment it\u2019s ready.'}</div>
            </div>
          </label>
          {/* TCPA/CTIA point-of-consent disclosure \u2014 outside the label so tapping it doesn't toggle. */}
          <div style={{ fontSize: 11, color: SG.gray, marginTop: 8, lineHeight: 1.5, fontFamily: 'Barlow Condensed', paddingLeft: 30 }}>
            By checking this box you agree to receive automated service text messages from YJ Electric Group at the number provided
            (booking confirmation, arrival window, and job/quote status). Msg frequency varies; msg &amp; data rates may apply.
            Reply STOP to opt out, HELP for help. Consent is not a condition of service.
          </div>
        </div>
        {/* Marketing consent — SEPARATE from the transactional textOptIn above,
            UNCHECKED by default (documented affirmative opt-in for CAN-SPAM/TCPA). */}
        <div style={{ marginTop: 14, padding: '14px 18px', background: '#fff', border: `1.5px solid ${SG.grayLight}`, borderRadius: 4 }}>
          <label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', cursor: 'pointer' }}>
            <input type="checkbox" checked={data.marketingConsent}
              onChange={e => setData(d => ({ ...d, marketingConsent: e.target.checked }))}
              style={{ width: 20, height: 20, marginTop: 1, accentColor: SG.gold, cursor: 'pointer' }} />
            <div style={{ fontFamily: 'Barlow Condensed', fontSize: 13, color: SG.gray, lineHeight: 1.5 }}>
              Send me occasional maintenance reminders, seasonal tips, and promotions by email or automated text.
              Msg frequency varies; msg &amp; data rates may apply. Consent isn’t a condition of purchase.
              Reply STOP to opt out, HELP for help.
            </div>
          </label>
        </div>
        <VoltTurnstile key={turnstileNonce} onToken={setCaptchaToken} onUnavailable={onTurnstileUnavailable} />
        {turnstileFailed && (
          <div style={{ marginTop: 12, fontFamily: 'Barlow Condensed', fontSize: 12, color: SG.gray, lineHeight: 1.5 }}>
            Couldn’t load the verification widget (an ad-blocker may be interfering). You can still submit below — we’ll follow up by phone or text to confirm — or call us at 214-888-8799.
          </div>
        )}
        {submitError && (
          <div style={{ marginTop: 14, padding: '12px 16px', background: '#fdecec', border: '1.5px solid #e4b7b7', borderRadius: 4, fontFamily: 'Barlow Condensed', fontSize: 14, fontWeight: 600, color: '#8a2b2b' }}>
            {submitError}
            {slotConflict && onPickNewTime && (
              <button onClick={onPickNewTime} style={{
                display: 'block', marginTop: 10, background: SG.navy, color: '#fff', border: 'none',
                borderRadius: 4, padding: '10px 16px', cursor: 'pointer', fontFamily: 'Barlow Condensed',
                fontWeight: 800, fontSize: 13, letterSpacing: '0.06em', textTransform: 'uppercase',
              }}>Pick a new time →</button>
            )}
          </div>
        )}
        {/* Trust + reassurance right at the commit point (shows on mobile too, where
            the footer badge is hidden). */}
        <div style={{ marginTop: 14, display: 'flex', alignItems: 'center', gap: 8, fontFamily: 'Barlow Condensed', fontSize: 12, fontWeight: 600, color: SG.gray, letterSpacing: '0.02em' }}>
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ flexShrink: 0 }}><path d="M7 1L1.5 4v3c0 3 2.5 5.5 5.5 6 3-0.5 5.5-3 5.5-6V4z" stroke={SG.gold} strokeWidth="1.4"/><path d="M5 7l1.5 1.5L9 6" stroke={SG.gold} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
          Licensed &amp; Insured · TECL 41531 · No payment due today.
        </div>
        <div style={{ marginTop: 12, fontFamily: 'Barlow Condensed', fontSize: 14, color: SG.gray }}>
          Double-check everything above, then hit {booked ? (instantBook ? 'Confirm Booking' : 'Request This Time') : 'Submit Quote Request'} below.
        </div>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: 640, margin: '0 auto', paddingTop: 8 }}>
      <div style={{ marginBottom: 22 }}>
        <div style={{ fontFamily: 'Barlow Condensed', fontWeight: 700, fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: SG.gold, marginBottom: 6 }}>
          {booked ? "✓ You're Booked" : '✓ Request Received'}
        </div>
        <h1 style={{ fontFamily: 'Barlow Condensed', fontWeight: 900, fontSize: 'clamp(32px, 8vw, 44px)', textTransform: 'uppercase', color: SG.navy, margin: 0, letterSpacing: '-0.02em', lineHeight: 0.95 }}>
          {booked
            ? <React.Fragment>You're on<br/>the <span style={{ color: SG.gold }}>schedule.</span></React.Fragment>
            : isQuote
              ? <React.Fragment>Your quote is<br/>in <span style={{ color: SG.gold }}>the works.</span></React.Fragment>
              : <React.Fragment>Request<br/><span style={{ color: SG.gold }}>received.</span></React.Fragment>}
        </h1>
      </div>

      <div style={{ background: '#fff', border: `1.5px solid ${SG.navy}`, borderRadius: 4, boxShadow: '0 8px 24px rgba(0,0,0,0.1)', overflow: 'hidden' }}>
        {refNum && (
          <div style={{ background: SG.navy, color: '#fff', padding: '14px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <div style={{ fontFamily: 'Barlow Condensed', fontWeight: 800, fontSize: 11, letterSpacing: '0.18em', color: SG.gold, textTransform: 'uppercase' }}>{booked ? 'Work Order' : isQuote ? 'Quote Request' : 'Request'}</div>
            <div style={{ fontFamily: 'Barlow Condensed', fontWeight: 800, fontSize: 15, letterSpacing: '0.08em' }}>{refNum}</div>
          </div>
        )}
        <div style={{ padding: 20 }}>
          <V2ReviewGrid data={data} instant={booked} />
        </div>
      </div>

      {booked && (booking || (data.date && data.slot)) && (
        <button onClick={() => v2DownloadInvite(data, refNum, booking)} style={{
          marginTop: 14, display: 'inline-flex', alignItems: 'center', gap: 8,
          background: 'transparent', color: SG.navy, border: `1.5px solid ${SG.navy}`,
          borderRadius: 4, padding: '12px 18px', cursor: 'pointer',
          fontFamily: 'Barlow Condensed', fontWeight: 800, fontSize: 13,
          letterSpacing: '0.08em', textTransform: 'uppercase', transition: 'background 150ms ease, color 150ms ease',
        }}
          onMouseEnter={e => { e.currentTarget.style.background = SG.navy; e.currentTarget.style.color = '#fff'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = SG.navy; }}>
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ flexShrink: 0 }}>
            <rect x="1.5" y="2.5" width="11" height="10" rx="1" stroke="currentColor" strokeWidth="1.3"/>
            <line x1="1.5" y1="5.5" x2="12.5" y2="5.5" stroke="currentColor" strokeWidth="1.3"/>
            <line x1="4" y1="1" x2="4" y2="4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
            <line x1="10" y1="1" x2="10" y2="4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
          </svg>
          Add to Calendar
        </button>
      )}

      {photoNote && photoNote.attempted > photoNote.uploaded && (
        <div style={{ marginTop: 14, padding: '11px 15px', background: 'rgba(245,166,35,0.1)', border: `1px solid ${SG.gold}`, borderRadius: 4, fontFamily: 'Barlow Condensed', fontSize: 13, fontWeight: 600, color: SG.navy, lineHeight: 1.45 }}>
          {photoNote.uploaded > 0
            ? `${photoNote.uploaded} of ${photoNote.attempted} photos attached — `
            : 'We couldn’t attach your photos — '}
          you can text or email the rest to us and we’ll add them to your job.
        </div>
      )}

      <div style={{ marginTop: 24 }}>
        <div style={v2Eyebrow}>What Happens Next</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
          {(() => {
            // Honest delivery channel — Volt only texts if textOptIn, and only
            // emails if an address was given. Never claim "texted" when neither.
            const viaSms = !!data.textOptIn;
            const viaEmail = !!(data.email && data.email.trim());
            const confirmVia = viaSms
              ? `Confirmation texted to ${data.phone || 'your phone'}`
              : viaEmail
                ? `Confirmation emailed to ${data.email}`
                : 'We’ll reach out to confirm the details';
            const withLink = viaSms || viaEmail ? ' with your appointment portal link.' : '.';
            if (booked) {
              const rows = [['Confirmed', confirmVia + withLink]];
              if (viaSms) {
                rows.push(['Day before', 'Reminder text with your arrival window.']);
                rows.push(['Day of', 'On-the-way text when your technician heads out.']);
              }
              rows.push(['After', 'Invoice within 24 hours. Pay online or by check.']);
              return rows;
            }
            if (isQuote) {
              return [
                [viaSms || viaEmail ? 'Within 1 hour' : 'Next', confirmVia + '.'],
                ['Within 24 hours', 'Written quote — itemized, no surprises.'],
                ['Your move', 'Approve online and pick a time, or call us with questions.'],
              ];
            }
            // After-hours bookings can't be confirmed "within the hour" — Volt's
            // confirm SLA sleeps until morning, so promise the honest timing.
            const confirmWhen = afterHours ? 'First thing tomorrow' : 'Within the hour';
            const confirmCopy = viaSms
              ? `We’ll text ${data.phone || 'your phone'} to confirm your time.`
              : viaEmail ? `We’ll email ${data.email} to confirm your time.` : 'We’ll reach out to confirm your time.';
            return [
              [confirmWhen, confirmCopy],
              ['Once confirmed', 'You’ll get your appointment details, portal link, and reminders.'],
              ['Your move', 'Reply or call us to adjust anything.'],
            ];
          })().map(([when, what], i, arr) => (
            <div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 16, padding: '12px 0', borderBottom: i < arr.length - 1 ? `1px dashed ${SG.grayLight}` : 'none', fontFamily: 'Barlow Condensed' }}>
              <div style={{ width: 120, flexShrink: 0, fontWeight: 700, fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: SG.gold, paddingTop: 2 }}>{when}</div>
              <div style={{ fontSize: 15, fontWeight: 600, color: SG.navy, lineHeight: 1.5 }}>{what}</div>
            </div>
          ))}
        </div>
      </div>

      <div style={{ marginTop: 26, padding: '16px 18px', background: SG.navy, color: '#fff', borderRadius: 4, textAlign: 'center' }}>
        <div style={{ fontFamily: 'Barlow Condensed', fontSize: 11, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: SG.gold, marginBottom: 4 }}>Need to change something?</div>
        <a href="tel:+12148888799" style={{ fontFamily: 'Barlow Condensed', color: '#fff', textDecoration: 'none', fontSize: 22, fontWeight: 800, letterSpacing: '0.04em' }}>214-888-8799</a>
      </div>
    </div>
  );
}

// ── SHELL ──────────────────────────────────────────────────
function v2Btn(variant, disabled) {
  const base = {
    fontFamily: 'Barlow Condensed', fontWeight: 800, fontSize: 14,
    letterSpacing: '0.1em', textTransform: 'uppercase',
    padding: '14px 22px', borderRadius: 4, border: 'none',
    cursor: disabled ? 'not-allowed' : 'pointer', minHeight: 48,
    transition: 'all 150ms ease',
  };
  if (variant === 'primary') return { ...base, background: disabled ? SG.grayLight : SG.gold, color: disabled ? SG.gray : SG.navy, opacity: disabled ? 0.6 : 1 };
  return { ...base, background: 'transparent', color: SG.navy, border: `1.5px solid ${SG.grayLight}` };
}

const V2_STEPS = [
  { key: 'job',      num: '01', label: 'The Job' },
  { key: 'photos',   num: '02', label: 'Photos & Files' },
  { key: 'contact',  num: '03', label: 'Contact' },
  { key: 'schedule', num: '04', label: 'Schedule' },
  { key: 'confirm',  num: '05', label: 'Confirm' },
];

function ScheduleFlowV2({ onClose, quoteOnly = false }) {
  const isMobile = useIsMobileV2();
  const [step, setStep] = React.useState(0);
  const STEPS = React.useMemo(() => {
    const base = quoteOnly ? V2_STEPS.filter(s => s.key !== 'schedule') : V2_STEPS;
    return base.map((s, i) => ({ ...s, num: String(i + 1).padStart(2, '0') }));
  }, [quoteOnly]);
  const [data, setData] = React.useState(() => ({
    customerType: null, propertyType: null, workType: null, services: [],
    projectDetails: '', description: '', files: [],
    address: '', city: '', state: 'TX', zip: '',
    firstName: '', lastName: '', phone: '', email: '', textOptIn: true, contactPreference: null,
    howHeard: null, referredByName: '', marketingConsent: false,
    path: quoteOnly ? 'quote' : null, date: null, slot: null, slotStart: null, slotEnd: null,
    // Mode of the picked slot ('instant' | 'confirm'), captured from the live feed
    // so the pre-submit copy is honest (a 'confirm' slot lands as a request, not a
    // locked booking). null = mock/unknown feed → we use the softer request copy.
    slotMode: null,
    // Idempotency key — one per form session (survives retries), so a timed-out or
    // double-tapped submit can't create a duplicate booking. Regenerated per mount.
    submissionId: (typeof crypto !== 'undefined' && crypto.randomUUID)
      ? crypto.randomUUID()
      : `yjs-${Date.now()}-${Math.random().toString(36).slice(2)}`,
  }));
  const [refNum, setRefNum] = React.useState(null);
  const [submitted, setSubmitted] = React.useState(false);
  const [outcome, setOutcome] = React.useState(null);     // 'booked' | 'requested' | 'quote' (from Volt)
  const [booking, setBooking] = React.useState(null);     // {date, windowLabel, start, end} on a true booking
  const [submitting, setSubmitting] = React.useState(false);
  const [submitError, setSubmitError] = React.useState(null);
  const [captchaToken, setCaptchaToken] = React.useState(null);
  const [turnstileNonce, setTurnstileNonce] = React.useState(0); // bump → fresh challenge after a failed submit
  const [availSlots, setAvailSlots] = React.useState(null);       // live Volt availability; null = mock fallback
  const [availMode, setAvailMode] = React.useState(null);         // org booking mode: 'auto' | 'confirm' | null(mock/unknown)
  const [bookingOff, setBookingOff] = React.useState(false);      // Volt lane intentionally disabled (403)
  const [turnstileFailed, setTurnstileFailed] = React.useState(false); // captcha script couldn't load
  const [photoNote, setPhotoNote] = React.useState(null);         // {uploaded, attempted} when some photos didn't attach
  const [slotConflict, setSlotConflict] = React.useState(false);  // last submit failed with a 409 slot-taken
  const stepperRef = React.useRef(null);
  const stepRefs = React.useRef([]);
  const shellRef = React.useRef(null);

  // Pull Volt's 28-day availability up front (ready by the Schedule step). A 403
  // → the lane is intentionally OFF (booking really is disabled → show a "call
  // us" path, not a fake calendar); null → a TRANSIENT failure (keep the mock
  // grid, submit still re-checks). Also re-run after a 409 to refresh a stale grid.
  const loadAvailability = React.useCallback(() => {
    if (!(window.Volt && window.Volt.fetchAvailability)) return;
    window.Volt.fetchAvailability().then((payload) => {
      if (!payload) { setBookingOff(false); return; }                       // transient → mock
      if (payload.mode === 'off') { setBookingOff(true); setAvailSlots(null); return; }
      setBookingOff(false);
      // Org booking mode drives honest pre-submit copy: 'auto' → covered slots
      // book instantly ("Locked in"); 'confirm' (launch default) → every booking
      // lands as a request a dispatcher confirms ("we'll confirm your time").
      setAvailMode(payload.mode === 'auto' ? 'auto' : 'confirm');
      const days = window.Volt.transformDays(payload);
      if (days && days.length) setAvailSlots(days);
    }).catch(() => {});
  }, []);

  React.useEffect(() => { loadAvailability(); }, [loadAvailability]);

  // Funnel: the customer opened the booking flow (once per mount).
  React.useEffect(() => {
    if (window.Volt && window.Volt.track) window.Volt.track('booking_start', { flow: quoteOnly ? 'quote' : 'book' });
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Draft persistence ────────────────────────────────────────────────────────
  // A refresh, an iOS tab discard on app-switch, or an Android back press used to
  // silently wipe everything typed. Persist the serializable form fields (NOT File
  // objects, NOT the Date-typed slot pick, which is re-picked from the live grid)
  // to sessionStorage so the flow survives. Keyed per quote-vs-book so the two
  // don't clobber each other. Cleared on a successful submit / close.
  const DRAFT_KEY = quoteOnly ? 'yj_booking_draft_quote' : 'yj_booking_draft';
  const DRAFT_FIELDS = ['customerType', 'propertyType', 'workType', 'services', 'projectDetails',
    'description', 'address', 'city', 'state', 'zip', 'firstName', 'lastName', 'phone', 'email',
    'textOptIn', 'contactPreference', 'howHeard', 'referredByName', 'marketingConsent', 'submissionId'];
  const draftRestored = React.useRef(false);
  React.useEffect(() => {
    if (draftRestored.current) return;
    draftRestored.current = true;
    try {
      const raw = window.sessionStorage && sessionStorage.getItem(DRAFT_KEY);
      if (!raw) return;
      const saved = JSON.parse(raw);
      if (saved && saved.data) {
        setData(d => ({ ...d, ...saved.data }));
        if (typeof saved.step === 'number' && saved.step > 0) {
          // The schedule pick (path/date/slot) is intentionally NOT persisted —
          // it's re-picked from the live grid. So a BOOK-flow draft must never
          // resume PAST the Schedule step: landing on Confirm with path=null would
          // silently submit a quote and drop the customer's chosen slot. Cap the
          // restored step at Schedule for the book flow. (Quote-only has no
          // schedule step and its path is fixed, so it can resume to Confirm.)
          const scheduleIdx = STEPS.findIndex(s => s.key === 'schedule');
          const cap = (!quoteOnly && scheduleIdx !== -1) ? scheduleIdx : STEPS.length - 1;
          setStep(Math.min(saved.step, cap));
        }
      }
    } catch (_e) { /* corrupt/blocked storage — start fresh */ }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
  React.useEffect(() => {
    if (submitted) return; // don't re-save a completed booking
    try {
      if (!window.sessionStorage) return;
      const slim = {};
      for (const k of DRAFT_FIELDS) slim[k] = data[k];
      sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ data: slim, step }));
    } catch (_e) { /* quota/blocked — best-effort */ }
  }, [data, step, submitted]); // eslint-disable-line react-hooks/exhaustive-deps
  const clearDraft = () => { try { window.sessionStorage && sessionStorage.removeItem(DRAFT_KEY); } catch (_e) { /* noop */ } };

  const { work, bookable } = v2BookingInfo(data);

  // After-hours (before 7am / after 9pm local): Volt's confirm SLA sleeps until
  // morning, so "within the hour" would be a lie. Used for honest confirm copy.
  const afterHours = (() => { const h = new Date().getHours(); return h >= 21 || h < 7; })();

  // Will a booked-path submit land as an INSTANT booking, or as a request a
  // dispatcher confirms? Precise when we know the picked day's mode from the live
  // feed (same-day is forced to 'confirm' even in auto mode); else fall back to
  // the org-level mode; if we know neither (mock/unknown feed) default to the
  // softer "request" copy so we never over-promise "locked in". Drives the CTA
  // label + confirm-step copy (P0: the site used to say "Locked in / Confirm
  // Booking" even in confirm mode, where every booking becomes a request).
  const bookingIsInstant = data.path === 'book' && !quoteOnly && (
    data.slotMode ? data.slotMode === 'instant'
    : availMode ? availMode === 'auto'
    : false
  );

  const canProceed = () => {
    const key = STEPS[step].key;
    if (key === 'job') {
      if (!data.customerType || !data.propertyType || !data.workType) return false;
      return work?.style === 'project' ? data.projectDetails.trim().length > 0 : data.services.length > 0;
    }
    if (key === 'photos') return true;
    if (key === 'contact') {
      // No hard ZIP block (design §6): an out-of-area ZIP still submits — Volt
      // resolves the service-area tier server-side and soft-captures a far ZIP
      // as a quote. The on-screen checkZip feedback stays, but it can't block.
      return !!data.address.trim() && !!data.city.trim() && data.zip.length >= 5
        && !!data.firstName && data.phone.replace(/\D/g, '').length >= 10;
    }
    if (key === 'schedule') return quoteOnly || data.path === 'quote' || (data.path === 'book' && !!data.date && !!data.slot);
    if (key === 'confirm') {
      // Captcha gate — satisfied by a token, or bypassed if the Turnstile script
      // couldn't load (adblock/outage) so a blocked widget never dead-ends booking.
      const captchaOk = !(window.Volt && window.Volt.TURNSTILE_ENABLED) || !!captchaToken || turnstileFailed;
      // If they intended to book but the slot got cleared (e.g. after a 409),
      // block submit until they re-pick — otherwise a slot-less 'book' silently
      // downgrades to a quote.
      const bookSlotOk = !(data.path === 'book' && !quoteOnly) || (!!data.date && !!data.slot);
      return captchaOk && bookSlotOk && !submitting;
    }
    return true;
  };

  // One-line reason the Continue/Submit button is disabled — a gray dead button
  // with no explanation is a classic mobile drop-off. Null when nothing to hint.
  const disabledHint = () => {
    if (canProceed()) return null;
    const key = STEPS[step].key;
    if (key === 'job') {
      if (!data.customerType || !data.propertyType || !data.workType) return 'Pick who you are, your property, and the type of work to continue.';
      return work?.style === 'project' ? 'Add a short description of your project to continue.' : 'Select at least one service to continue.';
    }
    if (key === 'contact') {
      const missing = [];
      if (!data.address.trim()) missing.push('street address');
      if (!data.city.trim()) missing.push('city');
      if (data.zip.length < 5) missing.push('5-digit ZIP');
      if (!data.firstName) missing.push('first name');
      if (data.phone.replace(/\D/g, '').length < 10) missing.push('10-digit phone');
      return missing.length ? `Add your ${missing.join(', ')} to continue.` : null;
    }
    if (key === 'schedule' && data.path === 'book') return 'Pick a day and time window to continue.';
    if (key === 'confirm' && data.path === 'book' && !quoteOnly && !(data.date && data.slot)) return 'Pick a day and time window before you submit.';
    return null;
  };

  const scrollTop = () => {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        if (shellRef.current) {
          const y = shellRef.current.getBoundingClientRect().top + window.scrollY - 80;
          window.scrollTo({ top: Math.max(0, y), left: 0, behavior: 'auto' });
        } else {
          window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
        }
      });
    });
  };
  const track = (event, params) => { if (window.Volt && window.Volt.track) window.Volt.track(event, params); };
  const goToStep = (i) => { setStep(i); scrollTop(); };
  const next = () => {
    const target = Math.min(step + 1, STEPS.length - 1);
    track('booking_step', { step: STEPS[target] && STEPS[target].key, index: target, flow: quoteOnly ? 'quote' : 'book' });
    goToStep(target);
  };
  const back = () => goToStep(Math.max(step - 1, 0));
  const isLastStep = step === STEPS.length - 1;
  // POST to Volt (with Formspree backstop inside window.Volt.submit). Advances to
  // the confirmation only on success; a real reference + outcome come back from
  // the server. Photos upload best-effort in the background (never block/fail the
  // booking). On failure we stay on the confirm step and show the error, forcing
  // a fresh Turnstile challenge (tokens are single-use).
  const submitRequest = async () => {
    if (submitting) return;
    setSubmitError(null);
    setSlotConflict(false);
    setSubmitting(true);
    track('booking_submit', { flow: quoteOnly ? 'quote' : 'book', path: data.path || 'quote' });
    try {
      // Pass the Turnstile-unavailable signal so a token-less submit routes to the
      // Formspree backstop instead of dead-ending on the server's fail-closed 400.
      const result = await window.Volt.submit(data, captchaToken, { turnstileUnavailable: turnstileFailed });
      // Best-effort photos — awaited so we can flag partial failures, but a photo
      // problem never fails the booking. Note it if some didn't attach.
      if (result.uploadToken && data.files && data.files.length) {
        const up = await window.Volt.uploadPhotos(result.uploadToken, data.files).catch(() => ({ uploaded: 0, attempted: 0 }));
        if (up && up.attempted > up.uploaded) setPhotoNote(up);
      }
      setRefNum(result.reference || null);
      const finalOutcome = result.outcome || (data.path === 'book' && !quoteOnly ? 'requested' : 'quote');
      setOutcome(finalOutcome);
      setBooking(result.booking || null);
      // A book attempt the SERVER downgraded to a quote (far ZIP) must not keep
      // showing "Requested: <day · slot>" next to "your quote is in the works" —
      // drop the now-moot slot so the review grid reads the quote next-step.
      if (finalOutcome === 'quote') setData(d => ({ ...d, date: null, slot: null, slotStart: null, slotEnd: null }));
      track('booking_success', { outcome: finalOutcome, backstop: !!result.backstop, flow: quoteOnly ? 'quote' : 'book' });
      setSubmitted(true);
      clearDraft(); // booking landed — don't restore it on the next visit
      scrollTop();
    } catch (err) {
      // Slot just taken (409): the grid is stale. Refresh it, clear the picked
      // slot, and prompt a re-pick — never re-POST the same window (that loops the
      // 409 and burns the customer's retries against the intake rate limiter).
      if (err && err.slotConflict) {
        track('booking_slot_conflict', { flow: quoteOnly ? 'quote' : 'book' });
        loadAvailability();
        setData(d => ({ ...d, slot: null, slotStart: null, slotEnd: null }));
        setSlotConflict(true);
        setSubmitError('That time was just taken — pick another open slot.');
        setCaptchaToken(null);
        setTurnstileNonce((n) => n + 1);
        scrollTop();
        return;
      }
      setSubmitError((err && err.message) || 'Something went wrong. Please try again, or call us at 214-888-8799.');
      setCaptchaToken(null);
      setTurnstileNonce((n) => n + 1);
      scrollTop();
    } finally {
      setSubmitting(false);
    }
  };

  // Keep the active step visible in the horizontally-scrolling stepper
  React.useEffect(() => {
    const container = stepperRef.current;
    const el = stepRefs.current[step];
    if (!container || !el) return;
    const target = el.offsetLeft - (container.clientWidth - el.offsetWidth) / 2;
    const max = container.scrollWidth - container.clientWidth;
    container.scrollTo({ left: Math.max(0, Math.min(target, max)), behavior: 'smooth' });
  }, [step]);

  const key = STEPS[step].key;

  return (
    <div ref={shellRef} style={{
      width: '100%', maxWidth: 960,
      background: SG.cream,
      borderRadius: isMobile ? 0 : 4,
      boxShadow: isMobile ? 'none' : '0 12px 40px rgba(0,0,0,0.14), 0 0 0 1px rgba(27,42,74,0.08)',
      display: 'flex', flexDirection: 'column', position: 'relative',
      fontFamily: 'Barlow Condensed, sans-serif',
    }}>
      {/* Blueprint grid overlay */}
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        backgroundImage: 'linear-gradient(rgba(27,42,74,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(27,42,74,0.04) 1px, transparent 1px)',
        backgroundSize: '24px 24px',
      }} />

      {/* Header */}
      <div style={{
        background: SG.navy, color: '#fff', padding: isMobile ? '14px 18px' : '18px 28px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        borderBottom: `3px solid ${SG.gold}`, position: 'relative', zIndex: 1,
        borderRadius: isMobile ? 0 : '4px 4px 0 0',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div>
            <div style={{ fontWeight: 800, fontSize: 11, letterSpacing: '0.18em', color: SG.gold, textTransform: 'uppercase' }}>{quoteOnly ? 'Quote Request' : "Let's Get to Work"}</div>
            <div style={{ fontWeight: 900, fontSize: 20, textTransform: 'uppercase', letterSpacing: '0.02em', lineHeight: 1.1 }}>{quoteOnly ? 'Get a Free Quote' : 'Book Now'}</div>
          </div>
        </div>
        <button onClick={onClose} aria-label="Close" style={{
          background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.2)',
          color: '#fff', width: 40, height: 40, borderRadius: 4, cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
        }}
          onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.16)'}
          onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}>
          <svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 2l10 10M12 2L2 12" stroke="#fff" strokeWidth="1.8" strokeLinecap="round"/></svg>
        </button>
      </div>

      {/* Stepper */}
      <div ref={stepperRef} style={{
        background: '#fff', padding: isMobile ? '14px 18px' : '16px 28px', borderBottom: `1px solid ${SG.grayLight}`,
        display: 'flex', alignItems: 'center', gap: 0, position: 'relative', zIndex: 1,
        overflowX: 'auto', WebkitOverflowScrolling: 'touch',
      }}>
        {STEPS.map((s, i) => {
          const active = i === step;
          const done = i < step;
          return (
            <React.Fragment key={s.num}>
              <div ref={el => stepRefs.current[i] = el} style={{ display: 'flex', alignItems: 'center', gap: 8, opacity: active || done ? 1 : 0.4, flexShrink: 0, whiteSpace: 'nowrap' }}>
                <div style={{ fontWeight: 900, fontSize: 20, color: active ? SG.gold : (done ? SG.navy : SG.gray), lineHeight: 1, letterSpacing: '-0.02em' }}>{s.num}</div>
                <div style={{ fontWeight: 700, fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: active || done ? SG.navy : SG.gray }}>{s.label}</div>
                {done && (
                  <svg width="13" height="13" viewBox="0 0 14 14" style={{ flexShrink: 0 }}>
                    <path d="M2 7l3.5 3.5L12 4" stroke={SG.gold} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                )}
              </div>
              {i < STEPS.length - 1 && (
                <div style={{ flex: 1, minWidth: 14, height: 2, margin: '0 10px', background: i < step ? SG.gold : SG.grayLight, transition: 'background 250ms ease' }} />
              )}
            </React.Fragment>
          );
        })}
      </div>

      {/* Body */}
      <div style={{ flex: 1, padding: isMobile ? '24px 18px 28px' : '32px 40px 32px', position: 'relative', zIndex: 1 }}>
        {key === 'job' && <V2StepJob data={data} setData={setData} quoteOnly={quoteOnly} />}
        {key === 'photos' && <V2StepPhotos data={data} setData={setData} />}
        {key === 'contact' && <V2StepContact data={data} setData={setData} />}
        {key === 'schedule' && <V2StepSchedule data={data} setData={setData} goToStep={goToStep} quoteOnly={quoteOnly} slots={availSlots} bookingOff={bookingOff} mode={availMode} />}
        {key === 'confirm' && <V2StepConfirm data={data} setData={setData} refNum={refNum} submitted={submitted} outcome={outcome} booking={booking} submitError={submitError} submitting={submitting} captchaToken={captchaToken} setCaptchaToken={setCaptchaToken} turnstileNonce={turnstileNonce} turnstileFailed={turnstileFailed} onTurnstileUnavailable={() => setTurnstileFailed(true)} photoNote={photoNote} quoteOnly={quoteOnly} instant={bookingIsInstant} afterHours={afterHours} slotConflict={slotConflict} onPickNewTime={() => goToStep(STEPS.findIndex(s => s.key === 'schedule'))} stepNum={STEPS[STEPS.length - 1].num} goToStep={goToStep} />}
        {/* Why the button below is disabled — no silent dead-end. */}
        {!(isLastStep && submitted) && disabledHint() && (
          <div style={{ marginTop: 18, padding: '10px 14px', background: 'rgba(107,114,128,0.06)', borderRadius: 4, fontFamily: 'Barlow Condensed', fontSize: 13, fontWeight: 600, color: SG.gray, lineHeight: 1.4 }}>
            {disabledHint()}
          </div>
        )}
      </div>

      {/* Sticky footer */}
      {!(isLastStep && submitted) && (
        <div style={{
          background: '#fff', borderTop: `1px solid ${SG.grayLight}`, padding: isMobile ? '12px 18px' : '16px 28px',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
          position: 'sticky', bottom: 0, zIndex: 2,
          borderRadius: isMobile ? 0 : '0 0 4px 4px',
        }}>
          {!isMobile && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, fontWeight: 600, color: SG.gray, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1L1.5 4v3c0 3 2.5 5.5 5.5 6 3-0.5 5.5-3 5.5-6V4z" stroke={SG.gold} strokeWidth="1.4"/><path d="M5 7l1.5 1.5L9 6" stroke={SG.gold} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
              Licensed &amp; Insured · State of Texas
            </div>
          )}
          <div style={{ display: 'flex', gap: 10, flex: isMobile ? 1 : 'initial' }}>
            {step > 0 && (
              <button onClick={back} style={v2Btn('ghost')}>← Back</button>
            )}
            <button onClick={isLastStep ? submitRequest : next} disabled={!canProceed()}
              style={{ ...v2Btn('primary', !canProceed()), flex: isMobile ? 1 : 'initial' }}>
              {isLastStep
                ? (submitting ? 'Sending…' : ((data.path === 'book' && !quoteOnly) ? (bookingIsInstant ? 'Confirm Booking' : 'Request This Time') : 'Submit Quote Request'))
                : 'Continue →'}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// ── SCHEDULE PAGE (full page route) ────────────────────────
function SchedulePage({ setPage }) {
  const isMobile = useIsMobileV2();
  return (
    <section data-screen-label="Book / Quote Portal" className="v2-full-bleed" style={{
      background: SG.cream,
      padding: isMobile ? 0 : '40px 20px 72px',
      display: 'flex', justifyContent: 'center',
    }}>
      <ScheduleFlowV2 onClose={() => setPage('home')} />
    </section>
  );
}

// ── QUOTE PAGE (same flow, quote-only) ───────────────
function QuotePage({ setPage }) {
  const isMobile = useIsMobileV2();
  return (
    <section data-screen-label="Quote Portal" className="v2-full-bleed" style={{
      background: SG.cream,
      padding: isMobile ? 0 : '40px 20px 72px',
      display: 'flex', justifyContent: 'center',
    }}>
      <ScheduleFlowV2 onClose={() => setPage('home')} quoteOnly />
    </section>
  );
}

Object.assign(window, { ScheduleFlowV2, SchedulePage, QuotePage, V2StepConfirm, V2ReviewGrid });
