/* global React, Icon, MegaFooter, RentalsSubHero */
//
// Crew Onboarding wizard — /crew/onboard.
//
// Semi-hidden multi-step form for prospective freelance crew to join
// the Valley Films roster. Linked only from the Films footer. Reuses
// the Rentals "Open Account" wizard chrome (oa-grid, oa-intro,
// step-indicator, oa-actions) so the visual language matches across
// our application surfaces.
//
// Step order:
//   0  Welcome
//   1  About you — name, email, phone, primary + optional secondary roles
//   2  Headshot
//   3  Address & emergency contact
//   4  Background — company (optional), website, dietary (optional), notes (optional)
//   5  Review — recap + consent + submit
//
// Submission posts to /api/crew with the headshot inline as base64 —
// one round trip, no orphaned upload state to clean up.

const { useState: useStateCrew, useEffect: useEffectCrew, useRef: useRefCrew, useMemo: useMemoCrew } = React;

// Canonical list — must match the 34 options on the Notion `Position` /
// `Primary Role` / `Secondary Roles` columns. Re-order with care; the
// order shown is the order the dropdowns render.
const CREW_ROLES = [
  'Director', 'Spark', '1st AC', 'Editor', 'Producer', 'Sound Recordist',
  'DOP', 'Camera Operator', 'Filmmaker', 'Sound Designer', 'Composer',
  'Screenwriter', 'Gaffer', 'Photographer', '2nd AC', '1st AD',
  'Camera Trainee', 'Camera PA', 'Colourist', 'Shadow/ Runner/ Assistant',
  'Actor', 'HMUA', 'VFX Artist', 'Stedicam Op', 'SFX Artist',
  'Production Manager', 'Cinema Manager', 'Writer', 'Performer',
  'Drone Op', 'PA', 'Live Soundie', 'Art Dept', '3rd AD',
];

// Match the 7 options on the Notion `Country` select. Order is a-z
// except UK first since most of the roster is UK-based.
const CREW_COUNTRIES = [
  'United Kingdom', 'United States', 'Germany', 'Netherlands',
  'New Zealand', 'Denmark', 'Australia',
];

// Must match the Notion `Dietary Requirements` multi_select options
// exactly (case-sensitive) — values are sent as-is to the column.
// Mirrored in DIETARY_OPTIONS in scripts/migrate-crew-schema.js and the
// DIETARY_LABELS set in lib/validation.js. Re-order with care; the order
// shown is the order the checkboxes render.
const CREW_DIETARY = [
  'N/A', 'Vegetarian', 'Vegan', 'Pescatarian', 'Gluten-free',
  'Dairy-free', 'Nut allergy', 'Halal', 'Kosher', 'Other',
];

// Wizard step labels — used by the step-indicator tooltips and the
// review screen's "Edit" jump targets. Keep keys in sync with the
// `step` integer used in CREW_DEFAULT_DATA / stepValid().
const CREW_STEP_NAMES = {
  1: 'About',
  2: 'Headshot',
  3: 'Address & contact',
  4: 'Background',
  5: 'Review',
};
const CREW_REAL_STEPS = 5;

// Loose URL check — accepts any string with a dot and no spaces, with
// or without scheme. Mirrors the server-side URLISH in lib/validation.js.
const CREW_URLISH = /^(?:https?:\/\/)?[^\s]+\.[^\s]+$/i;
const CREW_EMAIL  = /^\S+@\S+\.\S+$/;

const CREW_DEFAULT_DATA = {
  step: 0,
  about: {
    fullName: '',
    email: '',
    phone: '',
    primaryRole: '',
    secondaryRoles: ['', ''],
    company: '',
    website: '',
    streetAddress: '',
    postcode: '',
    city: '',
    country: 'United Kingdom',
    emergencyContactName: '',
    emergencyContactPhone: '',
    emergencyContactRelation: '',
    dietary: [],
    notes: '',
  },
  consent: { data: false },
};

// Resize a File (image) to fit within `maxSide` px on the long edge,
// preserving aspect ratio. Returns { base64, mime, dataUrl, width,
// height, sizeBytes } where base64 is the raw bytes (no data: prefix)
// and dataUrl is the full data:image/jpeg;base64,... for previews.
function resizeImage(file, maxSide = 1200, quality = 0.85) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const objectUrl = URL.createObjectURL(file);
    img.onload = () => {
      URL.revokeObjectURL(objectUrl);
      const longest = Math.max(img.naturalWidth, img.naturalHeight);
      const scale = longest > maxSide ? maxSide / longest : 1;
      const w = Math.round(img.naturalWidth * scale);
      const h = Math.round(img.naturalHeight * scale);
      const canvas = document.createElement('canvas');
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, w, h);
      canvas.toBlob(
        (blob) => {
          if (!blob) { reject(new Error('Image encoding failed')); return; }
          const reader = new FileReader();
          reader.onloadend = () => {
            const dataUrl = reader.result; // "data:image/jpeg;base64,XXX"
            const base64 = String(dataUrl).split(',')[1] || '';
            resolve({
              base64,
              mime: 'image/jpeg',
              dataUrl,
              width: w,
              height: h,
              sizeBytes: blob.size,
            });
          };
          reader.onerror = () => reject(new Error('Encoding read failed'));
          reader.readAsDataURL(blob);
        },
        'image/jpeg',
        quality
      );
    };
    img.onerror = () => {
      URL.revokeObjectURL(objectUrl);
      reject(new Error('Could not load image'));
    };
    img.src = objectUrl;
  });
}

