// MODI graft calculator — main app with multi-step flow
const { useState, useEffect, useMemo, useRef } = React;

// 캐주얼 흐름 (intro → … → result): 본인정보는 브라우저에만 머묾, 서버 미전송
// 수가 흐름 (price-consent → … → price-done): 명시 동의 후 시트로 전송 + 저장
const STEPS = [
  'intro', 'info', 'photo', 'pattern', 'area', 'hairline', 'density', 'result',
  'price-consent', 'price', 'price-done',
];
const CASUAL_STEPS = ['intro', 'info', 'photo', 'pattern', 'area', 'hairline', 'density', 'result'];
const PRICE_STEPS = ['price-consent', 'price', 'price-done'];

// ────────── 익명 세션 ID + 이벤트 트래킹 ──────────
// 사진 dataURL 리사이즈(공용). AI 분석은 512px, Storage 보관은 1280px.
// Vercel 서버리스 body 한도(4.5MB) 안에 들어오도록 클라이언트에서 줄여 보냄.
function resizeDataUrlShared(dataUrl, maxDim, quality) {
  return new Promise((resolve, reject) => {
    if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) {
      reject(new Error('not a data url'));
      return;
    }
    const img = new Image();
    img.onload = () => {
      let { width, height } = img;
      const r = Math.max(width, height) / maxDim;
      if (r > 1) { width = Math.round(width / r); height = Math.round(height / r); }
      const c = document.createElement('canvas');
      c.width = width; c.height = height;
      c.getContext('2d').drawImage(img, 0, 0, width, height);
      resolve(c.toDataURL('image/jpeg', quality));
    };
    img.onerror = reject;
    img.src = dataUrl;
  });
}

function getSessionId() {
  try {
    let id = localStorage.getItem('modi-session-id');
    if (!id) {
      id = (window.crypto && crypto.randomUUID) ? crypto.randomUUID()
        : 'sid-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10);
      localStorage.setItem('modi-session-id', id);
    }
    return id;
  } catch (e) { return ''; }
}

