/* FindMyFrag — Recommender.
   Editorial framing: this is a shortcut to fragrances you'd probably land on
   after a month of decant samples. It is NOT a promise that you'll love it —
   scent is personal. The honest framing is §7-aligned: we show matches, not
   "perfect picks", and we always tell you the limits.

   Logic:
   - 6-step quiz, one question per step.
   - Each answer contributes weighted points to tags on catalog entries.
   - Entries are ranked by score, with community score × log(n) as tie-break.
   - Top 3 shown with match-score + reasoning (which answers aligned).
*/

/* global React, FMF */
const { useState: useStateR, useMemo: useMemoR } = React;

const QUESTIONS = [
  {
    id: "occasion",
    prompt: "Where will you wear it most?",
    sub: "We weight the rest of the quiz around this. Pick the place you'd actually smell it on yourself.",
    options: [
      { slug: "office",     label: "Office / weekdays",       blurb: "Conference rooms, commutes, professional rooms." },
      { slug: "date-night", label: "Date night",              blurb: "Close proximity. The scent needs to land at arm's length." },
      { slug: "casual",     label: "Casual daily",            blurb: "Grocery runs, coffee, weekend errands. Inoffensive." },
      { slug: "evening",    label: "Evening out",             blurb: "Dinner, cocktails, bar seating. After dark." },
      { slug: "special",    label: "Special occasions only",  blurb: "Weddings, galas, signing days. Worn with intent." },
    ],
    axis: "occasion",
    weight: 3,
  },
  {
    id: "season",
    prompt: "When will you wear it?",
    sub: "Heat and cold change how a scent reads. A pineapple-smoke on a 95°F day is not the same fragrance at 35°F.",
    options: [
      { slug: "spring",     label: "Spring",      blurb: "Cool mornings, warming afternoons. Bright-green compositions shine." },
      { slug: "summer",     label: "Summer",      blurb: "Heat amplifies sweetness and projection. Fresh + citrus reads best." },
      { slug: "fall",       label: "Fall",        blurb: "Sweater weather. Spice, wood, smoke come alive." },
      { slug: "winter",     label: "Winter",      blurb: "Cold air mutes volatiles. Warm ambers and resins carry further." },
      { slug: "all-season", label: "Year-round",  blurb: "One signature. No rotation." },
    ],
    axis: "season",
    weight: 2,
  },
  {
    id: "vibe",
    prompt: "How do you want to be read when you walk in?",
    sub: "This is about the silhouette you project — not the notes. Most quiz-takers pick wrong here; pick what's honest, not aspirational.",
    options: [
      { slug: "sophisticated", label: "Composed & polished",    blurb: "Put-together. Knows the room." },
      { slug: "bold",          label: "Confident & deliberate", blurb: "Not loud — deliberate. Makes decisions." },
      { slug: "sensual",       label: "Warm & close",           blurb: "Reads intimate at arm's length." },
      { slug: "fresh",         label: "Clean & uncomplicated",  blurb: "Signals hygiene and ease, not perfume." },
      { slug: "cozy",          label: "Warm & approachable",    blurb: "Comfortable to be near. Never performs." },
      { slug: "quiet",         label: "Restrained & private",   blurb: "Barely a fragrance. A close orbit only." },
    ],
    axis: "vibe",
    weight: 3,
  },
  {
    id: "family",
    prompt: "Which of these appeals most?",
    sub: "Don't overthink — pick the sentence you'd nod at in a store. We'll use this to break ties when two fragrances tie on lifestyle fit.",
    options: [
      { slug: "fresh-fruit",   label: "Sun-warmed fruit & ocean air",              blurb: "Pineapple, citrus, salt. Bright, energetic, clean." },
      { slug: "woody-smoke",   label: "Old library & a cracked-open cigar box",    blurb: "Oud, cedar, birch smoke, tobacco. Warm and serious." },
      { slug: "spicy-warm",    label: "Mulled wine in a wool coat",                blurb: "Cinnamon, clove, amber, resin. Close and enveloping." },
      { slug: "sweet-gourmand", label: "Vanilla cake shared after dinner",         blurb: "Tonka, vanilla, honey, cocoa. Edible, comforting." },
      { slug: "green-herbal",  label: "Cold dew on cut grass",                     blurb: "Violet leaf, mint, lavender, vetiver. Sharp and alert." },
      { slug: "floral-soft",   label: "Hotel-lobby orchid at dusk",                blurb: "Jasmine, rose, iris. Powdery or bright, rarely both." },
    ],
    /* Each family maps to note keywords; scoring uses keyword hits in notes[top,heart,base]. */
    axis: "notes",
    weight: 4,
    familyMap: {
      "fresh-fruit":    ["pineapple", "bergamot", "grapefruit", "citron", "lemon", "apple", "blackcurrant", "mint"],
      "woody-smoke":    ["oud", "cedar", "birch", "sandalwood", "vetiver", "tobacco", "guaiac", "labdanum"],
      "spicy-warm":     ["cinnamon", "pepper", "cardamom", "amber", "incense", "nutmeg", "saffron", "olibanum"],
      "sweet-gourmand": ["vanilla", "tonka", "honey", "cocoa", "benzoin", "chocolate"],
      "green-herbal":   ["lavender", "violet", "iris", "mint", "rosemary", "mate", "lemon verbena"],
      "floral-soft":    ["jasmine", "rose", "orange blossom", "neroli", "geranium", "iris", "violet"],
    },
  },
  {
    id: "gender",
    prompt: "Which marketing shelf do you browse?",
    sub: "This is a shopping hint only — anyone can wear anything. We use it to prioritize suggestions, not to gate them.",
    options: [
      { slug: "men",    label: "Men's",    blurb: "Culturally-skewed masculine marketing." },
      { slug: "unisex", label: "Unisex",   blurb: "Designed without a gendered target." },
      { slug: "women",  label: "Women's",  blurb: "Culturally-skewed feminine marketing." },
      { slug: "any",    label: "No preference — show me everything", blurb: "Gender shelves are a retail convenience. Ignore them." },
    ],
    axis: "gender",
    weight: 2,
  },
  {
    id: "budget",
    prompt: "How much are you willing to spend (landed, for a full bottle)?",
    sub: "Landed = sticker − coupons + shipping + tax. The full amount your card gets charged. No hidden fees on this site.",
    options: [
      { slug: "under-150",   label: "Under $150",     min: 0,   max: 150 },
      { slug: "150-250",     label: "$150 – $250",    min: 150, max: 250 },
      { slug: "250-400",     label: "$250 – $400",    min: 250, max: 400 },
      { slug: "400-plus",    label: "$400+",          min: 400, max: 9999 },
      { slug: "any-budget",  label: "No ceiling — show me the best",   min: 0, max: 9999 },
    ],
    axis: "budget",
    weight: 3,
  },
];