// Postcode lookup against postcodes.io. Cached per session so a typo +
// correction only fires one network request per unique postcode. Used
// to auto-fill the City field once a postcode resolves.
const crewPostcodeCache = new Map();
async function crewLookupPostcode(raw) {
  const pc = String(raw || '').replace(/\s+/g, '').toUpperCase();
  if (!pc) return { state: 'idle' };
  if (crewPostcodeCache.has(pc)) return crewPostcodeCache.get(pc);
  try {
    const r = await fetch(`https://api.postcodes.io/postcodes/${encodeURIComponent(pc)}`);
    if (r.status === 404) {
      const out = { state: 'invalid', postcode: pc };
      crewPostcodeCache.set(pc, out);
      return out;
    }
    if (!r.ok) return { state: 'network_error', postcode: pc };
    const data = await r.json();
    const res = data?.result;
    if (!res) {
      const out = { state: 'invalid', postcode: pc };
      crewPostcodeCache.set(pc, out);
      return out;
    }
    // post_town is closer to "what people call their city" than
    // admin_district (e.g. "London" not "Ealing"). Fall back to
    // admin_district if post_town is missing.
    const city = res.post_town || res.admin_district || '';
    const out = {
      state: 'valid',
      postcode: res.postcode,
      city,
      area: [res.admin_ward, res.admin_district].filter(Boolean).join(', '),
    };
    crewPostcodeCache.set(pc, out);
    return out;
  } catch (e) {
    return { state: 'network_error', postcode: pc, error: e?.message };
  }
}