// ────────── 유입 경로 추적 (attribution) ──────────
// 진입 URL의 ref / utm_* 파라미터를 캡처해 localStorage에 보관.
// "last tagged touch": 태그가 있는 방문이 있으면 갱신, 태그 없는(direct) 방문은
// 기존 값을 덮어쓰지 않음 → 마지막으로 클릭한 캠페인 링크가 기록됨.
// 모듈 로드 시 1회 실행.
const ATTRIBUTION_KEY = 'modi-calc-attribution';
function captureAttribution() {
  try {
    const params = new URLSearchParams(window.location.search);
    const ref = params.get('ref') || '';
    const utmSource = params.get('utm_source') || '';
    const utmMedium = params.get('utm_medium') || '';
    const utmCampaign = params.get('utm_campaign') || '';
    // 태그가 하나도 없으면 기존 값 유지(덮어쓰지 않음)
    if (!ref && !utmSource && !utmMedium && !utmCampaign) return;
    const data = {
      ref, utmSource, utmMedium, utmCampaign,
      capturedAt: new Date().toISOString(),
    };
    localStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(data));
  } catch (e) {}
}
function getAttribution() {
  try {
    const raw = localStorage.getItem(ATTRIBUTION_KEY);
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (e) { return null; }
}
captureAttribution();

function track(eventType, metadata) {
  try {
    fetch('/api/track/', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        app: 'patient_calculator',
        event_type: eventType,
        session_id: getSessionId(),
        metadata: metadata || {},
      }),
      keepalive: true,
    }).catch(() => {});
  } catch (e) {}
}
function useTrackOnMount(eventType) {
  const firedRef = useRef(false);
  useEffect(() => {
    if (firedRef.current) return;
    firedRef.current = true;
    track(eventType);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
}

function App() {
  const [step, setStep] = useState('intro');
  const [data, setData] = useState({
    // 캐주얼 흐름에서 수집
    gender: '',
    photos: { front: null, top: null, side: null },
    pattern: null,
    areas: [],
    hairlineLower: null,
    density: 'standard',
    // 수가 흐름에서 수집 (동의 후)
    name: '',
    birthY: '', birthM: '', birthD: '',
    phone1: '010', phone2: '', phone3: '',
    methodId: 'incision',
    stemId: 'none',
    // AI 보정 결과 (ResultStep에서 분석 완료 시 저장 → 수가 계산에서 사용)
    aiAdjustDelta: 0,
    aiAdjustReason: '',
    // 동의 플래그
    consentContact: false,
    consentSensitive: false,
    consentInternational: false,
    consentMarketing: false,
    consentedAt: null,
  });

  // Persist to localStorage
  useEffect(() => {
    try {
      const saved = localStorage.getItem('modi-calc-state');
      if (saved) {
        const s = JSON.parse(saved);
        if (s.step) setStep(s.step);
        if (s.data) setData(d => ({ ...d, ...s.data, photos: s.data.photos || d.photos }));
      }
    } catch (e) {}
  }, []);
  useEffect(() => {
    try {
      const lite = { step, data: { ...data, photos: {
        front: data.photos.front ? 'saved' : null,
        top: data.photos.top ? 'saved' : null,
        side: data.photos.side ? 'saved' : null,
      } } };
      localStorage.setItem('modi-calc-state', JSON.stringify(lite));
    } catch (e) {}
  }, [step, data]);

  // 진행률: 캐주얼은 intro~result 구간, 수가 흐름은 별도 구간으로 표시
  const isCasualStep = CASUAL_STEPS.includes(step);
  const isPriceStep = PRICE_STEPS.includes(step);
  const stepIdx = STEPS.indexOf(step);
  const casualStepIdx = CASUAL_STEPS.indexOf(step);
  const priceStepIdx = PRICE_STEPS.indexOf(step);
  const progress = step === 'intro' ? 0
    : step === 'result' ? 1
    : isCasualStep ? casualStepIdx / (CASUAL_STEPS.length - 1)
    : isPriceStep ? (priceStepIdx + 1) / PRICE_STEPS.length
    : 1;

  const update = (k, v) => setData(d => ({ ...d, [k]: v }));

  const reset = () => {
    setData({
      gender: '',
      photos: { front: null, top: null, side: null },
      pattern: null, areas: [], hairlineLower: null, density: 'standard',
      name: '',
      birthY: '', birthM: '', birthD: '',
      phone1: '010', phone2: '', phone3: '',
      methodId: 'incision', stemId: 'none',
      aiAdjustDelta: 0, aiAdjustReason: '',
      consentContact: false, consentSensitive: false, consentInternational: false, consentMarketing: false,
      consentedAt: null,
    });
    setStep('intro');
    try { localStorage.removeItem('modi-calc-state'); } catch (e) {}
    try { sessionStorage.removeItem('modi-calc-submitted'); } catch (e) {}
  };

  const goTo = (s) => { setStep(s); try { window.scrollTo({ top: 0 }); } catch (e) {} };

  // 시퀀셜 next/back은 같은 흐름(캐주얼 or 가격) 내에서만 동작
  const flowOf = (s) => CASUAL_STEPS.includes(s) ? CASUAL_STEPS : PRICE_STEPS.includes(s) ? PRICE_STEPS : null;
  const next = () => {
    const flow = flowOf(step);
    if (!flow) return;
    const i = flow.indexOf(step);
    if (i < flow.length - 1) goTo(flow[i + 1]);
  };
  const back = () => {
    const flow = flowOf(step);
    if (!flow) return;
    const i = flow.indexOf(step);
    if (i > 0) goTo(flow[i - 1]);
    else if (flow === PRICE_STEPS) goTo('result'); // 수가 흐름 첫 단계에서 뒤로 = 결과 화면
  };

  const isTerminal = step === 'result' || step === 'price-done';
  const onBackClick = isTerminal ? reset : back;
  const showProgress = !isTerminal && step !== 'intro';

  return (
    <div className="app" data-style="A">
      {step === 'intro' && <LanguageSwitcher />}
      {step !== 'intro' && (
        <React.Fragment>
          <div className="topbar">
            <button
              className="topbar-back"
              aria-label={window.t('common.back_aria')}
              onClick={onBackClick}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <polyline points="15 18 9 12 15 6"/>
              </svg>
            </button>
            <img src="assets/modi_logo_en_transparent.png" alt="MODI" className="topbar-logo" />
            {isCasualStep && step !== 'result' && (
              <span className="topbar-step">{window.t('topbar.step_casual', { current: casualStepIdx, total: CASUAL_STEPS.length - 2 })}</span>
            )}
            {isPriceStep && step !== 'price-done' && (
              <span className="topbar-step">{window.t('topbar.step_price', { current: priceStepIdx + 1 })}</span>
            )}
            {isTerminal && (
              <button className="topbar-step" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--modi-green)', fontWeight: 600 }} onClick={reset}>{window.t('common.reset')}</button>
            )}
          </div>
          {showProgress && (
            <div className="progress">
              <div className="progress-fill" style={{ width: `${progress * 100}%` }} />
            </div>
          )}
        </React.Fragment>
      )}

      {step === 'intro' && <IntroScreen onStart={() => goTo('info')} />}
      {step === 'info' && <InfoStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'photo' && <PhotoStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'pattern' && <PatternStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'area' && <AreaStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'hairline' && <HairlineStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'density' && <DensityStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'result' && <ResultStep data={data} update={update} onReset={reset} onGetPrice={() => goTo('price-consent')} />}
      {step === 'price-consent' && <PriceConsentStep data={data} update={update} onNext={next} onBack={back} />}
      {step === 'price' && <PriceStep data={data} update={update} onNext={next} onBack={back} onSubmitted={() => goTo('price-done')} />}
      {step === 'price-done' && <PriceDoneStep data={data} onReset={reset} />}
    </div>
  );
}

// ────────── Language Switcher ──────────
// Intro 화면에서만 노출 (상단 우측). 선택 시 location 새로고침으로 전체 트리 리렌더.
function LanguageSwitcher() {
  const cur = window.currentLocale || 'ko';
  return (
    <div className="lang-switcher">
      <button type="button"
        className={`lang-btn${cur === 'ko' ? ' on' : ''}`}
        onClick={() => cur !== 'ko' && window.setLocale('ko')}>
        한국어
      </button>
      <button type="button"
        className={`lang-btn${cur === 'en' ? ' on' : ''}`}
        onClick={() => cur !== 'en' && window.setLocale('en')}>
        English
      </button>
      <button type="button"
        className={`lang-btn${cur === 'zh-TW' ? ' on' : ''}`}
        onClick={() => cur !== 'zh-TW' && window.setLocale('zh-TW')}>
        繁中
      </button>
    </div>
  );
}

// ────────── Intro ──────────
function IntroScreen({ onStart }) {
  useTrackOnMount('intro_view');
  return (
    <div className="intro fade-in">
      <div className="intro-logo-wrap">
        <img src="assets/modi_drop.jpg" alt="" className="intro-mark" />
        <div>
          <h1 className="intro-headline">{window.t('intro.headline_line1')}<br/>{window.t('intro.headline_line2')}</h1>
          <p className="intro-sub" style={{ marginTop: 14 }}>
            {window.t('intro.sub_line1')}<br/>{window.t('intro.sub_line2')}
          </p>
        </div>
        <div className="intro-meta">
          <div className="intro-meta-item"><span className="intro-meta-dot"></span>{window.t('intro.meta_duration')}</div>
          <div className="intro-meta-item"><span className="intro-meta-dot"></span>{window.t('intro.meta_free')}</div>
          <div className="intro-meta-item"><span className="intro-meta-dot"></span>{window.t('intro.meta_review')}</div>
        </div>
      </div>
      <div style={{ width: '100%' }}>
        <button className="btn btn-primary intro-cta" onClick={onStart}>
          {window.t('intro.cta')} <window.Glyph name="arrow" size={18} />
        </button>
        <p className="intro-tos" dangerouslySetInnerHTML={{ __html: window.t('intro.tos_html') }} />
      </div>
    </div>
  );
}

// ────────── Info step — 이름·성별·생년월일·연락처 ──────────
// 핵심: 캐주얼 흐름에서 받은 값은 브라우저(React state + localStorage)에만 머묾.
// 우리 서버로 전송되는 시점은 수가 흐름의 PriceStep 제출 + 동의 후뿐.
function InfoStep({ data, update, onNext, onBack }) {
  const valid = data.name.trim().length >= 2
    && !!data.gender
    && data.birthY.length === 4
    && data.birthM.length >= 1
    && data.birthD.length >= 1
    && data.phone2.length >= 3
    && data.phone3.length === 4;

  const genders = [
    { id: 'male', labelKey: 'info.gender_male' },
    { id: 'female', labelKey: 'info.gender_female' },
  ];

  // 숫자 입력 자동 포커스 이동 (010 칸 제외)
  const seq = ['birthY','birthM','birthD','phone2','phone3'];
  const maxLen = { birthY: 4, birthM: 2, birthD: 2, phone1: 3, phone2: 4, phone3: 4 };
  const refs = {
    birthY: useRef(null), birthM: useRef(null), birthD: useRef(null),
    phone1: useRef(null), phone2: useRef(null), phone3: useRef(null),
  };
  const handleNum = (key) => (e) => {
    const v = e.target.value.replace(/\D/g, '').slice(0, maxLen[key]);
    update(key, v);
    if (v.length === maxLen[key]) {
      const nx = seq[seq.indexOf(key) + 1];
      if (nx) refs[nx].current?.focus();
    }
  };
  const handleNumKeyDown = (key) => (e) => {
    if (e.key === 'Backspace' && !e.currentTarget.value) {
      const prev = seq[seq.indexOf(key) - 1];
      if (prev) { e.preventDefault(); refs[prev].current?.focus(); }
    }
  };

  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('info.eyebrow')}</p>
        <h2 className="step-title">{window.t('info.title_line1')}<br/>{window.t('info.title_line2')}</h2>
        <p className="step-sub">{window.t('info.sub_line1')}<br/>{window.t('info.sub_line2')}</p>

        <div className="field">
          <label className="field-label">{window.t('info.name_label')}<span className="req">*</span></label>
          <input className="input" placeholder={window.t('info.name_placeholder')} value={data.name}
            onChange={e => update('name', e.target.value)} />
        </div>

        <div className="field">
          <label className="field-label">{window.t('info.gender_label')}<span className="req">*</span></label>
          <div className="toggle-group">
            {genders.map(g => (
              <button key={g.id} type="button"
                className={`toggle-btn${data.gender === g.id ? ' on' : ''}`}
                onClick={() => update('gender', g.id)}>
                {window.t(g.labelKey)}
              </button>
            ))}
          </div>
        </div>

        <div className="field">
          <label className="field-label">{window.t('info.birth_label')}<span className="req">*</span></label>
          <div className="field-row">
            <input ref={refs.birthY} className="input" placeholder={window.t('info.birth_y')} maxLength={4} inputMode="numeric"
              value={data.birthY} onChange={handleNum('birthY')} onKeyDown={handleNumKeyDown('birthY')} />
            <input ref={refs.birthM} className="input" placeholder={window.t('info.birth_m')} maxLength={2} inputMode="numeric"
              value={data.birthM} onChange={handleNum('birthM')} onKeyDown={handleNumKeyDown('birthM')} />
            <input ref={refs.birthD} className="input" placeholder={window.t('info.birth_d')} maxLength={2} inputMode="numeric"
              value={data.birthD} onChange={handleNum('birthD')} onKeyDown={handleNumKeyDown('birthD')} />
          </div>
        </div>

        <div className="field">
          <label className="field-label">{window.t('info.phone_label')}<span className="req">*</span></label>
          <div className="field-row">
            <input ref={refs.phone1} className="input" maxLength={3} inputMode="numeric"
              value={data.phone1}
              onChange={e => update('phone1', e.target.value.replace(/\D/g, ''))} />
            <input ref={refs.phone2} className="input" placeholder={window.t('info.phone_mid_ph')} maxLength={4} inputMode="numeric"
              value={data.phone2} onChange={handleNum('phone2')} onKeyDown={handleNumKeyDown('phone2')} />
            <input ref={refs.phone3} className="input" placeholder={window.t('info.phone_last_ph')} maxLength={4} inputMode="numeric"
              value={data.phone3} onChange={handleNum('phone3')} onKeyDown={handleNumKeyDown('phone3')} />
          </div>
        </div>

        <div className="note-box" style={{ marginTop: 8 }}>
          <window.Glyph name="shield" size={18} color="var(--modi-green)" />
          <span dangerouslySetInnerHTML={{ __html: window.t('info.security_html') }} />
        </div>
      </div>
      <div className="footer">
        <button className="btn btn-primary" disabled={!valid} onClick={onNext}>{window.t('common.next')}</button>
      </div>
    </React.Fragment>
  );
}