function Recommend() {
  const [answers, setAnswers] = useStateR({});
  const [step, setStep] = useStateR(0);
  const [submitted, setSubmitted] = useStateR(false);

  const total = QUESTIONS.length;
  const q = QUESTIONS[step];
  const answered = Object.keys(answers).length;

  const [leaving, setLeaving] = useStateR(false);

  function pick(slug, meta) {
    const next = { ...answers, [q.id]: { slug, meta } };
    setAnswers(next);
    // Two-phase transition so the next question doesn't snap in:
    //   1. Hold on the picked state for ~260ms (reader registers the choice)
    //   2. Fade the current panel out for ~200ms
    //   3. Swap step, the new panel fades in via keyed CSS animation
    setTimeout(() => setLeaving(true), 260);
    setTimeout(() => {
      if (step + 1 < total) setStep(step + 1);
      else setSubmitted(true);
      setLeaving(false);
    }, 460);
  }

  function reset() {
    setAnswers({});
    setStep(0);
    setSubmitted(false);
  }

  return (
    <main>
      <section className="container recommend" style={{ padding: "48px 32px 28px" }}>
        <div style={{ borderTop: "1px solid var(--ink)", paddingTop: 28, maxWidth: 820 }}>
          <div className="mono body-xs" style={{ color: "var(--ink-mute)", letterSpacing: "0.12em", textTransform: "uppercase", marginBottom: 14 }}>
            Recommender · editorial · <Cite n={7} check="separated" /> §7 firewall
          </div>
          <h1 className="fraun" style={{ fontSize: "clamp(40px, 5vw, 64px)", lineHeight: 1.05, letterSpacing: "-0.02em", margin: "0 0 18px" }}>
            Find the <span className="fraun-itl">next one.</span>
          </h1>
          <p className="body-l" style={{ color: "var(--ink-soft)", maxWidth: 680, lineHeight: 1.55, margin: 0 }}>
            Six questions. We rank the catalog against your answers, show the
            top three, and explain why each matched. This is a shortcut —{" "}
            <strong style={{ color: "var(--ink)" }}>not a promise you'll love it.</strong>{" "}
            Scent is personal; your nose is the only authority. Treat the output
            as the shortlist you'd end up with after a month of samples.
          </p>
        </div>
      </section>

      <hr className="hairline" />

      {!submitted ? (
        <QuizPanel
          q={q}
          step={step}
          total={total}
          answers={answers}
          onPick={pick}
          onJump={(i) => setStep(Math.min(i, answered))}
          leaving={leaving}
        />
      ) : (
        <ResultsPanel answers={answers} onReset={reset} />
      )}
    </main>
  );
}