function CrewOnboardingPage({ onGoto }) {
  const [data, setData] = useStateCrew(CREW_DEFAULT_DATA);
  const { step, about, consent } = data;

  // Progressive disclosure for secondary roles. Starts at 0 (only
  // Primary visible). Click "+ Add a secondary role" to increment.
  const [secondaryCount, setSecondaryCount] = useStateCrew(0);

  // Headshot state machine: idle (no upload yet), processing (resizing),
  // ready (have base64 + preview), error.
  const [headshot, setHeadshot] = useStateCrew(null);
  const [headshotState, setHeadshotState] = useStateCrew('idle');
  const [headshotError, setHeadshotError] = useStateCrew('');

  // Anti-bot: hidden honeypot + time-to-submit. The startedAt timestamp
  // is recorded when the applicant first leaves the welcome step so the
  // server can flag suspiciously-fast submissions.
  const [honeypot, setHoneypot] = useStateCrew('');
  const [applicationStartedAt, setApplicationStartedAt] = useStateCrew(null);

  const [submitting, setSubmitting] = useStateCrew(false);
  const [submitted, setSubmitted] = useStateCrew(false);
  const [submitError, setSubmitError] = useStateCrew('');

  // Postcode validation state machine: idle, pending, valid, invalid,
  // network_error. See crewLookupPostcode.
  const [postcodeCheck, setPostcodeCheck] = useStateCrew({ state: 'idle' });

  // Highest step the applicant has ever reached. Drives the
  // step-indicator's "jumpable" affordance so they can hop back to
  // sections they've already seen, but can't jump forward past their
  // current progress.
  const [maxStepReached, setMaxStepReached] = useStateCrew(0);
  useEffectCrew(() => {
    if (step > maxStepReached) setMaxStepReached(step);
  }, [step, maxStepReached]);

  const setAbout   = (patch) => setData((d) => ({ ...d, about:   { ...d.about,   ...patch } }));
  const setConsent = (patch) => setData((d) => ({ ...d, consent: { ...d.consent, ...patch } }));

  // Dietary tickbox toggle. Adds or removes a label from about.dietary.
  const toggleDietary = (label) => {
    setAbout({
      dietary: about.dietary.includes(label)
        ? about.dietary.filter((d) => d !== label)
        : [...about.dietary, label],
    });
  };

  // Debounced postcode lookup (UK only). Fires ~450ms after the last
  // keystroke. Auto-fills the City field on a valid result, but does
  // NOT overwrite a non-empty city the applicant already typed — they
  // might prefer "London" over the post_town label or vice-versa.
  useEffectCrew(() => {
    if (about.country !== 'United Kingdom') {
      setPostcodeCheck({ state: 'idle' });
      return;
    }
    const raw = about.postcode.trim();
    if (!raw) {
      setPostcodeCheck({ state: 'idle' });
      return;
    }
    setPostcodeCheck((s) => (s.state === 'pending' ? s : { state: 'pending', postcode: raw }));
    const tid = setTimeout(async () => {
      const out = await crewLookupPostcode(raw);
      setPostcodeCheck(out);
      if (out.state === 'valid' && out.city) {
        setData((d) => (d.about.city ? d : { ...d, about: { ...d.about, city: out.city } }));
      }
    }, 450);
    return () => clearTimeout(tid);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [about.postcode, about.country]);

  // Role-picker helpers. Roles already picked anywhere are filtered out
  // of the OTHER pickers' dropdowns so a role can't be selected twice.
  const pickedRoles = useMemoCrew(() => {
    const list = [about.primaryRole, ...about.secondaryRoles.slice(0, secondaryCount)].filter(Boolean);
    return new Set(list);
  }, [about.primaryRole, about.secondaryRoles, secondaryCount]);

  const setPrimaryRole = (value) => {
    setAbout({ primaryRole: value });
    if (!value) setSecondaryCount(0);
  };
  const setSecondaryRole = (idx, value) => {
    const next = [...about.secondaryRoles];
    next[idx] = value;
    setAbout({ secondaryRoles: next });
  };
  const addSecondarySlot = () => setSecondaryCount((n) => Math.min(2, n + 1));
  const removeSecondarySlot = (idx) => {
    const next = [...about.secondaryRoles];
    next[idx] = '';
    setAbout({ secondaryRoles: next });
    setSecondaryCount((n) => Math.max(0, n - 1));
  };

  // Headshot intake — handles File from drop, click-to-choose, or paste.
  const onHeadshotFile = async (file) => {
    if (!file) return;
    setHeadshotError('');
    if (!/^image\/(jpeg|png|webp|heic|heif)$/i.test(file.type)) {
      setHeadshotError('Please choose a JPEG, PNG or WebP image.');
      return;
    }
    setHeadshotState('processing');
    try {
      const out = await resizeImage(file, 1200, 0.85);
      setHeadshot({ ...out, originalName: file.name || 'headshot' });
      setHeadshotState('ready');
    } catch (e) {
      setHeadshotError(e?.message || 'Could not process that image.');
      setHeadshotState('error');
    }
  };
  const clearHeadshot = () => {
    setHeadshot(null);
    setHeadshotState('idle');
    setHeadshotError('');
  };

  // --- Per-step validation ----------------------------------------------
  // Each step's Continue button is gated on its own slice of required
  // fields. The final Submit button is additionally gated on the
  // consent checkbox via canSubmit.
  const stepValid = (n) => {
    if (n === 0) return true;
    if (n === 1) {
      return !!(
        about.fullName.trim()
        && CREW_EMAIL.test(about.email.trim())
        && about.phone.trim()
        && about.primaryRole
      );
    }
    if (n === 2) return headshotState === 'ready';
    if (n === 3) {
      return !!(
        about.country
        && about.postcode.trim()
        && about.city.trim()
        && about.streetAddress.trim()
        && about.emergencyContactName.trim()
        && about.emergencyContactRelation.trim()
        && about.emergencyContactPhone.trim()
      );
    }
    if (n === 4) {
      const url = about.website.trim();
      return !!(url && CREW_URLISH.test(url));
    }
    if (n === 5) return !!consent.data;
    return false;
  };

  // Final submit gate. All prior steps must still validate (in case the
  // applicant jumped back, cleared a field, then forward to Review), the
  // consent box is ticked, and we're not already mid-submission.
  const canSubmit = !!(
    stepValid(1) && stepValid(2) && stepValid(3) && stepValid(4) && stepValid(5)
    && !submitting
  );

  const setStep = (n) => {
    const clamped = Math.max(0, Math.min(CREW_REAL_STEPS, n));
    setData((d) => ({ ...d, step: clamped }));
    if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'instant' });
  };
  const next = () => {
    if (!stepValid(step)) return;
    if (step === 0 && !applicationStartedAt) setApplicationStartedAt(Date.now());
    setStep(step + 1);
  };
  const prev = () => setStep(step - 1);

  const submit = async () => {
    setSubmitError('');
    setSubmitting(true);
    try {
      const secondaries = about.secondaryRoles.slice(0, secondaryCount).filter(Boolean);
      const payload = {
        about: { ...about, secondaryRoles: secondaries },
        consent,
        hasHeadshot: !!headshot,
        headshotBase64: headshot?.base64 || '',
        headshotMime: headshot?.mime || 'image/jpeg',
        headshotFilename: (headshot?.originalName || 'headshot').replace(/\.[^.]+$/, '') + '.jpg',
        _hp: honeypot,
        applicationStartedAt,
      };
      const r = await fetch('/api/crew', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msg = body?.errors?.length
          ? body.errors.join('; ')
          : (body?.detail || body?.error || `Submit failed (${r.status})`);
        setSubmitError(msg);
        setSubmitting(false);
        return;
      }
      setSubmitted(true);
    } catch (e) {
      setSubmitError(e?.message || 'Network error');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <main className="page active rentals-light crew-onboard" data-screen-label="08 Crew · Onboard">
      <RentalsSubHero
        idx="VF—5.0"
        label="Crew"
        title="Join the Valley Films <em>crew</em>"
        lead="A short form to add yourself to our freelance crew roster. We'll be in touch when we have a project that suits your role. Not a job application, not an onboarding step — just letting us know you exist." />

      <section className="rentals-open-account crew-onboard-section">
        <div className={`container oa-grid ${step > 0 ? 'is-focused' : ''}`}>
          {/* Welcome sidebar — visible on step 0, fades when the wizard begins. */}
          <div className="oa-intro" aria-hidden={step > 0}>
            <p className="oa-who">
              For freelance crew. We'll be in touch when a project fits your role.
            </p>
            <ul className="oa-checklist">
              <li>
                <span className="oa-checklist-icon" aria-hidden="true"><Icon name="user" size={16} /></span>
                Your details, role, and a headshot
              </li>
              <li>
                <span className="oa-checklist-icon" aria-hidden="true"><Icon name="phone" size={16} /></span>
                Address and emergency contact
              </li>
              <li>
                <span className="oa-checklist-icon" aria-hidden="true"><Icon name="arrow-up-right" size={16} /></span>
                A website or portfolio to point us to
              </li>
            </ul>
            <p className="oa-trust-line">
              Stored privately and only used to book you on shoots. Ask anytime to delete.
            </p>
          </div>

          <div className="form-card">
            {submitted ? (
              <div className="crew-submitted">
                <div className="crew-submitted-check"><Icon name="check" size={28} /></div>
                <h3>You're on the roster.</h3>
                <p>
                  Thanks{about.fullName ? `, ${about.fullName.split(' ')[0]}` : ''}. We've sent a copy of what you submitted to <strong>{about.email}</strong>.
                </p>
                <p>
                  We'll be in touch when a project comes up that fits your role. If your details change before then, just reply to that email.
                </p>
              </div>
            ) : (
              <form onSubmit={(e) => e.preventDefault()}>
                {/* Hidden honeypot — bots fill every input they see. */}
                <div className="oa-honeypot" aria-hidden="true">
                  <label htmlFor="crew-hp-website">Leave this field blank</label>
                  <input
                    id="crew-hp-website"
                    type="text"
                    name="website"
                    autoComplete="off"
                    tabIndex={-1}
                    value={honeypot}
                    onChange={(e) => setHoneypot(e.target.value)}
                  />
                </div>

                {/* Step indicator — hidden on welcome. */}
                {step > 0 && (
                  <div className="step-indicator">
                    {[1, 2, 3, 4, 5].map((i) => {
                      const fill = i < step ? 1 : i === step ? (stepValid(i) ? 1 : 0.5) : 0;
                      const status = i < step
                        ? 'completed'
                        : i === step ? 'in-progress' : 'pending';
                      const jumpable = i <= maxStepReached;
                      const jumpTo = () => { if (jumpable && i !== step) setStep(i); };
                      return (
                        <div
                          key={i}
                          className={`dot is-${status} ${fill > 0 ? 'filling' : ''} ${jumpable ? 'is-jumpable' : ''}`}
                          tabIndex={jumpable ? 0 : -1}
                          role={jumpable ? 'button' : undefined}
                          aria-label={`${CREW_STEP_NAMES[i]}${jumpable && i !== step ? ' — jump to this step' : ''}`}
                          onClick={(e) => { e.preventDefault(); jumpTo(); }}
                          onKeyDown={(e) => {
                            if (!jumpable) return;
                            if (e.key === 'Enter' || e.key === ' ') {
                              e.preventDefault();
                              jumpTo();
                            }
                          }}
                        >
                          <span className="fill" style={{ transform: `scaleX(${fill})` }} />
                          <span className="dot-tooltip" role="tooltip">
                            <strong className="dot-tooltip-title">{CREW_STEP_NAMES[i]}</strong>
                          </span>
                        </div>
                      );
                    })}
                  </div>
                )}

                {step === 0 && (
                  <React.Fragment>
                    <h3>Join the crew</h3>
                    <p className="desc">Add yourself to our freelance roster. We'll be in touch when a project fits.</p>
                    <div className="oa-welcome-meta">
                      <div><div className="num">~2</div><div className="lbl">minutes</div></div>
                      <div className="sep" aria-hidden="true" />
                      <div><div className="num">5</div><div className="lbl">short steps</div></div>
                      <div className="sep" aria-hidden="true" />
                      <div><div className="num">No</div><div className="lbl">interview</div></div>
                    </div>
                    <button type="button" className="submit" onClick={(e) => { e.preventDefault(); next(); }}>
                      Let's begin <Icon name="arrow-right" />
                    </button>
                    <p className="oa-priv">
                      Your details are stored securely and only used to consider you for work. See our <a href="/privacy" onClick={(e) => { if (onGoto) { e.preventDefault(); onGoto('/privacy'); } }}>privacy notice</a> for full retention details.
                    </p>
                  </React.Fragment>
                )}

                {step === 1 && (
                  <React.Fragment>
                    <h3>About you</h3>
                    <p className="desc">Your name and the role you'd headline as. Add up to two extras if you wear multiple hats.</p>
                    <div className="field-row">
                      <div className="field full">
                        <label htmlFor="crew-fullName">Full name<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-fullName" type="text" value={about.fullName} onChange={(e) => setAbout({ fullName: e.target.value })} autoComplete="name" required />
                      </div>
                    </div>
                    <div className="field-row">
                      <div className="field">
                        <label htmlFor="crew-email">Email<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-email" type="email" value={about.email} onChange={(e) => setAbout({ email: e.target.value })} autoComplete="email" required />
                      </div>
                      <div className="field">
                        <label htmlFor="crew-phone">Phone<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-phone" type="tel" value={about.phone} onChange={(e) => setAbout({ phone: e.target.value })} autoComplete="tel" required />
                      </div>
                    </div>
                    <RoleCombobox
                      label="Primary role"
                      value={about.primaryRole}
                      onChange={setPrimaryRole}
                      pickedRoles={pickedRoles}
                      required
                    />
                    {secondaryCount >= 1 && (
                      <RoleCombobox
                        label="Secondary role"
                        value={about.secondaryRoles[0]}
                        onChange={(v) => setSecondaryRole(0, v)}
                        pickedRoles={pickedRoles}
                        onRemove={() => removeSecondarySlot(0)}
                      />
                    )}
                    {secondaryCount >= 2 && (
                      <RoleCombobox
                        label="Third role"
                        value={about.secondaryRoles[1]}
                        onChange={(v) => setSecondaryRole(1, v)}
                        pickedRoles={pickedRoles}
                        onRemove={() => removeSecondarySlot(1)}
                      />
                    )}
                    {about.primaryRole && secondaryCount < 2 && (
                      <button type="button" className="crew-add-role" onClick={addSecondarySlot}>
                        <Icon name="plus" size={14} /> Add {secondaryCount === 0 ? 'a secondary' : 'a third'} role
                      </button>
                    )}
                  </React.Fragment>
                )}

                {step === 2 && (
                  <React.Fragment>
                    <h3>Headshot</h3>
                    <p className="desc">A recent photo of you. We use it to recognise you on set and to put a face to the name when shortlisting. Resized to 1200px on upload — no need to compress beforehand.</p>
                    <HeadshotUpload
                      state={headshotState}
                      headshot={headshot}
                      error={headshotError}
                      onFile={onHeadshotFile}
                      onClear={clearHeadshot}
                    />
                  </React.Fragment>
                )}

                {step === 3 && (
                  <React.Fragment>
                    <h3>Address &amp; emergency contact</h3>
                    <p className="desc">Useful for invoicing and figuring out who's local for which jobs. UK postcodes auto-fill your city. Won't share outside Valley.</p>
                    <div className="field-row">
                      <div className="field full">
                        <label htmlFor="crew-country">Country<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <select id="crew-country" value={about.country} onChange={(e) => setAbout({ country: e.target.value })} autoComplete="country-name" required>
                          {CREW_COUNTRIES.map((c) => (<option key={c} value={c}>{c}</option>))}
                        </select>
                      </div>
                    </div>
                    <div className="field-row">
                      <div className="field">
                        <label htmlFor="crew-postcode">Postcode<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-postcode" type="text" value={about.postcode} onChange={(e) => setAbout({ postcode: e.target.value })} autoComplete="postal-code" required />
                        {about.country === 'United Kingdom' && postcodeCheck.state !== 'idle' && (
                          <span className={`crew-postcode-status is-${postcodeCheck.state}`}>
                            {postcodeCheck.state === 'pending'       && 'Checking postcode…'}
                            {postcodeCheck.state === 'valid'         && `Found ${postcodeCheck.area || postcodeCheck.city}`}
                            {postcodeCheck.state === 'invalid'       && `${postcodeCheck.postcode} isn't a valid UK postcode`}
                            {postcodeCheck.state === 'network_error' && `Couldn't check that postcode — type your city below`}
                          </span>
                        )}
                      </div>
                      <div className="field">
                        <label htmlFor="crew-city">City<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-city" type="text" value={about.city} onChange={(e) => setAbout({ city: e.target.value })} autoComplete="address-level2" required />
                      </div>
                    </div>
                    <div className="field-row">
                      <div className="field full">
                        <label htmlFor="crew-street">Street address<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-street" type="text" value={about.streetAddress} onChange={(e) => setAbout({ streetAddress: e.target.value })} autoComplete="street-address" placeholder="Flat 4, 23 Acacia Avenue" required />
                      </div>
                    </div>

                    <h4 className="crew-step-subhead">Emergency contact</h4>
                    <p className="desc">Who we ring if something goes wrong on set. Only used if needed.</p>
                    <div className="field-row">
                      <div className="field">
                        <label htmlFor="crew-ec-name">Name<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-ec-name" type="text" value={about.emergencyContactName} onChange={(e) => setAbout({ emergencyContactName: e.target.value })} required />
                      </div>
                      <div className="field">
                        <label htmlFor="crew-ec-rel">Relation<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-ec-rel" type="text" value={about.emergencyContactRelation} onChange={(e) => setAbout({ emergencyContactRelation: e.target.value })} placeholder="Partner, Parent, Friend…" required />
                      </div>
                    </div>
                    <div className="field-row">
                      <div className="field full">
                        <label htmlFor="crew-ec-phone">Phone<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-ec-phone" type="tel" value={about.emergencyContactPhone} onChange={(e) => setAbout({ emergencyContactPhone: e.target.value })} required />
                      </div>
                    </div>
                  </React.Fragment>
                )}

                {step === 4 && (
                  <React.Fragment>
                    <h3>Background</h3>
                    <p className="desc">Where we can see your work, and anything else worth flagging up front.</p>
                    <div className="field-row">
                      <div className="field">
                        <label htmlFor="crew-company">Company name</label>
                        <input id="crew-company" type="text" value={about.company} onChange={(e) => setAbout({ company: e.target.value })} autoComplete="organization" placeholder="Trade name, or leave blank" />
                      </div>
                      <div className="field">
                        <label htmlFor="crew-website">Website<span className="oa-required-mark" aria-hidden="true">*</span></label>
                        <input id="crew-website" type="text" value={about.website} onChange={(e) => setAbout({ website: e.target.value })} placeholder="yourname.com, instagram.com/you, vimeo.com/you" autoComplete="url" required />
                      </div>
                    </div>

                    <h4 className="crew-step-subhead">Dietary requirements</h4>
                    <p className="desc">Tick anything that applies — we use this when catering for shoots. Tick "N/A" if you eat anything. Add more detail in the Notes box below if you tick "Other".</p>
                    <div className="crew-dietary-grid">
                      {CREW_DIETARY.map((label) => (
                        <label key={label} className="crew-dietary-opt">
                          <input
                            type="checkbox"
                            checked={about.dietary.includes(label)}
                            onChange={() => toggleDietary(label)}
                          />
                          <span>{label}</span>
                        </label>
                      ))}
                    </div>

                    <h4 className="crew-step-subhead">Anything else</h4>
                    <p className="desc">Availability windows, certifications, gear you bring — whatever's useful.</p>
                    <div className="field-row">
                      <div className="field full">
                        <label htmlFor="crew-notes">Notes</label>
                        <textarea id="crew-notes" value={about.notes} onChange={(e) => setAbout({ notes: e.target.value })} rows={4} placeholder="e.g. typically free Tue–Thu, IPAF + UK driving licence, own Atomos Ninja V." />
                      </div>
                    </div>
                  </React.Fragment>
                )}

                {step === 5 && (
                  <React.Fragment>
                    <h3>Review &amp; submit</h3>
                    <p className="desc">A last look at what we'll save. Tap a section to go back and edit anything.</p>
                    <CrewReview
                      about={about}
                      secondaryCount={secondaryCount}
                      headshot={headshot}
                      goToStep={setStep}
                    />
                    <label className="crew-consent">
                      <input
                        type="checkbox"
                        checked={consent.data}
                        onChange={(e) => setConsent({ data: e.target.checked })}
                      />
                      <span>
                        I agree to Valley Films storing these details to consider me for future work. I can ask for them to be deleted at any time by emailing <a href="mailto:info@valley.film">info@valley.film</a>. See the <a href="/privacy">privacy notice</a> for full retention details.
                      </span>
                    </label>
                  </React.Fragment>
                )}

                {submitError && step >= 1 && (
                  <div className="crew-error" role="alert">{submitError}</div>
                )}

                {step > 0 && (
                  <div className="oa-actions">
                    <button type="button" className="oa-back" onClick={(e) => { e.preventDefault(); prev(); }}>Back</button>
                    {step < CREW_REAL_STEPS ? (
                      <button
                        type="button"
                        className="submit"
                        disabled={!stepValid(step)}
                        onClick={(e) => { e.preventDefault(); next(); }}
                      >
                        Continue <Icon name="arrow-right" />
                      </button>
                    ) : (
                      <button
                        type="button"
                        className="submit"
                        disabled={!canSubmit}
                        onClick={(e) => { e.preventDefault(); submit(); }}
                      >
                        {submitting ? 'Submitting…' : <>Submit application <Icon name="arrow-right" /></>}
                      </button>
                    )}
                  </div>
                )}
              </form>
            )}
          </div>
        </div>
      </section>

      <MegaFooter onGoto={onGoto} isRentals={false} />
    </main>
  );
}

