/* global React, Icon, MegaFooter, RentalsSubHero */
//
// Kit Prep Tech application wizard.
//
// Mounted at /rentals/careers. Linked from the Rentals footer only —
// semi-hidden discovery. Mirrors the Open Account wizard's chrome and
// fraud-signal capture (honeypot + fast-submit + IP/UA on the server)
// but with a smaller field set, no references, no business proof,
// and no save-and-resume.
//
// Submission flow:
//   1. POST /api/applications/submit-kit-prep      → creates Notion row,
//                                                    returns applicantId
//   2. POST /api/applications/upload-kit-prep?slot=cv&applicantId=...
//   3. POST /api/applications/upload-kit-prep?slot=photo&applicantId=...
//   4. Show the confirmation screen.

const { useState: useStateJobs, useEffect: useEffectJobs, useRef: useRefJobs } = React;

// Studio location — Access House, 207-211 The Vale, Acton W3 7QS.
// Coordinates resolved via postcodes.io. Used as the origin for the
// commute-distance check on the postcode field below.
const VF_STUDIO_LAT = 51.506183;
const VF_STUDIO_LNG = -0.259514;
// Threshold for the "we're hiring locals only" gate. London transit
// averages ~7-9 mph end-to-end (walks + waits + changes included), so
// 7 miles as the crow flies is a reasonable proxy for ~45 minutes
// door-to-door. Generous enough not to filter out genuinely commutable
// candidates; tight enough to keep out south-of-river or zone-5+ folks.
const VF_MAX_COMMUTE_MILES = 7;

// Haversine distance between two lat/lng points, returned in miles.
function haversineMiles(lat1, lng1, lat2, lng2) {
  const R = 3958.8;
  const toRad = (d) => (d * Math.PI) / 180;
  const dLat = toRad(lat2 - lat1);
  const dLng = toRad(lng2 - lng1);
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

// Postcode lookup against postcodes.io. Cached so a typo + correction
// only fires one network request per unique postcode. Returns a state
// object the wizard renders inline.
const kpPostcodeCache = new Map();
async function kpLookupPostcode(raw) {
  const pc = String(raw || '').replace(/\s+/g, '').toUpperCase();
  if (!pc) return { state: 'idle' };
  if (kpPostcodeCache.has(pc)) return kpPostcodeCache.get(pc);
  try {
    const r = await fetch(`https://api.postcodes.io/postcodes/${encodeURIComponent(pc)}`);
    if (r.status === 404) {
      const out = { state: 'invalid', postcode: pc };
      kpPostcodeCache.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 };
      kpPostcodeCache.set(pc, out);
      return out;
    }
    const miles = haversineMiles(res.latitude, res.longitude, VF_STUDIO_LAT, VF_STUDIO_LNG);
    const area = [res.admin_ward, res.admin_district].filter(Boolean).join(', ');
    const label = area ? `${area} (${res.postcode})` : res.postcode;
    const out = {
      state: miles > VF_MAX_COMMUTE_MILES ? 'too_far' : 'valid',
      postcode: res.postcode,
      lat: res.latitude,
      lng: res.longitude,
      area,
      label,
      miles,
    };
    kpPostcodeCache.set(pc, out);
    return out;
  } catch (e) {
    return { state: 'network_error', postcode: pc, error: e?.message };
  }
}