// ────────── Photo step ──────────
function PhotoStep({ data, update, onNext, onBack }) {
  // 슬롯 탭 시 OS 기본 시트("사진 찍기 / 사진 보관함 / 파일 선택")를
  // 띄우기 위해 capture 속성은 의도적으로 생략. 카메라 강제 진입 X.
  const slots = [
    { id: 'front', labelKey: 'photo.slot_front_label', hintKey: 'photo.slot_front_hint' },
    { id: 'top', labelKey: 'photo.slot_top_label', hintKey: 'photo.slot_top_hint' },
    { id: 'side', labelKey: 'photo.slot_side_label', hintKey: 'photo.slot_side_hint' },
  ];

  const handleFile = (slotId, file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (e) => {
      update('photos', { ...data.photos, [slotId]: e.target.result });
    };
    reader.readAsDataURL(file);
  };

  const remove = (slotId) => {
    update('photos', { ...data.photos, [slotId]: null });
  };

  const count = Object.values(data.photos).filter(Boolean).length;
  const valid = count >= 1; // at least one photo

  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('photo.eyebrow')}</p>
        <h2 className="step-title">{window.t('photo.title')}</h2>
        <p className="step-sub">{window.t('photo.sub_line1')}<br/>{window.t('photo.sub_line2')}</p>

        <div className="photo-grid">
          {slots.map(s => {
            const src = data.photos[s.id];
            const label = window.t(s.labelKey);
            return (
              <label key={s.id} className={`photo-slot${src ? ' full' : ''}`}>
                {src ? (
                  <React.Fragment>
                    <img src={src} alt={label} />
                    <span className="photo-overlay-tag">{label}</span>
                    <button className="photo-remove" onClick={(e) => { e.preventDefault(); remove(s.id); }}>×</button>
                  </React.Fragment>
                ) : (
                  <React.Fragment>
                    <window.Glyph name="camera" size={28} color="var(--modi-green)" />
                    <span className="photo-slot-label">{label}</span>
                    <span className="photo-slot-hint">{window.t(s.hintKey)}</span>
                  </React.Fragment>
                )}
                <input type="file" accept="image/*"
                  style={{ display: 'none' }}
                  onChange={(e) => handleFile(s.id, e.target.files?.[0])} />
              </label>
            );
          })}
          <label className="photo-slot" style={{ background: 'transparent', borderStyle: 'dashed' }}>
            <window.Glyph name="sparkle" size={24} color="var(--muted)" />
            <span className="photo-slot-label" style={{ color: 'var(--muted)' }}>{window.t('photo.skip_label')}</span>
            <span className="photo-slot-hint">{window.t('photo.skip_hint')}</span>
            <input type="checkbox" style={{ display: 'none' }} onChange={() => onNext()} />
          </label>
        </div>

        <div className="note-box">
          <window.Glyph name="info" size={18} color="var(--modi-green)" />
          <span dangerouslySetInnerHTML={{ __html: window.t('photo.info_html') }} />
        </div>
      </div>
      <div className="footer">
        <button className="btn btn-primary" disabled={!valid} onClick={onNext}>
          {window.t('common.next')}
        </button>
      </div>
    </React.Fragment>
  );
}

// ────────── Pattern step ──────────
function PatternStep({ data, update, onNext, onBack }) {
  const isFemale = data.gender === 'female';
  const patterns = isFemale ? window.FEMALE_PATTERNS : window.PATTERNS;
  const valid = !!data.pattern;
  const labelKeyFor = (id) => `pattern.${isFemale ? 'female' : 'male'}.${id}_label`;
  const descKeyFor = (id) => `pattern.${isFemale ? 'female' : 'male'}.${id}_desc`;
  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('pattern.eyebrow')}</p>
        <h2 className="step-title">{window.t('pattern.title_line1')}<br/>{window.t('pattern.title_line2')}</h2>
        <p className="step-sub">{isFemale
          ? <>{window.t('pattern.sub_female_line1')}<br/>{window.t('pattern.sub_female_line2')}</>
          : <>{window.t('pattern.sub_male_line1')}<br/>{window.t('pattern.sub_male_line2')}</>}</p>

        <div className="pattern-grid">
          {patterns.map(p => {
            const selected = data.pattern === p.id;
            return (
              <button key={p.id}
                className={`pattern-card${selected ? ' selected' : ''}`}
                onClick={() => update('pattern', p.id)}>
                <div className="pattern-check"><window.Glyph name="check" size={11} color="white" /></div>
                <div className="pattern-card-svg">
                  <img src={p.img} alt="" />
                </div>
                <p className="pattern-card-label">{window.t(labelKeyFor(p.id))}</p>
                <p className="pattern-card-desc">{window.t(descKeyFor(p.id))}</p>
              </button>
            );
          })}
        </div>
      </div>
      <div className="footer">
        <button className="btn btn-primary" disabled={!valid} onClick={onNext}>{window.t('common.next')}</button>
      </div>
    </React.Fragment>
  );
}

// ────────── Area step ──────────
function AreaStep({ data, update, onNext, onBack }) {
  const toggle = (id) => {
    const cur = data.areas;
    update('areas', cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id]);
  };
  const valid = data.areas.length > 0;
  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('area.eyebrow')}</p>
        <h2 className="step-title">{window.t('area.title')}</h2>
        <p className="step-sub">{window.t('area.sub')}</p>

        <div className="chip-grid">
          {window.AREAS.map(a => {
            const selected = data.areas.includes(a.id);
            return (
              <button key={a.id}
                className={`chip${selected ? ' selected' : ''}`}
                onClick={() => toggle(a.id)}>
                <span className="chip-icon"><img src={(data.gender === 'female' ? window.AREA_IMG_FEMALE : window.AREA_IMG)[a.id]} alt="" style={{ width: 56, height: 56, objectFit: 'contain' }} /></span>
                {window.t('area.' + a.id)}
              </button>
            );
          })}
        </div>
      </div>
      <div className="footer">
        <button className="btn btn-primary" disabled={!valid} onClick={onNext}>{window.t('common.next')}</button>
      </div>
    </React.Fragment>
  );
}

