/* global React */
const {
  useState,
  useEffect,
  useRef,
  useMemo,
  createContext,
  useContext,
} = React;

// =====================================================================
// Published text overrides — static, read-only edits for the public site
// =====================================================================
const EDITS_FILE_PATH = "data/site_edits.json";
const EditorContext = createContext(null);

function emptyEdits() {
  return { texts: {}, hiddenBubbles: {} };
}

function isPlainObject(value) {
  return value && typeof value === "object" && !Array.isArray(value);
}

function normalizeEdits(value) {
  if (!isPlainObject(value)) return emptyEdits();
  return {
    texts: isPlainObject(value.texts) ? { ...value.texts } : {},
    hiddenBubbles: isPlainObject(value.hiddenBubbles) ? { ...value.hiddenBubbles } : {},
  };
}

function hasOwn(map, key) {
  return Object.prototype.hasOwnProperty.call(map, key);
}

function toText(value) {
  return value == null ? "" : String(value);
}

function EditorProvider({ children }) {
  const [edits, setEdits] = useState(emptyEdits);

  useEffect(() => {
    let active = true;
    fetch(EDITS_FILE_PATH, { cache: "no-store" })
      .then((response) => {
        if (!response.ok) throw new Error("Unable to load published edits");
        return response.json();
      })
      .then((publishedEdits) => {
        if (active) setEdits(normalizeEdits(publishedEdits));
      })
      .catch(() => {
        // Missing edits are fine; the published site can render from source text.
      });

    return () => {
      active = false;
    };
  }, []);

  const value = useMemo(() => ({
    editMode: false,
    edits,
    getText(id, fallback) {
      if (!id || !hasOwn(edits.texts, id)) return toText(fallback);
      return toText(edits.texts[id]);
    },
    isBubbleRemoved(id) {
      return Boolean(id && edits.hiddenBubbles[id]);
    },
  }), [edits]);

  return (
    <EditorContext.Provider value={value}>
      {children}
    </EditorContext.Provider>
  );
}

function useEditor() {
  return useContext(EditorContext);
}

function EditableText({
  id,
  text,
  as: Tag = "span",
  className = "",
  multiline,
  placeholder,
  ...rest
}) {
  const editor = useEditor();
  const original = toText(text);
  const value = editor ? editor.getText(id, original) : original;

  const cls = `${className} editable-text`.trim();

  return (
    <Tag
      className={cls || undefined}
      {...rest}
    >
      {value}
    </Tag>
  );
}

function messageKey(message, index) {
  if (message && message.message_id) return `message-${message.message_id}`;
  if (message && message.local_datetime) return `time-${message.local_datetime}`;
  return `message-${index}`;
}

// =====================================================================
// Reveal — fade/slide up on scroll
// =====================================================================
function Reveal({ children, delay = 0, className = "", as: Tag = "div", ...rest }) {
  const ref = useRef(null);
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          if (e.isIntersecting) {
            setShown(true);
            io.unobserve(e.target);
          }
        }
      },
      { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  const cls = `reveal ${shown ? "in" : ""} ${delay ? `delay-${delay}` : ""} ${className}`;
  return (
    <Tag ref={ref} className={cls.trim()} {...rest}>
      {children}
    </Tag>
  );
}

// =====================================================================
// Bubble — iMessage-style
// =====================================================================
function Bubble({ sender, text, animate = false, id }) {
  const editor = useEditor();
  const cls = sender === "Michael" ? "michael" : "christina";
  const bubbleId = id || `${cls}-${toText(text).slice(0, 48)}`;

  if (editor && editor.isBubbleRemoved(bubbleId)) return null;

  return (
    <div className={`bubble-row ${cls} ${animate ? "pop" : ""}`}>
      <div className={`bubble ${cls}`}>
        <EditableText id={`${bubbleId}.text`} text={text} multiline />
      </div>
    </div>
  );
}

function TypingBubble({ side = "christina" }) {
  return (
    <div className={`bubble-row ${side}`}>
      <div className="bubble typing">
        <span></span>
        <span></span>
        <span></span>
      </div>
    </div>
  );
}