// Tiny size formatter for the file-preview metadata row. Matches the
// "12.3 KB" / "1.4 MB" style used by Open Account's fmtSize.
function kpFmtSize(bytes) {
  if (!bytes && bytes !== 0) return '';
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

const KP_DEFAULT_DATA = {
  step: 0,
  applicationStartedAt: null,
  about: {
    fullName: '',
    email: '',
    phone: '',
    postcode: '',
    location: '',
    driversLicence: '',
    experience: '',
    additional: '',
  },
};

const KP_STEPS = [
  { num: 0, label: 'Welcome' },
  { num: 1, label: 'About you' },
  { num: 2, label: 'Experience & docs' },
  { num: 3, label: 'Review' },
];

function KitPrepTechPage({ onGoto }) {
  const [data, setData] = useStateJobs(KP_DEFAULT_DATA);
  const { step, about } = data;

  // Highest step the applicant has ever reached. Drives the step-
  // indicator's "jumpable" affordance so they can click back to a
  // section they've already visited (e.g. from Review → fix typo →
  // back to Review without losing field state). Forward jumps to
  // unvisited steps stay locked so validation order holds.
  const [maxStepReached, setMaxStepReached] = useStateJobs(0);
  useEffectJobs(() => {
    if (step > maxStepReached) setMaxStepReached(step);
  }, [step, maxStepReached]);

  // File state lives outside `data` because File objects can't be
  // serialised. cv = PDF/doc; photo = JPEG/PNG selfie.
  const [files, setFiles] = useStateJobs({ cv: null, photo: null });

  // Honeypot — bots fill every input they see. A real human can't see
  // this one (CSS off-screen), so it should always be ''. Server tags
  // submissions with non-empty values as `honeypot_filled`.
  const [honeypot, setHoneypot] = useStateJobs('');

  const [submitting, setSubmitting] = useStateJobs(false);
  const [submitted, setSubmitted] = useStateJobs(false);
  const [submitError, setSubmitError] = useStateJobs('');
  const [uploadStatus, setUploadStatus] = useStateJobs({ cv: 'idle', photo: 'idle' });

  // Postcode validation state. State machine:
  //   idle          — nothing typed
  //   pending       — request in flight (debounced)
  //   valid         — resolved + within commute radius
  //   too_far       — resolved but > VF_MAX_COMMUTE_MILES away
  //   invalid       — postcodes.io returned 404
  //   network_error — request failed (CORS, offline, etc.)
  const [postcodeCheck, setPostcodeCheck] = useStateJobs({ state: 'idle' });

  // Modal shown when a postcode resolves to a too-far location. Fires
  // once per "fresh" too_far result so re-typing the same bad postcode
  // doesn't repeatedly nag.
  const [showTooFarModal, setShowTooFarModal] = useStateJobs(false);
  const tooFarShownForRef = useRefJobs('');

  // Debounced postcode lookup. Fires ~450ms after the last keystroke.
  // The pending state surfaces while we wait so the inline feedback
  // doesn't lag behind typing.
  useEffectJobs(() => {
    const raw = about.postcode.trim();
    if (!raw) {
      setPostcodeCheck({ state: 'idle' });
      // Clear any derived label so a cleared postcode doesn't leave
      // a stale "Acton, Ealing" stored on submission.
      if (about.location) setData((d) => ({ ...d, about: { ...d.about, location: '' } }));
      return;
    }
    setPostcodeCheck((s) => (s.state === 'pending' ? s : { state: 'pending', postcode: raw }));
    const t = setTimeout(async () => {
      const out = await kpLookupPostcode(raw);
      setPostcodeCheck(out);
      if (out.state === 'valid' || out.state === 'too_far') {
        // Persist the human-readable area + canonical postcode onto
        // the form so submission carries it to Notion as `Location`.
        setData((d) => ({ ...d, about: { ...d.about, location: out.label || '' } }));
      }
      if (out.state === 'too_far' && tooFarShownForRef.current !== out.postcode) {
        tooFarShownForRef.current = out.postcode;
        setShowTooFarModal(true);
      }
    }, 450);
    return () => clearTimeout(t);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [about.postcode]);

  // Refs for focusing the first input on step change.
  const firstInputRef = useRefJobs(null);
  useEffectJobs(() => {
    if (firstInputRef.current && step > 0 && step < 3) {
      try { firstInputRef.current.focus(); } catch {}
    }
  }, [step]);

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

  const goStep = (n) => {
    setData((d) => {
      const next = { ...d, step: n };
      // Record startedAt when the applicant first leaves the welcome
      // step. Submissions less than 70s from this point are flagged
      // server-side as suspicious.
      if (d.step === 0 && n > 0 && !d.applicationStartedAt) {
        next.applicationStartedAt = Date.now();
      }
      return next;
    });
    window.scrollTo({ top: 0, behavior: 'instant' });
  };

  // Per-step completeness (0..1) — drives both the Continue gate and
  // the inline progress bar fill. Each required field contributes an
  // equal share; the bar grows live as the applicant types.
  const stepScore = (n) => {
    if (n === 1) {
      const reqs = [
        about.fullName.trim().length > 0,
        /^\S+@\S+\.\S+$/.test(about.email.trim()),
        about.phone.trim().length > 0,
        postcodeCheck.state === 'valid',
        !!about.driversLicence,
      ];
      return reqs.filter(Boolean).length / reqs.length;
    }
    if (n === 2) {
      const reqs = [
        about.experience.trim().length > 0,
        about.additional.trim().length > 0,
        !!files.cv,
        !!files.photo,
      ];
      return reqs.filter(Boolean).length / reqs.length;
    }
    return 1;
  };
  const stepValid = (n) => {
    if (n === 0) return true;
    return stepScore(n) >= 0.999;
  };

  const submit = async () => {
    setSubmitError('');
    setSubmitting(true);
    try {
      // 1. Create the Notion row.
      const r = await fetch('/api/applications/submit-kit-prep', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          about,
          _hp: honeypot,
          applicationStartedAt: data.applicationStartedAt,
        }),
      });
      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;
      }
      const applicantId = body.applicantId;

      // 2. Upload CV + photo in parallel. Don't fail the whole submit if
      //    one upload errors out — surface it but treat the application
      //    as submitted.
      const upload = async (slot, file) => {
        setUploadStatus((s) => ({ ...s, [slot]: 'uploading' }));
        try {
          const ur = await fetch(
            `/api/applications/upload-kit-prep?applicantId=${encodeURIComponent(applicantId)}&slot=${slot}`,
            {
              method: 'POST',
              headers: {
                'Content-Type': file.type || 'application/octet-stream',
                'X-Filename': file.name || `${slot}`,
              },
              body: file,
            }
          );
          if (!ur.ok) {
            const ub = await ur.json().catch(() => ({}));
            setUploadStatus((s) => ({ ...s, [slot]: `error: ${ub.error || ur.status}` }));
            return false;
          }
          setUploadStatus((s) => ({ ...s, [slot]: 'done' }));
          return true;
        } catch (e) {
          setUploadStatus((s) => ({ ...s, [slot]: `error: ${e.message}` }));
          return false;
        }
      };
      await Promise.all([
        files.cv ? upload('cv', files.cv) : Promise.resolve(false),
        files.photo ? upload('photo', files.photo) : Promise.resolve(false),
      ]);

      setSubmitted(true);
    } catch (e) {
      setSubmitError(e?.message || 'Network error');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <main className="page active rentals-light" data-screen-label="03c Rentals · Careers">
      <RentalsSubHero
        idx="VR—5.0"
        label="Careers"
        title="Kit Prep Technician<br/><em>application</em>"
        lead="A hands-on prep role at our Acton studio. You'll be the one making sure every camera kit, lens set and grip cart goes out the door clean, complete and labelled. Apply below — we'll come back to you when there's a fit." />

      <section id="kit-prep" className="rentals-open-account rentals-kit-prep">
        <div className={`container oa-grid ${step > 0 ? 'is-focused' : ''}`}>
          {/* LEFT — intro panel, collapses past welcome step */}
          <div className="oa-intro" aria-hidden={step > 0}>
            <p className="oa-who">
              We hire Kit Prep Technicians as occasional freelancers — typically a day or two each week around busy prep windows. Reliable, methodical and gear-fluent matters more than years of experience.
            </p>
            <h3>What we'll ask for</h3>
            <ul className="oa-checklist">
              <li>
                <span className="oa-checklist-icon" aria-hidden="true"><Icon name="file" size={16} /></span>
                Your CV
              </li>
              <li>
                <span className="oa-checklist-icon" aria-hidden="true"><Icon name="user" size={16} /></span>
                A recent photo of you
              </li>
              <li>
                <span className="oa-checklist-icon" aria-hidden="true"><Icon name="file-text" size={16} /></span>
                A short note on your experience
              </li>
            </ul>
            <p className="oa-trust-line">
              Reviewed by the team in person — usually back to you within a week.
            </p>
          </div>

          {/* RIGHT — wizard */}
          <div className="form-card oa-wizard">
            {submitted ? (
              <div className="contact-sent">
                <div className="check"><Icon name="check" /></div>
                <h3>Application received</h3>
                <p>
                  Thanks{about.fullName ? `, ${about.fullName.split(' ')[0]}` : ''}. We've sent a copy of this to <strong>{about.email}</strong> for your records.
                </p>
                <p>
                  We review applications as a team — usually back to you within a week. If we'd like you to come in for a prep day, we'll get in touch by email.
                </p>
              </div>
            ) : (
              <React.Fragment>
                {/* Honeypot — visually-hidden via CSS. Real humans never
                    see it; bots fill every input. */}
                <label className="oa-honeypot" aria-hidden="true">
                  Leave this field blank
                  <input
                    type="text"
                    tabIndex={-1}
                    autoComplete="off"
                    value={honeypot}
                    onChange={(e) => setHoneypot(e.target.value)}
                  />
                </label>

                {/* Step indicator bars — past steps render full, the
                    current step's bar fills proportionally as each
                    required field is completed (blue scaleX via CSS).
                    Previously-visited dots are clickable so applicants
                    can hop back to fix a typo without losing state. */}
                {step > 0 && (
                  <div className="step-indicator">
                    {KP_STEPS.filter((s) => s.num > 0).map((stp) => {
                      const isActive = step === stp.num;
                      const isDone = step > stp.num;
                      const fill = isDone ? 1 : isActive ? stepScore(stp.num) : 0;
                      const jumpable = stp.num <= maxStepReached;
                      const jumpTo = () => {
                        if (jumpable && stp.num !== step) {
                          setData((d) => ({ ...d, step: stp.num }));
                          window.scrollTo({ top: 0, behavior: 'instant' });
                        }
                      };
                      const cls = `dot${isActive ? ' is-active' : ''}${isDone ? ' is-done' : ''}${jumpable ? ' is-jumpable' : ''}`;
                      return (
                        <span
                          key={stp.num}
                          className={cls}
                          tabIndex={jumpable ? 0 : -1}
                          role={jumpable ? 'button' : undefined}
                          aria-label={`${stp.label}${jumpable && !isActive ? ' — 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">{stp.label}</span>
                        </span>
                      );
                    })}
                  </div>
                )}

                {step === 0 && <KpStepWelcome onContinue={() => goStep(1)} />}
                {step === 1 && (
                  <KpStepAbout
                    about={about}
                    setAbout={setAbout}
                    postcodeCheck={postcodeCheck}
                    firstInputRef={firstInputRef}
                    onBack={() => goStep(0)}
                    onContinue={() => goStep(2)}
                    canContinue={stepValid(1)}
                  />
                )}
                {step === 2 && (
                  <KpStepDocs
                    about={about}
                    setAbout={setAbout}
                    files={files}
                    setFiles={setFiles}
                    firstInputRef={firstInputRef}
                    onBack={() => goStep(1)}
                    onContinue={() => goStep(3)}
                    canContinue={stepValid(2)}
                  />
                )}
                {step === 3 && (
                  <KpStepReview
                    about={about}
                    files={files}
                    onBack={() => goStep(2)}
                    onSubmit={submit}
                    submitting={submitting}
                    submitError={submitError}
                    uploadStatus={uploadStatus}
                  />
                )}
              </React.Fragment>
            )}
          </div>
        </div>
      </section>

      <MegaFooter onGoto={onGoto} isRentals />

      {showTooFarModal && (
        <KpTooFarModal
          postcodeCheck={postcodeCheck}
          onDismiss={() => setShowTooFarModal(false)}
        />
      )}
    </main>
  );
}

// --- Step components ------------------------------------------------------

function KpStepWelcome({ onContinue }) {
  return (
    <div>
      <h3>Apply: Kit Prep Tech</h3>
      <p className="desc">
        A short application — about five minutes. We'll ask for your contact details, a recent photo, your CV, and a brief note on your experience. There's no test or unpaid task.
      </p>
      <div className="oa-welcome-meta">
        <div>
          <div className="num">~5</div>
          <div className="lbl">minutes</div>
        </div>
        <div className="sep" aria-hidden="true" />
        <div>
          <div className="num">3</div>
          <div className="lbl">short steps</div>
        </div>
        <div className="sep" aria-hidden="true" />
        <div>
          <div className="num">1</div>
          <div className="lbl">week to hear back</div>
        </div>
      </div>
      <button type="button" className="submit" onClick={onContinue}>
        Start the application <Icon name="arrow-right" />
      </button>
      <p className="oa-priv">
        We use your details only to assess this application and to contact you about it. See our <a href="/privacy">privacy notice</a> for the full picture.
      </p>
    </div>
  );
}

function KpStepAbout({ about, setAbout, postcodeCheck, firstInputRef, onBack, onContinue, canContinue }) {
  const pc = postcodeCheck || { state: 'idle' };
  const pcClass = `kp-postcode-status kp-postcode-${pc.state}`;
  // Keep the field silent on the happy path. We only surface inline
  // feedback when the applicant needs to act on something: a postcode
  // that's too far (with the modal as the primary signal), or one we
  // can't resolve. Pending + valid states render nothing — the form
  // just keeps going.
  const pcMessage = (() => {
    if (pc.state === 'too_far') return `${pc.label} · ~${pc.miles.toFixed(1)} miles — outside our current radius`;
    if (pc.state === 'invalid') return "We couldn't find that postcode. Double-check the format (e.g. W3 7QS).";
    if (pc.state === 'network_error') return "Couldn't reach the postcode lookup right now. Try again in a moment.";
    return '';
  })();
  return (
    <div>
      <h3>About you</h3>
      <p className="desc">The basics. We'll only use these to contact you about this application.</p>
      <div className="form-row">
        <label>
          Full name<span className="oa-required-mark" aria-hidden="true">*</span>
          <input
            ref={firstInputRef}
            type="text"
            value={about.fullName}
            onChange={(e) => setAbout({ fullName: e.target.value })}
            autoComplete="name"
            required
          />
        </label>
      </div>
      <div className="form-row two-col">
        <label>
          Email<span className="oa-required-mark" aria-hidden="true">*</span>
          <input
            type="email"
            value={about.email}
            onChange={(e) => setAbout({ email: e.target.value })}
            autoComplete="email"
            required
          />
        </label>
        <label>
          Phone<span className="oa-required-mark" aria-hidden="true">*</span>
          <input
            type="tel"
            value={about.phone}
            onChange={(e) => setAbout({ phone: e.target.value })}
            autoComplete="tel"
            required
          />
        </label>
      </div>
      <div className="form-row">
        <label>
          Your postcode<span className="oa-required-mark" aria-hidden="true">*</span>
          <input
            type="text"
            value={about.postcode}
            onChange={(e) => setAbout({ postcode: e.target.value.toUpperCase() })}
            placeholder="e.g. W3 7QS"
            autoComplete="postal-code"
            inputMode="text"
            required
          />
        </label>
        {pcMessage && (
          <div className={pcClass} role="status" aria-live="polite">
            {pcMessage}
          </div>
        )}
      </div>
      <fieldset className="kp-radio-group">
        <legend>Do you hold a UK driving licence?<span className="oa-required-mark" aria-hidden="true">*</span></legend>
        <label className={`kp-radio ${about.driversLicence === 'Yes' ? 'is-selected' : ''}`}>
          <input
            type="radio"
            name="driversLicence"
            value="Yes"
            checked={about.driversLicence === 'Yes'}
            onChange={() => setAbout({ driversLicence: 'Yes' })}
            required
          />
          <span>Yes</span>
        </label>
        <label className={`kp-radio ${about.driversLicence === 'No' ? 'is-selected' : ''}`}>
          <input
            type="radio"
            name="driversLicence"
            value="No"
            checked={about.driversLicence === 'No'}
            onChange={() => setAbout({ driversLicence: 'No' })}
          />
          <span>No</span>
        </label>
      </fieldset>
      <div className="oa-actions">
        <button type="button" className="oa-back" onClick={onBack}>Back</button>
        <button
          type="button"
          className="submit"
          onClick={onContinue}
          disabled={!canContinue}
        >
          Continue <Icon name="arrow-right" />
        </button>
      </div>
    </div>
  );
}

function KpStepDocs({ about, setAbout, files, setFiles, firstInputRef, onBack, onContinue, canContinue }) {
  const onCv = (e) => {
    const f = e.target.files?.[0];
    if (f) setFiles((s) => ({ ...s, cv: f }));
  };
  const onPhoto = (e) => {
    const f = e.target.files?.[0];
    if (f) setFiles((s) => ({ ...s, photo: f }));
  };
  return (
    <div>
      <h3>Your experience &amp; documents</h3>
      <p className="desc">
        Tell us about yourself in your own words. No need to write a cover letter — a paragraph or two on what you've done is plenty.
      </p>
      <div className="form-row">
        <label>
          Experience<span className="oa-required-mark" aria-hidden="true">*</span>
          <textarea
            ref={firstInputRef}
            value={about.experience}
            onChange={(e) => setAbout({ experience: e.target.value })}
            rows={6}
            placeholder="What kit have you worked with? Any prep, AC, DIT or grip experience? Studios or rental houses you've been through?"
            required
          />
        </label>
      </div>
      <div className="form-row">
        <label>
          Anything else<span className="oa-required-mark" aria-hidden="true">*</span>
          <textarea
            value={about.additional}
            onChange={(e) => setAbout({ additional: e.target.value })}
            rows={4}
            placeholder="Days you're typically available, certifications, anything else we should know."
            required
          />
        </label>
      </div>

      {/* CV + photo — reuse the Open Account .oa-file-drop chrome so the
          drop zones read as the same affordance across the two wizards.
          Stacked rather than side-by-side; on Open Account the doc slots
          stack too. */}
      <div className="field full oa-file-field">
        <label>
          CV (PDF or doc, max 10MB)
          <span className="oa-required-mark" aria-hidden="true">*</span>
        </label>
        {files.cv ? (
          <div className="oa-file-preview">
            <div className="oa-file-thumb"><Icon name="file" /></div>
            <div className="oa-file-info">
              <div className="oa-file-name">{files.cv.name}</div>
              <div className="oa-file-meta">{kpFmtSize(files.cv.size)} · {files.cv.type || 'file'}</div>
            </div>
            <button
              type="button"
              className="oa-file-remove"
              onClick={() => setFiles((s) => ({ ...s, cv: null }))}
            >
              Remove
            </button>
          </div>
        ) : (
          <label className="oa-file-drop">
            <Icon name="upload" />
            <span className="oa-file-drop-lbl">Choose file or drop here</span>
            <input
              type="file"
              accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
              onChange={onCv}
              required
            />
          </label>
        )}
      </div>

      <div className="field full oa-file-field">
        <label>
          A recent photo of you
          <span className="oa-required-mark" aria-hidden="true">*</span>
        </label>
        {files.photo ? (
          <div className="oa-file-preview">
            <div className="oa-file-thumb"><Icon name="user" /></div>
            <div className="oa-file-info">
              <div className="oa-file-name">{files.photo.name}</div>
              <div className="oa-file-meta">{kpFmtSize(files.photo.size)} · {files.photo.type || 'file'}</div>
            </div>
            <button
              type="button"
              className="oa-file-remove"
              onClick={() => setFiles((s) => ({ ...s, photo: null }))}
            >
              Remove
            </button>
          </div>
        ) : (
          <label className="oa-file-drop">
            <Icon name="upload" />
            <span className="oa-file-drop-lbl">Choose file or drop here</span>
            <input
              type="file"
              accept="image/*"
              capture="user"
              onChange={onPhoto}
              required
            />
          </label>
        )}
      </div>

      <div className="oa-actions">
        <button type="button" className="oa-back" onClick={onBack}>Back</button>
        <button
          type="button"
          className="submit"
          onClick={onContinue}
          disabled={!canContinue}
        >
          Review <Icon name="arrow-right" />
        </button>
      </div>
    </div>
  );
}

function KpStepReview({ about, files, onBack, onSubmit, submitting, submitError, uploadStatus }) {
  const Row = ({ label, value }) => (
    <div className="kp-review-row">
      <div className="kp-review-label">{label}</div>
      <div className="kp-review-value">{value || <em className="kp-review-empty">—</em>}</div>
    </div>
  );
  const fmtStatus = (s) => {
    if (s === 'idle') return null;
    if (s === 'uploading') return 'uploading…';
    if (s === 'done') return 'uploaded';
    return s.replace(/^error:\s*/, 'failed: ');
  };
  return (
    <div>
      <h3>Review &amp; submit</h3>
      <p className="desc">Quick check — anything wrong, hit Back to fix it.</p>
      <div className="kp-review">
        <Row label="Full name" value={about.fullName} />
        <Row label="Email" value={about.email} />
        <Row label="Phone" value={about.phone} />
        <Row label="Postcode" value={about.postcode} />
        <Row label="Area" value={about.location} />
        <Row label="Drivers licence" value={about.driversLicence} />
        <Row label="CV" value={files.cv?.name} />
        <Row label="Photo" value={files.photo?.name} />
        <Row label="Experience" value={about.experience} />
        <Row label="Additional" value={about.additional} />
      </div>

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

      {(uploadStatus.cv !== 'idle' || uploadStatus.photo !== 'idle') && (
        <div className="kp-upload-status">
          <div>CV: {fmtStatus(uploadStatus.cv) || '—'}</div>
          <div>Photo: {fmtStatus(uploadStatus.photo) || '—'}</div>
        </div>
      )}

      <div className="oa-actions">
        <button type="button" className="oa-back" onClick={onBack} disabled={submitting}>Back</button>
        <button
          type="button"
          className="submit"
          onClick={onSubmit}
          disabled={submitting}
        >
          {submitting ? 'Submitting…' : <>Submit application <Icon name="arrow-right" /></>}
        </button>
      </div>
    </div>
  );
}

// Modal shown when a postcode resolves to a location too far from
// the studio. Dismissable, but the Continue button remains disabled
// (via stepValid) until the applicant types a closer postcode.
function KpTooFarModal({ postcodeCheck, onDismiss }) {
  const onBackdropClick = (e) => {
    if (e.target === e.currentTarget) onDismiss();
  };
  const onKeyDown = (e) => {
    if (e.key === 'Escape') onDismiss();
  };
  useEffectJobs(() => {
    document.body.classList.add('vf-modal-open');
    return () => document.body.classList.remove('vf-modal-open');
  }, []);
  return (
    <div
      className="kp-modal-backdrop"
      role="dialog"
      aria-modal="true"
      aria-labelledby="kp-too-far-title"
      onClick={onBackdropClick}
      onKeyDown={onKeyDown}
      tabIndex={-1}
    >
      <div className="kp-modal">
        <h3 id="kp-too-far-title">Sorry — locals only at this stage</h3>
        <p>
          {postcodeCheck.area ? <strong>{postcodeCheck.label}</strong> : <strong>{postcodeCheck.postcode}</strong>}
          {postcodeCheck.miles != null && <> is about {postcodeCheck.miles.toFixed(1)} miles from our Acton studio</>}
          {' '}— a little further than we can reasonably ask for at this stage.
        </p>
        <p>
          We're currently only hiring Kit Prep Technicians who can comfortably commute to W3 7QS within about 45 minutes door-to-door.
        </p>
        <p className="kp-modal-fineprint">
          If you think this is wrong, or you'd be moving closer soon, email <a href="mailto:rentals@valley.film">rentals@valley.film</a> and we'll take a look.
        </p>
        <button type="button" className="submit" onClick={onDismiss} autoFocus>
          OK
        </button>
      </div>
    </div>
  );
}

Object.assign(window, { KitPrepTechPage });
