/* ============================================================
   lib.jsx — shared hooks, primitives, PDF generator
   ============================================================ */
const { useState, useEffect, useRef, useCallback } = React;

/* Scroll-reveal: one IntersectionObserver for every [data-reveal] node.
   Adds .is-in when it enters view. Re-scans on demand via window event. */
function useRevealObserver() {
  useEffect(() => {
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
      });
    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });

    const scan = () => document.querySelectorAll('.reveal:not(.is-in), .draw-x:not(.is-in), .draw-y:not(.is-in)')
      .forEach((el) => io.observe(el));
    scan();
    const t = setInterval(scan, 600); // catch nodes mounted later (calendar/form state changes)
    return () => { clearInterval(t); io.disconnect(); };
  }, []);
}

/* Thin scroll-progress bar at the very top. */
function ScrollProgress() {
  const ref = useRef(null);
  useEffect(() => {
    const onScroll = () => {
      const h = document.documentElement;
      const max = h.scrollHeight - h.clientHeight;
      const p = max > 0 ? (h.scrollTop / max) * 100 : 0;
      if (ref.current) ref.current.style.width = p.toFixed(2) + '%';
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); };
  }, []);
  return <div className="scroll-progress" ref={ref} aria-hidden="true" />;
}

/* Numbered section label with a hairline that draws on reveal. */
function SectionLabel({ num, title }) {
  return (
    <div className="seclabel">
      <span className="seclabel__num">{num}</span>
      <span className="seclabel__title">{title}</span>
      <span className="seclabel__line draw-x" />
    </div>
  );
}

/* Animated count-up for a numeric value (sober, runs once on view). */
function Counter({ to, suffix, duration = 1100 }) {
  const ref = useRef(null);
  const [val, setVal] = useState(0);
  useEffect(() => {
    let raf, started = false;
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) { setVal(to); return; }
    const io = new IntersectionObserver((ents) => {
      ents.forEach((e) => {
        if (e.isIntersecting && !started) {
          started = true;
          const t0 = performance.now();
          const tick = (t) => {
            const p = Math.min(1, (t - t0) / duration);
            const eased = 1 - Math.pow(1 - p, 3);
            setVal(Math.round(eased * to));
            if (p < 1) raf = requestAnimationFrame(tick);
          };
          raf = requestAnimationFrame(tick);
          io.disconnect();
        }
      });
    }, { threshold: 0.4 });
    if (ref.current) io.observe(ref.current);
    return () => { cancelAnimationFrame(raf); io.disconnect(); };
  }, [to, duration]);
  return <span ref={ref} className="tnum">{val}{suffix}</span>;
}

/* Cascade title — letters fade in slowly, drifting down from the top.
   Words stay unbreakable; staggered animation-delay drives the cascade.
   No caret. Honours line breaks (\n). */
function Typewriter({ text, step = 10, startDelay = 40, className }) {
  const reduce = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const lines = text.split('\n');
  let gi = -1; // running letter index across the whole title
  return (
    <span className={'tw' + (className ? ' ' + className : '')} aria-label={text.replace(/\n/g, ' ')}>
      {lines.map((line, li) => (
        <React.Fragment key={li}>
          {li > 0 && <br />}
          {line.split(' ').map((word, wi) => (
            <React.Fragment key={wi}>
              {wi > 0 && ' '}
              <span className="tw-word">
                {word.split('').map((ch, ci) => {
                  gi++;
                  const delay = reduce ? 0 : startDelay + gi * step;
                  return (
                    <span className="tw-ch" key={ci} aria-hidden="true"
                          style={{ animationDelay: delay + 'ms' }}>{ch}</span>
                  );
                })}
              </span>
            </React.Fragment>
          ))}
        </React.Fragment>
      ))}
    </span>
  );
}

const Arrow = () => (
  <svg className="btn__arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
    <path d="M3 8h9M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="square" />
  </svg>
);

/* ----------------------------------------------------------------
   Minimal PDF generator — builds a real, downloadable A4 contract.
   WinAnsi encoding so French accents render correctly.
   ---------------------------------------------------------------- */
