// Zgłoś zbiórkę — publiczny formularz zgłoszeniowy (klikalna makieta)
// Spec: docs/superpowers/specs/2026-07-29-zglos-zbiorke-makieta-design.md
//
// MAKIETA: dopóki window.PW_ZGLOSZENIA_ENDPOINT jest puste, zgłoszenie
// NIGDZIE nie trafia — payload ląduje w konsoli, a strona pokazuje ekran
// potwierdzenia. Podpięcie n8n = wpisanie adresu w config.js, bez zmian tutaj.

const { useState, useRef, useEffect } = React;

// Kategorie z data.js (ten sam wzorzec zabezpieczenia co w zbiorka.jsx:287)
const ZG_CATEGORIES = (typeof PROTON_CATEGORIES !== 'undefined' ? PROTON_CATEGORIES : [])
  .filter(c => c.id !== 'all');

const ROLES = [
  { id: 'beneficjent',  label: 'Jestem beneficjentem',        sub: 'Zbiórka dotyczy mnie lub mojej najbliższej rodziny.' },
  { id: 'organizacja',  label: 'Reprezentuję organizację',    sub: 'Fundacja, stowarzyszenie, schronisko, placówka, koło.' },
  { id: 'osoba_trzecia',label: 'Zgłaszam cudzą sprawę',       sub: 'Znam osobę lub organizację, której warto pomóc.' },
];

// Zbiórki finansują agregaty prądotwórcze, więc zgłaszający wskazuje potrzebną
// MOC, a nie kwotę. Wartości `id` = dokładnie to, co ląduje w Airtable
// (ten sam wzorzec co beneficiary_type — bez tłumaczenia po drodze).
//
// `price` to orientacyjna wartość zbiórki dla danej mocy. Kwota jest z niej
// WYLICZANA — zgłaszający nie ma jej gdzie wpisać, bo pola do wpisania nie ma.
// null = brak ceny w tabeli, kwotę ustala doradca po dobraniu modelu.
//
// !!! DO UZUPEŁNIENIA z katalogu BaseLinkera przed uruchomieniem formularza.
// Cen nie zgadujemy: ta liczba staje się celem zbiórki pokazywanym publicznie
// przy pasku postępu, więc zmyślona kwota to zmyślony cel.
const POWER_OPTIONS = [
  { id: 'do 3 kW',       price: null, label: 'Do 3 kW — oświetlenie, lodówka, zasilanie awaryjne' },
  { id: '3–6 kW',        price: null, label: '3–6 kW — dom jednorodzinny, podstawowe urządzenia' },
  { id: '6–10 kW',       price: null, label: '6–10 kW — dom z pompą ciepła, mały warsztat' },
  { id: '10–20 kW',      price: null, label: '10–20 kW — gospodarstwo, placówka, większy obiekt' },
  { id: 'powyżej 20 kW', price: null, label: 'Powyżej 20 kW' },
  { id: 'nie wiem',      price: null, label: 'Nie wiem — proszę o dobór przez doradcę' },
];

// Jedyne miejsce, w którym powstaje kwota zbiórki na etapie zgłoszenia.
const priceForPower = (power) => {
  const opt = POWER_OPTIONS.find(p => p.id === power);
  return opt && opt.price > 0 ? opt.price : null;
};

// Przykładowa treść do zrzutów ekranu — otwórz stronę z ?demo=1
const DEMO = {
  title: 'Rehabilitacja Zosi po wypadku',
  beneficiary_type: 'dzieci',
  short_description: 'Zosia oddycha przy pomocy respiratora. Każda przerwa w dostawie prądu w ich wsi to dla rodziny realne zagrożenie życia dziecka.',
  long_description: 'Zosia od urodzenia wymaga wspomagania oddechu. Respirator, koncentrator tlenu i ssak pracują u niej w domu przez całą dobę. Wieś, w której mieszka rodzina, kilka razy w roku traci prąd na kilkanaście godzin — przy wichurach nawet na dobę. Akumulator w respiratorze wystarcza na dwie godziny, po tym czasie rodzice muszą wentylować córkę ręcznie i czekać na pogotowie. Rodzina utrzymuje jeszcze dwoje młodszych dzieci, a mama zrezygnowała z pracy, żeby opiekować się Zosią.',
  goal_summary: 'Agregat, który utrzyma pracę respiratora Zosi przy każdej przerwie w dostawie prądu.',
  generator_power: '3–6 kW',
  why_important: 'Przy kolejnej wichurze rodzice zostaną sami z ręczną wentylacją i telefonem po karetkę. Agregat zdejmuje z nich ten scenariusz raz na zawsze — nie jest wygodą, tylko zabezpieczeniem życia.',
  source_url: 'https://www.facebook.com/pomocdlazosi',
  submitter_name: 'Anna Kowalczyk',
  submitter_email: 'anna.kowalczyk@example.com',
  submitter_phone: '601 234 567',
  submitter_role: 'osoba_trzecia',
  org_name: '',
  org_id: '',
  consent_terms: true,
  consent_truthful: true,
  consent_phone: true,
};

