#!/usr/bin/env node

const fs = require("node:fs/promises");
const path = require("node:path");

const DEFAULT_URL = "http://127.0.0.1:3000/api/sidebar";
const DEFAULT_OUTPUT = "tmp/task180-mindmap-ghost-candidate-audit/result.json";

function stringOrNull(value) {
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  return trimmed || null;
}

function asArray(value) {
  return Array.isArray(value) ? value : [];
}

function unwrapSidebarPayload(payload) {
  let unwrapped = payload && typeof payload === "object" ? payload : {};
  if (unwrapped.data && typeof unwrapped.data === "object") {
    unwrapped = unwrapped.data;
  }
  const result = unwrapped.result && typeof unwrapped.result === "object" ? unwrapped.result : null;
  if (Array.isArray(result?.items)) {
    return {
      ...unwrapped,
      ...result,
      kernelFileTreeProjection: { items: result.items },
    };
  }
  if (Array.isArray(unwrapped.items)) {
    return {
      ...unwrapped,
      kernelFileTreeProjection: { items: unwrapped.items },
    };
  }
  return unwrapped;
}

function readAssetId(asset) {
  return stringOrNull(asset?.id) ?? stringOrNull(asset?.assetId) ?? stringOrNull(asset?.asset_id);
}

function readDocumentId(value) {
  return stringOrNull(value?.document_id) ?? stringOrNull(value?.documentId) ?? stringOrNull(value?.docId);
}

function isActiveAsset(asset) {
  return !stringOrNull(asset?.deleted_at) &&
    !stringOrNull(asset?.deletedAt) &&
    !stringOrNull(asset?.purged_at) &&
    !stringOrNull(asset?.purgedAt);
}

function isMindmapAsset(asset) {
  const type = stringOrNull(asset?.asset_type) ?? stringOrNull(asset?.assetType);
  return type === "mindmap" || Boolean(readAssetId(asset)?.startsWith("mindmap"));
}

function normalizeMindmapAsset(asset) {
  return {
    assetId: readAssetId(asset),
    documentId: readDocumentId(asset),
    deletedAt: stringOrNull(asset?.deleted_at) ?? stringOrNull(asset?.deletedAt),
    title: stringOrNull(asset?.file_name) ?? stringOrNull(asset?.fileName) ?? stringOrNull(asset?.title),
  };
}

function readProjectionItems(sidebar) {
  const projection = sidebar.kernelFileTreeProjection ?? sidebar.kernel_file_tree_projection ?? {};
  return asArray(projection.items);
}

function readFileTreeMindmapRow(item) {
  const resourceMeta = item?.resourceMeta ?? item?.resource_meta ?? {};
  const assetId =
    stringOrNull(resourceMeta.assetId) ??
    stringOrNull(resourceMeta.asset_id) ??
    stringOrNull(item?.assetId) ??
    stringOrNull(item?.asset_id);
  const documentId =
    stringOrNull(resourceMeta.documentId) ??
    stringOrNull(resourceMeta.document_id) ??
    readDocumentId(item);
  const resourceKind = stringOrNull(resourceMeta.resourceKind) ?? stringOrNull(resourceMeta.resource_kind);
  const rowKind = stringOrNull(item?.rowKind) ?? stringOrNull(item?.row_kind);
  const objectKind = stringOrNull(item?.objectKind) ?? stringOrNull(resourceMeta.objectKind);
  const isMindmap =
    resourceKind === "mindmap" ||
    objectKind === "mindmap" ||
    Boolean(assetId?.startsWith("mindmap"));
  if (!isMindmap) return null;
  return {
    rowId: stringOrNull(item?.rowId) ?? stringOrNull(item?.row_id),
    rowKind,
    documentId,
    assetId,
    title: stringOrNull(item?.title),
  };
}

function groupBy(items, keyFn) {
  const map = new Map();
  for (const item of items) {
    const key = keyFn(item);
    if (!key) continue;
    const bucket = map.get(key) ?? [];
    bucket.push(item);
    map.set(key, bucket);
  }
  return map;
}