// ---------------------------------------------------------------------
// CrewReview — recap shown on the final step. Groups the applicant's
// answers under the same headings as the wizard steps; clicking the
// "Edit" link on a group jumps straight back to that step.
// ---------------------------------------------------------------------
function CrewReview({ about, secondaryCount, headshot, goToStep }) {
  const secondaries = about.secondaryRoles.slice(0, secondaryCount).filter(Boolean);
  const addressLine = [about.streetAddress, about.city, about.postcode, about.country].filter(Boolean).join(', ');
  const dietary = (about.dietary || []).join(', ');

  return (
    <div className="crew-review">
      <CrewReviewGroup title="About you" onEdit={() => goToStep(1)}>
        <CrewReviewRow k="Name"    v={about.fullName} />
        <CrewReviewRow k="Email"   v={about.email} />
        <CrewReviewRow k="Phone"   v={about.phone} />
        <CrewReviewRow k="Role"    v={[about.primaryRole, ...secondaries].filter(Boolean).join(', ')} />
      </CrewReviewGroup>

      <CrewReviewGroup title="Headshot" onEdit={() => goToStep(2)}>
        {headshot ? (
          <div className="crew-review-headshot">
            <img src={headshot.dataUrl} alt="" />
            <div className="crew-review-headshot-meta">
              <div className="crew-review-headshot-filename">{headshot.originalName}</div>
              <div className="crew-review-headshot-info">{headshot.width}×{headshot.height} · {(headshot.sizeBytes / 1024).toFixed(0)} KB</div>
            </div>
          </div>
        ) : (
          <CrewReviewRow k="Headshot" v="(none)" />
        )}
      </CrewReviewGroup>

      <CrewReviewGroup title="Address & contact" onEdit={() => goToStep(3)}>
        <CrewReviewRow k="Address"           v={addressLine} />
        <CrewReviewRow k="Emergency"         v={`${about.emergencyContactName}${about.emergencyContactRelation ? ` (${about.emergencyContactRelation})` : ''}`} />
        <CrewReviewRow k="Emergency phone"   v={about.emergencyContactPhone} />
      </CrewReviewGroup>

      <CrewReviewGroup title="Background" onEdit={() => goToStep(4)}>
        {about.company && <CrewReviewRow k="Company" v={about.company} />}
        <CrewReviewRow k="Website"  v={about.website} />
        {dietary       && <CrewReviewRow k="Dietary" v={dietary} />}
        {about.notes   && <CrewReviewRow k="Notes"   v={about.notes} />}
      </CrewReviewGroup>
    </div>
  );
}