const EMPTY = {
  title: '', beneficiary_type: '', short_description: '',
  long_description: '', goal_summary: '', generator_power: '',
  why_important: '', source_url: '',
  submitter_name: '', submitter_email: '', submitter_phone: '',
  submitter_role: '', org_name: '', org_id: '',
  consent_terms: false, consent_truthful: false, consent_phone: false,
};

const IS_DEMO = new URLSearchParams(window.location.search).get('demo') === '1';

const MAX_TITLE = 80;
const MAX_SHORT = 200;
const MIN_LONG  = 100;

// Załączniki. Limity muszą się zgadzać z bucketem zgloszenia-dokumenty —
// Storage odrzuci plik niezależnie od tego, co przepuści formularz.
const MAX_FILES     = 5;
const MAX_FILE_SIZE = 10 * 1024 * 1024;
const DOC_BUCKET    = 'zgloszenia-dokumenty';
const ACCEPT_TYPES  = [
  'application/pdf', 'image/jpeg', 'image/png',
  'image/webp', 'image/heic', 'image/heif',
];
const ACCEPT_ATTR = '.pdf,.jpg,.jpeg,.png,.webp,.heic,.heif';

const fmtSize = (b) =>
  b >= 1048576 ? (b / 1048576).toFixed(1).replace('.', ',') + ' MB'
               : Math.max(1, Math.round(b / 1024)) + ' KB';

// Nazwa pliku w Storage: bez polskich znaków, spacji i podwójnych kropek.
function safeName(name) {
  const map = { ą:'a', ć:'c', ę:'e', ł:'l', ń:'n', ó:'o', ś:'s', ź:'z', ż:'z',
                Ą:'A', Ć:'C', Ę:'E', Ł:'L', Ń:'N', Ó:'O', Ś:'S', Ź:'Z', Ż:'Z' };
  return (name || 'plik')
    .replace(/[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g, (c) => map[c] || c)
    .replace(/[^A-Za-z0-9._-]+/g, '-')
    .replace(/-+/g, '-')
    .slice(-80);
}

function fileError(file, existing) {
  if (!ACCEPT_TYPES.includes(file.type)) {
    return `„${file.name}" — nieobsługiwany format. Przyjmujemy PDF i zdjęcia.`;
  }
  if (file.size > MAX_FILE_SIZE) {
    return `„${file.name}" waży ${fmtSize(file.size)} — limit to 10 MB na plik.`;
  }
  if (existing.some(f => f.name === file.name && f.size === file.size)) {
    return `„${file.name}" jest już na liście.`;
  }
  return '';
}

// ---------- walidacja ----------
const digitsOnly = (s) => (s || '').replace(/\D/g, '');

function normalizePhone(raw) {
  let d = digitsOnly(raw);
  if (d.length === 11 && d.startsWith('48')) d = d.slice(2);
  return d;
}

function validateField(name, v) {
  const val = typeof v[name] === 'string' ? v[name].trim() : v[name];

  switch (name) {
    case 'title':
      if (!val) return 'Podaj tytuł zbiórki.';
      if (val.length > MAX_TITLE) return `Tytuł może mieć najwyżej ${MAX_TITLE} znaków.`;
      return '';
    case 'beneficiary_type':
      return val ? '' : 'Wybierz kategorię zbiórki.';
    case 'short_description':
      if (!val) return 'Napisz krótki opis — to on pojawi się na kafelku zbiórki.';
      if (val.length > MAX_SHORT) return `Krótki opis może mieć najwyżej ${MAX_SHORT} znaków.`;
      return '';
    case 'long_description':
      if (!val) return 'Opisz sprawę szerzej — to na tej podstawie ją ocenimy.';
      if (val.length < MIN_LONG) return `Opis jest za krótki — brakuje jeszcze ${MIN_LONG - val.length} znaków.`;
      return '';
    case 'goal_summary':
      return val ? '' : 'Napisz jednym zdaniem, co ma się dzięki tym środkom wydarzyć.';
    case 'generator_power':
      return val ? '' : 'Wskaż moc agregatu — to on jest celem tej zbiórki.';
    case 'why_important':
      return val ? '' : 'Napisz, dlaczego ta sprawa jest ważna.';
    case 'source_url':
      if (!val) return '';
      if (/\s/.test(val) || !/\./.test(val)) return 'Podaj poprawny adres, np. https://przyklad.pl';
      return '';
    case 'submitter_name':
      return val ? '' : 'Podaj imię i nazwisko.';
    case 'submitter_email':
      if (!val) return 'Podaj adres e-mail — na niego odpiszemy.';
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val)) return 'Ten adres e-mail wygląda na niepoprawny.';
      return '';
    case 'submitter_phone': {
      if (!val) return 'Podaj numer telefonu.';
      const d = normalizePhone(val);
      if (d.length !== 9) return 'Numer telefonu powinien mieć 9 cyfr.';
      return '';
    }
    case 'submitter_role':
      return val ? '' : 'Zaznacz, w jakiej roli zgłaszasz zbiórkę.';
    case 'org_name':
      return val ? '' : 'Podaj nazwę organizacji.';
    case 'org_id':
      return val ? '' : 'Podaj numer KRS lub NIP organizacji.';
    case 'consent_terms':
      return val ? '' : 'Bez akceptacji regulaminu i zapoznania się z klauzulą RODO nie możemy przyjąć zgłoszenia.';
    case 'consent_truthful':
      return val ? '' : 'Potwierdź prawdziwość podanych danych.';
    default:
      return '';
  }
}

