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

// ---------- helpers ----------
function groupByPage(words) {
  const map = new Map();
  for (const w of words || []) {
    if (typeof w.page !== "number") continue;
    if (!map.has(w.page)) map.set(w.page, []);
    map.get(w.page).push(w);
  }
  for (const arr of map.values()) {
    arr.sort((a, b) => {
      const la = a.lineIndex ?? 0, lb = b.lineIndex ?? 0;
      if (la !== lb) return la - lb;
      const al = a._orderLeft ?? a.left ?? 0;
      const bl = b._orderLeft ?? b.left ?? 0;
      return al - bl;
    });
  }
  return map;
}

function groupIntoLines(pageWords) {
  const lines = new Map();
  for (const w of pageWords) {
    const k = w.lineIndex ?? 0;
    if (!lines.has(k)) lines.set(k, []);
    lines.get(k).push(w);
  }
  const sortedKeys = [...lines.keys()].sort((a, b) => a - b);
  return sortedKeys.map(k => ({ lineIndex: k, words: lines.get(k) }));
}

function isActiveWord(active, w) {
  return active && active.page === w.page && active.wordIndex === w.wordIndex;
}

function confidenceColor(c) {
  if (typeof c !== "number") return "#807e74";
  if (c >= 90) return "oklch(0.7 0.16 145)";
  if (c >= 75) return "oklch(0.78 0.14 95)";
  if (c >= 55) return "oklch(0.78 0.15 70)";
  return "oklch(0.65 0.18 25)";
}

// ---------- Transcribe tab ----------
function TranscribePane({ letterId, letter, updateWord, addWord, deleteWord, splitWord, repoParsed, branch }) {
  const wordsByPage = useMemo(
    () => groupByPage(letter?.transcription?.wordPositions),
    [letter]
  );
  // Pages: from word data, image filenames suggested by metadata location URLs
  // Fallback: pages [1,2] if no words yet but image likely exists.
  const pages = useMemo(() => {
    const fromWords = [...wordsByPage.keys()];
    const fromUrls = (letter?.metadata?.location?.urls || [])
      .map(u => {
        const m = u.match(/page_(\d+)/i);
        return m ? parseInt(m[1], 10) : null;
      })
      .filter(n => n != null);
    const all = new Set([...fromWords, ...fromUrls]);
    if (all.size === 0) all.add(1);
    return [...all].sort((a, b) => a - b);
  }, [wordsByPage, letter]);

  const [page, setPage] = useState(pages[0] ?? 1);
  useEffect(() => {
    if (!pages.includes(page) && pages.length) setPage(pages[0]);
  }, [pages.join(","), page]); // eslint-disable-line

  const [activeWord, setActiveWord] = useState(null);
  const [zoom, setZoom] = useState(1);
  const [showAllBoxes, setShowAllBoxes] = useState(true);
  const [drawMode, setDrawMode] = useState(false);
  // Pending new word draft from a drag: { rect, page } where rect is normalized.
  const [draft, setDraft] = useState(null);
  // For inserting a new word: chosen reference word + side ('before'|'after')
  const [insertTarget, setInsertTarget] = useState(null);

  const pageWords = wordsByPage.get(page) || [];
  
  let pageImage = `letters/${letterId}/images/page_${String(page).padStart(2, "0")}.jpg`;
  if (repoParsed && branch) {
    pageImage = `https://github.com/${repoParsed.owner}/${repoParsed.repo}/raw/${branch}/${pageImage}`;
  }

  return (
    <div className="transcribe">
      <ImagePane
        image={pageImage}
        page={page}
        words={pageWords}
        activeWord={activeWord}
        setActiveWord={setActiveWord}
        zoom={zoom}
        setZoom={setZoom}
        showAllBoxes={showAllBoxes}
        setShowAllBoxes={setShowAllBoxes}
        pages={pages}
        setPage={setPage}
        drawMode={drawMode}
        setDrawMode={setDrawMode}
        draft={draft}
        setDraft={setDraft}
        insertTarget={insertTarget}
        deleteWord={deleteWord}
      />
      <TextPane
        page={page}
        words={pageWords}
        activeWord={activeWord}
        setActiveWord={setActiveWord}
        updateWord={updateWord}
        deleteWord={deleteWord}
        splitWord={splitWord}
        draft={draft}
        setDraft={setDraft}
        insertTarget={insertTarget}
        setInsertTarget={setInsertTarget}
        addWord={addWord}
      />
    </div>
  );
}