// ────────── Hairline lowering step ──────────
function HairlineStep({ data, update, onNext, onBack }) {
  const options = [
    { id: 'no', labelKey: 'hairline.no_label', descKey: 'hairline.no_desc' },
    { id: '1', labelKey: 'hairline.cm1_label', descKey: 'hairline.cm1_desc' },
    { id: '2', labelKey: 'hairline.cm2_label', descKey: 'hairline.cm2_desc' },
    { id: '3', labelKey: 'hairline.cm3_label', descKey: 'hairline.cm3_desc' },
    { id: 'unsure', labelKey: 'hairline.unsure_label', descKey: 'hairline.unsure_desc' },
  ];
  const valid = !!data.hairlineLower;
  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('hairline.eyebrow')}</p>
        <h2 className="step-title">{window.t('hairline.title_line1')}<br/>{window.t('hairline.title_line2')}</h2>
        <p className="step-sub">{window.t('hairline.sub_line1')}<br/>{window.t('hairline.sub_line2')}</p>

        <div className="chip-grid" style={{ gridTemplateColumns: '1fr' }}>
          {options.map(o => {
            const selected = data.hairlineLower === o.id;
            return (
              <button key={o.id}
                className={`chip${selected ? ' selected' : ''}`}
                onClick={() => update('hairlineLower', o.id)}>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                  <span style={{ fontSize: 15, fontWeight: 600 }}>{window.t(o.labelKey)}</span>
                  <span style={{ fontSize: 12, color: 'var(--muted)', fontWeight: 400 }}>{window.t(o.descKey)}</span>
                </div>
              </button>
            );
          })}
        </div>
      </div>
      <div className="footer">
        <button className="btn btn-primary" disabled={!valid} onClick={onNext}>{window.t('common.next')}</button>
      </div>
    </React.Fragment>
  );
}

// ────────── Density step ──────────
function DensityStep({ data, update, onNext, onBack }) {
  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('density.eyebrow')}</p>
        <h2 className="step-title">{window.t('density.title')}</h2>
        <p className="step-sub">{window.t('density.sub_line1')}<br/>{window.t('density.sub_line2')}</p>

        <div className="chip-grid" style={{ gridTemplateColumns: '1fr' }}>
          {window.DENSITY.map(d => {
            const selected = data.density === d.id;
            return (
              <button key={d.id}
                className={`chip${selected ? ' selected' : ''}`}
                onClick={() => update('density', d.id)}
                style={{ flexDirection: 'row', justifyContent: 'space-between', padding: '16px 18px' }}>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'flex-start' }}>
                  <span style={{ fontSize: 15, fontWeight: 600 }}>{window.t('density.' + d.id + '_label')}</span>
                  <span style={{ fontSize: 12.5, color: 'var(--muted)', fontWeight: 400 }}>{window.t('density.' + d.id + '_desc')}</span>
                </div>
                {selected && <window.Glyph name="check" size={18} color="var(--modi-green)" />}
              </button>
            );
          })}
        </div>
      </div>
      <div className="footer">
        <button className="btn btn-primary" onClick={onNext}>{window.t('density.result_button')}</button>
      </div>
    </React.Fragment>
  );
}

