/**
 * Редактор серверной SQLite: дерево bind → таблицы, сетка с сохранением через /api/admin/db/*.
 */

function escHtml(s) {
  return String(s)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function parseInputValue(raw, sampleOriginal) {
  const t = raw.trim();
  if (t === "" && (sampleOriginal === null || sampleOriginal === undefined)) return null;
  if (t === "") return "";
  if (/^-?\d+$/.test(t)) return Number(t);
  if (/^-?\d+\.\d+$/.test(t)) return Number(t);
  if (t === "true") return 1;
  if (t === "false") return 0;
  return raw;
}

function closeModal() {
  const m = document.getElementById("adminSqliteModal");
  if (m) m.hidden = true;
  document.body.style.overflow = "";
}

let browseState = {
  bind: "recipes",
  table: null,
  limit: 50,
  offset: 0,
  page: null,
};

/** Колбэк выбора таблицы (нужен для повторной отрисовки дерева). */
let dbTreePickCallback = null;

async function fetchJson(url, options) {
  const r = await fetch(url, options);
  const data = await r.json().catch(() => ({}));
  if (!r.ok) {
    throw new Error(data.message || `HTTP ${r.status}`);
  }
  return data;
}

function renderTree(binds, preferredBind, onPickTable) {
  dbTreePickCallback = onPickTable;
  const root = document.getElementById("adminDbTree");
  if (!root) return;
  root.innerHTML = binds
    .map((b) => {
      const open = b.bind === preferredBind ? "is-open" : "";
      const rows = (b.tables || [])
        .map(
          (t) =>
            `<li class="wesp-db-tree-leaf"><button type="button" class="wesp-db-tree-table ant-btn ant-btn-sm" data-bind="${escHtml(b.bind)}" data-table="${escHtml(t.name)}">${escHtml(t.name)}</button></li>`,
        )
        .join("");
      return `<li class="wesp-db-tree-node ${open}">
        <button type="button" class="wesp-db-tree-caret" aria-expanded="${b.bind === preferredBind ? "true" : "false"}"><span class="wesp-db-tree-caret-icon">▶</span></button>
        <span class="wesp-db-tree-folder">${escHtml(b.bind)}</span>
        <ul class="wesp-db-tree-children">${rows}</ul>
      </li>`;
    })
    .join("");

  root.querySelectorAll(".wesp-db-tree-caret").forEach((btn) => {
    btn.addEventListener("click", () => {
      const li = btn.closest(".wesp-db-tree-node");
      if (!li) return;
      const open = li.classList.toggle("is-open");
      btn.setAttribute("aria-expanded", open ? "true" : "false");
    });
  });

  root.querySelectorAll(".wesp-db-tree-table").forEach((btn) => {
    btn.addEventListener("click", () => {
      const bind = btn.getAttribute("data-bind");
      const table = btn.getAttribute("data-table");
      root.querySelectorAll(".wesp-db-tree-table.is-active").forEach((b) => b.classList.remove("is-active"));
      btn.classList.add("is-active");
      onPickTable(bind, table);
    });
  });
}

function renderGrid(data, setStatus) {
  const meta = document.getElementById("adminDbGridMeta");
  const wrap = document.getElementById("adminDbGridWrap");
  const pager = document.getElementById("adminDbPager");
  if (!meta || !wrap || !pager) return;

  browseState.page = data;
  const { columns, rows, total, has_rowid: hasRowid } = data;
  meta.textContent = `${data.bind} / ${data.table} — строк ${total}, показано ${rows.length} (offset ${browseState.offset})`;

  const colMetaByName = Object.fromEntries((columns || []).map((c) => [c.name, c]));
  const displayCols = (data.column_names || []).filter((n) => n !== "_wesp_rowid");

  let thead =
    "<thead><tr>" +
    (hasRowid ? "<th>rowid</th>" : "") +
    displayCols.map((n) => `<th title="${escHtml(colMetaByName[n]?.type || "")}">${escHtml(n)}</th>`).join("") +
    "<th class=\"wesp-db-grid-actions-col\">Действия</th></tr></thead>";

  const body = rows
    .map((row) => {
      const rowKey = {};
      if (hasRowid && row._wesp_rowid != null) rowKey._wesp_rowid = row._wesp_rowid;
      (data.pk_names || []).forEach((pk) => {
        if (pk in row) rowKey[pk] = row[pk];
      });

      const cells = displayCols
        .map((name) => {
          const cm = colMetaByName[name];
          const val = row[name];
          if (cm && !cm.editable) {
            return `<td class="wesp-db-cell-readonly">(BLOB)</td>`;
          }
          const v = val == null ? "" : String(val);
          return `<td><input type="text" class="ant-input wesp-db-cell" data-col="${escHtml(name)}" value="${escHtml(v)}"/></td>`;
        })
        .join("");

      const ridCell = hasRowid
        ? `<td class="wesp-db-cell-readonly">${row._wesp_rowid != null ? escHtml(String(row._wesp_rowid)) : "—"}</td>`
        : "";

      const rowKeyEnc = encodeURIComponent(JSON.stringify(rowKey));
      const rowJsonEnc = encodeURIComponent(JSON.stringify(row));
      return `<tr data-row-key="${rowKeyEnc}" data-row-json="${rowJsonEnc}">
      ${ridCell}${cells}
      <td class="wesp-db-row-actions">
        <button type="button" class="ant-btn ant-btn-sm ant-btn-primary wesp-db-row-save"><span>Сохранить</span></button>
        <button type="button" class="ant-btn ant-btn-sm ant-btn-danger wesp-db-row-delete"><span>Удалить</span></button>
      </td>
    </tr>`;
    })
    .join("");

  wrap.innerHTML = `<table class="ant-table wesp-db-grid-table">${thead}<tbody>${body}</tbody></table>`;

  wrap.querySelectorAll(".wesp-db-row-delete").forEach((btn) => {
    btn.addEventListener("click", async () => {
      const tr = btn.closest("tr");
      if (!tr) return;
      let rowKey;
      try {
        rowKey = JSON.parse(decodeURIComponent(tr.getAttribute("data-row-key") || "%7B%7D"));
      } catch (e) {
        setStatus("Ошибка разбора строки", true);
        return;
      }
      btn.disabled = true;
      try {
        const prev = await fetchJson("/api/admin/db/row/delete-preview", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            bind: browseState.bind,
            table: browseState.table,
            row_key: rowKey,
          }),
        });
        let msg = `Будет удалено строк: ${prev.total}\n\n${(prev.lines || []).join("\n")}`;
        if (prev.truncated) msg += "\n\n… (в списке не все строки)";
        msg += "\n\nУчитываются только связи, объявленные внешними ключами SQLite.\nПродолжить удаление?";
        if (!window.confirm(msg)) {
          setStatus("Удаление отменено");
          return;
        }
        await fetchJson("/api/admin/db/row/delete", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            bind: browseState.bind,
            table: browseState.table,
            row_key: rowKey,
          }),
        });
        setStatus(`Удалено строк: ${prev.total}`);
        await loadTablePage(setStatus);
      } catch (e) {
        setStatus(e.message, true, "Ошибка удаления");
      } finally {
        btn.disabled = false;
      }
    });
  });

  wrap.querySelectorAll(".wesp-db-row-save").forEach((btn) => {
    btn.addEventListener("click", async () => {
      const tr = btn.closest("tr");
      if (!tr) return;
      let rowKey;
      let original;
      try {
        rowKey = JSON.parse(decodeURIComponent(tr.getAttribute("data-row-key") || "%7B%7D"));
        original = JSON.parse(decodeURIComponent(tr.getAttribute("data-row-json") || "%7B%7D"));
      } catch (e) {
        setStatus("Ошибка разбора строки", true);
        return;
      }
      const changes = {};
      tr.querySelectorAll(".wesp-db-cell[data-col]").forEach((inp) => {
        const col = inp.getAttribute("data-col");
        if (!col || inp.readOnly) return;
        const oldVal = Object.prototype.hasOwnProperty.call(original, col) ? original[col] : undefined;
        const newVal = parseInputValue(inp.value, oldVal);
        const same =
          (oldVal == null && (newVal === "" || newVal === null)) ||
          JSON.stringify(oldVal) === JSON.stringify(newVal === "" && oldVal == null ? null : newVal);
        if (!same) {
          changes[col] = newVal === "" && oldVal == null ? null : newVal;
        }
      });
      if (!Object.keys(changes).length) {
        setStatus("Нет изменений в строке");
        return;
      }
      btn.disabled = true;
      try {
        await fetchJson("/api/admin/db/row", {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            bind: browseState.bind,
            table: browseState.table,
            row_key: rowKey,
            changes,
          }),
        });
        setStatus("Строка сохранена на сервере");
        await loadTablePage(setStatus);
      } catch (e) {
        setStatus(e.message, true, "Ошибка сохранения");
      } finally {
        btn.disabled = false;
      }
    });
  });

  const prev = browseState.offset > 0;
  const next = browseState.offset + rows.length < total;
  pager.innerHTML = `
    <button type="button" class="ant-btn ant-btn-sm" id="adminDbPrev" ${prev ? "" : "disabled"}><span>Назад</span></button>
    <button type="button" class="ant-btn ant-btn-sm" id="adminDbNext" ${next ? "" : "disabled"}><span>Вперёд</span></button>
  `;
  document.getElementById("adminDbPrev")?.addEventListener("click", async () => {
    browseState.offset = Math.max(0, browseState.offset - browseState.limit);
    await loadTablePage(setStatus);
  });
  document.getElementById("adminDbNext")?.addEventListener("click", async () => {
    browseState.offset += browseState.limit;
    await loadTablePage(setStatus);
  });
}