function ImagePane({
  image, page, words, activeWord, setActiveWord,
  zoom, setZoom, showAllBoxes, setShowAllBoxes,
  pages, setPage,
  drawMode, setDrawMode,
  draft, setDraft,
  insertTarget,
  deleteWord,
}) {
  const scrollRef = useRef(null);
  const frameRef = useRef(null);
  const boxRefs = useRef(new Map());

  useEffect(() => {
    if (!activeWord) return;
    const el = boxRefs.current.get(activeWord.wordIndex);
    const scroller = scrollRef.current;
    if (!el || !scroller) return;
    const elR = el.getBoundingClientRect();
    const scR = scroller.getBoundingClientRect();
    if (elR.top < scR.top + 40 || elR.bottom > scR.bottom - 40) {
      const targetTop =
        scroller.scrollTop + (elR.top - scR.top) - scR.height / 2 + elR.height / 2;
      scroller.scrollTo({ top: targetTop, behavior: "smooth" });
    }
  }, [activeWord]);

  // ---- Drawing a new bbox ----
  const [drag, setDrag] = useState(null); // { startX, startY, x, y } in normalized coords

  const onFrameMouseDown = (e) => {
    if (!drawMode) return;
    if (e.button !== 0) return;
    const rect = frameRef.current.getBoundingClientRect();
    const nx = (e.clientX - rect.left) / rect.width;
    const ny = (e.clientY - rect.top) / rect.height;
    setDrag({ startX: nx, startY: ny, x: nx, y: ny });
    e.preventDefault();
  };
  const onFrameMouseMove = (e) => {
    if (!drag) return;
    const rect = frameRef.current.getBoundingClientRect();
    const nx = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    const ny = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
    setDrag(d => d ? { ...d, x: nx, y: ny } : d);
  };
  const onFrameMouseUp = () => {
    if (!drag) return;
    const left = Math.min(drag.startX, drag.x);
    const top = Math.min(drag.startY, drag.y);
    const width = Math.abs(drag.x - drag.startX);
    const height = Math.abs(drag.y - drag.startY);
    setDrag(null);
    if (width < 0.005 || height < 0.005) return; // too small, ignore
    setDrawMode(false);
    setDraft({ page, rect: { left, top, width, height } });
  };

  const renderDragRect = () => {
    if (!drag) return null;
    const left = Math.min(drag.startX, drag.x);
    const top = Math.min(drag.startY, drag.y);
    const width = Math.abs(drag.x - drag.startX);
    const height = Math.abs(drag.y - drag.startY);
    return (
      <div
        className="bbox draft-rect"
        style={{
          left: `${left * 100}%`, top: `${top * 100}%`,
          width: `${width * 100}%`, height: `${height * 100}%`,
        }}
      />
    );
  };

  // Render the committed-but-unsaved draft rect (after drag, awaiting text)
  const renderDraftRect = () => {
    if (!draft || draft.page !== page) return null;
    const r = draft.rect;
    return (
      <div
        className="bbox draft-rect committed"
        style={{
          left: `${r.left * 100}%`, top: `${r.top * 100}%`,
          width: `${r.width * 100}%`, height: `${r.height * 100}%`,
        }}
      />
    );
  };

  return (
    <div className="image-pane">
      <div className="image-toolbar">
        <div className="pages">
          {pages.map(p => (
            <button
              key={p}
              className={p === page ? "active" : ""}
              onClick={() => setPage(p)}
            >Page {p}</button>
          ))}
        </div>
        <div className="spacer" />
        <button
          className={drawMode ? "primary" : ""}
          onClick={() => { setDrawMode(v => !v); setDraft(null); }}
          title="Draw a box for a missing word"
        >
          {drawMode ? "Cancel draw" : "+ Add word"}
        </button>
        <label className="ctrl">
          <input
            type="checkbox"
            checked={showAllBoxes}
            onChange={e => setShowAllBoxes(e.target.checked)}
          /> Show boxes
        </label>
        <div className="ctrl">
          <span>Zoom</span>
          <input
            type="range" min="0.5" max="2.5" step="0.05"
            value={zoom}
            onChange={e => setZoom(parseFloat(e.target.value))}
          />
          <span style={{ fontVariantNumeric: "tabular-nums", width: 36 }}>
            {Math.round(zoom * 100)}%
          </span>
        </div>
      </div>
      <div className="image-scroll" ref={scrollRef}>
        <div
          className={"image-frame" + (drawMode ? " drawing" : "")}
          ref={frameRef}
          style={{ width: `${zoom * 100}%`, maxWidth: `${zoom * 900}px` }}
          onMouseDown={onFrameMouseDown}
          onMouseMove={onFrameMouseMove}
          onMouseUp={onFrameMouseUp}
          onMouseLeave={() => setDrag(null)}
        >
          <img src={image} alt="page" draggable={false} />
          {words.map(w => {
            const isUserAdded = w.source === "user_added";
            const isUserCorrected = w.source === "user_corrected";
            return (
              <div
                key={w.wordIndex}
                ref={el => {
                  if (el) boxRefs.current.set(w.wordIndex, el);
                  else boxRefs.current.delete(w.wordIndex);
                }}
                className={
                  "bbox" +
                  (isActiveWord(activeWord, w) ? " active" : "") +
                  (showAllBoxes ? " show-all" : "") +
                  (isUserAdded ? " user-added" : "") +
                  (isUserCorrected ? " user-corrected" : "")
                }
                style={{
                  left: `${w.left * 100}%`,
                  top: `${w.top * 100}%`,
                  width: `${w.width * 100}%`,
                  height: `${w.height * 100}%`,
                }}
                title={w.final_text}
                onMouseEnter={() => setActiveWord({ page: w.page, wordIndex: w.wordIndex })}
                onMouseLeave={() => setActiveWord(null)}
                onContextMenu={(e) => {
                  e.preventDefault();
                  if (confirm(`Delete word "${w.final_text}"?`)) {
                    deleteWord(w.page, w.wordIndex);
                  }
                }}
              >
                <button
                  className="bbox-del"
                  title={`Delete bounding box for "${w.final_text}"`}
                  onClick={(e) => {
                    e.stopPropagation();
                    if (confirm(`Delete bounding box for "${w.final_text}"?`)) {
                      deleteWord(w.page, w.wordIndex);
                    }
                  }}
                >×</button>
              </div>
            );
          })}
          {renderDragRect()}
          {renderDraftRect()}
        </div>
      </div>
    </div>
  );
}