// ────────── Result step ──────────
function ResultStep({ data, update, onReset, onGetPrice }) {
  useTrackOnMount('result_view');
  // 캐주얼 흐름에서 birthY는 없음 (수가 흐름 진입 후 입력) → age는 null
  const age = useMemo(() => {
    const y = parseInt(data.birthY, 10);
    if (!y) return null;
    return new Date().getFullYear() - y;
  }, [data.birthY]);

  const baseResult = useMemo(() => window.calculateGrafts({
    patternId: data.pattern,
    areaIds: data.areas,
    densityId: data.density,
    hairlineLower: data.hairlineLower,
    age,
    gender: data.gender,
  }), [data, age]);

  // 캐주얼 흐름에서는 시트 자동 제출하지 않음 (수가 단계 동의 후에만 제출)

  // ────────── AI 정밀 분석 (Claude vision) ──────────
  const photoCount = ['front','top','side'].filter(k => data.photos?.[k]).length;
  // 사진을 업로드한 환자는 결과 진입 시 자동으로 AI 분석 시작 → 초기 state를 loading으로
  const [aiState, setAiState] = useState(photoCount > 0 ? 'loading' : 'idle'); // idle | loading | done | error
  const [aiAdjust, setAiAdjust] = useState(null); // { delta, reason }
  const [aiError, setAiError] = useState('');

  const resizeDataUrl = (dataUrl, maxDim = 512) => resizeDataUrlShared(dataUrl, maxDim, 0.8);

  // ref 기반 동시 실행 락. state 기반 가드는 초기값이 'loading'이라 첫 자동 호출이 즉시 return되던 버그.
  const runningRef = useRef(false);
  const runAi = async () => {
    if (runningRef.current) return;
    runningRef.current = true;
    setAiState('loading');
    setAiError('');
    try {
      const photos = {};
      for (const k of ['front','top','side']) {
        const url = data.photos?.[k];
        if (!url || typeof url !== 'string' || !url.startsWith('data:')) continue;
        photos[k] = await resizeDataUrl(url, 512);
      }

      const patient = {
        gender: data.gender,
        age,
        patternLabel: pattern ? window.tKo('pattern.' + (data.gender === 'female' ? 'female' : 'male') + '.' + pattern.id + '_label') : null,
        areaLabels,
        densityLabel: density ? window.tKo('density.' + density.id + '_label') : null,
        baseLow: baseResult.low,
        baseHigh: baseResult.high,
        locale: window.currentLocale || 'ko',
      };

      const res = await fetch('/api/analyze-photo/', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ photos, patient }),
      });
      if (!res.ok) {
        const errText = await res.text().catch(() => '');
        throw new Error(`서버 오류 (${res.status}). ${errText.slice(0, 120)}`);
      }
      const parsed = await res.json();
      const delta = Math.max(-25, Math.min(25, parseInt(parsed.delta_pct, 10) || 0));
      setAiAdjust({ delta, reason: parsed.reason || '', crownLowDensity: parsed.crown_low_density === true });
      setAiState('done');
      // 수가 흐름에서 동일한 보정값을 쓰도록 App data에도 저장
      update('aiAdjustDelta', delta);
      update('aiAdjustReason', parsed.reason || '');
    } catch (e) {
      console.error('AI 분석 실패:', e);
      setAiError(String(e?.message || e).slice(0, 200));
      setAiState('error');
    } finally {
      runningRef.current = false;
    }
  };

  // 사진 업로드한 환자는 결과 진입 시 1회 자동 분석
  const autoStartedRef = useRef(false);
  useEffect(() => {
    if (autoStartedRef.current) return;
    if (photoCount === 0 || !baseResult) return;
    autoStartedRef.current = true;
    runAi();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [baseResult, photoCount]);

  // 모수 보정 적용
  const result = useMemo(() => {
    if (!baseResult) return null;
    if (aiState !== 'done' || !aiAdjust) return baseResult;
    const m = 1 + aiAdjust.delta / 100;
    const round = (n) => Math.round((n * m) / 100) * 100;
    const low = round(baseResult.low);
    const high = round(baseResult.high);
    const mid = Math.round(((low + high) / 2) / 100) * 100;
    return { low, high, mid };
  }, [baseResult, aiState, aiAdjust]);

  if (!result) return <div className="scroll"><p>{window.t('common.error')}</p></div>;

  const { low: lowRaw, high: highRaw, mid: midRaw } = result;
  const CAP = 6000;
  const low = Math.min(lowRaw, CAP);
  const high = Math.min(highRaw, CAP);
  const mid = Math.min(midRaw, CAP);
  const pattern = (data.gender === 'female' ? window.FEMALE_PATTERNS : window.PATTERNS).find(p => p.id === data.pattern);
  const density = window.DENSITY.find(d => d.id === data.density);
  // 표시용(현재 로케일)
  const patternLabelDisplay = pattern ? window.t('pattern.' + (data.gender === 'female' ? 'female' : 'male') + '.' + pattern.id + '_label') : '';
  const densityLabelDisplay = density ? window.t('density.' + density.id + '_label') : '';
  const areaLabelsDisplay = data.areas.map(id => window.t('area.' + id)).filter(Boolean);
  // 서버 전송용(한국어 고정, admin은 한국어)
  const areaLabels = data.areas.map(id => window.tKo('area.' + id)).filter(Boolean);

  const phoneNumber = '02-544-8659';
  const kakaoUrl = 'https://pf.kakao.com/_ExmRuT/chat';

  // 영업시간 판정 (KST 기준): 평일 09:30~18:30, 토요일 09:30~16:30, 일요일 휴무
  const isOpenNow = React.useMemo(() => {
    const nowKST = new Date(Date.now() + (new Date().getTimezoneOffset() + 540) * 60000);
    const day = nowKST.getDay(); // 0=Sun
    const minutes = nowKST.getHours() * 60 + nowKST.getMinutes();
    if (day === 0) return false;
    if (day === 6) return minutes >= 570 && minutes <= 990; // 09:30~16:30
    return minutes >= 570 && minutes <= 1110; // 09:30~18:30
  }, []);

  return (
    <div className="scroll fade-in" style={{ paddingBottom: 32 }}>
      <p className="step-eyebrow" style={{ marginTop: 8 }}>{window.t('result.eyebrow')}</p>

      <div className="result-hero">
        <p className="result-eyebrow">{window.t('result.hero_eyebrow')}</p>
        <p className="result-greeting">{window.t('result.hero_greeting', { name: data.name || window.t('result.hero_default_name') })}</p>
        <div className="result-number">
          {window.numberFmt(low)}<span className="unit">~ {window.numberFmt(high)}{window.t('result.unit_graft')}</span>
        </div>
        <p className="result-range">{window.t('result.range_text', { mid: window.numberFmt(mid) })}</p>
        {photoCount > 0 && (aiState === 'loading' || aiState === 'done' || aiState === 'error') && (
          <div className={`result-ai-pill result-ai-pill-${aiState}`}>
            {aiState === 'loading' && (
              <React.Fragment>
                <span className="result-ai-spinner" />
                <span>{window.t('result.ai_pill_loading')}</span>
              </React.Fragment>
            )}
            {aiState === 'done' && (
              <React.Fragment>
                <window.Glyph name="check" size={13} color="white" />
                <span>{window.t('result.ai_pill_done')}</span>
              </React.Fragment>
            )}
            {aiState === 'error' && (
              <React.Fragment>
                <window.Glyph name="info" size={13} color="white" />
                <span>{window.t('result.ai_pill_error')}</span>
              </React.Fragment>
            )}
          </div>
        )}
      </div>

      {photoCount > 0 && (
        <div className={`ai-card ai-${aiState}`}>
          {(aiState === 'loading' || aiState === 'idle') && (
            <React.Fragment>
              <div className="ai-card-head">
                <span className="ai-spinner" />
                <span className="ai-card-title">{window.t('result.ai_card_loading_title')}</span>
              </div>
              <p className="ai-card-desc" dangerouslySetInnerHTML={{ __html: window.t('result.ai_card_loading_desc_html', { photoCount }) }} />
            </React.Fragment>
          )}
          {aiState === 'done' && (
            <React.Fragment>
              <div className="ai-card-head">
                <window.Glyph name="sparkle" size={20} color="var(--modi-green)" />
                <span className="ai-card-title">{window.t('result.ai_card_done_title')}</span>
              </div>
              <div className="ai-card-delta">
                {aiAdjust.delta > 0 && <span className="ai-delta-up">{window.t('result.ai_delta_up', { delta: aiAdjust.delta })}</span>}
                {aiAdjust.delta < 0 && <span className="ai-delta-down">{window.t('result.ai_delta_down', { delta: aiAdjust.delta })}</span>}
                {aiAdjust.delta === 0 && <span className="ai-delta-zero">{window.t('result.ai_delta_zero')}</span>}
              </div>
              {aiAdjust.reason && <p className="ai-card-desc" style={{ marginBottom: 0 }}>{aiAdjust.reason}</p>}
            </React.Fragment>
          )}
          {aiState === 'error' && (
            <React.Fragment>
              <div className="ai-card-head">
                <window.Glyph name="info" size={18} color="#b54040" />
                <span className="ai-card-title">{window.t('result.ai_error_title')}</span>
              </div>
              <p className="ai-card-desc" dangerouslySetInnerHTML={{ __html: window.t('result.ai_error_desc_html') }} />
              {aiError && <p className="ai-card-desc" style={{ fontSize: 11, color: '#b54040', wordBreak: 'break-word' }}>{aiError}</p>}
              <button className="btn btn-ghost ai-card-btn" onClick={runAi}>{window.t('result.ai_retry')}</button>
            </React.Fragment>
          )}
        </div>
      )}

      <div className="result-card">
        <p className="result-card-title">{window.t('result.breakdown_title')}</p>
        <div className="breakdown-row">
          <span className="label">{window.t('result.breakdown_pattern')}</span>
          <span className="val">{patternLabelDisplay}</span>
        </div>
        <div className="breakdown-row">
          <span className="label">{window.t('result.breakdown_area')}</span>
          <span className="val">{areaLabelsDisplay.join(', ') || window.t('common.dash')}</span>
        </div>
        <div className="breakdown-row">
          <span className="label">{window.t('result.breakdown_density')}</span>
          <span className="val">{densityLabelDisplay}</span>
        </div>
      </div>

      <div className="note-box">
        <window.Glyph name="info" size={18} color="var(--modi-green)" />
        <span dangerouslySetInnerHTML={{ __html: window.t('result.info_html') }} />
      </div>

      {highRaw > 6000 && (
        <div className="crown-tip" style={{ background: '#fdf4e7', borderColor: '#e8d4a8' }}>
          <div className="crown-tip-head">
            <window.Glyph name="info" size={18} color="#a66a14" />
            <span style={{ color: '#7a4f0e' }}>{window.t('result.cap_title')}</span>
          </div>
          <p className="crown-tip-desc" dangerouslySetInnerHTML={{ __html: window.t('result.cap_text_html') }} />
        </div>
      )}

      {(() => {
        const isMale = data.gender === 'male';
        const isFemale = data.gender === 'female';
        const hasTopArea = data.areas?.includes('top');
        const malePatternCrown = isMale && (data.pattern === 'crown' || data.pattern === 'mc');
        const aiCrown = aiState === 'done' && aiAdjust?.crownLowDensity;
        const showCrownTip = hasTopArea || malePatternCrown || isFemale || aiCrown;
        if (!showCrownTip) return null;
        const tipHtml = aiCrown
          ? window.t('result.crown_tip_ai_html')
          : isFemale
            ? window.t('result.crown_tip_female_html')
            : window.t('result.crown_tip_male_html');
        return (
          <div className="crown-tip">
            <div className="crown-tip-head">
              <window.Glyph name="sparkle" size={18} color="var(--modi-green)" />
              <span>{window.t('result.crown_tip_title')}</span>
              {aiCrown && <span className="ai-tag" style={{ marginLeft: 'auto' }}>{window.t('result.ai_tag')}</span>}
            </div>
            <p className="crown-tip-desc" dangerouslySetInnerHTML={{ __html: tipHtml }} />
          </div>
        );
      })()}

      <div className="price-cta-card">
        <div className="price-cta-head">
          <window.Glyph name="sparkle" size={20} color="var(--modi-green)" />
          <div>
            <div className="price-cta-title">{window.t('result.price_cta_title')}</div>
            <div className="price-cta-sub">{window.t('result.price_cta_sub')}</div>
          </div>
        </div>
        <button className="btn btn-primary" onClick={onGetPrice}>
          {window.t('result.price_cta_button')} <window.Glyph name="arrow" size={16} color="white" />
        </button>
      </div>

      <p className="section-label" style={{ marginTop: 22 }}>{window.t('result.contact_section_label')}</p>
      <div className="footer-stack" style={{ marginBottom: 12 }}>
        <a className="btn btn-kakao" href={kakaoUrl}>
          <window.Glyph name="kakao" size={18} color="#181600" />
          {window.t('result.kakao_button')}
        </a>
        {isOpenNow && (
          <a className="btn btn-ghost" href={`tel:${phoneNumber.replace(/-/g, '')}`}>
            <window.Glyph name="phone" size={18} color="var(--modi-green)" />
            {window.t('result.phone_button', { phone: phoneNumber })}
          </a>
        )}
        <button className="btn btn-ghost" onClick={onReset}>{window.t('result.retake_button')}</button>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center', justifyContent: 'center', marginTop: 18, fontSize: 11, color: 'var(--muted)', lineHeight: 1.6 }}>
        <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
          <window.Glyph name="clock" size={12} color="var(--muted)" />
          <span>{window.t('result.hours_weekday')}</span>
        </div>
        <div>{window.t('result.hours_closed')}</div>
      </div>
    </div>
  );
}