async function loadTablePage(setStatus) {
  const errEl = document.getElementById("adminSqliteError");
  if (errEl) errEl.textContent = "";
  if (!browseState.table) return;
  const u = `/api/admin/db/table?bind=${encodeURIComponent(browseState.bind)}&table=${encodeURIComponent(browseState.table)}&limit=${browseState.limit}&offset=${browseState.offset}`;
  const data = await fetchJson(u);
  renderGrid(data, setStatus);
}

export async function refreshAdminDbTree(setStatus) {
  const modal = document.getElementById("adminSqliteModal");
  const errEl = document.getElementById("adminSqliteError");
  if (!modal || modal.hidden) return;
  try {
    const tree = await fetchJson("/api/admin/db/tree");
    const pref = browseState.bind || "recipes";
    const cb = dbTreePickCallback;
    if (typeof cb !== "function") {
      if (typeof setStatus === "function") setStatus("Подождите, дерево ещё загружается");
      return;
    }
    renderTree(tree.binds || [], pref, cb);
    document.querySelectorAll(".wesp-db-tree-table").forEach((btn) => {
      if (
        btn.getAttribute("data-bind") === browseState.bind &&
        btn.getAttribute("data-table") === browseState.table
      ) {
        document.querySelectorAll(".wesp-db-tree-table.is-active").forEach((b) => b.classList.remove("is-active"));
        btn.classList.add("is-active");
      }
    });
    if (typeof setStatus === "function") setStatus("Каталог таблиц обновлён");
  } catch (e) {
    if (errEl) errEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось выполнить операцию") || "Не удалось выполнить операцию";
    if (typeof setStatus === "function") setStatus(e.message, true, "Не удалось выполнить операцию");
  }
}