function TextPane({
  page, words, activeWord, setActiveWord, updateWord, deleteWord, splitWord,
  draft, setDraft, insertTarget, setInsertTarget, addWord,
}) {
  const lines = useMemo(() => groupIntoLines(words), [words]);
  const wordRefs = useRef(new Map());
  const [tip, setTip] = useState(null);

  // Auto-pick nearest neighbor when a draft exists.
  useEffect(() => {
    if (!draft || draft.page !== page) {
      if (insertTarget) setInsertTarget(null);
      return;
    }
    if (insertTarget) return;
    // Find nearest existing word by center distance.
    const cx = draft.rect.left + draft.rect.width / 2;
    const cy = draft.rect.top + draft.rect.height / 2;
    let best = null, bestD = Infinity;
    for (const w of words) {
      const wx = w.left + w.width / 2;
      const wy = w.top + w.height / 2;
      const d = Math.hypot(wx - cx, wy - cy);
      if (d < bestD) { bestD = d; best = w; }
    }
    if (best) {
      // Side: 'after' if draft center is to the right or below
      const wx = best.left + best.width / 2;
      const wy = best.top + best.height / 2;
      const sameLine = Math.abs(cy - wy) < Math.max(best.height, 0.015);
      const after = sameLine ? cx > wx : cy > wy;
      setInsertTarget({ wordIndex: best.wordIndex, side: after ? "after" : "before" });
    }
  }, [draft, page, words, insertTarget]);

  useEffect(() => {
    if (!activeWord || activeWord.page !== page) return;
    const el = wordRefs.current.get(activeWord.wordIndex);
    if (!el) return;
    const scroller = el.closest(".text-scroll");
    if (!scroller) return;
    const elR = el.getBoundingClientRect();
    const scR = scroller.getBoundingClientRect();
    if (elR.top < scR.top + 40 || elR.bottom > scR.bottom - 40) {
      const targetTop =
        scroller.scrollTop + (elR.top - scR.top) - scR.height / 2 + elR.height / 2;
      scroller.scrollTo({ top: targetTop, behavior: "smooth" });
    }
  }, [activeWord, page]);

  const inserting = !!(draft && draft.page === page);

  return (
    <div className="text-pane">
      <div className="text-toolbar">
        {inserting ? (
          <span className="hint" style={{ color: "var(--accent)" }}>
            Click between words to choose where the new word goes, then enter the text below.
          </span>
        ) : (
          <span className="hint">
            Hover for confidence. Click a word to edit. Right-click for split / delete.
          </span>
        )}
        <div className="spacer" />
        <span style={{ fontVariantNumeric: "tabular-nums" }}>
          {words.length} word{words.length === 1 ? "" : "s"} · page {page}
        </span>
      </div>
      <div className="text-scroll">
        <div className="transcript">
          {lines.length === 0 && (
            <div style={{ color: "var(--ink-3)", fontStyle: "italic" }}>
              No bounding boxes for this page. Click "+ Add word" on the image side.
            </div>
          )}
          {lines.map(({ lineIndex, words: lineWords }) => (
            <div className="line" key={lineIndex}>
              {lineWords.map((w, i) => (
                <React.Fragment key={w.wordIndex}>
                  {inserting && i === 0 && (
                    <InsertMarker
                      active={
                        insertTarget &&
                        insertTarget.wordIndex === w.wordIndex &&
                        insertTarget.side === "before"
                      }
                      onClick={() =>
                        setInsertTarget({ wordIndex: w.wordIndex, side: "before" })
                      }
                    />
                  )}
                  <Word
                    word={w}
                    active={isActiveWord(activeWord, w)}
                    onEnter={(e) => {
                      setActiveWord({ page: w.page, wordIndex: w.wordIndex });
                      const r = e.currentTarget.getBoundingClientRect();
                      setTip({ word: w, x: r.left + r.width / 2, y: r.top });
                    }}
                    onMove={(e) => {
                      const r = e.currentTarget.getBoundingClientRect();
                      setTip(prev => prev ? { ...prev, x: r.left + r.width / 2, y: r.top } : prev);
                    }}
                    onLeave={() => { setActiveWord(null); setTip(null); }}
                    onChange={(text) => updateWord(w.page, w.wordIndex, text)}
                    onSplit={() => {
                      const a = prompt(`Split "${w.final_text}" — first word:`, w.final_text);
                      if (a == null || a.trim() === "") return;
                      const b = prompt("Second word:", "");
                      if (b == null || b.trim() === "") return;
                      splitWord(w.page, w.wordIndex, a, b);
                    }}
                    onDelete={() => {
                      if (confirm(`Delete word "${w.final_text}"?`)) {
                        deleteWord(w.page, w.wordIndex);
                      }
                    }}
                    registerRef={(el) => {
                      if (el) wordRefs.current.set(w.wordIndex, el);
                      else wordRefs.current.delete(w.wordIndex);
                    }}
                    isLast={i === lineWords.length - 1}
                  />
                  {inserting && (
                    <InsertMarker
                      active={
                        insertTarget &&
                        insertTarget.wordIndex === w.wordIndex &&
                        insertTarget.side === "after"
                      }
                      onClick={() =>
                        setInsertTarget({ wordIndex: w.wordIndex, side: "after" })
                      }
                    />
                  )}
                </React.Fragment>
              ))}
            </div>
          ))}
        </div>

        {inserting && (
          <NewWordEditor
            draft={draft}
            insertTarget={insertTarget}
            words={words}
            onCancel={() => { setDraft(null); setInsertTarget(null); }}
            onCommit={(text) => {
              addWord(draft.page, draft.rect, text, insertTarget, words);
              setDraft(null);
              setInsertTarget(null);
            }}
          />
        )}
      </div>
      {tip && <WordTooltip {...tip} />}
    </div>
  );
}