/* ══════════════════════════════════════════════════════════════════════
   QUIZ PANEL — one question at a time. Click a tile to pick and advance.
   Progress bar + click-to-jump for previously answered steps.
   ══════════════════════════════════════════════════════════════════════ */
function QuizPanel({ q, step, total, answers, onPick, onJump, leaving }) {
  const picked = answers[q.id]?.slug;
  return (
    <section className="container" style={{ padding: "32px 32px 72px" }}>
      {/* Progress strip — stays mounted across step changes so the dots don't
          re-animate on every pick. Only the question pane below re-mounts. */}
      <div className="quiz-progress">
        {QUESTIONS.map((question, i) => {
          const isDone = answers[question.id];
          const isActive = i === step;
          return (
            <button
              key={question.id}
              type="button"
              onClick={() => onJump(i)}
              className={`quiz-progress__step ${isActive ? "is-active" : ""} ${isDone ? "is-done" : ""}`}
              aria-current={isActive ? "step" : undefined}
              aria-label={`Question ${i + 1} of ${total}: ${question.prompt}`}
            >
              <span className="quiz-progress__num">{String(i + 1).padStart(2, "0")}</span>
              <span className="quiz-progress__label">{question.id}</span>
            </button>
          );
        })}
      </div>

      {/* Keyed pane — key={q.id} forces a remount so the enter animation plays
          each time the step advances. `is-leaving` triggers the fade-out before
          the remount for a smooth hand-off between questions. */}
      <div
        key={q.id}
        className={`quiz-pane ${leaving ? "is-leaving" : "is-entering"}`}
        style={{ display: "grid", gridTemplateColumns: "1fr", gap: 24, marginTop: 40, maxWidth: 880 }}
      >
        <div>
          <div className="mono body-xs" style={{ color: "var(--ink-mute)", letterSpacing: "0.14em", textTransform: "uppercase", marginBottom: 14 }}>
            Question {step + 1} of {total}
          </div>
          <h2 className="fraun" style={{ fontSize: "clamp(28px, 3.4vw, 40px)", lineHeight: 1.12, letterSpacing: "-0.015em", margin: 0 }}>
            {q.prompt}
          </h2>
          {q.sub && (
            <p className="body-m" style={{ color: "var(--ink-soft)", marginTop: 14, maxWidth: 640, lineHeight: 1.6 }}>
              {q.sub}
            </p>
          )}
        </div>

        <div className="quiz-options">
          {q.options.map((opt, idx) => {
            const active = opt.slug === picked;
            return (
              <button
                key={opt.slug}
                type="button"
                onClick={() => onPick(opt.slug, opt)}
                className={`quiz-option ${active ? "is-active" : ""}`}
                aria-pressed={active}
                style={{ animationDelay: `${40 + idx * 35}ms` }}
              >
                <span className="quiz-option__marker" aria-hidden="true">{active ? "●" : "○"}</span>
                <span className="quiz-option__body">
                  <span className="quiz-option__label fraun">{opt.label}</span>
                  {opt.blurb && <span className="quiz-option__blurb mono body-xs">{opt.blurb}</span>}
                </span>
              </button>
            );
          })}
        </div>
      </div>
    </section>
  );
}