function CrewReviewGroup({ title, onEdit, children }) {
  return (
    <div className="crew-review-group">
      <div className="crew-review-group-head">
        <h4>{title}</h4>
        <button type="button" className="crew-review-edit" onClick={(e) => { e.preventDefault(); onEdit(); }}>
          Edit
        </button>
      </div>
      <dl className="crew-review-rows">{children}</dl>
    </div>
  );
}

function CrewReviewRow({ k, v }) {
  if (!v) return null;
  return (
    <React.Fragment>
      <dt>{k}</dt>
      <dd>{v}</dd>
    </React.Fragment>
  );
}

// ---------------------------------------------------------------------
// RoleCombobox — typeahead picker over CREW_ROLES.
//
// Click input or focus → dropdown opens. Type → filter by substring.
// Keyboard: ↑/↓ move highlight, Enter/Tab pick, Escape close.
// Roles in `pickedRoles` (selected anywhere else) are hidden so the
// user can't pick the same role twice across slots.
// ---------------------------------------------------------------------
function RoleCombobox({ label, value, onChange, pickedRoles, required, onRemove }) {
  const [open, setOpen] = useStateCrew(false);
  const [query, setQuery] = useStateCrew(value || '');
  const [highlighted, setHighlighted] = useStateCrew(0);
  const rootRef = useRefCrew(null);
  const inputRef = useRefCrew(null);

  useEffectCrew(() => { setQuery(value || ''); }, [value]);

  useEffectCrew(() => {
    if (!open) return;
    const onDown = (e) => {
      if (!rootRef.current) return;
      if (!rootRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);

  const filtered = useMemoCrew(() => {
    const q = query.trim().toLowerCase();
    return CREW_ROLES
      .filter((r) => r === value || !pickedRoles.has(r))
      .filter((r) => !q || r.toLowerCase().includes(q));
  }, [query, value, pickedRoles]);

  const pick = (role) => {
    onChange(role);
    setQuery(role);
    setOpen(false);
  };

  const onKey = (e) => {
    if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
      setOpen(true);
      return;
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setHighlighted((h) => Math.min(filtered.length - 1, h + 1));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setHighlighted((h) => Math.max(0, h - 1));
    } else if (e.key === 'Enter' || e.key === 'Tab') {
      if (open && filtered[highlighted]) {
        e.preventDefault();
        pick(filtered[highlighted]);
      }
    } else if (e.key === 'Escape') {
      setOpen(false);
    }
  };

  return (
    <div className="field full crew-combo" ref={rootRef}>
      <div className="crew-combo-label-row">
        <label htmlFor={`crew-role-${label.replace(/\s+/g, '-').toLowerCase()}`}>
          {label}{required && <span className="oa-required-mark" aria-hidden="true">*</span>}
        </label>
        {onRemove && (
          <button
            type="button"
            className="crew-combo-remove"
            onClick={onRemove}
            aria-label={`Remove ${label}`}
          >
            <Icon name="close" size={12} /> remove
          </button>
        )}
      </div>
      <input
        id={`crew-role-${label.replace(/\s+/g, '-').toLowerCase()}`}
        ref={inputRef}
        type="text"
        value={query}
        onFocus={() => { setOpen(true); setHighlighted(0); }}
        onChange={(e) => {
          setQuery(e.target.value);
          setOpen(true);
          setHighlighted(0);
          // Clear the bound value if the user is typing something
          // that doesn't exactly match — the role isn't committed
          // until they click an option or press Enter.
          if (e.target.value !== value) onChange('');
        }}
        onKeyDown={onKey}
        placeholder="Start typing or pick from the list…"
        autoComplete="off"
        spellCheck={false}
        required={required}
      />
      {open && filtered.length > 0 && (
        <ul className="crew-combo-list" role="listbox">
          {filtered.map((role, i) => (
            <li
              key={role}
              role="option"
              aria-selected={role === value}
              className={`crew-combo-opt ${i === highlighted ? 'is-highlighted' : ''} ${role === value ? 'is-selected' : ''}`}
              onMouseEnter={() => setHighlighted(i)}
              onMouseDown={(e) => { e.preventDefault(); pick(role); }}
            >
              {role}
            </li>
          ))}
        </ul>
      )}
      {open && filtered.length === 0 && (
        <ul className="crew-combo-list">
          <li className="crew-combo-empty">No match — pick from the list when you clear the field.</li>
        </ul>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------
// HeadshotUpload — drag-and-drop or click-to-choose, with a preview
// thumbnail and a Replace button once a file is processed.
// ---------------------------------------------------------------------
function HeadshotUpload({ state, headshot, error, onFile, onClear }) {
  const [dragOver, setDragOver] = useStateCrew(false);
  const inputRef = useRefCrew(null);

  const onChange = (e) => {
    const f = e.target.files?.[0];
    if (f) onFile(f);
  };
  const onDrop = (e) => {
    e.preventDefault();
    setDragOver(false);
    const f = e.dataTransfer.files?.[0];
    if (f) onFile(f);
  };

  if (state === 'ready' && headshot) {
    return (
      <div className="crew-headshot-preview">
        <img src={headshot.dataUrl} alt="Headshot preview" />
        <div className="crew-headshot-meta">
          <div className="crew-headshot-filename">{headshot.originalName}</div>
          <div className="crew-headshot-info">
            {headshot.width}×{headshot.height} · {(headshot.sizeBytes / 1024).toFixed(0)} KB
          </div>
          <button type="button" className="crew-headshot-replace" onClick={onClear}>
            Replace
          </button>
        </div>
      </div>
    );
  }

  return (
    <div
      className={`crew-headshot-drop ${dragOver ? 'is-drag-over' : ''} ${state === 'processing' ? 'is-processing' : ''} ${state === 'error' ? 'is-error' : ''}`}
      onClick={() => inputRef.current?.click()}
      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragLeave={() => setDragOver(false)}
      onDrop={onDrop}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') inputRef.current?.click(); }}
    >
      <input
        ref={inputRef}
        type="file"
        accept="image/jpeg,image/png,image/webp,image/heic,image/heif"
        onChange={onChange}
        style={{ display: 'none' }}
      />
      {state === 'processing' ? (
        <span>Resizing image…</span>
      ) : (
        <>
          <Icon name="upload" size={20} />
          <div>
            <strong>Drop an image here</strong> or click to choose
          </div>
          <div className="crew-headshot-hint">JPEG, PNG, WebP · resized to 1200px on upload</div>
        </>
      )}
      {error && <div className="crew-headshot-error">{error}</div>}
    </div>
  );
}

Object.assign(window, { CrewOnboardingPage });