// =====================================================================
// Featured message group: a few real messages from a month, paced.
// =====================================================================
function FeatureBlock({ messages, caption, idBase = "feature" }) {
  if (!messages || !messages.length) return null;
  return (
    <Reveal className="feature-block">
      {messages.map((m, i) => (
        <Bubble
          key={i}
          id={`${idBase}.${messageKey(m, i)}`}
          sender={m.sender}
          text={m.text}
        />
      ))}
      {caption ? (
        <EditableText
          id={`${idBase}.caption`}
          text={caption}
          as="div"
          className="feature-caption"
        />
      ) : null}
    </Reveal>
  );
}

// =====================================================================
// Pull quote
// =====================================================================
function Pull({ children, id }) {
  return (
    <Reveal as="blockquote" className="pull">
      <EditableText id={id} text={children} multiline />
    </Reveal>
  );
}

// =====================================================================
// Divider
// =====================================================================
function Divider() {
  return (
    <div className="divider">
      <span className="line"></span>
      <span className="dot"></span>
      <span className="dot"></span>
      <span className="dot"></span>
      <span className="line"></span>
    </div>
  );
}

// =====================================================================
// Day-rhythm constellation — a row of dots, one per day of the month.
// Dot radius = scaled total messages. Color = whoever sent more that day.
// =====================================================================
function Constellation({ days, monthLabel, totalRows, idBase = "constellation" }) {
  if (!days || !days.length) return null;
  const W = 800;
  const H = 200;
  const padX = 30;
  const padY = 30;
  const max = Math.max(...days.map((d) => d.total || 0), 1);
  const n = days.length;
  const step = (W - padX * 2) / Math.max(n - 1, 1);

  const points = days.map((d, i) => {
    const r = 3 + (Math.sqrt(d.total / max) * 22);
    const x = padX + i * step;
    // y oscillates gently around middle, but biased by sender ratio
    const ratio = d.total > 0 ? (d.michael - d.christina) / d.total : 0; // -1..1
    const y = H / 2 + ratio * 38;
    const winner = d.michael >= d.christina ? "michael" : "christina";
    return { x, y, r, winner, total: d.total, day: d.day };
  });

  // path along the points (a flowing ribbon)
  const path = points
    .map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`))
    .join(" ");

  return (
    <Reveal className="constellation">
      <div className="constellation-title">
        <EditableText id={`${idBase}.title`} text="Daily Rhythm" />
        <em>
          <EditableText id={`${idBase}.month`} text={monthLabel} />
        </em>
        <EditableText
          id={`${idBase}.total`}
          text={totalRows ? totalRows.toLocaleString() + " msgs" : ""}
        />
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet">
        <path d={path} className="day-arc" />
        {points.map((p, i) => (
          <circle
            key={i}
            cx={p.x}
            cy={p.y}
            r={p.r}
            className={`day-dot ${p.winner}`}
            opacity={0.78}
          >
            <title>{`${p.day}: ${p.total} messages`}</title>
          </circle>
        ))}
      </svg>
      <div className="constellation-legend">
        <span>
          <i className="michael"></i>
          <EditableText id={`${idBase}.legend.michael`} text="Michael" />
        </span>
        <span>
          <i className="christina"></i>
          <EditableText id={`${idBase}.legend.christina`} text="Christina" />
        </span>
      </div>
    </Reveal>
  );
}

// =====================================================================
// Stat row
// =====================================================================
function StatRow({ stats, idBase = "stats" }) {
  return (
    <Reveal className="chapter-stats" delay={2}>
      {stats.map((s, i) => (
        <div className="stat" key={i}>
          <EditableText id={`${idBase}.${i}.num`} text={s.num} as="div" className="stat-num" />
          <EditableText id={`${idBase}.${i}.label`} text={s.label} as="div" className="stat-label" />
        </div>
      ))}
    </Reveal>
  );
}

// expose globals
Object.assign(window, {
  EditorProvider,
  EditableText,
  useEditor,
  Reveal,
  Bubble,
  TypingBubble,
  FeatureBlock,
  Pull,
  Divider,
  Constellation,
  StatRow,
});
