// Bible store — fichas (entities) + cronología del mundo (events).
// Herramienta INTERNA: solo el admin la ve, solo el admin la escribe.
//
// A diferencia de novelStore, esto NO se cachea en localStorage: son notas
// privadas y spoilers, y no queremos dejarlos en el disco del navegador.
// Se pide al servidor cada vez que se abre el panel (una sola llamada).
//
// Superficie pública:
//   window.BibleStore.useBible()   → { bible, setBible, status, reload, flush }
//   window.BibleStore.newId()      → uuid v4
//   window.BibleStore.KINDS, KIND_LABELS, STATUS_OPTIONS, ROLE_OPTIONS
//   window.BibleStore.FIELD_GROUPS → qué campos se muestran por tipo de ficha
//   window.BibleStore.blankEntity(kind, position)
//   window.BibleStore.blankEvent()
//   window.BibleStore.computeSortKey(year, month, day)
//   window.BibleStore.whenLabelFor(event)
//
// OJO (ver proto_global_scope_gotcha): los <script type="text/babel"> comparten
// scope global. Todo aquí lleva sufijo WB para no pisar nada de los otros .jsx.

const WB_SAVE_DEBOUNCE_MS = 1200;

const KINDS_WB = ["personaje", "lugar", "objeto", "faccion", "termino"];

const KIND_LABELS_WB = {
  personaje: { label: "PERSONAJES", singular: "PERSONAJE", icon: "☻" },
  lugar:     { label: "LUGARES",    singular: "LUGAR",     icon: "⌂" },
  objeto:    { label: "OBJETOS",    singular: "OBJETO",    icon: "◈" },
  faccion:   { label: "FACCIONES",  singular: "FACCIÓN",   icon: "⚑" },
  termino:   { label: "GLOSARIO",   singular: "TÉRMINO",   icon: "¶" },
};

const STATUS_OPTIONS_WB = [
  { v: "",            l: "—" },
  { v: "vivo",        l: "VIVO" },
  { v: "muerto",      l: "MUERTO" },
  { v: "desaparecido",l: "DESAPARECIDO" },
  { v: "desconocido", l: "DESCONOCIDO" },
];

const ROLE_OPTIONS_WB = [
  { v: "",             l: "—" },
  { v: "protagonista", l: "PROTAGONISTA" },
  { v: "antagonista",  l: "ANTAGONISTA" },
  { v: "secundario",   l: "SECUNDARIO" },
  { v: "mencion",      l: "MENCIÓN" },
];

// Qué campos largos se muestran según el tipo de ficha. El modelo de datos es
// el mismo para todas (columna `data` jsonb); esto solo decide qué se pinta.
const FIELD_GROUPS_WB = {
  personaje: [
    { k: "appearance",  l: "FÍSICO / CÓMO SE VE",        rows: 3 },
    { k: "personality", l: "PSICOLOGÍA / CÓMO ES",       rows: 4 },
    { k: "motivation",  l: "QUÉ QUIERE",                 rows: 3 },
    { k: "flaw",        l: "QUÉ LE ESTORBA / SU FALLA",  rows: 3 },
    { k: "voice",       l: "CÓMO HABLA (MULETILLAS, TONO)", rows: 3 },
    { k: "arcNote",     l: "ARCO NARRATIVO",             rows: 4 },
  ],
  lugar: [
    { k: "appearance",  l: "CÓMO SE VE",        rows: 4 },
    { k: "personality", l: "AMBIENTE / QUÉ SE SIENTE", rows: 3 },
    { k: "arcNote",     l: "QUÉ PASA AQUÍ",     rows: 4 },
  ],
  objeto: [
    { k: "appearance",  l: "CÓMO SE VE",         rows: 3 },
    { k: "motivation",  l: "QUÉ HACE / PARA QUÉ SIRVE", rows: 3 },
    { k: "arcNote",     l: "SU HISTORIA",        rows: 4 },
  ],
  faccion: [
    { k: "motivation",  l: "QUÉ BUSCA",          rows: 3 },
    { k: "personality", l: "CÓMO OPERA",         rows: 3 },
    { k: "arcNote",     l: "SU HISTORIA",        rows: 4 },
  ],
  termino: [
    { k: "arcNote",     l: "QUÉ SIGNIFICA",      rows: 4 },
  ],
};

// Campos que existen en todas las fichas, al final del formulario.
const COMMON_TAIL_FIELDS_WB = [
  { k: "secrets", l: "SECRETOS / SPOILERS (SOLO TÚ)", rows: 3, secret: true },
  { k: "notes",   l: "NOTAS SUELTAS",                 rows: 3 },
];