export async function refreshAdminDbGrid(setStatus) {
  const errEl = document.getElementById("adminSqliteError");
  if (!browseState.table) {
    if (typeof setStatus === "function") setStatus("Сначала выберите таблицу в дереве");
    return;
  }
  if (errEl) errEl.textContent = "";
  try {
    await loadTablePage(setStatus);
    if (typeof setStatus === "function") setStatus("Таблица обновлена с сервера");
  } catch (e) {
    if (errEl) errEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось выполнить операцию") || "Не удалось выполнить операцию";
    if (typeof setStatus === "function") setStatus(e.message, true, "Не удалось выполнить операцию");
  }
}

export async function openAdminDbBrowser(options = {}) {
  const { preferredBind = "recipes", setStatus } = options;
  const modal = document.getElementById("adminSqliteModal");
  const errEl = document.getElementById("adminSqliteError");
  if (!modal || !errEl) {
    if (typeof setStatus === "function") setStatus("Нет разметки редактора БД", true);
    return;
  }

  browseState = {
    bind: preferredBind,
    table: null,
    limit: 50,
    offset: 0,
    page: null,
  };

  modal.hidden = false;
  document.body.style.overflow = "hidden";
  errEl.textContent = "";
  document.getElementById("adminDbGridMeta").textContent = "";
  document.getElementById("adminDbGridWrap").innerHTML =
    '<p class="wesp-db-browser__muted">Выберите таблицу в дереве слева.</p>';
  document.getElementById("adminDbPager").innerHTML = "";

  try {
    const tree = await fetchJson("/api/admin/db/tree");
    renderTree(tree.binds || [], preferredBind, async (bind, table) => {
      browseState.bind = bind;
      browseState.table = table;
      browseState.offset = 0;
      errEl.textContent = "";
      try {
        await loadTablePage(setStatus);
      } catch (e) {
        errEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось выполнить операцию") || "Не удалось выполнить операцию";
        if (typeof setStatus === "function") setStatus(e.message, true, "Не удалось выполнить операцию");
      }
    });
    if (typeof setStatus === "function") setStatus("Дерево БД загружено");
  } catch (e) {
    errEl.textContent = globalThis.WespUserMessages?.userFacingMessage?.(e.message, "Не удалось выполнить операцию") || "Не удалось выполнить операцию";
    if (typeof setStatus === "function") setStatus(e.message, true, "Не удалось выполнить операцию");
  }
}

export function wireAdminDbBrowserModal() {
  document.getElementById("adminSqliteModalClose")?.addEventListener("click", closeModal);
  document.getElementById("adminSqliteModalMask")?.addEventListener("click", closeModal);
}
