/* =========================================================
   CONNIE - dashboard layout + settings state (localStorage)

   One source of truth for the user's dashboard preferences, persisted under a
   single key ``connie.layout``:

     {
       order:    string[],            // flat preferred card-id order; applied
                                       // per column when rendering
       columns:  { [cardId]: ColName },  // per-card column override;
                                       // missing -> the card's natural column
       hidden:   string[],            // hidden card-ids
       collapsed: ColName[],          // collapsed column names (folded to a strip)
       sizes:    { [cardId]: "S" | "M" | "L" },   // rendered tile size step
       textSize: "S" | "M" | "L",     // speech text size -> --speech-size
       voiceId:  string | null,       // chosen ElevenLabs voice
       language: string | null        // chosen voice-language filter ("" / null
                                       // = all languages)
     }

   ColName is one of COLUMNS = ["left","left2","right","right2"] - a 4-column
   "2 | Orb | 2" widescreen layout. The two original names ("left","right") are
   kept on purpose so any pre-S3 stored layout stays valid with zero migration:
   old overrides still resolve, the two new columns ("left2","right2") simply
   start empty.

   v3.1: replaced the old up/down + compact model with real drag-and-drop
   (column override + flat order) and a discrete S/M/L size step that visibly
   changes the rendered tile height. Backward compatible: an old ``sizes`` value
   of "compact" maps to "S" and "normal" to "M"; an old layout without
   ``columns`` just falls back to each card's natural column.

   S3 (this revision): 2 columns -> 4 columns (widescreen) plus per-column
   collapse (``collapsed``). All four columns are collapsible; an unknown column
   name in a stored ``collapsed`` is dropped on load.

   Robustness: load() always returns a fully-formed object even when the stored
   JSON is missing, partial, an old shape, or corrupt. Unknown keys are ignored;
   missing keys fall back to defaults. Nothing here throws.
   ========================================================= */
const LAYOUT_KEY = "connie.layout";

const TEXT_SIZES = ["S", "M", "L"];
// Maps the discrete text-size choice to the CSS clamp() applied via --speech-size.
const TEXT_SIZE_CSS = {
  S: "clamp(16px, 1.6vw, 20px)",
  M: "clamp(20px, 2.0vw, 25px)",
  L: "clamp(24px, 2.6vw, 32px)",
};

// Tile size steps. "M" is the natural/content size (the pre-v3.1 default look).
const SIZE_STEPS = ["S", "M", "L"];
const DEFAULT_SIZE = "M";

// Four columns in a "2 | Orb | 2" widescreen layout. "left"/"right" keep their
// names from the old 2-column model so stored layouts migrate for free; the two
// new columns start empty. computeDrop / cardsForColumn / columnOf are all
// generic over this list - it is the single source of truth for column names.
const COLUMNS = ["left", "left2", "right", "right2"];

// Legacy size-value migration (old model used compact/normal).
const LEGACY_SIZE_MAP = { compact: "S", normal: "M" };

function defaultLayout() {
  return {
    order: [],
    columns: {},
    hidden: [],
    collapsed: [],
    sizes: {},
    textSize: "M",
    voiceId: null,
    language: null,
  };
}

// Coerce an arbitrary parsed value into a clean layout object (defensive).
function normalizeLayout(raw) {
  const d = defaultLayout();
  if (!raw || typeof raw !== "object") return d;

  const order = Array.isArray(raw.order)
    ? raw.order.filter((x) => typeof x === "string")
    : d.order;

  const hidden = Array.isArray(raw.hidden)
    ? raw.hidden.filter((x) => typeof x === "string")
    : d.hidden;

  // Collapsed columns: keep only known column names, de-duplicated.
  const collapsed = Array.isArray(raw.collapsed)
    ? [...new Set(raw.collapsed.filter((x) => COLUMNS.includes(x)))]
    : d.collapsed;

  let columns = {};
  if (raw.columns && typeof raw.columns === "object") {
    for (const [k, v] of Object.entries(raw.columns)) {
      if (typeof k === "string" && COLUMNS.includes(v)) columns[k] = v;
    }
  }

  let sizes = {};
  if (raw.sizes && typeof raw.sizes === "object") {
    for (const [k, v] of Object.entries(raw.sizes)) {
      if (typeof k !== "string") continue;
      // Accept the new S/M/L steps, and migrate the old compact/normal values.
      if (SIZE_STEPS.includes(v)) sizes[k] = v;
      else if (LEGACY_SIZE_MAP[v]) sizes[k] = LEGACY_SIZE_MAP[v];
    }
  }

  const textSize = TEXT_SIZES.includes(raw.textSize) ? raw.textSize : d.textSize;
  const voiceId = typeof raw.voiceId === "string" && raw.voiceId ? raw.voiceId : null;
  const language =
    typeof raw.language === "string" && raw.language ? raw.language : null;

  return { order, columns, hidden, collapsed, sizes, textSize, voiceId, language };
}

function loadLayout() {
  try {
    const rawStr = window.localStorage.getItem(LAYOUT_KEY);
    if (!rawStr) return defaultLayout();
    return normalizeLayout(JSON.parse(rawStr));
  } catch (_) {
    return defaultLayout();
  }
}

function saveLayout(layout) {
  try {
    window.localStorage.setItem(LAYOUT_KEY, JSON.stringify(normalizeLayout(layout)));
  } catch (_) {
    // Private mode / quota: preferences just won't persist this session.
  }
}