// ────────── 작은 헬퍼들 (수가 흐름 공통) ──────────
function CheckIcon() {
  return (
    <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">
      <path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function ConsentItem({ checked, onChange, required, optional, title, details, open, onToggle }) {
  return (
    <div className="consent-item">
      <label className={`chk ${checked ? 'on' : ''}`}>
        <input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
        <span className="chk-box"><CheckIcon /></span>
        <span className="chk-label">
          {required && <em className="req-tag">{window.t('common.required_tag')}</em>}
          {optional && <em className="opt-tag">{window.t('common.optional_tag')}</em>}
          {title}
        </span>
      </label>
      <button type="button" className="consent-detail-btn" onClick={onToggle} aria-expanded={open}>
        {open ? window.t('price_consent.details_collapse') : window.t('price_consent.details_expand')}
      </button>
      {open && (
        <div className="consent-detail">
          {details.map(([k, v]) => (
            <div key={k}><b>{k}</b><span>{v}</span></div>
          ))}
        </div>
      )}
    </div>
  );
}

// ────────── Price-consent step ──────────
function PriceConsentStep({ data, update, onNext, onBack }) {
  const [contact, setContact] = useState(!!data.consentContact);
  const [sensitive, setSensitive] = useState(!!data.consentSensitive);
  const [international, setInternational] = useState(!!data.consentInternational);
  const [marketing, setMarketing] = useState(!!data.consentMarketing);
  const [detailsOpen, setDetailsOpen] = useState(null);

  const requiredOK = contact && sensitive && international;
  const allChecked = contact && sensitive && international && marketing;
  const setAll = (v) => { setContact(v); setSensitive(v); setInternational(v); setMarketing(v); };

  const age = useMemo(() => {
    const y = parseInt(data.birthY, 10);
    if (!y) return null;
    return new Date().getFullYear() - y;
  }, [data.birthY]);

  const baseResult = useMemo(() => window.calculateGrafts({
    patternId: data.pattern, areaIds: data.areas, densityId: data.density,
    hairlineLower: data.hairlineLower, age, gender: data.gender,
  }), [data, age]);
  // AI 보정 적용 (ResultStep에서 분석 완료 시 data.aiAdjustDelta 저장됨)
  const adjustedMid = useMemo(() => {
    if (!baseResult) return 0;
    const delta = data.aiAdjustDelta || 0;
    const m = Math.round((baseResult.mid * (1 + delta / 100)) / 100) * 100;
    return Math.min(m, 6000);
  }, [baseResult, data.aiAdjustDelta]);

  const proceed = () => {
    if (!requiredOK) return;
    update('consentContact', true);
    update('consentSensitive', true);
    update('consentInternational', true);
    update('consentMarketing', marketing);
    update('consentedAt', new Date().toISOString());
    onNext();
  };

  return (
    <React.Fragment>
      <div className="scroll fade-in">
        <p className="step-eyebrow">{window.t('price_consent.eyebrow')}</p>
        <h2 className="step-title">{window.t('price_consent.title_line1')}<br/>{window.t('price_consent.title_line2')}</h2>
        <p className="step-sub">{window.t('price_consent.sub_line1')}<br/>{window.t('price_consent.sub_line2')}</p>

        <div className="gate-preview">
          <div className="gate-preview-row">
            <span className="gate-mk">{window.t('price_consent.gate_grafts_label')}</span>
            <b>{window.t('price_consent.gate_grafts_value', { value: window.numberFmt(adjustedMid) })}</b>
          </div>
          <div className="gate-preview-row gate-blur">
            <span className="gate-mk">{window.t('price_consent.gate_price_label')}</span>
            <b className="blurred">{window.t('price_consent.gate_price_blur')}</b>
          </div>
          <div className="gate-preview-foot">
            <window.Glyph name="info" size={13} color="var(--muted)" />
            <span>{window.t('price_consent.gate_note')}</span>
          </div>
        </div>

        <div className="disclaimer-box">
          <window.Glyph name="info" size={18} color="#a66a14" />
          <span dangerouslySetInnerHTML={{ __html: window.t('price_consent.disclaimer_html') }} />
        </div>

        <label className={`consent-master ${allChecked ? 'on' : ''}`}>
          <input type="checkbox" checked={allChecked} onChange={(e) => setAll(e.target.checked)} />
          <span className="chk-box"><CheckIcon /></span>
          <span className="chk-label">
            <b>{window.t('price_consent.master_label')}</b>
            <span className="chk-sub">{window.t('price_consent.master_sub')}</span>
          </span>
        </label>

        <div className="consent-list">
          <ConsentItem
            checked={contact} onChange={setContact} required
            title={window.t('price_consent.contact_title')}
            details={window.t('price_consent.contact_items')}
            open={detailsOpen === 'contact'}
            onToggle={() => setDetailsOpen(detailsOpen === 'contact' ? null : 'contact')}
          />
          <ConsentItem
            checked={sensitive} onChange={setSensitive} required
            title={window.t('price_consent.sensitive_title')}
            details={window.t('price_consent.sensitive_items')}
            open={detailsOpen === 'sensitive'}
            onToggle={() => setDetailsOpen(detailsOpen === 'sensitive' ? null : 'sensitive')}
          />
          <ConsentItem
            checked={international} onChange={setInternational} required
            title={window.t('price_consent.international_title')}
            details={window.t('price_consent.international_items')}
            open={detailsOpen === 'international'}
            onToggle={() => setDetailsOpen(detailsOpen === 'international' ? null : 'international')}
          />
          <ConsentItem
            checked={marketing} onChange={setMarketing} optional
            title={window.t('price_consent.marketing_title')}
            details={window.t('price_consent.marketing_items')}
            open={detailsOpen === 'marketing'}
            onToggle={() => setDetailsOpen(detailsOpen === 'marketing' ? null : 'marketing')}
          />
        </div>

        <p className="privacy-link">
          <a href="privacy/" target="_blank" rel="noopener">{window.t('price_consent.privacy_link')}</a>
        </p>
      </div>
      <div className="footer">
        <button className="btn btn-primary" disabled={!requiredOK} onClick={proceed}>
          {window.t('price_consent.proceed')}
        </button>
        {!requiredOK && <p className="cta-hint">{window.t('price_consent.proceed_hint')}</p>}
      </div>
    </React.Fragment>
  );
}

// ────────── Price step — 시술 방식 + 모낭줄기세포 + 비용 명세 ──────────
function PriceStep({ data, update, onNext, onBack, onSubmitted }) {
  const age = useMemo(() => {
    const y = parseInt(data.birthY, 10);
    if (!y) return null;
    return new Date().getFullYear() - y;
  }, [data.birthY]);

  const baseResult = useMemo(() => window.calculateGrafts({
    patternId: data.pattern, areaIds: data.areas, densityId: data.density,
    hairlineLower: data.hairlineLower, age, gender: data.gender,
  }), [data, age]);

  // 결과 화면에서 본 것과 같은 AI 보정 적용된 모수를 수가 계산에 사용
  const grafts = useMemo(() => {
    if (!baseResult) return 0;
    const delta = data.aiAdjustDelta || 0;
    const m = Math.round((baseResult.mid * (1 + delta / 100)) / 100) * 100;
    return Math.min(m, 6000);  // CAP 6,000
  }, [baseResult, data.aiAdjustDelta]);

  // 디폴트는 가장 저렴한 조합 (절개 + 줄기세포 미선택). 사용자가 클릭해서 다른 옵션을 고르면 그 선택이 유지됨.
  const methodId = data.methodId || 'incision';
  const stemId = data.stemId || 'none';

  const method = window.METHODS.find(m => m.id === methodId);
  const stem = window.STEMCELL.find(s => s.id === stemId);
  const price = window.calcPrice(grafts, methodId, stemId);

  // 페이로드 빌더 (마운트 자동 제출 + 예약 버튼 클릭 시 양쪽에서 사용)
  // 현재 선택 상태(method/stem)를 스냅샷으로 잡아 보낸다.
  const buildPayload = () => {
    const photoFlags = {
      front: !!data.photos?.front,
      top: !!data.photos?.top,
      side: !!data.photos?.side,
    };
    // 라벨은 admin이 한국어이므로 사용자 로케일과 무관하게 한국어로 직렬화
    const genderGroup = data.gender === 'female' ? 'female' : 'male';
    const patternLabel = data.pattern ? window.tKo('pattern.' + genderGroup + '.' + data.pattern + '_label') : '';
    const areaLabels = data.areas.map(id => window.tKo('area.' + id)).filter(Boolean);
    const densityLabel = data.density ? window.tKo('density.' + data.density + '_label') : '';
    const hairlineLabelMap = { 'no': '내리지 않음', '1': '1cm', '2': '2cm', '3': '3cm 이상', 'unsure': '미정' };
    const hairlineLabel = hairlineLabelMap[data.hairlineLower] || '';
    const genderLabel = data.gender === 'female' ? '여성' : data.gender === 'male' ? '남성' : '';
    const consentedAt = data.consentedAt || new Date().toISOString();
    const retentionUntil = new Date(new Date(consentedAt).getTime() + 3 * 365 * 24 * 3600 * 1000).toISOString();

    return {
      submittedAt: new Date().toISOString(),
      sessionId: getSessionId(),
      locale: window.currentLocale || 'ko',
      attribution: getAttribution(),
      name: data.name,
      gender: data.gender, genderLabel,
      birthDate: [data.birthY, data.birthM, data.birthD].filter(Boolean).join('-'),
      age,
      phone: [data.phone1, data.phone2, data.phone3].filter(Boolean).join('-'),
      pattern: data.pattern, patternLabel,
      areas: data.areas, areaLabels,
      hairlineLower: data.hairlineLower, hairlineLabel,
      density: data.density, densityLabel,
      photosAttached: photoFlags,
      // mid = AI 보정 전 베이스, midAdjusted = AI 보정 + 6000모 cap 적용 후 (환자가 본 값)
      result: {
        low: baseResult.low,
        high: baseResult.high,
        mid: baseResult.mid,
        midAdjusted: grafts,
        aiAdjustDelta: data.aiAdjustDelta || 0,
      },
      pricing: {
        methodId, methodName: window.tKo('method.' + methodId + '_name'),
        stemId, stemLabel: window.tKo('price.stem_' + stemId + '_label'),
        transplantCost: Math.round(price.transplant * 10000),
        stemCost: Math.round(price.stem * 10000),
        totalCost: Math.round(price.total * 10000),
      },
      consents: {
        contact: !!data.consentContact,
        sensitive: !!data.consentSensitive,
        international: !!data.consentInternational,
        marketing: !!data.consentMarketing,
        consentedAt,
        retentionUntil,
      },
      ua: typeof navigator !== 'undefined' ? navigator.userAgent : '',
    };
  };

  // 마운트 직후 1회 자동 제출. 동의는 이전 단계에서 이미 받았으므로
  // "수가 페이지에 도달 = lead로 기록"이 사장님 요구사항.
  // sessionStorage 키로 중복 제출 방지 (re-render, 새로고침 시 한 번만).
  const submittedRef = useRef(false);
  useEffect(() => {
    if (submittedRef.current) return;
    const sessionKey = 'modi-calc-pricing-submitted-' + (data.consentedAt || '');
    if (sessionStorage.getItem(sessionKey)) { submittedRef.current = true; return; }
    submittedRef.current = true;
    track('pricing_view');
    (async () => {
      try {
        const res = await fetch('/api/submit-lead/', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ...buildPayload(), reservationRequested: false }),
        });
        let leadId = null;
        let uploadToken = null;
        try {
          const j = await res.json();
          leadId = j?.lead_id || null;
          uploadToken = j?.upload_token || null;
        } catch (e) {}
        // 요청이 실제로 접수된 경우(2xx)에만 중복 제출 가드를 건다. 429(rate limit)
        // 나 5xx 면 플래그를 세우지 않아, 새로고침/리마운트 시 재시도가 가능하다 —
        // 한도 초과로 리드가 조용히 유실되지 않도록.
        if (res.ok) { try { sessionStorage.setItem(sessionKey, '1'); } catch (e) {} }
        // 동의 후 → 사진을 Supabase Storage에 업로드 (캐주얼은 X, 여기서만 O).
        // 원본은 아이폰 기준 한 장에 4~5MB → 3장 base64 = 15MB+로 Vercel 4.5MB
        // 본문 한도를 넘어 핸들러 진입 전 차단된다. 1280px / JPEG q=0.85로 줄여 보냄
        // (의료진 검토용 디테일은 충분히 남음).
        if (leadId && uploadToken && data.consentSensitive) {
          const photos = {};
          for (const k of ['front', 'top', 'side']) {
            const v = data.photos?.[k];
            if (typeof v !== 'string' || !v.startsWith('data:')) continue;
            try { photos[k] = await resizeDataUrlShared(v, 1280, 0.85); }
            catch (e) { console.warn('사진 리사이즈 실패:', k, e); }
          }
          if (Object.keys(photos).length > 0) {
            // fire-and-forget 이지만 실패를 묻지 않는다(CLAUDE.md 과거버그 §3).
            // 응답을 await 해서 부분/전체 업로드 실패를 최소한 콘솔에 남긴다.
            try {
              const upRes = await fetch('/api/upload-photos/', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ lead_id: leadId, token: uploadToken, photos }),
              });
              let upJson = null;
              try { upJson = await upRes.json(); } catch (e) {}
              if (!upRes.ok || (upJson && upJson.ok === false)) {
                console.warn('사진 업로드 실패:', upRes.status, upJson && (upJson.error || upJson.uploadErrors));
              } else if (upJson && Array.isArray(upJson.errors) && upJson.errors.length) {
                console.warn('사진 일부 업로드 실패:', upJson.errors);
              }
            } catch (err) {
              console.warn('사진 업로드 요청 실패:', err);
            }
          }
        }
      } catch (err) {
        console.warn('수가 lead 제출 실패 (무시):', err);
      }
    })();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // 전화 상담 예약 버튼은 사장님 요구사항대로 "옵션"일 뿐.
  // DB 행은 이미 마운트 시점에 생성됐으므로, 버튼은 단순히 완료 화면으로 이동.
  // (별도 마킹이 필요하면 추후 reservationRequested 컬럼 추가 + Apps Script 업데이트로 처리)
  const submitReservation = () => {
    onSubmitted();
  };

  if (!baseResult) return <div className="scroll"><p>{window.t('common.error')}</p></div>;
  const methodNameDisplay = window.t('method.' + methodId + '_name');

  return (
    <React.Fragment>
      <div className="scroll fade-in" style={{ paddingBottom: 120 }}>
        <p className="step-eyebrow">{window.t('price.eyebrow')}</p>
        <h2 className="step-title">{window.t('price.title')}</h2>
        <p className="step-sub">{window.t('price.sub_line1')}<br/>{window.t('price.sub_line2')}</p>

        <div className="price-hero">
          <div className="price-hero-row">
            <span className="ph-mk">{window.t('price.hero_grafts_label')}</span>
            <b>{window.t('price.hero_grafts_value', { value: window.numberFmt(grafts) })}</b>
          </div>
          <div className="price-hero-row">
            <span className="ph-mk">{window.t('price.hero_method_label')}</span>
            <b>{methodNameDisplay}</b>
          </div>
          <div className="price-hero-row price-hero-total">
            <span className="ph-mk">{window.t('price.hero_total_label')}</span>
            <b>{window.wonFmtI18n(price.total)}</b>
          </div>
        </div>

        <p className="price-discount-note">{window.t('price.discount_note')}</p>

        <section className="price-section">
          <div className="price-section-head">
            <h3>{window.t('price.method_section_title')}</h3>
            <p>{window.t('price.method_section_desc')}</p>
          </div>
          <div className="method-list">
            {window.METHODS.map(m => {
              const p = (m.vat * grafts) / 1000;
              const selected = m.id === methodId;
              return (
                <button key={m.id} type="button"
                  className={`method-row${selected ? ' on' : ''}`}
                  onClick={() => update('methodId', m.id)}>
                  <span className="radio" aria-hidden="true" />
                  <div className="method-row-mid">
                    <div className="method-row-name">
                      {window.t('method.' + m.id + '_name')}
                      <span className="method-row-sub">{window.t('method.' + m.id + '_sub')}</span>
                    </div>
                    <div className="method-row-desc">{window.t('method.' + m.id + '_desc')}</div>
                    <div className="method-row-tags">
                      <span className="tag-small">{window.t('method.' + m.id + '_tag1')}</span>
                      <span className="tag-small">{window.t('method.' + m.id + '_tag2')}</span>
                      <span className="tag-small">{window.t('method.' + m.id + '_recovery')}</span>
                    </div>
                  </div>
                  <div className="method-row-price">
                    <div className="mrp-amount">{window.manwonFmtI18n(Math.round(p))}</div>
                    <div className="mrp-per">{window.t('price.method_per_graft', { value: window.perGraftFmt(m.vat * 10) })}</div>
                  </div>
                </button>
              );
            })}
          </div>
          <p className="price-section-foot">{window.t('price.method_footer')}</p>
        </section>

        <section className="price-section">
          <div className="price-section-head">
            <h3>{window.t('price.stem_section_title')} <span className="optional">{window.t('price.stem_section_optional')}</span></h3>
            <p>{window.t('price.stem_section_desc')}</p>
          </div>
          <div className="stem-grid">
            {window.STEMCELL.map(s => {
              const selected = s.id === stemId;
              const savings = s.alone - s.bundle;
              return (
                <button key={s.id} type="button"
                  className={`stem-card${selected ? ' on' : ''}`}
                  onClick={() => update('stemId', s.id)}>
                  <div className="stem-card-h">
                    <div className="stem-card-title">{window.t('price.stem_' + s.id + '_label')}</div>
                    {s.rounds > 0 && <div className="stem-rounds">{window.t('price.stem_rounds', { n: s.rounds })}</div>}
                  </div>
                  {s.rounds === 0 ? (
                    <div className="stem-card-body">
                      <div className="stem-price">{window.t('price.stem_none_price')}</div>
                      <div className="stem-sub">{window.t('price.stem_none_sub')}</div>
                    </div>
                  ) : (
                    <div className="stem-card-body">
                      <div className="stem-price">+{window.manwonFmtI18n(s.bundle)}</div>
                      <div className="stem-sub">
                        <s>{window.manwonFmtI18n(s.alone)}</s>
                        <span className="stem-save">{window.t('price.stem_savings', { value: window.manwonFmtI18n(savings) })}</span>
                      </div>
                    </div>
                  )}
                </button>
              );
            })}
          </div>
        </section>

        <section className="price-section breakdown">
          <div className="price-section-head">
            <h3>{window.t('price.breakdown_title')}</h3>
            <p>{window.t('price.breakdown_desc')}</p>
          </div>
          <div className="bd-rows">
            <div className="bd-row">
              <div>
                <div className="bd-name">{window.t('price.breakdown_transplant_name', { method: methodNameDisplay })}</div>
                <div className="bd-mk">{window.t('price.breakdown_transplant_desc', { unit: window.manwonFmtI18n(method?.vat), grafts: window.numberFmt(grafts) })}</div>
              </div>
              <div className="bd-amt">{window.wonFmtI18n(price.transplant)}</div>
            </div>
            {stem.rounds > 0 && (
              <div className="bd-row">
                <div>
                  <div className="bd-name">{window.t('price.breakdown_stem_name', { n: stem.rounds })}</div>
                  <div className="bd-mk" dangerouslySetInnerHTML={{ __html: window.t('price.breakdown_stem_desc_html', { alone: window.manwonFmtI18n(stem.alone), bundle: window.manwonFmtI18n(stem.bundle) }) }} />
                </div>
                <div className="bd-amt">{window.wonFmtI18n(price.stem)}</div>
              </div>
            )}
          </div>
          <div className="bd-total">
            <span>{window.t('price.breakdown_total')}</span>
            <strong>{window.wonFmtI18n(price.total)}</strong>
          </div>
        </section>

        <div className="disclaimer-box">
          <window.Glyph name="info" size={18} color="#a66a14" />
          <span dangerouslySetInnerHTML={{ __html: window.t('price.disclaimer_html') }} />
        </div>

        <div className="price-callout">
          <window.Glyph name="phone" size={18} color="var(--modi-green)" />
          <div>
            <div className="pc-title">{window.t('price.callout_title')}</div>
            <div className="pc-body" dangerouslySetInnerHTML={{ __html: window.t('price.callout_body_html') }} />
          </div>
        </div>

      </div>
      <div className="footer">
        <button className="btn btn-primary" onClick={submitReservation}>
          {window.t('price.confirm')}
        </button>
      </div>
    </React.Fragment>
  );
}