const STEP1_FIELDS = [
  'title', 'beneficiary_type', 'short_description',
  'long_description', 'goal_summary', 'generator_power', 'why_important', 'source_url',
];

function step2Fields(v) {
  const base = ['submitter_name', 'submitter_email', 'submitter_phone', 'submitter_role'];
  // Ukryte pola organizacji nie są walidowane
  const org = v.submitter_role === 'organizacja' ? ['org_name', 'org_id'] : [];
  return base.concat(org, ['consent_terms', 'consent_truthful']);
}

// ---------- wysyłka ----------
// Nazwy pól = kolumny public.campaign_submissions. Płasko, nie zagnieżdżone:
// wiersz idzie do Supabase bez żadnego przemapowania po drodze.
function buildPayload(v, submissionId, documents) {
  const isOrg = v.submitter_role === 'organizacja';
  return {
    id:                submissionId,
    documents:         documents || [],
    title:             v.title.trim(),
    beneficiary_type:  v.beneficiary_type,
    beneficiary_name:  isOrg ? v.org_name.trim() : null,
    short_description: v.short_description.trim(),
    long_description:  v.long_description.trim(),
    goal_summary:      v.goal_summary.trim(),
    generator_power:   v.generator_power,
    why_important:     v.why_important.trim(),
    // Kwota nigdy nie pochodzi z tego, co ktoś wpisał — wyłącznie z tabeli cen
    // dla wybranej mocy. Brak ceny → null, czyli „Cel PLN" w Airtable zostaje
    // puste i wypełnia je doradca po dobraniu modelu.
    target_amount:     priceForPower(v.generator_power),
    source_url:        v.source_url.trim() || null,

    submitter_name:  v.submitter_name.trim(),
    submitter_email: v.submitter_email.trim(),
    submitter_phone: normalizePhone(v.submitter_phone),
    submitter_role:  v.submitter_role,
    org_id:          isOrg ? v.org_id.trim() : null,

    consent_terms_rodo:    !!v.consent_terms,
    consent_truthful:      !!v.consent_truthful,
    consent_phone_contact: !!v.consent_phone,
  };
}

// Wgranie załączników do prywatnego bucketa. Ścieżka zaczyna się od id zgłoszenia,
// dlatego id generujemy w przeglądarce PRZED zapisem wiersza — inaczej nie byłoby
// czym nazwać folderu, a pliki i zgłoszenie rozjechałyby się przy błędzie.
async function uploadDocuments(submissionId, files, onProgress) {
  if (!files.length) return [];
  const cfg = window.SUPABASE_CONFIG || {};
  const headers = { apikey: cfg.anonKey, Authorization: 'Bearer ' + cfg.anonKey };
  const out = [];

  for (let i = 0; i < files.length; i++) {
    const f = files[i];
    if (onProgress) onProgress(i + 1, files.length);
    const path = `${submissionId}/${i + 1}-${safeName(f.name)}`;
    const r = await fetch(
      `${cfg.url}/storage/v1/object/${DOC_BUCKET}/${encodeURI(path)}`,
      { method: 'POST', headers: { ...headers, 'Content-Type': f.type || 'application/octet-stream' }, body: f }
    );
    if (!r.ok) {
      let detail = '';
      try { detail = (await r.json()).message || ''; } catch (e) {}
      throw new Error(`Nie udało się wgrać pliku „${f.name}". ${detail}`.trim());
    }
    out.push({ path, name: f.name, size: f.size, type: f.type });
  }
  return out;
}