function InsertMarker({ active, onClick }) {
  return (
    <span
      className={"insert-marker" + (active ? " active" : "")}
      onClick={onClick}
      title="Insert new word here"
    />
  );
}

function NewWordEditor({ draft, insertTarget, words, onCancel, onCommit }) {
  const [text, setText] = useState("");
  const inputRef = useRef(null);
  useEffect(() => { inputRef.current?.focus(); }, []);

  const targetWord = words.find(w => w.wordIndex === insertTarget?.wordIndex);

  return (
    <div className="new-word-editor">
      <div className="new-word-head">
        <span className="dot" /> New word
      </div>
      <div className="new-word-body">
        <div className="field" style={{ flex: 1 }}>
          <label>Text</label>
          <input
            ref={inputRef}
            type="text"
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder="Type the word…"
            onKeyDown={(e) => {
              if (e.key === "Enter" && text.trim()) onCommit(text.trim());
              if (e.key === "Escape") onCancel();
            }}
          />
        </div>
        <div className="field">
          <label>Insert</label>
          <div className="placement-readout">
            {targetWord
              ? `${insertTarget.side} "${targetWord.final_text}"`
              : "(pick a position)"}
          </div>
        </div>
      </div>
      <div className="new-word-actions">
        <button className="subtle" onClick={onCancel}>Cancel</button>
        <button
          className="primary"
          disabled={!text.trim() || !targetWord}
          onClick={() => onCommit(text.trim())}
        >Add word</button>
      </div>
    </div>
  );
}