// Resolve a card's column: an explicit override wins; otherwise the card's
// natural column from ``naturalCols`` ({cardId -> "left"|"right"}); otherwise
// "left" as a last resort (a brand-new card with no natural mapping).
function columnOf(cardId, layout, naturalCols) {
  const ov = layout.columns && layout.columns[cardId];
  if (COLUMNS.includes(ov)) return ov;
  const nat = naturalCols && naturalCols[cardId];
  return COLUMNS.includes(nat) ? nat : "left";
}

// The ordered list of card-ids that belong to ``colName``. We take EVERY known
// card (from naturalCols), keep those whose resolved column == colName, then
// sort by the saved flat order (ids in ``order`` first, in that order; unknown/
// new ids keep their natural position at the end). This is robust against old
// keys: a new card in code always appears (it just isn't in ``order`` yet) and
// a card can never disappear because of a stale ``order``.
function cardsForColumn(naturalCols, layout, colName) {
  const allIds = Object.keys(naturalCols || {});
  const inCol = allIds.filter((id) => columnOf(id, layout, naturalCols) === colName);
  const ord = Array.isArray(layout.order) ? layout.order : [];
  const inSaved = ord.filter((id) => inCol.includes(id));
  const rest = inCol.filter((id) => !inSaved.includes(id));
  return [...inSaved, ...rest];
}

// Visible subset of a column (hidden cards filtered out), in order.
function visibleCardsForColumn(naturalCols, layout, colName) {
  const hidden = Array.isArray(layout.hidden) ? layout.hidden : [];
  return cardsForColumn(naturalCols, layout, colName).filter(
    (id) => !hidden.includes(id)
  );
}

// Compute a new {order, columns} after dropping ``draggedId`` into ``targetCol``
// immediately BEFORE ``beforeId`` (or at the end of the column when
// ``beforeId`` is null/unknown). Pure: returns a new partial layout patch
// ({order, columns}); does not mutate the input.
//
// Strategy: build the full per-column ordered id lists (including hidden cards,
// so their relative order is preserved), remove the dragged id from wherever it
// is, insert it at the requested spot in the target column, then flatten back
// into one ``order`` array (left column first, then right) and record the
// dragged id's column override.
function computeDrop(naturalCols, layout, draggedId, targetCol, beforeId) {
  if (!COLUMNS.includes(targetCol)) return { order: layout.order, columns: layout.columns };
  // Dropping a card immediately before itself is a no-op (the UI already excludes
  // the dragged card from drop candidates, but guard defensively here too).
  if (beforeId === draggedId) return { order: layout.order, columns: layout.columns };

  // Current full ordered lists per column (hidden included).
  const lists = {};
  for (const c of COLUMNS) lists[c] = cardsForColumn(naturalCols, layout, c);

  // Remove the dragged id from every column list.
  for (const c of COLUMNS) lists[c] = lists[c].filter((id) => id !== draggedId);

  // Insert into the target column at the right index.
  const tgt = lists[targetCol];
  let idx = tgt.length; // default: append
  if (beforeId && beforeId !== draggedId) {
    const at = tgt.indexOf(beforeId);
    if (at !== -1) idx = at;
  }
  tgt.splice(idx, 0, draggedId);

  // Flatten back to a single flat order (all columns in COLUMNS order). Per-column
  // rendering re-derives each column from this + the columns map, so the cross-
  // column interleaving in the flat array is irrelevant - only intra-column order
  // matters. Generic over COLUMNS so it covers all four columns without a hardcode.
  const order = COLUMNS.flatMap((c) => lists[c]);

  // Record the column override for the dragged card.
  const columns = { ...(layout.columns || {}), [draggedId]: targetCol };

  return { order, columns };
}

// Set (or normalise) a card's size step. Returns a new ``sizes`` object.
function setCardSize(layout, cardId, step) {
  const next = SIZE_STEPS.includes(step) ? step : DEFAULT_SIZE;
  return { ...(layout.sizes || {}), [cardId]: next };
}

// Resolve a card's current size step (default "M").
function sizeOf(layout, cardId) {
  const s = layout.sizes && layout.sizes[cardId];
  return SIZE_STEPS.includes(s) ? s : DEFAULT_SIZE;
}

// Is ``col`` currently collapsed (folded to a strip)?
function isCollapsed(layout, col) {
  const c = layout && Array.isArray(layout.collapsed) ? layout.collapsed : [];
  return c.includes(col);
}

// Toggle a column's collapsed state. Returns a new ``collapsed`` array (does not
// mutate). No-ops for an unknown column name (only COLUMNS can be collapsed).
function toggleCollapsed(layout, col) {
  const cur = layout && Array.isArray(layout.collapsed) ? layout.collapsed : [];
  if (!COLUMNS.includes(col)) return [...cur];
  return cur.includes(col)
    ? cur.filter((c) => c !== col)
    : [...cur, col];
}

window.ConnieLayout = {
  LAYOUT_KEY,
  TEXT_SIZES,
  TEXT_SIZE_CSS,
  SIZE_STEPS,
  DEFAULT_SIZE,
  COLUMNS,
  defaultLayout,
  normalizeLayout,
  loadLayout,
  saveLayout,
  columnOf,
  cardsForColumn,
  visibleCardsForColumn,
  computeDrop,
  setCardSize,
  sizeOf,
  isCollapsed,
  toggleCollapsed,
};
