/* Live matchMedia as React state — used to pick the hero's video source.
   The choice has to happen in JS: <source media="..."> was dropped from
   the video element in every current browser, so a <video> cannot switch
   resolution by viewport on its own. */
const useMediaQuery = (query) => {
  const [matches, setMatches] = React.useState(
    () => typeof window !== "undefined" && window.matchMedia(query).matches
  );
  React.useEffect(() => {
    const mq = window.matchMedia(query);
    const onChange = (e) => setMatches(e.matches);
    setMatches(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, [query]);
  return matches;
};

/* Los teléfonos reciben un RECORTE VERTICAL de cada clip (534×950), no una
   versión reducida del apaisado. El hero ocupa la pantalla entera y el
   vídeo va con object-fit:cover, así que de un fichero apaisado el
   teléfono sólo enseña una franja central y la estira: del 1280×634 que
   se servía antes salían 293 píxeles de origen estirados a 750 de
   pantalla. Medido con VMAF sobre lo que llega a la pantalla, aquello
   puntuaba 43 y esto puntúa 94, y además pesa menos —21,6 MB los cuatro
   frente a 23,8— porque deja de enviar el 72 % del fotograma que el
   teléfono tira. La receta está en scripts/build-home-clips.sh.
   El corte es por ancho, no por proporción: lo que importa es cuántos
   píxeles de pantalla tiene que llenar el vídeo. */
const HERO_MOBILE_MQ = "(max-width: 700px)";
const heroSrcFor = (src, small) =>
  small && src ? src.replace(/\.mp4(\?|$)/, ".mobile.mp4$1") : src;

/* HeroCarousel — full-bleed video carousel.
 *
 * Each slide carries a `video` field (an .mp4 path, concatenation of several
 * source cuts). The carousel plays one slide's clip in full, then advances
 * via the `ended` event — no fixed setTimeout. Manual navigation (arrow / dot
 * click) sets the index and the new slide's video autoplays immediately;
 * `videoRef.load() + .play()` covers Safari/iOS which don't restart playback
 * on src swaps reliably.
 *
 * The dot progress ring on the active dot is driven by requestAnimationFrame:
 * a loop reads `video.currentTime / video.duration` every frame and mutates
 * the SVG circle's `stroke-dashoffset` directly via ref — no React re-renders
 * per frame, smooth at 60fps. When the active slide changes the ref points
 * at the new active dot's circle and the loop continues seamlessly.
 *
 * Backward-compatible: if a slide ever omits `video`, it falls back to the
 * `img` poster + a fixed 6s setTimeout for that slide only.
 */
const HeroCarousel = ({ setRoute }) => {
  const { t } = useLang();
  const smallScreen = useMediaQuery(HERO_MOBILE_MQ);
  const slides = t.home.heroSlides;
  const [i, setI] = React.useState(0);
  /* Refs to ALL slide videos — keyed by slide index. We mount one <video>
     per slide and control play/pause through these refs (rather than only
     rendering the active video and swapping it back to an <img> on
     deactivation). Keeping every video mounted means the cross-fade between
     slides is video↔video — no more freeze-frame of the poster image while
     the old slide fades out. */
  const videoRefs = React.useRef([]);
  const ringRef   = React.useRef(null);
  const RING_R = 9;
  const RING_C = 2 * Math.PI * RING_R;   // ≈ 56.55

  /* Staged preloading. Every slide used to mount with preload="auto", so a
     cold load fired four full video downloads at once and the FIRST slide —
     the only one anyone sees immediately — had to share bandwidth with three
     clips nobody was watching yet. Now slide 0 alone preloads eagerly; the
     rest sit at preload="none" until slide 0 reports it can play, at which
     point they're upgraded and told to load, so they're buffered well before
     their turn comes round. */
  /* Índice del crédito activo dentro del vídeo de la diapositiva. Los
     vídeos de portada son montajes: el de instalaciones singulares abre
     con la torre del Banco de Bilbao, de Sáenz de Oíza, y a los trece
     segundos pasa a otros edificios de Miguel de Oriol e Ybarra. Un
     único crédito fijo dejaba el nombre equivocado en pantalla durante
     la mayor parte del vídeo. */
  const [creditIdx, setCreditIdx] = React.useState(0);
  const [primed, setPrimed] = React.useState(false);
  React.useEffect(() => {
    if (!primed) return;
    videoRefs.current.forEach((vid, idx) => {
      /* Never re-load slide 0 — it's the one on screen, and .load() would
         yank it back to a blank frame. */
      if (vid && idx !== 0) vid.load();
    });
  }, [primed]);

  // Slide-change orchestration:
  //   • Pause every inactive video — but DON'T rewind it. We want the
  //     outgoing slide to fade out on the frame it was just showing,
  //     not snap back to frame 0 mid-transition.
  //   • Reset + play the active video so it starts from the beginning.
  //   • Drive the active dot's ring via rAF reading currentTime/duration.
  React.useEffect(() => {
    videoRefs.current.forEach((vid, idx) => {
      if (!vid) return;
      if (idx === i) {
        vid.currentTime = 0;
        const p = vid.play();
        if (p && typeof p.catch === "function") p.catch(() => {});
      } else {
        vid.pause();
      }
    });
    if (ringRef.current) ringRef.current.style.strokeDashoffset = String(RING_C);
    setCreditIdx(0);
    let raf;
    const tick = () => {
      const v = videoRefs.current[i];
      const r = ringRef.current;
      if (v && r && v.duration > 0) {
        const pct = Math.min(1, Math.max(0, v.currentTime / v.duration));
        r.style.strokeDashoffset = String(RING_C * (1 - pct));
      }
      /* El crédito se elige aquí porque este bucle ya está leyendo
         currentTime para el anillo. Solo se toca el estado cuando el
         tramo cambia de verdad — si no, serían sesenta renderizados por
         segundo para escribir el mismo nombre. */
      const creds = slides[i] && slides[i].architects;
      if (v && creds && creds.length) {
        let k = 0;
        while (k < creds.length - 1 && creds[k].until != null && v.currentTime >= creds[k].until) k++;
        setCreditIdx((prev) => (prev === k ? prev : k));
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [i, RING_C, slides]);

  // Image-only fallback: if the active slide has no video, advance after 6s.
  // Live-only when the current slide actually lacks `video`, so video-driven
  // slides never run this timer in parallel with their own ended event.
  React.useEffect(() => {
    if (slides[i] && slides[i].video) return;
    const tm = setTimeout(() => setI(x => (x + 1) % slides.length), 6000);
    return () => clearTimeout(tm);
  }, [i, slides]);

  const advance = () => setI(x => (x + 1) % slides.length);
  const next = () => advance();
  const prev = () => setI(x => (x - 1 + slides.length) % slides.length);

  /* CTA target — every slide names an area, so the button drops into that
     area's landing page. Falls back to the areas hub if a slide ever ships
     without a sectorId. */
  const goSlide = (slide) => {
    if (slide && slide.sectorId) setRoute({route: "sector", sectorId: slide.sectorId});
    else setRoute("proyectos");
  };

  return (
    <section className="hero">
      {slides.map((s, idx) => {
        const isActive = idx === i;
        return (
          <div key={idx} className={"hero__slide " + (isActive ? "is-active" : "")}>
            {!s.video ? (
              /* Still frame — only for a slide that ships without a clip.
                 Every viewport now gets the video, phones included. */
              <img src={s.img} alt="" className="hero__img"
                   /* Same staged load as the videos, and for the same reason
                      the poster failed before: every slide sits at inset:0
                      under the first one, so the lazy heuristic never queues
                      the ones behind and advancing the carousel landed on a
                      blank slide. Slide 0 loads eagerly; once it is in, the
                      rest are upgraded and fetched ahead of their turn. */
                   loading={idx === 0 || primed ? "eager" : "lazy"}
                   onLoad={idx === 0 ? () => setPrimed(true) : undefined}
                   style={s.objectPosition ? {objectPosition: s.objectPosition} : null} />
            ) : (
              <video ref={el => { videoRefs.current[idx] = el; }}
                     className="hero__video"
                     src={heroSrcFor(s.video, smallScreen)}
                     muted playsInline
                     preload={idx === 0 ? "auto" : (primed ? "auto" : "none")}
                     onCanPlay={idx === 0 ? () => setPrimed(true) : undefined}
                     onEnded={isActive ? advance : undefined}
                     style={s.objectPosition ? {objectPosition: s.objectPosition} : null}
                     aria-hidden="true" />
            )}
            <div className="hero__overlay" />
          </div>
        );
      })}
      {/* Copy lives OUTSIDE the slides, as a sibling of the bottom scrim.
          Each slide is its own stacking context (see .hero__slide), so
          anything nested inside one is trapped at the slide's level and
          the scrim — which has to sit above the video to do its job —
          would wash over the eyebrow and headline. Hoisted out here the
          copy shares a layer with the rest of the hero chrome (arrows,
          dots, credit) and stays clear of the gradient.
          One block per slide, cross-fading on the same 1000ms curve as
          the slides themselves, so the text still hands over rather than
          cutting. */}
      {slides.map((s, idx) => (
        <div key={idx} className={"hero__inner " + (idx === i ? "is-active" : "")}>
          <div className="hero__content">
            {/* The area name IS the headline now — the marketing sentence
                that used to sit here is gone, so the slide says what it
                shows and the button says where it goes. `headline` is
                still carried in the data, unused, in case it comes back. */}
            <h1 className="hero__headline">{s.eyebrow}</h1>
            <div className="hero__cta">
              <ArrowCTA outline dark onClick={() => goSlide(s)}>{t.home.viewProjects}</ArrowCTA>
            </div>
          </div>
        </div>
      ))}
      <button className="hero__arrow hero__arrow--l" onClick={prev} aria-label="Prev"><Icon.Chevron dir="left" s={22} c="#fff"/></button>
      <button className="hero__arrow hero__arrow--r" onClick={next} aria-label="Next"><Icon.Chevron s={22} c="#fff"/></button>
      {/* Dot strip — ref-driven progress ring. The ring SVG is rendered only
          for the active dot; the rAF loop above mutates its strokeDashoffset
          based on video.currentTime/duration each frame. Clicking another dot
          jumps + resets via the same effect. */}
      <div className="hero__dots">
        {slides.map((_, idx) => {
          const isActive = idx === i;
          return (
            <button key={idx}
                    type="button"
                    className={"hero__dot " + (isActive ? "is-on" : "")}
                    onClick={() => setI(idx)}
                    aria-label={"Slide " + (idx + 1)}>
              <span className="hero__dot-core" />
              {isActive && (
                <svg className="hero__dot-ring" width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
                  <circle ref={ringRef}
                          cx="14" cy="14" r={RING_R}
                          fill="none"
                          stroke="#FFFFFF"
                          strokeWidth="1.5"
                          strokeLinecap="round"
                          strokeDasharray={RING_C}
                          strokeDashoffset={RING_C}
                          style={{transform: "rotate(-90deg)", transformOrigin: "50% 50%"}} />
                </svg>
              )}
            </button>
          );
        })}
      </div>
      {/* Architect credit for the active slide — bottom-right of the
          hero. Only renders when the slide carries an `architect`
          field. Same font / colour vocabulary as the sector stage
          credit so the brand reads consistently across hero shells. */}
      {(() => {
        const sl = slides[i];
        if (!sl) return null;
        /* `architects` es una lista con tramos: cada entrada vale hasta
           el segundo `until`, y la última hasta el final. `architect`
           sigue funcionando para las diapositivas de un solo autor. */
        const name = sl.architects && sl.architects.length
          ? (sl.architects[Math.min(creditIdx, sl.architects.length - 1)] || {}).name
          : sl.architect;
        if (!name) return null;
        return (
          <span className="hero__credit" aria-label={name}>{name}</span>
        );
      })()}
    </section>
  );
};

const ArticleCard = ({ a, onClick }) => {
  const { t } = useLang();
  return (
    <a className="acard" onClick={onClick} style={onClick ? {cursor:"pointer"} : null}>
      <div className="acard__ph"><img src={a.img} alt="" loading="lazy" /></div>
      <div className="acard__body">
        <span className="acard__tag">{a.tag}</span>
        <h3 className="acard__title">{a.title}</h3>
        <p className="acard__excerpt">{a.excerpt}</p>
        <span className="acard__cta">{t.home.readMore} →</span>
      </div>
    </a>
  );
};

/* Person schema (required): name, photo, degree. The legacy `role`
   field carried the academic credential and was renamed to `degree`;
   we still read it as a fallback so half-migrated data renders. */
const TeamCard = ({ t }) => (
  <div className="tcard">
    <div className="tcard__photo">
      {t.photo
        ? <img src={t.photo} alt={t.name} loading="lazy" />
        : <span>{t.name.split(" ").map(w => w[0]).join("").slice(0,2)}</span>}
    </div>
    <div className="tcard__meta">
      <div className="tcard__name">{t.name}</div>
      {/* El puesto en la firma manda; el título académico queda de
          reserva mientras los puestos no estén rellenos, para que la
          tarjeta no salga vacía. En cuanto se escriba un puesto en el
          CMS, esa persona pasa a enseñarlo sin tocar nada más. */}
      <div className="tcard__degree">{t.role || t.degree}</div>
    </div>
  </div>
);

/* Position schema · v2 (Accordion is the public-facing renderer):
     required: title, city, requirements[], open
     optional: department, description
   `city`/`department`/`description` fall back to the legacy field
   names (loc/dept/desc) so half-migrated data still renders. The
   max-height of the panel scales with content because of the
   requirements list — a fixed 360px would cut long lists off. */
const Accordion = ({ items, applyLabel, applyEmail }) => {
  const [open, setOpen] = React.useState(0);
  return (
    <div className="acc">
      {items.map((it, i) => {
        const city = it.city || it.loc;
        const dept = it.department || it.dept;
        const desc = it.description || it.desc;
        const reqs = Array.isArray(it.requirements) ? it.requirements : [];
        return (
        <div key={i} className={"acc__item " + (open === i ? "is-open" : "")}>
          <button className="acc__head" onClick={() => setOpen(open === i ? -1 : i)}>
            <div className="acc__titlewrap">
              <span className="acc__title">{it.title}</span>
              <div className="acc__meta">
                {city && <span className="acc__pill">{city}</span>}
                {dept && <span className="acc__dept">{dept}</span>}
              </div>
            </div>
            <span className="acc__icon">{open === i ? "–" : "+"}</span>
          </button>
          {/* Generous max-height so the requirements list can breathe;
              the actual visible content shrinks back via inner padding
              when collapsed. */}
          <div className="acc__body" style={{maxHeight: open === i ? 800 : 0}}>
            <div className="acc__body-inner">
              {desc && <p>{desc}</p>}
              {reqs.length > 0 && (
                <ul className="acc__reqs">
                  {reqs.map((r, k) => <li key={k}>{r}</li>)}
                </ul>
              )}
              {/* Pulsar enseña la dirección en vez de abrir el cliente de
                  correo. Si desde ahí se pulsa la dirección, el asunto ya
                  viene con el nombre del puesto para que Recursos Humanos
                  pueda clasificar la candidatura. Las candidaturas van a su
                  propio buzón, no al de contacto general. */}
              <MailReveal email={applyEmail}
                          subject={"Candidatura — " + it.title}>
                {applyLabel}
              </MailReveal>
            </div>
          </div>
        </div>
        );
      })}
    </div>
  );
};

/* ClientStrip — continuous left→right marquee of client logos.
   Each logo renders as an <img> when `logo` path is present in the data,
   else as a text placeholder using the client name. Both render through
   the same desaturated-by-default / colour-on-hover treatment, so the
   strip works the moment real logo files arrive (no component change). */
const ClientStrip = ({ eyebrowKey }) => {
  const { t } = useLang();
  const eyebrow = (t.home && t.home[eyebrowKey || "clientsEyebrow"]) || "";
  const clients = t.clients || [];
  if (clients.length === 0) return null;
  // Duplicate the list so the loop is seamless: when the first copy
  // scrolls fully off-screen, the second copy is already mid-track.
  const loop = [...clients, ...clients];
  return (
    <section className="client-strip">
      {eyebrow && (
        <div className="container client-strip__head">
          <Eyebrow muted>{eyebrow}</Eyebrow>
        </div>
      )}
      <div className="client-strip__viewport">
        <div className="client-strip__track">
          {loop.map((c, i) => {
            // Per-logo overrides:
            //  `scale`   — published as the `--logo-scale` CSS custom property
            //              on the slot. The CSS rule for the slot AND the img
            //              both reference this variable in `calc(... * var())`,
            //              so the box and the image grow together with no
            //              specificity battles.
            //  `noBlend` — disable mix-blend-mode (multiply). Needed for logos
            //              with WHITE marks on a coloured shape (e.g. El Corte
            //              Inglés' white text on a green pennant) — multiply
            //              would erase the white lettering.
            const slotStyle = c.scale ? { "--logo-scale": c.scale } : null;
            const imgStyle  = c.noBlend ? { mixBlendMode: "normal" } : null;
            return (
              <div key={i} className="client-strip__item" title={c.name} style={slotStyle}>
                {c.logo
                  ? <img src={c.logo} alt={c.name} loading="lazy" style={imgStyle} />
                  : <span className="client-strip__placeholder">{c.name}</span>}
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
};

Object.assign(window, { HeroCarousel, ArticleCard, TeamCard, Accordion, ClientStrip });
