/* global React, ReactDOM */
// Copy editor — edits.html
// Lets Debra change ANY line of text on any creative, beyond the dates &
// details covered by the Library's "Edit values" panel.
//
// How it works: each creative is rendered off-screen, every text string is
// extracted into an editable box, and changes are stored in localStorage
// under "ccc-custom-edits" as { "original text": "replacement" }. The
// Library's applyReplacements() (library.jsx) runs the same map everywhere —
// cards, previews, PNG/PDF downloads, and the full-kit zip — so an edit made
// here shows up across the whole kit automatically.
//
// NOTE: keep REPLACEMENTS / applyValues in sync with library.jsx.

(function(){
const { useState, useEffect, useMemo, useRef } = React;

// ─── Tokens (mirror library.jsx) ──────────────────────────────────────
const C = {
  inkwell:  "#1C492C",
  vellum:   "#EFE9A8",
  cream:    "#F4F0C2",
  sage:     "#AFCC75",
  oxblood:  "#4B781C",
  charcoal: "#22382A",
  lime:     "#9DAE20",
  paper:    "#FBF6D8",
  body:     "#F7F2CC",
  border:   "rgba(28,73,44,0.22)"
};
const MONO = '"JetBrains Mono", ui-monospace, monospace';
const SERIF = '"Frank Ruhl Libre", serif';

// ─── Values + replacement machinery (mirror of library.jsx) ──────────
const DEFAULT_VALUES = {
  cohort: "01",
  cohortStartIso: "2026-06-01",
  date: "June 1",
  date_long: "June 1, 2026",
  founders_rate: "$249",
  regular_rate: "$499",
  phone: "(289) 207-2617",
  phone_dotted: "(289) 207-2617",
  url: "cccrecovery.ca",
  email: "debra@cccrecovery.ca",
  location: "Toronto"
};

const REPLACEMENTS = [
  ["June 1, 2026", "date_long"],
  ["June 1", "date"],
  ["$249 founders'", "founders_rate"],
  ["$249", "founders_rate"],
  ["$499 regular", "regular_rate"],
  ["$499", "regular_rate"],
  ["(289) 207-2617", "phone"],
  ["(289) 207-2617", "phone_dotted"],
  ["cccrecovery.ca", "url"],
  ["debra@cccrecovery.ca", "email"],
  ["Toronto", "location"]
];
const WHOLE_NODE_REPLACEMENTS = [["June", "__month"], ["1.", "__day_period"]];

function deriveDateParts(values) {
  const iso = values.cohortStartIso;
  if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return {};
  const [y, m, d] = iso.split("-").map(Number);
  const date = new Date(y, m - 1, d, 12);
  if (isNaN(+date)) return {};
  return {
    __month: date.toLocaleDateString("en-US", { month: "long" }),
    __day_period: d + "."
  };
}

function loadValues() {
  let v = { ...DEFAULT_VALUES };
  try {
    const raw = localStorage.getItem("ccc-lib-values");
    if (raw) v = { ...v, ...JSON.parse(raw) };
  } catch {}
  return v;
}

// Values pass only (no custom edits) — used to extract baseline strings.
function applyValues(root, values) {
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
  const nodes = [];
  let n;
  while ((n = walker.nextNode())) nodes.push(n);
  const parts = deriveDateParts(values);
  for (const tn of nodes) {
    const orig = tn.__libOrig ?? (tn.__libOrig = tn.nodeValue);
    let next = orig;
    const trimmed = orig.trim();
    for (const [needle, key] of WHOLE_NODE_REPLACEMENTS) {
      if (trimmed === needle) {
        const replacement = parts[key];
        if (replacement) next = orig.replace(needle, replacement);
        break;
      }
    }
    for (const [needle, key] of REPLACEMENTS) {
      if (next.includes(needle)) next = next.split(needle).join(values[key] || "");
    }
    if (tn.nodeValue !== next) tn.nodeValue = next;
  }
  const cohortNeedle = "Cohort 01";
  for (const tn of nodes) {
    const orig = tn.__libOrigCohort ?? (tn.__libOrigCohort = tn.nodeValue);
    if (orig.includes(cohortNeedle)) {
      const replaced = orig.split(cohortNeedle).join("Cohort " + (values.cohort || "01"));
      if (tn.nodeValue !== replaced) tn.nodeValue = replaced;
    }
  }
}

// Values pass + custom edits (edits passed explicitly for live preview).
function applyAll(root, values, edits) {
  applyValues(root, values);
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
  let n;
  while ((n = walker.nextNode())) {
    const cur = n.nodeValue;
    const t = cur.trim();
    if (t && Object.prototype.hasOwnProperty.call(edits, t) && edits[t] !== t) {
      const lead = (cur.match(/^\s*/) || [""])[0];
      const trail = (cur.match(/\s*$/) || [""])[0];
      n.nodeValue = lead + edits[t] + trail;
    }
  }
}

// ─── Custom edits storage ─────────────────────────────────────────────
const EDITS_KEY = "ccc-custom-edits";
function loadEdits() {
  try {
    const raw = localStorage.getItem(EDITS_KEY);
    if (raw) {
      const p = JSON.parse(raw);
      if (p && typeof p === "object") return p;
    }
  } catch {}
  return {};
}
function persistEdits(edits) {
  try { localStorage.setItem(EDITS_KEY, JSON.stringify(edits)); } catch {}
}

// ─── Catalog (mirror of library.jsx MANIFEST, names + components) ────
const CATALOG = [
  { group: "Brand system", items: [
    { name: "Business card — front",  comp: "BS_BusinessCardFront", w: 1050, h: 600 },
    { name: "Business card — back",   comp: "BS_BusinessCardBack",  w: 1050, h: 600 },
    { name: "Day 1 affirmation poster", comp: "BS_Day1Poster",      w: 850,  h: 1100 },
    { name: "Sticker — wordmark",     comp: "BS_StickerWordmark",   w: 1000, h: 1000, transparent: true },
    { name: "Sticker — Recovery is possible", comp: "BS_StickerPossible", w: 1000, h: 1000, transparent: true },
    { name: "Sticker — scan to apply", comp: "BS_StickerScan",      w: 1000, h: 1000, transparent: true },
    { name: "Story — H.A.L.T.",       comp: "BS_StoryHalt",         w: 1080, h: 1920 },
    { name: "Wordmark sheet",         comp: "BS_WordmarkSheet",     w: 1400, h: 900 },
    { name: "Palette + Type specimen", comp: "BS_Specimen",         w: 1400, h: 900 },
    { name: "Vocabulary reference",   comp: "BS_VocabularyReference", w: 1400, h: 900 }
  ]},
  { group: "Recovery Is Possible", items: [
    { name: "Evergreen flyer",        comp: "LK_FlyerEvergreen",   w: 850,  h: 1100 },
    { name: "Cohort launch flyer",    comp: "LK_FlyerCohort",      w: 850,  h: 1100 },
    { name: "IG — brand intro",       comp: "LK_IGIntro",          w: 1080, h: 1080 },
    { name: "IG — inspirational",     comp: "LK_IGInspirational",  w: 1080, h: 1080 },
    { name: "IG — cohort launch",     comp: "LK_IGCohort",         w: 1080, h: 1080 },
    { name: "Story — evergreen",      comp: "LK_StoryEvergreen",   w: 1080, h: 1920 },
    { name: "Story — cohort launch",  comp: "LK_StoryCohort",      w: 1080, h: 1920 },
    { name: "Referral card",          comp: "LK_LandscapeReferral", w: 1200, h: 630 }
  ]},
  { group: "First Monday", items: [
    { name: "Manifesto poster",       comp: "FM_Manifesto",        w: 850,  h: 1100 },
    { name: "Index card",             comp: "FM_IndexCard",        w: 1200, h: 630 },
    { name: "Greenhouse poster",      comp: "FM_GreenhousePoster", w: 850,  h: 1100 },
    { name: "Eight-week grid",        comp: "FM_EightWeekGrid",    w: 1080, h: 1080 },
    { name: "Day-timer poster",       comp: "FM_ArtifactDayTimer", w: 850,  h: 1100 },
    { name: "F.E.A.R. notebook",      comp: "FM_ArtifactNotebook", w: 1080, h: 1080 },
    { name: "Voicemail square",       comp: "FM_VoicemailSquare",  w: 1080, h: 1080 },
    { name: "Story — what we don't say", comp: "FM_StoryLitany",   w: 1080, h: 1920 },
    { name: "Story — field guide",    comp: "FM_StoryFieldGuide",  w: 1080, h: 1920 },
    { name: "Billboard",              comp: "FM_Billboard",        w: 1800, h: 600 },
    { name: "Billboard — evergreen",  comp: "FM_BillboardEvergreen", w: 1800, h: 600 },
    { name: "Bumper magnet",          comp: "FM_CarMagnetBumper",  w: 1200, h: 300 },
    { name: "Door magnet",            comp: "FM_CarMagnetSquare",  w: 1200, h: 1200 }
  ]}
];

// ─── Small bits ───────────────────────────────────────────────────────
function Tally({ size = 22, color = C.inkwell }) {
  const w = size * 1.6;
  return (
    <svg width={w} height={size} viewBox="0 0 48 30" fill="none" aria-hidden="true" style={{ display: "block" }}>
      <path d="M8 3.2 L7.2 27.4" stroke={color} strokeWidth="2.4" strokeLinecap="round" />
      <path d="M19 2.6 L19.4 27.8" stroke={color} strokeWidth="2.4" strokeLinecap="round" />
      <path d="M30 3.6 L29.6 27.2" stroke={color} strokeWidth="2.4" strokeLinecap="round" />
    </svg>
  );
}

function MonoLabel({ children, color = C.charcoal, size = 10, style = {} }) {
  return (
    <span style={{
      fontFamily: MONO, fontSize: size, letterSpacing: "0.14em",
      textTransform: "uppercase", color, ...style
    }}>{children}</span>
  );
}

// ─── Live preview pane ────────────────────────────────────────────────
function PreviewPane({ sel, values, edits }) {
  const wrapRef = useRef(null);
  const innerRef = useRef(null);
  const [scale, setScale] = useState(0);

  useEffect(() => {
    const measure = () => {
      if (wrapRef.current) setScale(wrapRef.current.offsetWidth / sel.w);
    };
    measure();
    window.addEventListener("resize", measure);
    return () => window.removeEventListener("resize", measure);
  }, [sel]);

  // One persistent root for the preview container — created once, re-rendered
  // per selection. (Re-creating a root on the same container while the old
  // one unmounts is what blanked the preview.)
  const rootRef = useRef(null);
  useEffect(() => {
    if (!innerRef.current) return;
    rootRef.current = ReactDOM.createRoot(innerRef.current);
    return () => {
      const r = rootRef.current;
      rootRef.current = null;
      setTimeout(() => { try { r.unmount(); } catch {} }, 0);
    };
  }, []);

  // Render the selected creative at native size.
  useEffect(() => {
    const Comp = window[sel.comp];
    if (!Comp || !rootRef.current) return;
    rootRef.current.render(<div style={{ width: sel.w, height: sel.h }}><Comp /></div>);
  }, [sel]);

  // Re-apply values + custom edits whenever anything changes.
  useEffect(() => {
    const id = requestAnimationFrame(() => requestAnimationFrame(() => {
      if (innerRef.current) applyAll(innerRef.current, values, edits);
    }));
    // Belt-and-braces: re-apply once more after commit settles (idempotent).
    const t = setTimeout(() => {
      if (innerRef.current) applyAll(innerRef.current, values, edits);
    }, 150);
    return () => { cancelAnimationFrame(id); clearTimeout(t); };
  }, [sel, values, edits]);

  return (
    <div ref={wrapRef}>
      <div style={{
        position: "relative", width: "100%", height: sel.h * scale,
        overflow: "hidden", borderRadius: 4,
        background: sel.transparent ? "transparent" : "#fff",
        boxShadow: sel.transparent ? "none" : "0 1px 8px rgba(28,73,44,0.14)"
      }}>
        <div ref={innerRef} style={{
          width: sel.w, height: sel.h,
          transform: `scale(${scale})`, transformOrigin: "top left",
          position: "absolute", top: 0, left: 0
        }}></div>
      </div>
      <div style={{ marginTop: 10, textAlign: "center" }}>
        <MonoLabel color={`${C.inkwell}88`} size={9}>Live preview — updates as you type</MonoLabel>
      </div>
    </div>
  );
}

// ─── One editable text row ────────────────────────────────────────────
function FieldRow({ orig, value, onChange, onReset }) {
  const current = value !== undefined ? value : orig;
  const modified = value !== undefined && value !== orig;
  const rows = Math.max(1, Math.min(6, Math.ceil(current.length / 52)));
  return (
    <div style={{
      background: C.paper, border: `1px solid ${modified ? C.oxblood : C.border}`,
      borderRadius: 6, padding: "10px 12px"
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <textarea
          value={current}
          rows={rows}
          onChange={(e) => onChange(e.target.value)}
          style={{
            flex: 1, resize: "vertical", border: "none", outline: "none",
            background: "transparent", color: C.inkwell,
            fontFamily: '"Inter", sans-serif', fontSize: 13.5, lineHeight: 1.5,
            padding: 0
          }}
        ></textarea>
        {modified &&
          <button
            onClick={onReset}
            title="Restore original text"
            style={{
              border: `1px solid ${C.border}`, background: "#fff", color: C.oxblood,
              borderRadius: 4, padding: "4px 8px", cursor: "pointer",
              fontFamily: MONO, fontSize: 9, letterSpacing: "0.12em", textTransform: "uppercase",
              whiteSpace: "nowrap", alignSelf: "flex-start"
            }}
          >↺ Undo</button>
        }
      </div>
      {modified &&
        <div style={{
          marginTop: 8, paddingTop: 8, borderTop: `1px dashed ${C.border}`,
          fontFamily: '"Inter", sans-serif', fontSize: 11, color: `${C.charcoal}99`
        }}>Was: “{orig}”</div>
      }
    </div>
  );
}

// ─── App ──────────────────────────────────────────────────────────────
function App() {
  const values = useMemo(loadValues, []);
  const [sel, setSel] = useState(CATALOG[0].items[2]); // Day 1 poster — copy-rich
  const [edits, setEdits] = useState(loadEdits);
  const [fields, setFields] = useState(null); // null = extracting

  // Extract the creative's text strings (values-resolved baselines).
  useEffect(() => {
    setFields(null);
    const Comp = window[sel.comp];
    if (!Comp) { setFields([]); return; }
    const host = document.createElement("div");
    host.style.cssText = `position:fixed;top:-99999px;left:-99999px;width:${sel.w}px;height:${sel.h}px;`;
    document.body.appendChild(host);
    const root = ReactDOM.createRoot(host);
    root.render(<div style={{ width: sel.w, height: sel.h }}><Comp /></div>);
    let cancelled = false;
    const cleanup = () => setTimeout(() => {
      try { root.unmount(); } catch {}
      try { host.remove(); } catch {}
    }, 0);
    const finish = () => {
      if (!cancelled) {
        applyValues(host, values);
        const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT, null);
        const seen = new Set();
        const out = [];
        let n;
        while ((n = walker.nextNode())) {
          const t = n.nodeValue.trim();
          if (t && !seen.has(t)) { seen.add(t); out.push(t); }
        }
        setFields(out);
        cleanup();
      }
    };
    const t = setTimeout(finish, 150);
    return () => { cancelled = true; clearTimeout(t); cleanup(); };
  }, [sel, values]);

  const setField = (orig, val) => {
    const next = { ...edits };
    if (val === orig) delete next[orig];
    else next[orig] = val;
    setEdits(next);
    persistEdits(next);
  };

  const resetCreative = () => {
    if (!fields || !fields.some(f => edits[f] !== undefined)) return;
    const next = { ...edits };
    for (const f of fields) delete next[f];
    setEdits(next);
    persistEdits(next);
  };

  const totalEdits = Object.keys(edits).length;
  const editsHere = fields ? fields.filter(f => edits[f] !== undefined && edits[f] !== f).length : 0;

  return (
    <div style={{ minHeight: "100vh", background: C.body, color: C.inkwell, fontFamily: '"Inter", sans-serif' }}>
      {/* Header */}
      <header style={{ background: C.vellum, borderBottom: `1px solid ${C.border}`, padding: "24px 0" }}>
        <div style={{ maxWidth: 1160, margin: "0 auto", padding: "0 24px", display: "flex", alignItems: "center", gap: 18, flexWrap: "wrap" }}>
          <Tally size={22} />
          <div>
            <h1 style={{ margin: 0, fontFamily: SERIF, fontWeight: 500, fontSize: 26, letterSpacing: "-0.015em", color: C.inkwell }}>
              Copy editor <span style={{ color: C.charcoal, fontWeight: 400 }}>— change any line, on any creative</span>
            </h1>
            <div style={{ marginTop: 4, fontFamily: SERIF, fontStyle: "italic", fontSize: 14, color: C.charcoal }}>
              Pick a creative, rewrite any text box, and the change applies across the whole kit — previews, downloads, and the zip.
            </div>
          </div>
          <a href="index.html" style={{
            marginLeft: "auto", whiteSpace: "nowrap",
            fontFamily: MONO, fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase",
            color: C.oxblood, textDecoration: "none",
            border: `1px solid ${C.border}`, background: C.paper,
            padding: "8px 12px", borderRadius: 4
          }}>← Back to the Library</a>
        </div>
      </header>

      <main style={{ maxWidth: 1160, margin: "0 auto", padding: "28px 24px 96px" }}>
        {/* Guidance */}
        <div style={{
          background: C.paper, border: `1px solid ${C.border}`, borderRadius: 8,
          padding: "14px 18px", fontSize: 13, lineHeight: 1.6, color: C.charcoal
        }}>
          <strong style={{ color: C.inkwell }}>Two things to know.</strong>{" "}
          Dates, prices, phone and web address still live in the Library's <em>Edit dates &amp; details</em> panel — change them there so every creative updates together. And if the same sentence appears on more than one creative, editing it here updates it everywhere it appears.
          {totalEdits > 0 &&
            <span style={{ display: "block", marginTop: 6 }}>
              <MonoLabel color={C.oxblood} size={9}>{totalEdits} custom edit{totalEdits === 1 ? "" : "s"} saved on this device</MonoLabel>
            </span>
          }
        </div>

        {/* Creative picker */}
        {CATALOG.map((group) => (
          <div key={group.group} style={{ marginTop: 22 }}>
            <MonoLabel color={`${C.inkwell}88`} size={9}>{group.group}</MonoLabel>
            <div style={{ marginTop: 8, display: "flex", flexWrap: "wrap", gap: 8 }}>
              {group.items.map((item) => {
                const active = sel.comp === item.comp;
                return (
                  <button key={item.comp} onClick={() => setSel(item)} style={{
                    border: `1px solid ${active ? C.inkwell : C.border}`,
                    background: active ? C.inkwell : C.paper,
                    color: active ? C.cream : C.inkwell,
                    borderRadius: 99, padding: "7px 14px", cursor: "pointer",
                    fontFamily: '"Inter", sans-serif', fontSize: 12.5
                  }}>{item.name}</button>
                );
              })}
            </div>
          </div>
        ))}

        {/* Editor */}
        <div style={{ marginTop: 36, display: "flex", flexWrap: "wrap", gap: 36, alignItems: "flex-start" }}>
          {/* Preview */}
          <div style={{ flex: "1 1 320px", maxWidth: 460, position: "sticky", top: 20 }}>
            <PreviewPane sel={sel} values={values} edits={edits} />
          </div>

          {/* Fields */}
          <div style={{ flex: "1 1 380px", minWidth: 300 }}>
            <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12, borderBottom: `1px solid ${C.border}`, paddingBottom: 8 }}>
              <h2 style={{ margin: 0, fontFamily: SERIF, fontWeight: 500, fontSize: 20, color: C.inkwell }}>
                {sel.name}
              </h2>
              {editsHere > 0 &&
                <button onClick={resetCreative} style={{
                  border: "none", background: "transparent", color: C.oxblood,
                  cursor: "pointer", fontFamily: MONO, fontSize: 9,
                  letterSpacing: "0.12em", textTransform: "uppercase", padding: 0,
                  textDecoration: "underline"
                }}>Reset this creative ({editsHere})</button>
              }
            </div>

            {fields === null &&
              <div style={{ marginTop: 18, fontFamily: SERIF, fontStyle: "italic", fontSize: 14, color: C.charcoal }}>Reading the creative…</div>
            }
            {fields !== null && fields.length === 0 &&
              <div style={{ marginTop: 18, fontFamily: SERIF, fontStyle: "italic", fontSize: 14, color: C.charcoal }}>No editable text found on this creative.</div>
            }
            {fields !== null && fields.length > 0 &&
              <div style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 10 }}>
                {fields.map((orig) => (
                  <FieldRow
                    key={orig}
                    orig={orig}
                    value={edits[orig]}
                    onChange={(v) => setField(orig, v)}
                    onReset={() => setField(orig, orig)}
                  />
                ))}
              </div>
            }

            <div style={{ marginTop: 22, fontSize: 11.5, lineHeight: 1.6, color: `${C.charcoal}aa` }}>
              Changes save automatically on this device. Keep edits roughly the same length as the original so the layout stays balanced — the live preview shows exactly what will print.
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
})();