// ────────── Price-done step ──────────
function PriceDoneStep({ data, onReset }) {
  useTrackOnMount('reservation_view');
  const age = useMemo(() => {
    const y = parseInt(data.birthY, 10);
    if (!y) return null;
    return new Date().getFullYear() - y;
  }, [data.birthY]);
  const baseResult = useMemo(() => window.calculateGrafts({
    patternId: data.pattern, areaIds: data.areas, densityId: data.density,
    hairlineLower: data.hairlineLower, age, gender: data.gender,
  }), [data, age]);
  const grafts = useMemo(() => {
    if (!baseResult) return 0;
    const delta = data.aiAdjustDelta || 0;
    const m = Math.round((baseResult.mid * (1 + delta / 100)) / 100) * 100;
    return Math.min(m, 6000);
  }, [baseResult, data.aiAdjustDelta]);
  const method = window.METHODS.find(m => m.id === data.methodId);
  const price = window.calcPrice(grafts, data.methodId, data.stemId);
  return (
    <div className="scroll fade-in price-done">
      <div className="done-check">
        <svg viewBox="0 0 56 56" width="64" height="64">
          <circle cx="28" cy="28" r="26" fill="none" stroke="currentColor" strokeWidth="1.5" opacity="0.22"/>
          <path d="M16 28.5l8 8 16-16" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      </div>
      <h2 className="step-title" style={{ textAlign: 'center' }}>{window.t('price_done.heading')}</h2>
      <p className="step-sub" style={{ textAlign: 'center' }}>
        {window.t('price_done.message_line1', { name: data.name })}<br/>
        <span dangerouslySetInnerHTML={{ __html: window.t('price_done.message_line2_html') }} />
      </p>

      <div className="done-summary">
        <div className="ds-row"><span>{window.t('price_done.summary_grafts')}</span><b>{window.graftCountFmt(grafts)}</b></div>
        <div className="ds-row"><span>{window.t('price_done.summary_method')}</span><b>{data.methodId ? window.t('method.' + data.methodId + '_name') : ''}</b></div>
        <div className="ds-row ds-total"><span>{window.t('price_done.summary_total')}</span><b>{window.wonFmtI18n(price.total)}</b></div>
      </div>

      <div className="footer-stack" style={{ marginTop: 20 }}>
        <a className="btn btn-kakao" href="https://pf.kakao.com/_ExmRuT/chat">
          <window.Glyph name="kakao" size={18} color="#181600" />
          {window.t('price_done.kakao_button')}
        </a>
        <button className="btn btn-ghost" onClick={onReset}>{window.t('price_done.reset')}</button>
      </div>
    </div>
  );
}

window.MODIApp = App;