/* ══════════════════════════════════════════════════════════════════════
   RESULTS PANEL — compute ranked matches and render the top 3 with
   reasoning. Scoring is deterministic and publishable (see §4 method).
   ══════════════════════════════════════════════════════════════════════ */
function ResultsPanel({ answers, onReset }) {
  const ranked = useMemoR(() => rankCatalog(answers), [answers]);
  const top = ranked.slice(0, 3);
  const rest = ranked.slice(3, 8);

  return (
    <>
      <section className="container" style={{ padding: "32px 32px 24px" }}>
        <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 14, marginBottom: 28 }}>
          <div>
            <div className="mono body-xs" style={{ color: "var(--ink-mute)", letterSpacing: "0.14em", textTransform: "uppercase", marginBottom: 8 }}>
              Your shortlist
            </div>
            <h2 className="fraun" style={{ fontSize: "clamp(32px, 4vw, 48px)", lineHeight: 1.1, letterSpacing: "-0.018em", margin: 0 }}>
              Three places to <span className="fraun-itl">start.</span>
            </h2>
          </div>
          <button type="button" onClick={onReset} className="mono body-xs quiz-reset">
            ↻ Retake quiz
          </button>
        </div>
        <AnswersSummary answers={answers} />
      </section>

      <section className="container" style={{ padding: "16px 32px 56px" }}>
        {top.length === 0 ? (
          <div className="mono body-m" style={{ color: "var(--ink-soft)", padding: "40px 0", textAlign: "center", borderTop: "1px solid var(--rule)", borderBottom: "1px solid var(--rule)" }}>
            No fragrance in the catalog strongly matched your answers. This is
            honest — v0.1 catalog is ~12 entries, weighted toward men's niche.{" "}
            <a href="#/refusals" style={{ color: "var(--ink)", borderBottom: "1px solid currentColor" }}>Here's what we refuse to do</a> to fill gaps.
          </div>
        ) : (
          <div className="recommend-results">
            {top.map((r, i) => <ResultCard key={r.frag.slug} result={r} rank={i + 1} />)}
          </div>
        )}
      </section>

      {rest.length > 0 && (
        <>
          <hr className="hairline" />
          <section className="container" style={{ padding: "32px 32px 56px" }}>
            <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 18 }}>
              <h3 className="kicker" style={{ margin: 0 }}>Also worth a decant</h3>
              <span className="mono body-xs" style={{ color: "var(--ink-mute)" }}>scored match · sorted by fit</span>
            </div>
            <div className="recommend-runners">
              {rest.map(r => <RunnerRow key={r.frag.slug} result={r} />)}
            </div>
          </section>
        </>
      )}

      <hr className="hairline" />
      <section className="container" style={{ padding: "40px 32px 72px" }}>
        <div style={{ maxWidth: 760, borderTop: "1px solid var(--ink)", paddingTop: 24 }}>
          <div className="mono body-xs" style={{ color: "var(--ink-mute)", letterSpacing: "0.12em", textTransform: "uppercase", marginBottom: 12 }}>
            Method <Cite n={4} check="clean" />
          </div>
          <p className="body-m" style={{ color: "var(--ink-soft)", lineHeight: 1.65, margin: 0 }}>
            Score = sum of your answers' weights × each tag match on the catalog entry.
            Notes-family question contributes a keyword hit across top/heart/base notes.
            Budget excludes entries above your ceiling. Gender filter is soft — "unisex"
            counts toward both men's and women's selections. Ties break on community score × log(n).{" "}
            <strong style={{ color: "var(--ink)" }}>No affiliate boost</strong> — we sort on fit, full stop.{" "}
            <Cite n={2} check="independent" />
          </p>
          <p className="body-m" style={{ color: "var(--ink-soft)", lineHeight: 1.65, marginTop: 14 }}>
            <strong style={{ color: "var(--ink)" }}>Before you commit:</strong> order a 5ml
            decant. A shortlist from an algorithm is not a replacement for wearing
            something on your own skin for a week. If the top pick doesn't land,
            decant the runner-up — that's what the decants strip on each detail page is for.
          </p>
        </div>
      </section>
    </>
  );
}