function WordTooltip({ word, x, y }) {
  const conf = word.textractConfidence;
  const color = confidenceColor(conf);
  const TIP_W = 240;
  const left = Math.min(Math.max(8, x - TIP_W / 2), window.innerWidth - TIP_W - 8);
  const top = Math.max(8, y - 10);

  const showAlt =
    word.final_text !== word.textract_text && word.textract_text != null;

  return (
    <div
      className="word-tip"
      style={{ left, top, transform: "translateY(-100%)", width: TIP_W }}
    >
      <div className="tip-row">
        <span className="tip-val bigword">{word.final_text || "—"}</span>
      </div>
      <div className="tip-row" style={{ marginTop: 6 }}>
        <span className="tip-label">Confidence</span>
        <span className="conf-dot" style={{ background: color }} />
        <span className="conf-bar-track">
          <span
            className="conf-bar-fill"
            style={{
              width: `${typeof conf === "number" ? Math.max(0, Math.min(100, conf)) : 0}%`,
              background: color,
            }}
          />
        </span>
        <span className="tip-val">
          {typeof conf === "number" ? conf.toFixed(1) + "%" : "—"}
        </span>
      </div>
      <div className="tip-row">
        <span className="tip-label">Source</span>
        <span className="source-tag">{word.source || "—"}</span>
      </div>
      {typeof word.alignment_score === "number" && (
        <div className="tip-row">
          <span className="tip-label">Alignment</span>
          <span className="tip-val">{word.alignment_score.toFixed(2)}</span>
        </div>
      )}
      {word.zone && (
        <div className="tip-row">
          <span className="tip-label">Zone</span>
          <span className="tip-val">{word.zone}</span>
        </div>
      )}
      {showAlt && (
        <div className="tip-row">
          <span className="tip-label">OCR raw</span>
          <span className="tip-val alt">{word.textract_text}</span>
        </div>
      )}
    </div>
  );
}

function Word({
  word, active, onEnter, onMove, onLeave, onChange, onSplit, onDelete,
  registerRef, isLast,
}) {
  const ref = useRef(null);
  const text = word.final_text ?? "";

  useEffect(() => {
    if (!ref.current) return;
    if (document.activeElement === ref.current) return;
    if (ref.current.textContent !== text) {
      ref.current.textContent = text;
    }
  }, [text]);

  const onInput = (e) => {
    const v = e.currentTarget.textContent ?? "";
    onChange(v);
  };

  const edited =
    word.final_text !== word._original_final_text &&
    word._original_final_text !== undefined;
  const userAdded = word.source === "user_added";

  const conf = word.textractConfidence;
  const barColor = confidenceColor(conf);
  const showBar = typeof conf === "number";

  const [menu, setMenu] = useState(null); // { x, y } | null
  useEffect(() => {
    if (!menu) return;
    const close = () => setMenu(null);
    window.addEventListener("click", close);
    window.addEventListener("scroll", close, true);
    return () => {
      window.removeEventListener("click", close);
      window.removeEventListener("scroll", close, true);
    };
  }, [menu]);

  return (
    <>
      <span
        ref={(el) => { ref.current = el; registerRef(el); }}
        className={
          "word" +
          (active ? " active" : "") +
          (edited ? " edited" : "") +
          (userAdded ? " user-added" : "")
        }
        contentEditable
        suppressContentEditableWarning
        spellCheck={false}
        onMouseEnter={onEnter}
        onMouseMove={onMove}
        onMouseLeave={onLeave}
        onInput={onInput}
        onKeyDown={(e) => {
          if (e.key === "Enter") { e.preventDefault(); e.currentTarget.blur(); }
        }}
        onContextMenu={(e) => {
          e.preventDefault();
          setMenu({ x: e.clientX, y: e.clientY });
        }}
        style={showBar ? { boxShadow: `inset 0 -2px 0 ${barColor}` } : undefined}
        data-word-index={word.wordIndex}
      />
      {!isLast ? " " : ""}
      {menu && (
        <div
          className="ctx-menu"
          style={{ left: menu.x, top: menu.y }}
          onClick={(e) => e.stopPropagation()}
        >
          <button onClick={() => { setMenu(null); onSplit(); }}>Split into two…</button>
          <button onClick={() => { setMenu(null); onDelete(); }} className="danger">Delete word</button>
        </div>
      )}
    </>
  );
}

window.TranscribePane = TranscribePane;