// Jedyne miejsce styku ze światem.
// Domyślnie zapis wprost do Supabase (RLS: anon może tylko INSERT ze statusem NEW).
// Ustawienie PW_ZGLOSZENIA_ENDPOINT przekierowuje na webhook n8n — ten sam payload,
// bez zmian tutaj — gdy dojdzie mail potwierdzający i powiadomienie zespołu.
async function submitZgloszenie(payload) {
  const endpoint = (window.PW_ZGLOSZENIA_ENDPOINT || '').trim();

  if (endpoint) {
    const r = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    if (!r.ok) throw new Error('HTTP ' + r.status);
    let data = {};
    try { data = await r.json(); } catch (e) { data = { status: 'ok' }; }
    const status = data.status || 'ok';
    return { ok: status === 'ok', status };
  }

  const cfg = window.SUPABASE_CONFIG || {};
  if (!cfg.url || !cfg.anonKey) throw new Error('Brak konfiguracji Supabase');

  const r = await fetch(cfg.url + '/rest/v1/campaign_submissions', {
    method: 'POST',
    headers: {
      'apikey': cfg.anonKey,
      'Authorization': 'Bearer ' + cfg.anonKey,
      'Content-Type': 'application/json',
      // Bez tego PostgREST próbuje zwrócić wstawiony wiersz, a anon nie ma
      // prawa odczytu — zapis by się udał, ale odpowiedź byłaby błędem.
      'Prefer': 'return=minimal',
    },
    body: JSON.stringify(payload),
  });

  if (r.status === 201) return { ok: true, status: 'ok' };

  let detail = '';
  try { detail = (await r.json()).message || ''; } catch (e) {}
  console.error('[zgłoszenie] Supabase', r.status, detail);
  return { ok: false, status: r.status === 400 || r.status === 409 ? 'invalid' : 'error' };
}

// ---------- elementy formularza ----------
function Field({ id, label, required, optional, hint, error, counter, children }) {
  return (
    <div className="zg-field">
      <label className="zg-label" htmlFor={id}>
        {label}
        {required && <span className="req" aria-hidden="true">*</span>}
        {optional && <span className="opt">— opcjonalne</span>}
      </label>
      {hint && <p className="zg-hint" id={id + '-hint'}>{hint}</p>}
      {children}
      {(error || counter) && (
        <div className="zg-meta">
          {error ? <p className="zg-err" id={id + '-err'}>{error}</p> : <span />}
          {counter && (
            <p className={'zg-counter' + (counter.now > counter.max ? ' over' : '')}>
              {counter.now}/{counter.max}
            </p>
          )}
        </div>
      )}
    </div>
  );
}

const fmtZl = (n) => n.toLocaleString('pl-PL', { maximumFractionDigits: 0 }) + ' zł';

// Kwota zbiórki — komunikat, nie pole. Puste pole formularza to zaproszenie
// do wpisania czegoś; skoro kwoty nikt tu nie wpisuje, nie udajemy pola.
// Dopóki nie znamy ceny dla wybranej mocy, mówimy wprost, kto ją ustali.
function TargetAmountNote({ power }) {
  const price = priceForPower(power);
  return (
    <p className="zg-costnote" role="status" aria-live="polite">
      {price ? (
        <>
          Szacowana wartość zbiórki: <strong>{fmtZl(price)}</strong> — wyliczona
          z wybranego celu. Ostateczną kwotę potwierdzi doradca po dobraniu modelu.
        </>
      ) : (
        <>Kwoty nie podajesz — wartość zbiórki ustali nasz doradca po dobraniu konkretnego modelu agregatu.</>
      )}
    </p>
  );
}

// Załączniki. Weryfikacja wiarygodności opiera się na tym, co tu wpadnie,
// więc pole jest widoczne i opisane, a nie schowane pod „opcjonalne".
function DocumentPicker({ files, error, onAdd, onRemove }) {
  const inputRef = useRef(null);
  const left = MAX_FILES - files.length;

  const pick = (e) => {
    onAdd(Array.from(e.target.files || []));
    e.target.value = '';  // ten sam plik da się wybrać ponownie po usunięciu
  };

  return (
    <div className="zg-field">
      <span className="zg-label" id="docs-label">
        Dokumenty potwierdzające
        <span className="opt">— opcjonalne, ale mocno pomagają</span>
      </span>
      <p className="zg-hint" id="docs-hint">
        Orzeczenie, kosztorys, faktura, zaświadczenie, pismo z placówki — cokolwiek
        uwiarygodnia sprawę. PDF lub zdjęcie, do {MAX_FILES} plików po 10 MB.
        Dokumentów nie publikujemy — służą wyłącznie weryfikacji.
      </p>

      {files.length > 0 && (
        <ul className="zg-files">
          {files.map((f, i) => (
            <li className="zg-file" key={f.name + f.size + i}>
              <span className="zg-file-ico" aria-hidden="true">
                {f.type === 'application/pdf' ? 'PDF' : 'IMG'}
              </span>
              <span className="zg-file-name" title={f.name}>{f.name}</span>
              <span className="zg-file-size">{fmtSize(f.size)}</span>
              <button type="button" className="zg-row-del" onClick={() => onRemove(i)}
                      title="Usuń załącznik" aria-label={'Usuń załącznik ' + f.name}>×</button>
            </li>
          ))}
        </ul>
      )}

      {left > 0 ? (
        <>
          <input ref={inputRef} id="documents" type="file" multiple accept={ACCEPT_ATTR}
                 className="sr-only" onChange={pick}
                 aria-labelledby="docs-label" aria-describedby="docs-hint" />
          <button type="button" className="zg-addrow" onClick={() => inputRef.current && inputRef.current.click()}>
            <span aria-hidden="true">+</span> Dodaj {files.length ? 'kolejny plik' : 'dokument'}
          </button>
        </>
      ) : (
        <p className="zg-hint" style={{ margin: 0 }}>
          Osiągnięto limit {MAX_FILES} plików. Usuń któryś, żeby dodać inny.
        </p>
      )}

      {error && <div className="zg-meta"><p className="zg-err" id="docs-err">{error}</p></div>}
    </div>
  );
}