/* Compact summary of the user's picks — editable by click-to-jump back. */
function AnswersSummary({ answers }) {
  return (
    <div className="answers-summary">
      {QUESTIONS.map(q => {
        const a = answers[q.id];
        if (!a) return null;
        return (
          <div key={q.id} className="answers-summary__chip">
            <span className="answers-summary__k">{q.id}</span>
            <span className="answers-summary__v">{a.meta?.label || a.slug}</span>
          </div>
        );
      })}
    </div>
  );
}

/* Single-fragrance result card — rank, name, match score, why it matched,
   landed price, and a link into the detail page. */
function ResultCard({ result, rank }) {
  const { frag, score, maxScore, reasons } = result;
  const pct = Math.round((score / maxScore) * 100);
  return (
    <article className="result-card">
      <div className="result-card__rank mono">
        <span className="result-card__rank-num">0{rank}</span>
        <span className="result-card__rank-sub">match</span>
      </div>

      <a href={`#/fragrance/${frag.slug}`} className="result-card__art" aria-label={`${frag.brand} ${frag.name} detail`}>
        <Bottle slug={frag.slug} label={frag.name} showLabel={false}
                style={{ width: "100%", aspectRatio: "4 / 5", padding: "8% 22%" }} />
      </a>

      <div className="result-card__body">
        <div className="mono body-xs" style={{ color: "var(--ink-mute)", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 6 }}>
          {frag.brand}
        </div>
        <a href={`#/fragrance/${frag.slug}`} className="fraun result-card__title">
          <span className="fraun-itl">{frag.name}</span>
        </a>
        <div className="mono body-xs" style={{ color: "var(--ink-mute)", marginTop: 8 }}>
          {frag.family} · {frag.size}
        </div>

        <div className="result-card__score">
          <div className="result-card__bar">
            <div className="result-card__bar-fill" style={{ width: `${pct}%` }} />
          </div>
          <div className="mono body-xs" style={{ color: "var(--ink-mute)", marginTop: 6 }}>
            {pct}% fit · {score.toFixed(0)} of {maxScore.toFixed(0)} possible points
          </div>
        </div>

        <ul className="result-card__reasons">
          {reasons.map((r, i) => (
            <li key={i} className="body-s">
              <span className="result-card__reason-tick">✓</span> {r}
            </li>
          ))}
        </ul>

        <div className="result-card__cta">
          <a href={`#/fragrance/${frag.slug}`} className="shop-btn shop-btn--ghost">
            See fragrance
            <svg width="9" height="9" viewBox="0 0 10 10" aria-hidden="true" style={{ marginLeft: 6 }}>
              <path d="M1 9 L9 1 M3 1 H9 V7" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" />
            </svg>
          </a>
          <span className="mono body-xs" style={{ color: "var(--ink-soft)" }}>
            landed <strong style={{ color: "var(--ink)" }}>{FMF.fmtUsd(frag.landed)}</strong>
          </span>
        </div>
      </div>
    </article>
  );
}

/* Runner-up row — compact, no illustration. Score + name + landed + why. */
function RunnerRow({ result }) {
  const { frag, score, maxScore, reasons } = result;
  const pct = Math.round((score / maxScore) * 100);
  return (
    <a href={`#/fragrance/${frag.slug}`} className="runner-row">
      <div className="mono runner-row__pct">{pct}%</div>
      <div className="runner-row__name">
        <div className="mono body-xs" style={{ color: "var(--ink-mute)", letterSpacing: "0.08em", textTransform: "uppercase" }}>{frag.brand}</div>
        <div className="fraun"><span className="fraun-itl">{frag.name}</span></div>
      </div>
      <div className="mono body-xs runner-row__why">{reasons[0] || "partial match"}</div>
      <div className="mono runner-row__price">{FMF.fmtUsd(frag.landed)}</div>
    </a>
  );
}

