const crypto = require("crypto");

const PERMISSION_LEVELS = Object.freeze(["edit", "edit_review", "edit_review_implement"]);
const STATUS_VALUES = Object.freeze(["draft", "published", "archived"]);
const VISIBILITY_VALUES = Object.freeze(["user", "mod", "admin"]);
const REVIEW_STATES = Object.freeze(["draft", "review_pending", "approved", "rejected"]);
const VISIBILITY_RANK = Object.freeze({ user: 0, mod: 1, admin: 2 });
const AI_SEARCH_STOPWORDS = new Set([
  "a", "an", "and", "are", "about", "can", "could", "describe", "do", "does", "for",
  "help", "how", "i", "identify", "is", "me", "my", "of", "please", "tell", "the", "to",
  "use", "using", "what", "where", "who", "was", "were", "you", "your"
]);

function ensureTables(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS okf_entries (
      id TEXT PRIMARY KEY,
      slug TEXT NOT NULL UNIQUE,
      title TEXT NOT NULL,
      category TEXT NOT NULL DEFAULT '',
      tags_json TEXT NOT NULL DEFAULT '[]',
      aliases_json TEXT NOT NULL DEFAULT '[]',
      summary TEXT NOT NULL DEFAULT '',
      user_markdown TEXT NOT NULL DEFAULT '',
      moderator_markdown TEXT NOT NULL DEFAULT '',
      admin_markdown TEXT NOT NULL DEFAULT '',
      ai_facts_markdown TEXT NOT NULL DEFAULT '',
      source_links_json TEXT NOT NULL DEFAULT '[]',
      visibility TEXT NOT NULL DEFAULT 'user',
      status TEXT NOT NULL DEFAULT 'draft',
      review_state TEXT NOT NULL DEFAULT 'draft',
      created_by TEXT,
      updated_by TEXT,
      reviewed_by TEXT,
      published_by TEXT,
      created_at INTEGER NOT NULL,
      updated_at INTEGER NOT NULL,
      reviewed_at INTEGER,
      published_at INTEGER,
      archived_at INTEGER,
      deleted_at INTEGER
    );

    CREATE INDEX IF NOT EXISTS okf_entries_status_idx ON okf_entries (status, visibility, updated_at);
    CREATE INDEX IF NOT EXISTS okf_entries_slug_idx ON okf_entries (slug);

    CREATE TABLE IF NOT EXISTS okf_versions (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      entry_id TEXT NOT NULL,
      version_number INTEGER NOT NULL,
      changed_by TEXT,
      change_type TEXT NOT NULL,
      previous_json TEXT,
      next_json TEXT,
      note TEXT,
      created_at INTEGER NOT NULL
    );

    CREATE INDEX IF NOT EXISTS okf_versions_entry_idx ON okf_versions (entry_id, version_number);

    CREATE TABLE IF NOT EXISTS okf_permissions (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id TEXT NOT NULL,
      level TEXT NOT NULL,
      granted_by TEXT,
      notes TEXT,
      created_at INTEGER NOT NULL,
      revoked_at INTEGER
    );

    CREATE INDEX IF NOT EXISTS okf_permissions_user_idx ON okf_permissions (user_id, revoked_at);
  `);
}

function okfPermissionForUser(db, userId) {
  if (!userId) return null;
  const row = db
    .prepare(
      "SELECT level FROM okf_permissions WHERE user_id = ? AND revoked_at IS NULL ORDER BY created_at DESC LIMIT 1"
    )
    .get(userId);
  return PERMISSION_LEVELS.includes(row?.level) ? row.level : null;
}

function accessForUser(db, user) {
  const okfPermission = okfPermissionForUser(db, user?.id);
  const isAdmin = Boolean(user?.isAdmin);
  const isMod = Boolean(user?.isAdmin || user?.isMod);
  const capabilities = Object.freeze({
    read_user_fields: Boolean(user),
    read_mod_fields: isMod,
    read_admin_fields: isAdmin,
    edit_content: isAdmin || ["edit", "edit_review", "edit_review_implement"].includes(okfPermission),
    review: isAdmin || ["edit_review", "edit_review_implement"].includes(okfPermission),
    implement: isAdmin || okfPermission === "edit_review_implement",
    manage_permissions: isAdmin,
    management_view: isAdmin || ["edit", "edit_review", "edit_review_implement"].includes(okfPermission)
  });
  return {
    authenticated: Boolean(user),
    isAdmin,
    isMod,
    okfPermission,
    capabilities,
    canEdit: capabilities.edit_content,
    canReview: capabilities.review,
    canImplement: capabilities.implement,
    canManagePermissions: capabilities.manage_permissions,
    canViewManagement: capabilities.management_view,
    maxVisibility: capabilities.read_admin_fields ? "admin" : capabilities.read_mod_fields ? "mod" : "user"
  };
}

function canSeeEntry(access, entry, { management = false } = {}) {
  if (!access.authenticated || !entry || entry.deleted_at) return false;
  if (access.capabilities.read_admin_fields) return true;
  if (!canReadVisibility(access, entry.visibility)) return false;
  if (management && access.capabilities.management_view) return true;
  if (entry.status !== "published") return false;
  return true;
}

function canReadVisibility(access, visibility) {
  const required = VISIBILITY_RANK[visibility] ?? VISIBILITY_RANK.user;
  const maximum = VISIBILITY_RANK[access.maxVisibility] ?? VISIBILITY_RANK.user;
  return required <= maximum;
}

function allowedVisibility(value, access, fallback = "user") {
  const requested = normalizeChoice(value, VISIBILITY_VALUES, fallback);
  if (canReadVisibility(access, requested)) return requested;
  return canReadVisibility(access, fallback) ? fallback : access.maxVisibility;
}

function visibleSnapshot(snapshot, access) {
  if (!snapshot || typeof snapshot !== "object") return {};
  const visible = {
    slug: snapshot.slug,
    title: snapshot.title,
    category: snapshot.category,
    tags: Array.isArray(snapshot.tags) ? snapshot.tags : [],
    aliases: Array.isArray(snapshot.aliases) ? snapshot.aliases : [],
    summary: snapshot.summary,
    user_markdown: snapshot.user_markdown,
    visibility: snapshot.visibility,
    status: snapshot.status,
    review_state: snapshot.review_state
  };
  if (access.capabilities.read_mod_fields) visible.moderator_markdown = snapshot.moderator_markdown;
  if (access.capabilities.read_admin_fields) {
    visible.admin_markdown = snapshot.admin_markdown;
    visible.ai_facts_markdown = snapshot.ai_facts_markdown;
    visible.source_links = Array.isArray(snapshot.source_links) ? snapshot.source_links : [];
  }
  return visible;
}

function publicEntry(entry, access) {
  const base = {
    id: entry.id,
    slug: entry.slug,
    title: entry.title,
    category: entry.category,
    tags: parseJsonArray(entry.tags_json),
    aliases: parseJsonArray(entry.aliases_json),
    summary: entry.summary,
    user_markdown: entry.user_markdown,
    visibility: entry.visibility,
    status: entry.status,
    review_state: entry.review_state,
    updated_at: entry.updated_at,
    published_at: entry.published_at
  };
  if (access.capabilities.read_mod_fields) {
    base.moderator_markdown = entry.moderator_markdown;
  }
  if (access.capabilities.read_admin_fields) {
    base.admin_markdown = entry.admin_markdown;
    base.ai_facts_markdown = entry.ai_facts_markdown;
    base.source_links = parseJsonArray(entry.source_links_json);
    base.created_by = entry.created_by;
    base.updated_by = entry.updated_by;
    base.reviewed_by = entry.reviewed_by;
    base.published_by = entry.published_by;
    base.created_at = entry.created_at;
    base.reviewed_at = entry.reviewed_at;
    base.archived_at = entry.archived_at;
  }
  return base;
}

function listEntries(db, filters = {}, user, options = {}) {
  const access = accessForUser(db, user);
  const rows = db.prepare("SELECT * FROM okf_entries WHERE deleted_at IS NULL ORDER BY updated_at DESC").all();
  return rows
    .filter((entry) => canSeeEntry(access, entry, options))
    .filter((entry) => matchesFilters(entry, filters, access, options))
    .map((entry) => publicEntry(entry, access));
}

function getEntryBySlug(db, slug, user, options = {}) {
  const entry = db.prepare("SELECT * FROM okf_entries WHERE slug = ?").get(normalizeSlug(slug));
  const access = accessForUser(db, user);
  if (!canSeeEntry(access, entry, options)) return null;
  return publicEntry(entry, access);
}

function getEditableEntry(db, slug, user) {
  const entry = db.prepare("SELECT * FROM okf_entries WHERE slug = ? AND deleted_at IS NULL").get(normalizeSlug(slug));
  const access = accessForUser(db, user);
  return canSeeEntry(access, entry, { management: true }) ? publicEntry(entry, access) : null;
}

function createEntry(db, values, actor) {
  const access = accessForUser(db, actor);
  if (!access.canEdit) throw new Error("OKF edit permission is required.");
  const now = Date.now();
  const entry = normalizeValues(values, {
    actor,
    existing: null,
    access,
    now
  });
  entry.id = crypto.randomUUID();
  entry.created_by = String(actor.id);
  entry.updated_by = String(actor.id);
  entry.created_at = now;
  entry.updated_at = now;

  db.prepare(
    "INSERT INTO okf_entries (id, slug, title, category, tags_json, aliases_json, summary, user_markdown, moderator_markdown, admin_markdown, ai_facts_markdown, source_links_json, visibility, status, review_state, created_by, updated_by, reviewed_by, published_by, created_at, updated_at, reviewed_at, published_at, archived_at, deleted_at) " +
      "VALUES (@id, @slug, @title, @category, @tags_json, @aliases_json, @summary, @user_markdown, @moderator_markdown, @admin_markdown, @ai_facts_markdown, @source_links_json, @visibility, @status, @review_state, @created_by, @updated_by, @reviewed_by, @published_by, @created_at, @updated_at, @reviewed_at, @published_at, @archived_at, @deleted_at)"
  ).run(entry);
  addVersion(db, entry.id, "create", null, entry, actor, values.change_note || "Entry created.");
  return publicEntry(entry, access);
}

function updateEntry(db, slug, values, actor) {
  const access = accessForUser(db, actor);
  if (!access.canEdit) throw new Error("OKF edit permission is required.");
  const current = db.prepare("SELECT * FROM okf_entries WHERE slug = ? AND deleted_at IS NULL").get(normalizeSlug(slug));
  if (!canSeeEntry(access, current, { management: true })) throw new Error("OKF entry was not found.");
  const now = Date.now();
  const next = normalizeValues(values, {
    actor,
    existing: current,
    access,
    now
  });
  next.id = current.id;
  next.created_by = current.created_by;
  next.created_at = current.created_at;
  next.updated_by = String(actor.id);
  next.updated_at = now;
  next.reviewed_by = current.reviewed_by;
  next.reviewed_at = current.reviewed_at;
  next.published_by = current.published_by;
  next.published_at = current.published_at;
  next.archived_at = next.status === "archived" ? (current.archived_at || now) : null;
  next.deleted_at = null;

  db.prepare(
    "UPDATE okf_entries SET slug = @slug, title = @title, category = @category, tags_json = @tags_json, aliases_json = @aliases_json, summary = @summary, user_markdown = @user_markdown, moderator_markdown = @moderator_markdown, admin_markdown = @admin_markdown, ai_facts_markdown = @ai_facts_markdown, source_links_json = @source_links_json, visibility = @visibility, status = @status, review_state = @review_state, updated_by = @updated_by, updated_at = @updated_at, reviewed_by = @reviewed_by, published_by = @published_by, reviewed_at = @reviewed_at, published_at = @published_at, archived_at = @archived_at, deleted_at = @deleted_at WHERE id = @id"
  ).run(next);
  addVersion(db, next.id, "update", current, next, actor, values.change_note || "Entry updated.");
  return publicEntry(next, access);
}

function setEntryWorkflow(db, slug, action, actor, note = "") {
  const access = accessForUser(db, actor);
  const entry = db.prepare("SELECT * FROM okf_entries WHERE slug = ? AND deleted_at IS NULL").get(normalizeSlug(slug));
  if (!canSeeEntry(access, entry, { management: true })) throw new Error("OKF entry was not found.");
  const next = { ...entry };
  const now = Date.now();
  if (action === "review") {
    if (!access.canReview) throw new Error("OKF review permission is required.");
    next.review_state = "approved";
    next.reviewed_by = String(actor.id);
    next.reviewed_at = now;
  } else if (action === "publish") {
    if (!access.canImplement) throw new Error("OKF publish permission is required.");
    next.status = "published";
    next.review_state = "approved";
    next.published_by = String(actor.id);
    next.published_at = now;
    next.archived_at = null;
  } else if (action === "archive") {
    if (!access.canImplement) throw new Error("OKF archive permission is required.");
    next.status = "archived";
    next.archived_at = now;
  } else if (action === "restore") {
    if (!access.canImplement) throw new Error("OKF restore permission is required.");
    next.status = "draft";
    next.archived_at = null;
  } else if (action === "delete") {
    if (!access.canImplement) throw new Error("OKF delete permission is required.");
    next.deleted_at = now;
  } else {
    throw new Error("Unsupported OKF workflow action.");
  }
  next.updated_by = String(actor.id);
  next.updated_at = now;
  db.prepare(
    "UPDATE okf_entries SET status = @status, review_state = @review_state, reviewed_by = @reviewed_by, published_by = @published_by, updated_by = @updated_by, updated_at = @updated_at, reviewed_at = @reviewed_at, published_at = @published_at, archived_at = @archived_at, deleted_at = @deleted_at WHERE id = @id"
  ).run(next);
  addVersion(db, next.id, action, entry, next, actor, note || `Workflow action: ${action}.`);
  return publicEntry(next, access);
}

function listVersions(db, entryId, user) {
  const access = accessForUser(db, user);
  const entry = db.prepare("SELECT * FROM okf_entries WHERE id = ? AND deleted_at IS NULL").get(entryId);
  if (!canSeeEntry(access, entry, { management: true })) return [];
  return db
    .prepare("SELECT * FROM okf_versions WHERE entry_id = ? ORDER BY version_number DESC")
    .all(entryId)
    .map((row) => {
      const { previous_json, next_json, changed_by, ...metadata } = row;
      return {
        ...metadata,
        ...(access.capabilities.read_admin_fields ? { changed_by } : {}),
        previous: visibleSnapshot(parseJsonObject(previous_json), access),
        next: visibleSnapshot(parseJsonObject(next_json), access)
      };
    });
}

function restoreVersion(db, entryId, versionNumber, actor, note = "") {
  const access = accessForUser(db, actor);
  if (!access.canImplement) throw new Error("OKF restore permission is required.");
  const current = db.prepare("SELECT * FROM okf_entries WHERE id = ? AND deleted_at IS NULL").get(entryId);
  if (!canSeeEntry(access, current, { management: true })) throw new Error("OKF entry was not found.");
  const version = db
    .prepare("SELECT * FROM okf_versions WHERE entry_id = ? AND version_number = ?")
    .get(entryId, Number(versionNumber));
  const snapshot = parseJsonObject(version?.next_json);
  if (!version || !snapshot.title || !snapshot.slug) {
    throw new Error("OKF version snapshot is not restorable.");
  }
  const now = Date.now();
  const restored = {
    ...current,
    slug: normalizeSlug(snapshot.slug),
    title: cleanText(snapshot.title, 180),
    category: cleanText(snapshot.category, 120),
    tags_json: JSON.stringify(splitList(snapshot.tags || [])),
    aliases_json: JSON.stringify(splitLines(snapshot.aliases || [])),
    summary: cleanText(snapshot.summary, 800),
    user_markdown: cleanText(snapshot.user_markdown, 24000),
    moderator_markdown: access.capabilities.read_mod_fields ? cleanText(snapshot.moderator_markdown, 24000) : current.moderator_markdown,
    admin_markdown: access.capabilities.read_admin_fields ? cleanText(snapshot.admin_markdown, 24000) : current.admin_markdown,
    ai_facts_markdown: access.capabilities.read_admin_fields ? cleanText(snapshot.ai_facts_markdown, 24000) : current.ai_facts_markdown,
    source_links_json: access.capabilities.read_admin_fields ? JSON.stringify(cleanLinkList(snapshot.source_links || [])) : current.source_links_json,
    visibility: allowedVisibility(snapshot.visibility, access, current.visibility),
    status: normalizeChoice(snapshot.status, STATUS_VALUES, "draft"),
    review_state: normalizeChoice(snapshot.review_state, REVIEW_STATES, "draft"),
    updated_by: String(actor.id),
    updated_at: now,
    reviewed_by: snapshot.review_state === "approved" ? current.reviewed_by : null,
    published_by: snapshot.status === "published" ? current.published_by : null,
    reviewed_at: snapshot.review_state === "approved" ? current.reviewed_at : null,
    published_at: snapshot.status === "published" ? current.published_at : null,
    archived_at: snapshot.status === "archived" ? (current.archived_at || now) : null,
    deleted_at: null
  };
  db.prepare(
    "UPDATE okf_entries SET slug = @slug, title = @title, category = @category, tags_json = @tags_json, aliases_json = @aliases_json, summary = @summary, user_markdown = @user_markdown, moderator_markdown = @moderator_markdown, admin_markdown = @admin_markdown, ai_facts_markdown = @ai_facts_markdown, source_links_json = @source_links_json, visibility = @visibility, status = @status, review_state = @review_state, updated_by = @updated_by, updated_at = @updated_at, reviewed_by = @reviewed_by, published_by = @published_by, reviewed_at = @reviewed_at, published_at = @published_at, archived_at = @archived_at, deleted_at = @deleted_at WHERE id = @id"
  ).run(restored);
  addVersion(
    db,
    restored.id,
    "restore_version",
    current,
    restored,
    actor,
    note || `Restored version ${Number(versionNumber)}.`
  );
  return publicEntry(restored, access);
}

function grantPermission(db, values, actor) {
  if (!actor?.isAdmin) throw new Error("Only admins can grant OKF permissions.");
  const userId = String(values.user_id || "").trim();
  const level = String(values.level || "").trim();
  if (!userId) throw new Error("Select a user.");
  if (!PERMISSION_LEVELS.includes(level)) throw new Error("Select a valid OKF permission.");
  const now = Date.now();
  db.prepare("UPDATE okf_permissions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL").run(now, userId);
  db.prepare(
    "INSERT INTO okf_permissions (user_id, level, granted_by, notes, created_at, revoked_at) VALUES (?, ?, ?, ?, ?, NULL)"
  ).run(userId, level, String(actor.id), cleanText(values.notes, 500), now);
}

function revokePermission(db, id, actor) {
  if (!actor?.isAdmin) throw new Error("Only admins can revoke OKF permissions.");
  db.prepare("UPDATE okf_permissions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(Date.now(), Number(id));
}

function listPermissions(db, user) {
  if (!accessForUser(db, user).capabilities.manage_permissions) return [];
  return db
    .prepare(
      "SELECT okf_permissions.*, user_profiles.internal_username AS user_name, grantor.internal_username AS granted_by_name " +
        "FROM okf_permissions " +
        "LEFT JOIN user_profiles ON user_profiles.id = okf_permissions.user_id " +
        "LEFT JOIN user_profiles AS grantor ON grantor.id = okf_permissions.granted_by " +
        "ORDER BY okf_permissions.revoked_at IS NULL DESC, okf_permissions.created_at DESC"
    )
    .all();
}

function searchForAi(db, query, user, limit = 5) {
  const access = accessForUser(db, user);
  if (!access.capabilities.read_user_fields) return [];
  const queryText = cleanText(query, 4000);
  const queryTokens = aiSearchTokens(queryText);
  if (queryText && !queryTokens.size) return [];
  return listEntries(db, {}, user)
    .filter((entry) => entry.status === "published" && entry.review_state === "approved")
    .map((entry) => scoreAiEntry(entry, queryText, queryTokens, access))
    .filter((entry) => entry.matched || !queryTokens.size)
    .sort((left, right) =>
      right.source_metadata.score - left.source_metadata.score ||
      Number(right.updated_at || 0) - Number(left.updated_at || 0) ||
      left.title.localeCompare(right.title)
    )
    .slice(0, Math.max(1, Math.min(Number(limit) || 5, 25)))
    .map((entry) => ({
      id: entry.id,
      slug: entry.slug,
      title: entry.title,
      category: entry.category,
      visibility: entry.visibility,
      summary: entry.summary,
      facts: [
        entry.user_markdown,
        access.capabilities.read_mod_fields ? entry.moderator_markdown : "",
        access.capabilities.read_admin_fields ? entry.admin_markdown : "",
        access.capabilities.read_admin_fields ? entry.ai_facts_markdown : ""
      ]
        .filter(Boolean)
        .join("\n\n")
        .slice(0, 4000),
      source: `/plugins/okf/${entry.slug}`,
      source_metadata: entry.source_metadata
    }));
}

function scoreAiEntry(entry, queryText, queryTokens, access) {
  const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
  const tags = Array.isArray(entry.tags) ? entry.tags : [];
  const visibleFacts = [
    entry.summary,
    entry.user_markdown,
    access.capabilities.read_mod_fields ? entry.moderator_markdown : "",
    access.capabilities.read_admin_fields ? entry.admin_markdown : "",
    access.capabilities.read_admin_fields ? entry.ai_facts_markdown : ""
  ].filter(Boolean).join(" ");
  const titleTokens = aiSearchTokens(`${entry.title} ${tags.join(" ")}`);
  const aliasTokens = aiSearchTokens(aliases.join(" "));
  const bodyTokens = aiSearchTokens(`${entry.category} ${visibleFacts}`);
  const titleOverlap = intersectionSize(queryTokens, titleTokens);
  const aliasOverlap = intersectionSize(queryTokens, aliasTokens);
  const bodyOverlap = intersectionSize(queryTokens, bodyTokens);
  const titleFuzzyOverlap = fuzzyIntersectionSize(queryTokens, titleTokens);
  const aliasFuzzyOverlap = fuzzyIntersectionSize(queryTokens, aliasTokens);
  const bodyFuzzyOverlap = fuzzyIntersectionSize(queryTokens, bodyTokens);
  const totalOverlap = new Set([...titleTokens, ...aliasTokens, ...bodyTokens]);
  const exactCoverage = intersectionSize(queryTokens, totalOverlap);
  const fuzzyCoverage = fuzzyIntersectionSize(queryTokens, totalOverlap);
  const coverage = queryTokens.size ? (exactCoverage + (fuzzyCoverage * 0.7)) / queryTokens.size : 1;
  const normalizedQuery = normalizeSearchPhrase(queryText);
  const exactAlias = normalizedQuery && aliases.some((alias) => normalizeSearchPhrase(alias) === normalizedQuery);
  const exactTitle = normalizedQuery && normalizeSearchPhrase(entry.title) === normalizedQuery;
  const score = 300 +
    (titleOverlap * 90) +
    (aliasOverlap * 75) +
    (bodyOverlap * 35) +
    (titleFuzzyOverlap * 55) +
    (aliasFuzzyOverlap * 50) +
    (bodyFuzzyOverlap * 25) +
    Math.round(coverage * 120) +
    (exactAlias ? 240 : 0) +
    (exactTitle ? 180 : 0);
  return {
    ...entry,
    matched: titleOverlap + aliasOverlap + bodyOverlap + titleFuzzyOverlap + aliasFuzzyOverlap + bodyFuzzyOverlap > 0,
    source_metadata: {
      path: `/plugins/okf/${entry.slug}`,
      id: entry.id,
      heading: entry.title,
      score,
      excerpt: cleanText(entry.summary || entry.user_markdown, 360)
    }
  };
}

function aiSearchTokens(value) {
  const cleaned = cleanText(value, 4000);
  const expanded = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
  const tokens = new Set();
  for (const variant of [cleaned, expanded]) {
    const words = variant.toLowerCase().split(/[^a-z0-9]+/)
      .filter((token) => token.length >= 2 && !AI_SEARCH_STOPWORDS.has(token));
    words.forEach((token) => tokens.add(token));
    for (let index = 0; index < words.length - 1; index += 1) {
      const compact = `${words[index]}${words[index + 1]}`;
      if (compact.length <= 80) tokens.add(compact);
    }
  }
  return tokens;
}

function normalizeSearchPhrase(value) {
  return cleanText(value, 1000).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
}

function intersectionSize(left, right) {
  let count = 0;
  for (const value of left) {
    if (right.has(value)) count += 1;
  }
  return count;
}

function fuzzyIntersectionSize(queryTokens, candidateTokens) {
  let count = 0;
  for (const query of queryTokens) {
    if (candidateTokens.has(query) || query.length < 5) continue;
    if ([...candidateTokens].some((candidate) =>
      candidate.length >= 5 && Math.abs(candidate.length - query.length) <= 1 && editDistanceAtMostOne(query, candidate)
    )) count += 1;
  }
  return count;
}

function editDistanceAtMostOne(left, right) {
  if (left === right) return true;
  if (Math.abs(left.length - right.length) > 1) return false;
  let short = left;
  let long = right;
  if (short.length > long.length) [short, long] = [long, short];
  let edits = 0;
  for (let shortIndex = 0, longIndex = 0; shortIndex < short.length || longIndex < long.length;) {
    if (short[shortIndex] === long[longIndex]) {
      shortIndex += 1;
      longIndex += 1;
      continue;
    }
    edits += 1;
    if (edits > 1) return false;
    if (short.length === long.length) shortIndex += 1;
    longIndex += 1;
  }
  return true;
}

function matchesFilters(entry, filters, access, options) {
  if (filters.status && entry.status !== filters.status) return false;
  if (filters.category && entry.category !== filters.category) return false;
  if (filters.tag && !parseJsonArray(entry.tags_json).includes(filters.tag)) return false;
  const q = String(filters.q || "").trim().toLowerCase();
  if (!q) return true;
  const visibleText = [
    entry.title,
    entry.category,
    entry.summary,
    entry.user_markdown,
    entry.tags_json,
    entry.aliases_json
  ];
  if (access.capabilities.read_mod_fields) visibleText.push(entry.moderator_markdown);
  if (access.capabilities.read_admin_fields) visibleText.push(entry.admin_markdown, entry.ai_facts_markdown, entry.source_links_json);
  return visibleText.join(" ").toLowerCase().includes(q);
}

function normalizeValues(values, { actor, existing, access, now }) {
  const status = access.capabilities.implement
    ? normalizeChoice(values.status, STATUS_VALUES, existing?.status || "draft")
    : existing?.status || "draft";
  const reviewState = access.capabilities.implement
    ? normalizeChoice(values.review_state, REVIEW_STATES, existing?.review_state || "draft")
    : "review_pending";
  const publishedAt = status === "published" ? (existing?.published_at || now) : existing?.published_at || null;
  return {
    slug: normalizeSlug(values.slug || existing?.slug || values.title),
    title: cleanText(values.title || existing?.title, 180),
    category: cleanText(values.category ?? existing?.category, 120),
    tags_json: JSON.stringify(splitList(values.tags ?? parseJsonArray(existing?.tags_json))),
    aliases_json: JSON.stringify(splitLines(values.aliases ?? parseJsonArray(existing?.aliases_json))),
    summary: cleanText(values.summary ?? existing?.summary, 800),
    user_markdown: cleanText(values.user_markdown ?? existing?.user_markdown, 24000),
    moderator_markdown: access.capabilities.read_mod_fields ? cleanText(values.moderator_markdown ?? existing?.moderator_markdown, 24000) : existing?.moderator_markdown || "",
    admin_markdown: access.capabilities.read_admin_fields ? cleanText(values.admin_markdown ?? existing?.admin_markdown, 24000) : existing?.admin_markdown || "",
    ai_facts_markdown: access.capabilities.read_admin_fields ? cleanText(values.ai_facts_markdown ?? existing?.ai_facts_markdown, 24000) : existing?.ai_facts_markdown || "",
    source_links_json: access.capabilities.read_admin_fields ? JSON.stringify(cleanLinkList(values.source_links ?? parseJsonArray(existing?.source_links_json))) : existing?.source_links_json || "[]",
    visibility: allowedVisibility(values.visibility, access, existing?.visibility || "user"),
    status,
    review_state: reviewState,
    reviewed_by: existing?.reviewed_by || null,
    published_by: publishedAt && !existing?.published_by ? String(actor.id) : existing?.published_by || null,
    reviewed_at: existing?.reviewed_at || null,
    published_at: publishedAt,
    archived_at: existing?.archived_at || null,
    deleted_at: null
  };
}

function normalizeEntry(entry) {
  return {
    ...entry,
    tags: parseJsonArray(entry.tags_json),
    aliases: parseJsonArray(entry.aliases_json),
    source_links: parseJsonArray(entry.source_links_json)
  };
}

function addVersion(db, entryId, changeType, previous, next, actor, note) {
  const row = db.prepare("SELECT MAX(version_number) AS latest FROM okf_versions WHERE entry_id = ?").get(entryId);
  const version = Number(row?.latest || 0) + 1;
  db.prepare(
    "INSERT INTO okf_versions (entry_id, version_number, changed_by, change_type, previous_json, next_json, note, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
  ).run(
    entryId,
    version,
    actor?.id ? String(actor.id) : null,
    changeType,
    previous ? JSON.stringify(snapshotEntry(previous)) : null,
    next ? JSON.stringify(snapshotEntry(next)) : null,
    cleanText(note, 1000),
    Date.now()
  );
}

function snapshotEntry(entry) {
  return {
    slug: entry.slug,
    title: entry.title,
    category: entry.category,
    tags: parseJsonArray(entry.tags_json),
    aliases: parseJsonArray(entry.aliases_json),
    summary: entry.summary,
    user_markdown: entry.user_markdown,
    moderator_markdown: entry.moderator_markdown,
    admin_markdown: entry.admin_markdown,
    ai_facts_markdown: entry.ai_facts_markdown,
    source_links: parseJsonArray(entry.source_links_json),
    visibility: entry.visibility,
    status: entry.status,
    review_state: entry.review_state
  };
}

function normalizeSlug(value) {
  const slug = String(value || "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 120);
  if (!slug) throw new Error("OKF slug is required.");
  return slug;
}

function cleanText(value, maximum = 4000) {
  return String(value || "").replace(/\r\n?/g, "\n").trim().slice(0, maximum);
}

function splitList(value) {
  if (Array.isArray(value)) return value.map((item) => cleanText(item, 120)).filter(Boolean).slice(0, 50);
  return String(value || "")
    .split(/[,\n]/)
    .map((item) => cleanText(item, 120))
    .filter(Boolean)
    .slice(0, 50);
}

function splitLines(value) {
  if (Array.isArray(value)) return value.map((item) => cleanText(item, 240)).filter(Boolean).slice(0, 50);
  return String(value || "")
    .split(/\n+/)
    .map((item) => cleanText(item, 240))
    .filter(Boolean)
    .slice(0, 50);
}

function cleanLinkList(value) {
  return splitList(value)
    .filter((item) => /^(https?:\/\/|\/(?!\/)|#)/i.test(item))
    .slice(0, 50);
}

function normalizeChoice(value, allowed, fallback) {
  const normalized = String(value || "").trim();
  return allowed.includes(normalized) ? normalized : fallback;
}

function parseJsonArray(value) {
  if (Array.isArray(value)) return value;
  try {
    const parsed = JSON.parse(value || "[]");
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

function parseJsonObject(value) {
  try {
    const parsed = JSON.parse(value || "{}");
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
  } catch {
    return {};
  }
}

module.exports = {
  PERMISSION_LEVELS,
  REVIEW_STATES,
  STATUS_VALUES,
  VISIBILITY_VALUES,
  accessForUser,
  createEntry,
  ensureTables,
  getEditableEntry,
  getEntryBySlug,
  grantPermission,
  listEntries,
  listPermissions,
  listVersions,
  okfPermissionForUser,
  revokePermission,
  restoreVersion,
  searchForAi,
  setEntryWorkflow,
  updateEntry
};