function Steps({ step }) {
  const items = [
    { n: 1, label: 'O zbiórce' },
    { n: 2, label: 'Kto zgłasza' },
  ];
  return (
    <div className="zg-steps">
      {items.map((it, i) => (
        <React.Fragment key={it.n}>
          {i > 0 && <span className="zg-steps-sep" aria-hidden="true" />}
          <div
            className={'zg-step' + (step === it.n ? ' active' : step > it.n ? ' done' : '')}
            aria-current={step === it.n ? 'step' : undefined}
          >
            <span className="zg-step-num">{step > it.n ? '✓' : it.n}</span>
            <span>{it.label}</span>
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}

// ---------- ekran potwierdzenia ----------
function Done({ email }) {
  return (
    <div className="zg-done">
      <div className="zg-done-mark">
        <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round">
          <path d="M5 12.5l4.2 4.2L19 6.5" />
        </svg>
      </div>
      <h2>Zgłoszenie przyjęte</h2>
      <p className="zg-done-lead">
        Dziękujemy za zgłoszenie zbiórki. Przeczytamy je uważnie — każdą sprawę
        rozpatrujemy pojedynczo, a nie automatycznie.
      </p>

      <div className="zg-next">
        <h3>Co dalej</h3>
        <ol>
          <li>Sprawdzamy zgłoszenie i wiarygodność beneficjenta.</li>
          <li>Jeśli będziemy potrzebować szczegółów lub dokumentów, napiszemy
            na adres <b>{email || 'podany w zgłoszeniu'}</b>.</li>
          <li>O decyzji — pozytywnej albo odmownej — informujemy mailem.
            Zakwalifikowana zbiórka trafia na stronę programu, a klienci Protonu
            zaczynają ją wskazywać.</li>
        </ol>
      </div>

      <a className="zg-back" href="index.html">← Wróć do strony programu</a>
    </div>
  );
}

// ---------- główny komponent ----------
function ZgloszenieForm() {
  const [step,   setStep]   = useState(1);
  const [values, setValues] = useState(() => (IS_DEMO ? { ...EMPTY, ...DEMO } : EMPTY));
  const [errors, setErrors] = useState({});
  const [busy,   setBusy]   = useState(false);
  const [sendErr,setSendErr]= useState(null);
  const [done,   setDone]   = useState(false);
  const [docs,   setDocs]   = useState([]);
  const [docErr, setDocErr] = useState('');
  const [upStep, setUpStep] = useState(null);
  const topRef = useRef(null);

  // ----- załączniki -----
  const addDocs = (picked) => {
    setDocErr('');
    setDocs(prev => {
      const next = prev.slice();
      const problems = [];
      for (const f of picked) {
        if (next.length >= MAX_FILES) {
          problems.push(`Można załączyć najwyżej ${MAX_FILES} plików — „${f.name}" pominięty.`);
          continue;
        }
        const err = fileError(f, next);
        if (err) problems.push(err); else next.push(f);
      }
      if (problems.length) setDocErr(problems[0]);
      return next;
    });
  };

  const removeDoc = (i) => {
    setDocErr('');
    setDocs(prev => prev.filter((_, j) => j !== i));
  };

  const set = (name) => (e) => {
    const val = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    setValues(prev => ({ ...prev, [name]: val }));
    // Błąd znika, gdy tylko pole zostanie poprawione — nie każemy czekać do blur
    setErrors(prev => (prev[name] ? { ...prev, [name]: '' } : prev));
  };

  const blur = (name) => () => {
    setValues(prev => {
      const err = validateField(name, prev);
      setErrors(e => ({ ...e, [name]: err }));
      return prev;
    });
  };

  // Waliduje listę pól; przy błędach ustawia je i przenosi focus na pierwsze.
  // submitter_role to radiogrupa, a nie pojedyncze pole o tym id — celujemy
  // w pierwszy radio, żeby focus i przewinięcie w ogóle zadziałały.
  const focusTarget = (name) =>
    name === 'submitter_role'
      ? document.querySelector('input[name="submitter_role"]')
      : document.getElementById(name);

  const runValidation = (fields) => {
    const next = {};
    fields.forEach(f => { next[f] = validateField(f, values); });
    setErrors(prev => ({ ...prev, ...next }));
    const first = fields.find(f => next[f]);
    if (first) {
      const el = focusTarget(first);
      if (el) el.focus({ preventScroll: false });
      return false;
    }
    return true;
  };

  // Instant, nie smooth: `behavior: 'smooth'` bywa ignorowane (m.in. przy
  // prefers-reduced-motion i w części silników), a przy podmianie całej treści
  // przejazd przez znikający formularz i tak nic nie wnosi.
  const scrollToTop = () => {
    if (topRef.current) topRef.current.scrollIntoView({ block: 'start' });
  };

  const goToStep2 = () => {
    if (!runValidation(STEP1_FIELDS)) return;
    setStep(2);
    scrollToTop();
  };

  const backToStep1 = () => {
    setStep(1);
    scrollToTop();
  };

  const submit = async (e) => {
    e.preventDefault();
    if (!runValidation(step2Fields(values))) return;
    setBusy(true);
    setSendErr(null);
    try {
      // id z góry: pliki lądują w folderze o tej nazwie, zanim powstanie wiersz
      const submissionId = (window.crypto && window.crypto.randomUUID)
        ? window.crypto.randomUUID()
        : String(Date.now()) + '-' + Math.round(Math.random() * 1e9);

      let uploaded = [];
      if (docs.length) {
        uploaded = await uploadDocuments(submissionId, docs, (n, total) => setUpStep(`${n}/${total}`));
        setUpStep(null);
      }

      const res = await submitZgloszenie(buildPayload(values, submissionId, uploaded));
      if (res.ok) {
        setDone(true);
      } else {
        setSendErr('Nie udało się wysłać zgłoszenia. Spróbuj ponownie za chwilę.');
      }
    } catch (err) {
      setUpStep(null);
      // Błąd wgrywania niesie nazwę pliku — ogólnikowy komunikat kazałby zgadywać,
      // który załącznik jest problemem.
      setSendErr(
        /wgrać pliku/.test(err.message || '')
          ? err.message + ' Usuń go z listy albo spróbuj ponownie.'
          : 'Błąd połączenia. Spróbuj ponownie za chwilę albo napisz na kontakt@proton-polska.pl.'
      );
    }
    setBusy(false);
  };

  useEffect(() => {
    if (IS_DEMO) console.info('[zgłoszenie] tryb demo — pola wypełnione przykładową treścią');
  }, []);

  // Scroll dopiero po przerysowaniu. Wywołany w handlerze leciałby jeszcze przy
  // pełnej wysokości formularza — ekran potwierdzenia jest dużo krótszy, więc
  // zmiana layoutu zostawiała stronę przewiniętą w połowie.
  useEffect(() => {
    if (done) window.scrollTo(0, 0);
  }, [done]);

  if (done) return <Done email={values.submitter_email.trim()} />;

  const isOrg = values.submitter_role === 'organizacja';

  return (
    <div ref={topRef}>
      <Steps step={step} />

      <form onSubmit={submit} noValidate>
        {step === 1 ? (
          <section className="zg-section">
            <h2>O zbiórce</h2>
            <p className="zg-section-note">
              Program finansuje agregaty prądotwórcze. Im konkretniej opiszesz
              sprawę i to, co agregat ma zasilać, tym szybciej ją ocenimy.
            </p>

            <Field id="title" label="Tytuł zbiórki" required error={errors.title}
                   counter={{ now: values.title.length, max: MAX_TITLE }}>
              <input id="title" name="title" type="text" className="zg-input"
                     placeholder="np. Agregat dla Zosi na respiratorze"
                     value={values.title} onChange={set('title')} onBlur={blur('title')}
                     aria-invalid={errors.title ? 'true' : undefined}
                     aria-describedby={errors.title ? 'title-err' : undefined} />
            </Field>

            <Field id="beneficiary_type" label="Kategoria" required error={errors.beneficiary_type}>
              <select id="beneficiary_type" name="beneficiary_type" className="zg-select"
                      value={values.beneficiary_type} onChange={set('beneficiary_type')}
                      onBlur={blur('beneficiary_type')}
                      aria-invalid={errors.beneficiary_type ? 'true' : undefined}
                      aria-describedby={errors.beneficiary_type ? 'beneficiary_type-err' : undefined}>
                <option value="">— wybierz kategorię —</option>
                {ZG_CATEGORIES.map(c => <option key={c.id} value={c.id}>{c.label}</option>)}
              </select>
            </Field>

            <Field id="short_description" label="Krótki opis" required
                   hint="Jedno–dwa zdania. To one pojawią się na kafelku zbiórki na stronie programu."
                   error={errors.short_description}
                   counter={{ now: values.short_description.length, max: MAX_SHORT }}>
              <textarea id="short_description" name="short_description" className="zg-textarea"
                        style={{ minHeight: 84 }}
                        placeholder="np. Zosia oddycha przy pomocy respiratora, a we wsi kilka razy w roku brakuje prądu…"
                        value={values.short_description} onChange={set('short_description')}
                        onBlur={blur('short_description')}
                        aria-invalid={errors.short_description ? 'true' : undefined}
                        aria-describedby={'short_description-hint' + (errors.short_description ? ' short_description-err' : '')} />
            </Field>

            <Field id="long_description" label="Opis szczegółowy" required
                   hint="Kim jest beneficjent, co się stało, w jakiej sytuacji jest teraz."
                   error={errors.long_description}>
              <textarea id="long_description" name="long_description" className="zg-textarea"
                        style={{ minHeight: 150 }}
                        value={values.long_description} onChange={set('long_description')}
                        onBlur={blur('long_description')}
                        aria-invalid={errors.long_description ? 'true' : undefined}
                        aria-describedby={'long_description-hint' + (errors.long_description ? ' long_description-err' : '')} />
            </Field>

            <Field id="goal_summary" label="Cel w jednym zdaniu" required
                   hint="Co ma się wydarzyć dzięki tym środkom. To zdanie pokazujemy przy pasku postępu zbiórki."
                   error={errors.goal_summary}>
              <input id="goal_summary" name="goal_summary" type="text" className="zg-input"
                     placeholder="np. Agregat utrzymujący pracę respiratora przy każdej przerwie w dostawie prądu"
                     value={values.goal_summary} onChange={set('goal_summary')}
                     onBlur={blur('goal_summary')}
                     aria-invalid={errors.goal_summary ? 'true' : undefined}
                     aria-describedby={'goal_summary-hint' + (errors.goal_summary ? ' goal_summary-err' : '')} />
            </Field>

            {/* Nazwa pola wprost od klienta. Sekcja musi nazywać PRODUKT, na który
                idą środki — bez niej formularz nigdzie nie mówi, co beneficjent
                dostanie. Podpowiedź tłumaczy to językiem osoby zgłaszającej. */}
            <Field id="generator_power" label="Konkretny cel produktowy przekazanych środków" required
                   hint="Środki z tej zbiórki idą na agregat prądotwórczy. Wskaż przybliżoną moc — konkretny model dobierze nasz doradca w rozmowie z beneficjentem. Jeśli nie wiesz, zaznacz ostatnią opcję."
                   error={errors.generator_power}>
              <select id="generator_power" name="generator_power" className="zg-select"
                      value={values.generator_power} onChange={set('generator_power')}
                      onBlur={blur('generator_power')}
                      aria-invalid={errors.generator_power ? 'true' : undefined}
                      aria-describedby={'generator_power-hint' + (errors.generator_power ? ' generator_power-err' : '')}>
                <option value="">— wybierz moc agregatu —</option>
                {POWER_OPTIONS.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
            </Field>

            <TargetAmountNote power={values.generator_power} />

            <Field id="why_important" label="Dlaczego ta sprawa jest ważna" required
                   hint="Co się stanie, jeśli pomoc nie nadejdzie albo nadejdzie za późno."
                   error={errors.why_important}>
              <textarea id="why_important" name="why_important" className="zg-textarea"
                        value={values.why_important} onChange={set('why_important')}
                        onBlur={blur('why_important')}
                        aria-invalid={errors.why_important ? 'true' : undefined}
                        aria-describedby={'why_important-hint' + (errors.why_important ? ' why_important-err' : '')} />
            </Field>

            <Field id="source_url" label="Link do zbiórki lub strony beneficjenta" optional
                   hint="Strona organizacji, profil na Facebooku, istniejąca zbiórka — cokolwiek, co pomoże nam zweryfikować sprawę."
                   error={errors.source_url}>
              <input id="source_url" name="source_url" type="text" className="zg-input"
                     placeholder="https://…"
                     value={values.source_url} onChange={set('source_url')}
                     onBlur={blur('source_url')}
                     aria-invalid={errors.source_url ? 'true' : undefined}
                     aria-describedby={'source_url-hint' + (errors.source_url ? ' source_url-err' : '')} />
            </Field>

            <DocumentPicker files={docs} error={docErr} onAdd={addDocs} onRemove={removeDoc} />

            <div className="zg-actions">
              <span className="zg-spacer" />
              <button type="button" className="zg-btn zg-btn-primary" onClick={goToStep2}>
                Dalej — kto zgłasza →
              </button>
            </div>
          </section>
        ) : (
          <section className="zg-section">
            <h2>Kto zgłasza</h2>
            <p className="zg-section-note">
              Kontakt jest nam potrzebny, żeby dopytać o szczegóły i przekazać decyzję.
              Danych nie publikujemy na stronie programu.
            </p>

            <Field id="submitter_name" label="Imię i nazwisko" required error={errors.submitter_name}>
              <input id="submitter_name" name="submitter_name" type="text" className="zg-input"
                     value={values.submitter_name} onChange={set('submitter_name')}
                     onBlur={blur('submitter_name')}
                     aria-invalid={errors.submitter_name ? 'true' : undefined}
                     aria-describedby={errors.submitter_name ? 'submitter_name-err' : undefined} />
            </Field>

            <Field id="submitter_email" label="Adres e-mail" required error={errors.submitter_email}>
              <input id="submitter_email" name="submitter_email" type="email" className="zg-input"
                     placeholder="imie.nazwisko@example.com"
                     value={values.submitter_email} onChange={set('submitter_email')}
                     onBlur={blur('submitter_email')}
                     aria-invalid={errors.submitter_email ? 'true' : undefined}
                     aria-describedby={errors.submitter_email ? 'submitter_email-err' : undefined} />
            </Field>

            <Field id="submitter_phone" label="Telefon" required error={errors.submitter_phone}>
              <input id="submitter_phone" name="submitter_phone" type="tel" className="zg-input"
                     placeholder="601 234 567"
                     value={values.submitter_phone} onChange={set('submitter_phone')}
                     onBlur={blur('submitter_phone')}
                     aria-invalid={errors.submitter_phone ? 'true' : undefined}
                     aria-describedby={errors.submitter_phone ? 'submitter_phone-err' : undefined} />
            </Field>

            <div className="zg-field">
              <span className="zg-label" id="submitter_role">
                W jakiej roli zgłaszasz zbiórkę
                <span className="req" aria-hidden="true">*</span>
              </span>
              <div className="zg-radios" role="radiogroup" aria-labelledby="submitter_role">
                {ROLES.map(r => (
                  <label key={r.id}
                         className={'zg-radio' + (values.submitter_role === r.id ? ' checked' : '')}>
                    <input type="radio" name="submitter_role" value={r.id}
                           checked={values.submitter_role === r.id}
                           onChange={set('submitter_role')} />
                    <span className="zg-radio-label">
                      {r.label}
                      <span className="zg-radio-sub">{r.sub}</span>
                    </span>
                  </label>
                ))}
              </div>
              {errors.submitter_role && (
                <div className="zg-meta"><p className="zg-err">{errors.submitter_role}</p></div>
              )}
            </div>

            {isOrg && (
              <div className="zg-org">
                <Field id="org_name" label="Nazwa organizacji" required error={errors.org_name}>
                  <input id="org_name" name="org_name" type="text" className="zg-input"
                         placeholder="np. Fundacja Pomocna Dłoń"
                         value={values.org_name} onChange={set('org_name')}
                         onBlur={blur('org_name')}
                         aria-invalid={errors.org_name ? 'true' : undefined}
                         aria-describedby={errors.org_name ? 'org_name-err' : undefined} />
                </Field>
                <Field id="org_id" label="KRS lub NIP" required error={errors.org_id}>
                  <input id="org_id" name="org_id" type="text" className="zg-input"
                         placeholder="np. 0000123456"
                         value={values.org_id} onChange={set('org_id')}
                         onBlur={blur('org_id')}
                         aria-invalid={errors.org_id ? 'true' : undefined}
                         aria-describedby={errors.org_id ? 'org_id-err' : undefined} />
                </Field>
              </div>
            )}

            <div className="zg-field" style={{ marginTop: 30 }}>
              <span className="zg-label">Zgody</span>
              <div className="zg-consents">
                <label className={'zg-check' + (errors.consent_terms ? ' invalid' : '')}>
                  <input id="consent_terms" type="checkbox" checked={values.consent_terms}
                         onChange={set('consent_terms')} />
                  <span className="zg-check-text">
                    Akceptuję <a href="regulamin.html" target="_blank" rel="noopener">regulamin programu</a> i
                    zapoznałam/em się z <a href="rodo.html" target="_blank" rel="noopener">klauzulą informacyjną RODO</a>.
                    <span className="req" aria-hidden="true">*</span>
                  </span>
                </label>

                <label className={'zg-check' + (errors.consent_truthful ? ' invalid' : '')}>
                  <input id="consent_truthful" type="checkbox" checked={values.consent_truthful}
                         onChange={set('consent_truthful')} />
                  <span className="zg-check-text">
                    Oświadczam, że podane dane są prawdziwe, a opisaną sprawę zgłaszam
                    za wiedzą osoby lub organizacji, której dotyczy.
                    <span className="req" aria-hidden="true">*</span>
                  </span>
                </label>

                <label className="zg-check">
                  <input type="checkbox" checked={values.consent_phone}
                         onChange={set('consent_phone')} />
                  <span className="zg-check-text">
                    Zgadzam się na kontakt telefoniczny w sprawie tego zgłoszenia.
                  </span>
                </label>
              </div>
              {(errors.consent_terms || errors.consent_truthful) && (
                <div className="zg-meta">
                  <p className="zg-err">{errors.consent_terms || errors.consent_truthful}</p>
                </div>
              )}
            </div>

            {sendErr && <p className="zg-formerr">{sendErr}</p>}

            <div className="zg-actions">
              <button type="button" className="zg-btn zg-btn-ghost" onClick={backToStep1} disabled={busy}>
                ← Wróć
              </button>
              <span className="zg-spacer" />
              <button type="submit" className="zg-btn zg-btn-primary" disabled={busy}>
                {upStep ? `Wgrywanie plików ${upStep}…` : busy ? 'Wysyłanie…' : 'Wyślij zgłoszenie'}
              </button>
            </div>
          </section>
        )}
      </form>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<ZgloszenieForm />);