/* ══════════════════════════════════════════════════════════════════════
   SCORING — deterministic, publishable.
   ══════════════════════════════════════════════════════════════════════ */
function rankCatalog(answers) {
  const catalog = (FMF.CATALOG || []);
  // Pull answer slugs + the question definitions for weights + family keywords.
  const qById = Object.fromEntries(QUESTIONS.map(q => [q.id, q]));
  const occAns    = answers.occasion?.slug;
  const seasonAns = answers.season?.slug;
  const vibeAns   = answers.vibe?.slug;
  const familyAns = answers.family?.slug;
  const genderAns = answers.gender?.slug;
  const budgetAns = answers.budget?.meta;

  const maxScore =
    (occAns    ? qById.occasion.weight : 0) +
    (seasonAns ? qById.season.weight   : 0) +
    (vibeAns   ? qById.vibe.weight     : 0) +
    (familyAns ? qById.family.weight   : 0) +
    (genderAns ? qById.gender.weight   : 0);

  const results = catalog.map(frag => {
    let score = 0;
    const reasons = [];

    // Budget — hard filter (unless "any-budget").
    if (budgetAns && budgetAns.max < 9999) {
      if (frag.landed > budgetAns.max) {
        return { frag, score: 0, maxScore, reasons: [], excluded: `$${frag.landed.toFixed(0)} exceeds your ${budgetAns.label} ceiling` };
      }
    }

    const tags = frag.tags || {};

    if (occAns && tags.occasion?.includes(occAns)) {
      score += qById.occasion.weight;
      reasons.push(`Built for ${labelOf("occasion", occAns)} — core occasion match.`);
    }
    if (seasonAns) {
      if (tags.season?.includes(seasonAns)) {
        score += qById.season.weight;
        reasons.push(`Thrives in ${labelOf("season", seasonAns)}.`);
      } else if (tags.season?.includes("all-season")) {
        score += qById.season.weight * 0.6;
        reasons.push(`All-season composition — carries through ${labelOf("season", seasonAns)}.`);
      }
    }
    if (vibeAns && tags.vibe?.includes(vibeAns)) {
      score += qById.vibe.weight;
      reasons.push(`Reads ${labelOf("vibe", vibeAns)} — aligned to the silhouette you want.`);
    }
    if (familyAns) {
      const keywords = qById.family.familyMap[familyAns] || [];
      const allNotes = [
        ...(frag.notes?.top   || []),
        ...(frag.notes?.heart || []),
        ...(frag.notes?.base  || []),
      ].map(n => n.toLowerCase());
      const hits = keywords.filter(k => allNotes.some(n => n.includes(k)));
      if (hits.length > 0) {
        const familyScore = Math.min(qById.family.weight, (hits.length / 3) * qById.family.weight);
        score += familyScore;
        const sample = hits.slice(0, 3).map(cap).join(", ");
        reasons.push(`Carries ${sample} — speaks the scent family you picked.`);
      }
    }
    if (genderAns && genderAns !== "any") {
      if (frag.gender === genderAns) {
        score += qById.gender.weight;
      } else if (frag.gender === "unisex") {
        score += qById.gender.weight * 0.7;
        reasons.push(`Unisex — sits comfortably on your shelf preference.`);
      } else {
        // mismatch: cross-shelf pick — small penalty-free 0, no reason added
      }
    } else if (genderAns === "any") {
      score += qById.gender.weight * 0.5;
    }

    // Tie-break signal: community score × log(n) — quiet, not a reason.
    const communityLift = Math.log((frag.community?.n || 1) + 1) * ((frag.community?.score || 0) / 10);

    return { frag, score, maxScore, reasons, communityLift };
  });

  const ranked = results
    .filter(r => !r.excluded && r.score > 0)
    .sort((a, b) => {
      if (b.score !== a.score) return b.score - a.score;
      return b.communityLift - a.communityLift;
    });

  return ranked;
}

function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }

function labelOf(qid, slug) {
  const q = QUESTIONS.find(x => x.id === qid);
  return q?.options.find(o => o.slug === slug)?.label?.toLowerCase() || slug;
}