function newIdWB() {
  if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
    const r = Math.random() * 16 | 0;
    const v = c === "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

const WB_PALETTE = ["#ff5aa0", "#5fb3ff", "#c96ad8", "#ffc04a", "#6fd8c3", "#e4b35a", "#a8d08a", "#ff7a5a"];

function blankEntityWB(kind, position) {
  return {
    id: newIdWB(),
    kind: kind || "personaje",
    name: "",
    oneLiner: "",
    role: "",
    faction: "",
    status: "",
    color: WB_PALETTE[(position || 0) % WB_PALETTE.length],
    image: "",
    imageOffsetX: 50,
    imageOffsetY: 50,
    imageScale: 1,
    alias: "",
    age: "",
    origin: "",
    appearance: "",
    personality: "",
    motivation: "",
    flaw: "",
    voice: "",
    arcNote: "",
    secrets: "",
    notes: "",
    firstChapterId: null,
    cardId: null,
    position: position || 0,
    chapters: [],
    links: [],
  };
}

function blankEventWB() {
  return {
    id: newIdWB(),
    title: "",
    description: "",
    era: "",
    year: null,
    month: null,
    day: null,
    spanYears: null,
    sortKey: 0,
    whenLabel: "",
    chapterId: null,
    color: "",
    importance: 1,
    entityIds: [],
  };
}

// Mismo cálculo que lib/bibleMap.ts (el server lo recalcula al guardar; aquí lo
// necesitamos para ordenar la línea sin esperar el round-trip).
function computeSortKeyWB(year, month, day) {
  const y = Number(year);
  if (year == null || year === "" || !isFinite(y)) return 0;
  const m = month != null && month >= 1 && month <= 12 ? Number(month) : 1;
  const d = day != null && day >= 1 && day <= 31 ? Number(day) : 1;
  return y + (m - 1) / 12 + (d - 1) / 372;
}

const WB_MONTHS = ["ENE","FEB","MAR","ABR","MAY","JUN","JUL","AGO","SEP","OCT","NOV","DIC"];

// Etiqueta que se muestra en la línea. Si Aaron escribió una a mano, gana ella.
function whenLabelForWB(ev) {
  if (ev.whenLabel && ev.whenLabel.trim()) return ev.whenLabel.trim();
  if (ev.year == null || ev.year === "") return "SIN FECHA";
  const parts = [];
  if (ev.day != null && ev.day !== "") parts.push(String(ev.day));
  if (ev.month != null && ev.month !== "") parts.push(WB_MONTHS[Number(ev.month) - 1] || "");
  parts.push(String(ev.year));
  const base = parts.filter(Boolean).join(" ");
  return ev.era ? `${base} ${ev.era}` : base;
}

function normalizeBibleWB(maybe) {
  return {
    entities: Array.isArray(maybe?.entities) ? maybe.entities : [],
    events: Array.isArray(maybe?.events) ? maybe.events : [],
  };
}

// Caché EN MEMORIA (no localStorage — son notas privadas, no las dejamos en
// disco). Sirve para que abrir el panel de referencia por segunda vez pinte al
// instante mientras revalida contra el servidor.
const WB_MEM = { data: null };

async function fetchBibleWB() {
  const r = await fetch("/api/bible", { credentials: "same-origin" });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  const data = normalizeBibleWB(await r.json());
  WB_MEM.data = data;
  return data;
}

async function putBibleWB(bible) {
  const r = await fetch("/api/bible", {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    credentials: "same-origin",
    body: JSON.stringify(normalizeBibleWB(bible)),
  });
  if (!r.ok) {
    const txt = await r.text().catch(() => "");
    throw new Error(`HTTP ${r.status} ${txt}`);
  }
  return true;
}

// Hook único del panel. Autosave con debounce: Aaron escribe, se guarda solo.
// status: "loading" | "idle" | "saving" | "saved" | "error"
function useBibleWB() {
  const [bible, setBibleState] = React.useState(() => WB_MEM.data || { entities: [], events: [] });
  const [status, setStatus] = React.useState(() => (WB_MEM.data ? "idle" : "loading"));
  const timerRef = React.useRef(null);
  const pendingRef = React.useRef(null);
  const mountedRef = React.useRef(true);
  // RED DE SEGURIDAD: el PUT es un sync completo (lo que no viene, se borra).
  // Si el GET inicial falló, lo que tenemos en pantalla NO es la verdad — y
  // guardar borraría fichas reales. Hasta que no haya una carga buena, no se
  // escribe nada. Va duplicado en ref + state a propósito: el ref lo consulta
  // setBible de forma síncrona, el state es el que la UI puede renderizar.
  const loadedRef = React.useRef(!!WB_MEM.data);
  const [loaded, setLoaded] = React.useState(!!WB_MEM.data);

  // Trae del servidor. `quiet` = revalidación silenciosa: no pintamos "cargando"
  // encima de lo que ya se ve (y así el efecto de montaje no setea estado de
  // forma síncrona, que dispara renders en cascada).
  const fetchInto = React.useCallback((quiet) => {
    if (!quiet) setStatus("loading");
    return fetchBibleWB()
      .then((b) => {
        loadedRef.current = true;
        if (!mountedRef.current) return;
        setLoaded(true);
        setBibleState(b);
        setStatus("idle");
      })
      .catch((e) => {
        console.error("[bibleStore] fetch failed", e);
        if (mountedRef.current) setStatus("error");
      });
  }, []);

  // Para el botón REINTENTAR: ahí sí queremos ver "CARGANDO…".
  const reload = React.useCallback(() => fetchInto(false), [fetchInto]);

  // Carga al montar. La promesa va inline (y no `fetchInto`) para que todo el
  // seteo de estado quede dentro de callbacks asíncronos: mismo patrón que
  // novelStore.jsx, y ningún render en cascada al montar.
  React.useEffect(() => {
    mountedRef.current = true;
    fetchBibleWB()
      .then((b) => {
        loadedRef.current = true;
        if (!mountedRef.current) return;
        setLoaded(true);
        setBibleState(b);
        setStatus("idle");
      })
      .catch((e) => {
        console.error("[bibleStore] fetch failed", e);
        if (mountedRef.current) setStatus("error");
      });
    return () => {
      mountedRef.current = false;
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, []);

  // Manda lo que haya pendiente ya mismo (al cerrar el panel, Ctrl+S, etc).
  const flush = React.useCallback(() => {
    if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
    const payload = pendingRef.current;
    if (!payload) return Promise.resolve();
    pendingRef.current = null;
    setStatus("saving");
    return putBibleWB(payload)
      .then(() => { if (mountedRef.current) setStatus("saved"); })
      .catch((e) => {
        console.error("[bibleStore] save failed", e);
        if (mountedRef.current) setStatus("error");
      });
  }, []);

  const setBible = React.useCallback((next) => {
    const safe = normalizeBibleWB(typeof next === "function" ? next(pendingRef.current || undefined) : next);
    setBibleState(safe);
    if (!loadedRef.current) {
      // Nunca cargamos bien: mostramos el cambio pero NO lo mandamos, para no
      // arrasar con lo que sí existe en la base.
      console.error("[bibleStore] cambio no guardado: la carga inicial falló");
      setStatus("error");
      return;
    }
    WB_MEM.data = safe;   // el otro panel (referencia / fichas) ve lo mismo
    pendingRef.current = safe;
    setStatus("saving");
    if (timerRef.current) clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => {
      timerRef.current = null;
      const payload = pendingRef.current;
      pendingRef.current = null;
      if (!payload) return;
      putBibleWB(payload)
        .then(() => { if (mountedRef.current) setStatus("saved"); })
        .catch((e) => {
          console.error("[bibleStore] save failed", e);
          if (mountedRef.current) setStatus("error");
        });
    }, WB_SAVE_DEBOUNCE_MS);
  }, []);

  // Aviso si cierras la pestaña con un guardado en vuelo.
  React.useEffect(() => {
    const handler = (e) => {
      if (!pendingRef.current) return;
      e.preventDefault();
      e.returnValue = "Se están guardando tus fichas. ¿Salir de todos modos?";
      return e.returnValue;
    };
    window.addEventListener("beforeunload", handler);
    return () => window.removeEventListener("beforeunload", handler);
  }, []);

  // `loaded` = hubo al menos una lectura buena. Mientras sea false NO se debe
  // dejar escribir: el PUT es un sync completo y borraría lo que no vimos.
  return { bible, setBible, status, reload, flush, loaded };
}

window.BibleStore = {
  useBible: useBibleWB,
  fetchBible: fetchBibleWB,
  newId: newIdWB,
  blankEntity: blankEntityWB,
  blankEvent: blankEventWB,
  computeSortKey: computeSortKeyWB,
  whenLabelFor: whenLabelForWB,
  KINDS: KINDS_WB,
  KIND_LABELS: KIND_LABELS_WB,
  STATUS_OPTIONS: STATUS_OPTIONS_WB,
  ROLE_OPTIONS: ROLE_OPTIONS_WB,
  FIELD_GROUPS: FIELD_GROUPS_WB,
  COMMON_TAIL_FIELDS: COMMON_TAIL_FIELDS_WB,
  PALETTE: WB_PALETTE,
};