function downloadContractPDF() {
  const esc = (s) => s.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
  // {t, size, bold, gap}  — gap = vertical advance before drawing this line
  const L = [
    { t: 'CONTRAT DE REMPLACEMENT', size: 17, bold: true, gap: 0 },
    { t: 'Masseur-kinésithérapeute libéral — modèle type', size: 10.5, bold: false, gap: 24 },
    { t: 'Conforme aux recommandations du Conseil national de l\'Ordre des MK.', size: 9.5, bold: false, gap: 16 },

    { t: 'ENTRE LES SOUSSIGNÉS', size: 11, bold: true, gap: 30 },
    { t: 'Le praticien titulaire : ............................................................ , inscrit au tableau de l\'Ordre', size: 10, bold: false, gap: 19 },
    { t: 'sous le n° ........................ , exerçant ............................................................................. ,', size: 10, bold: false, gap: 15 },
    { t: 'ci-après « le titulaire »,', size: 10, bold: false, gap: 15 },
    { t: 'Et le praticien remplaçant : ............................................................ , n° ADELI ........................ ,', size: 10, bold: false, gap: 19 },
    { t: 'ci-après « le remplaçant ».', size: 10, bold: false, gap: 15 },

    { t: 'ARTICLE 1 — OBJET', size: 11, bold: true, gap: 28 },
    { t: 'Le titulaire confie au remplaçant le soin d\'assurer les actes de masso-kinésithérapie', size: 10, bold: false, gap: 18 },
    { t: 'durant son absence, dans le respect des règles déontologiques de la profession.', size: 10, bold: false, gap: 15 },

    { t: 'ARTICLE 2 — DURÉE', size: 11, bold: true, gap: 26 },
    { t: 'Le remplacement est consenti du ...... / ...... / .......... au ...... / ...... / .......... inclus,', size: 10, bold: false, gap: 18 },
    { t: 'renouvelable par accord écrit des deux parties.', size: 10, bold: false, gap: 15 },

    { t: 'ARTICLE 3 — CONDITIONS FINANCIÈRES', size: 11, bold: true, gap: 26 },
    { t: 'Le remplaçant perçoit l\'intégralité des honoraires et rétrocède au titulaire une', size: 10, bold: false, gap: 18 },
    { t: 'redevance de ........ % au titre des frais de cabinet, réglée en fin de période.', size: 10, bold: false, gap: 15 },

    { t: 'ARTICLE 4 — ASSURANCE & RESPONSABILITÉ', size: 11, bold: true, gap: 26 },
    { t: 'Le remplaçant atteste être titulaire d\'une assurance de responsabilité civile', size: 10, bold: false, gap: 18 },
    { t: 'professionnelle en cours de validité et exerce sous sa seule responsabilité.', size: 10, bold: false, gap: 15 },

    { t: 'ARTICLE 5 — CLAUSE DE NON-CONCURRENCE', size: 11, bold: true, gap: 26 },
    { t: 'Le remplaçant s\'engage à ne pas s\'installer dans un rayon défini pendant une durée', size: 10, bold: false, gap: 18 },
    { t: 'convenue, sauf accord exprès du titulaire.', size: 10, bold: false, gap: 15 },

    { t: 'Fait à ........................................ , le ...... / ...... / ..........  en deux exemplaires.', size: 10, bold: false, gap: 32 },
    { t: 'Le titulaire                                                  Le remplaçant', size: 10, bold: false, gap: 26 },
    { t: '(signature)                                                  (signature)', size: 9.5, bold: false, gap: 16 },

    { t: 'Document modèle — à compléter et faire valider juridiquement avant signature.', size: 8.5, bold: false, gap: 40 },
  ];

  let y = 800, content = '';
  L.forEach((ln) => {
    y -= (ln.gap != null ? ln.gap : 16);
    const f = ln.bold ? 'F2' : 'F1';
    content += `BT /${f} ${ln.size} Tf 1 0 0 1 56 ${y} Tm (${esc(ln.t)}) Tj ET\n`;
  });

  const objs = [
    '<</Type/Catalog/Pages 2 0 R>>',
    '<</Type/Pages/Kids[3 0 R]/Count 1>>',
    '<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Resources<</Font<</F1 5 0 R/F2 6 0 R>>>>/Contents 4 0 R>>',
    `<</Length ${content.length}>>\nstream\n${content}endstream`,
    '<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>',
    '<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Bold/Encoding/WinAnsiEncoding>>',
  ];

  let pdf = '%PDF-1.4\n';
  const offsets = [];
  objs.forEach((o, i) => { offsets.push(pdf.length); pdf += `${i + 1} 0 obj\n${o}\nendobj\n`; });
  const xref = pdf.length;
  pdf += `xref\n0 ${objs.length + 1}\n0000000000 65535 f \n`;
  offsets.forEach((off) => { pdf += String(off).padStart(10, '0') + ' 00000 n \n'; });
  pdf += `trailer\n<</Size ${objs.length + 1}/Root 1 0 R>>\nstartxref\n${xref}\n%%EOF`;

  const bytes = new Uint8Array(pdf.length);
  for (let i = 0; i < pdf.length; i++) bytes[i] = pdf.charCodeAt(i) & 0xff;
  const blob = new Blob([bytes], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = 'Contrat-remplacement-modele.pdf';
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 4000);
}

Object.assign(window, { useRevealObserver, ScrollProgress, SectionLabel, Counter, Typewriter, Arrow, downloadContractPDF });