function auditSidebarMindmapGhostCandidates(payload) {
  const sidebar = unwrapSidebarPayload(payload);
  const documents = asArray(sidebar.documents);
  const documentIds = new Set(documents.map((doc) => stringOrNull(doc?.id)).filter(Boolean));
  const rawMindmapAssets = [
    ...asArray(sidebar.mindmapAssets),
    ...asArray(sidebar.mindmap_assets),
  ];
  const activeMindmapAssets = rawMindmapAssets
    .filter((asset) => isMindmapAsset(asset) && isActiveAsset(asset))
    .map(normalizeMindmapAsset)
    .filter((asset) => asset.assetId && asset.documentId);

  const byDocument = groupBy(activeMindmapAssets, (asset) => asset.documentId);
  const multipleActiveMindmapsByDocument = Array.from(byDocument.entries())
    .filter(([, assets]) => assets.length > 1)
    .map(([documentId, assets]) => ({
      documentId,
      mindmapIds: assets.map((asset) => asset.assetId),
      count: assets.length,
    }));

  const byAssetId = groupBy(activeMindmapAssets, (asset) => asset.assetId);
  const duplicateActiveAssetIds = Array.from(byAssetId.entries())
    .filter(([, assets]) => assets.length > 1)
    .map(([assetId, assets]) => ({
      assetId,
      documentIds: Array.from(new Set(assets.map((asset) => asset.documentId).filter(Boolean))),
      count: assets.length,
    }));

  const activeAssetsWithMissingDocument = activeMindmapAssets
    .filter((asset) => asset.documentId && !documentIds.has(asset.documentId))
    .map((asset) => ({
      assetId: asset.assetId,
      documentId: asset.documentId,
    }));

  const fileTreeMindmapRows = readProjectionItems(sidebar)
    .map(readFileTreeMindmapRow)
    .filter(Boolean);
  const byFileTreeObject = groupBy(fileTreeMindmapRows, (row) => `${row.documentId ?? ""}:${row.assetId ?? ""}`);
  const duplicateFileTreeMindmapRows = Array.from(byFileTreeObject.entries())
    .filter(([key, rows]) => !key.startsWith(":") && rows.length > 1)
    .map(([, rows]) => ({
      documentId: rows[0].documentId,
      assetId: rows[0].assetId,
      rowIds: rows.map((row) => row.rowId).filter(Boolean),
      count: rows.length,
    }));

  const candidates = {
    multipleActiveMindmapsByDocument,
    duplicateActiveAssetIds,
    activeAssetsWithMissingDocument,
    duplicateFileTreeMindmapRows,
  };
  const candidateCount = Object.values(candidates).reduce((sum, items) => sum + items.length, 0);
  return {
    ok: true,
    readOnly: true,
    summary: {
      documents: documents.length,
      activeMindmapAssets: activeMindmapAssets.length,
      fileTreeMindmapRows: fileTreeMindmapRows.length,
      documentGroupsWithMultipleActiveMindmaps: multipleActiveMindmapsByDocument.length,
      duplicateActiveAssetIds: duplicateActiveAssetIds.length,
      activeAssetsWithMissingDocument: activeAssetsWithMissingDocument.length,
      duplicateFileTreeMindmapRows: duplicateFileTreeMindmapRows.length,
      candidateCount,
    },
    candidates,
  };
}

function parseArgs(argv) {
  const args = {
    input: null,
    url: DEFAULT_URL,
    output: DEFAULT_OUTPUT,
    cookie: process.env.MNOTE_AUTH_COOKIE || "",
    failOnCandidates: false,
    help: false,
  };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--input") args.input = argv[++index] || null;
    else if (arg === "--url") args.url = argv[++index] || DEFAULT_URL;
    else if (arg === "--output") args.output = argv[++index] || DEFAULT_OUTPUT;
    else if (arg === "--cookie") args.cookie = argv[++index] || "";
    else if (arg === "--fail-on-candidates") args.failOnCandidates = true;
    else if (arg === "--help" || arg === "-h") args.help = true;
  }
  return args;
}

function printHelp() {
  console.log([
    "Usage:",
    "  node scripts/task180-mindmap-ghost-candidate-audit.js --input sidebar.json",
    "  node scripts/task180-mindmap-ghost-candidate-audit.js --url http://127.0.0.1:3000/api/sidebar --cookie '<cookie>'",
    "  node scripts/task180-mindmap-ghost-candidate-audit.js --url 'http://127.0.0.1:3000/api/tree/projections/file?workspaceId=<workspaceId>'",
    "",
    "Notes:",
    "  This script is read-only. It only reports cleanup candidates and never deletes data.",
    "  Sidebar payloads can audit duplicate assets, orphan assets, and duplicate File Tree rows.",
    "  File projection payloads only include projection rows, so they can audit duplicate File Tree rows.",
  ].join("\n"));
}

async function loadPayload(args) {
  if (args.input) {
    return JSON.parse(await fs.readFile(args.input, "utf8"));
  }
  const headers = { accept: "application/json" };
  if (args.cookie) headers.cookie = args.cookie;
  const response = await fetch(args.url, { headers });
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`sidebar_audit_fetch_failed:${response.status}:${text.slice(0, 300)}`);
  }
  return JSON.parse(text);
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    printHelp();
    return;
  }
  const payload = await loadPayload(args);
  const result = auditSidebarMindmapGhostCandidates(payload);
  await fs.mkdir(path.dirname(args.output), { recursive: true });
  await fs.writeFile(args.output, `${JSON.stringify(result, null, 2)}\n`, "utf8");
  console.log(JSON.stringify(result.summary, null, 2));
  if (args.failOnCandidates && result.summary.candidateCount > 0) {
    process.exitCode = 2;
  }
}

if (require.main === module) {
  main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });
}

module.exports = {
  auditSidebarMindmapGhostCandidates,
};
