import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { parse as parseYaml } from "yaml";
import { fromMarkdown } from "mdast-util-from-markdown";

const ENV_TYPES = new Set([
  "theorem",
  "lemma",
  "proposition",
  "corollary",
  "definition",
  "example",
  "remark",
  "proof",
]);

const ENV_LABELS = {
  theorem: "Theorem",
  lemma: "Lemma",
  proposition: "Proposition",
  corollary: "Corollary",
  definition: "Definition",
  example: "Example",
  remark: "Remark",
  proof: "Proof",
};

function tokenizeInfo(info) {
  return (
    info.match(/[^\s=]+=(?:"[^"]*"|'[^']*'|[^\s]+)|"[^"]*"|'[^']*'|[^\s]+/g) ??
    []
  );
}

function stripQuotes(value) {
  if (!value) return "";
  const first = value[0];
  const last = value[value.length - 1];
  if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
    return value.slice(1, -1);
  }
  return value;
}

function parseFenceInfo(node) {
  const lang = node.lang ?? "";
  const meta = node.meta ?? "";
  const raw = `${lang} ${meta}`.trim();
  const attrs = {
    classes: [],
    id: "",
    values: {},
    raw,
  };

  if (!raw) return attrs;

  let body = raw;
  if (body.startsWith("{")) {
    body = body.slice(1);
    if (body.endsWith("}")) body = body.slice(0, -1);
  } else if (lang) {
    attrs.classes.push(lang);
    body = meta;
  }

  for (let token of tokenizeInfo(body.trim())) {
    if (token.endsWith("}") && !token.includes("=")) token = token.slice(0, -1);
    if (!token) continue;

    if (token.startsWith(".")) {
      attrs.classes.push(token.slice(1));
      continue;
    }

    if (token.startsWith("#")) {
      attrs.id = token.slice(1);
      continue;
    }

    const eq = token.indexOf("=");
    if (eq > 0) {
      const key = token.slice(0, eq);
      const value = stripQuotes(token.slice(eq + 1));
      if (key === "id") attrs.id = value;
      attrs.values[key] = value;
    }
  }

  return attrs;
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

function attribute(value) {
  return escapeHtml(value).replaceAll("\n", " ");
}

function encodedJson(value) {
  return encodeURIComponent(JSON.stringify(value));
}

function csv(value) {
  return String(value || "")
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
}

function keyValueList(value) {
  const entries = {};
  for (const item of csv(value)) {
    const separator = item.indexOf(":");
    if (separator <= 0) continue;
    const key = item.slice(0, separator).trim();
    const entryValue = item.slice(separator + 1).trim();
    if (key && entryValue) entries[key] = entryValue;
  }
  return entries;
}

function booleanAttr(value, defaultValue = true) {
  if (value === undefined || value === "") return defaultValue;
  return !["0", "false", "no", "off"].includes(
    String(value).trim().toLowerCase(),
  );
}

function labelize(value) {
  return String(value || "")
    .replace(/[-_]+/g, " ")
    .replace(/\b\w/g, (letter) => letter.toUpperCase());
}

function plainText(value) {
  return String(value ?? "")
    .replace(/<br\s*\/?>/gi, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
    .replace(/\s+/g, " ")
    .trim();
}

function sortText(value) {
  return plainText(value)
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z0-9]+/gi, " ")
    .trim()
    .toLowerCase();
}

function inlineMarkup(value) {
  return escapeHtml(value ?? "")
    .replace(/&lt;br\s*\/?&gt;/gi, "<br>")
    .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2">$1</a>');
}

function semanticError(message) {
  return {
    type: "html",
    value: `<aside class="semantic-error">${message}</aside>`,
  };
}

function firstClass(attrs, excluded) {
  return attrs.classes.find((className) => !excluded.has(className));
}

function sourcePathForFile(file) {
  return file?.path || file?.history?.[0] || "";
}

function resolveDataPath(src, file) {
  if (!src) return "";
  if (path.isAbsolute(src)) return src;

  const sourcePath = sourcePathForFile(file);
  const baseDir = sourcePath ? path.dirname(sourcePath) : process.cwd();
  return path.resolve(baseDir, src);
}

function readDataRows(src, file) {
  const resolvedPath = resolveDataPath(src, file);
  if (!resolvedPath || !existsSync(resolvedPath)) {
    throw new Error(`Data file not found: ${src}`);
  }

  const raw = readFileSync(resolvedPath, "utf8");
  const extension = path.extname(resolvedPath).toLowerCase();
  const parsed = extension === ".json" ? JSON.parse(raw) : parseYaml(raw);
  const rows = Array.isArray(parsed) ? parsed : parsed?.items;

  if (!Array.isArray(rows)) {
    throw new Error(
      `Data file must contain an array or an object with an items array: ${src}`,
    );
  }

  return rows.filter((row) => row && typeof row === "object");
}

const VALUE_LABELS = {
  MOC: "Map / index",
  book: "Book",
  course: "Course",
  essay: "Essay",
  other: "Other",
  software: "Software",
  youtube: "YouTube",
};

function displayValue(value) {
  return VALUE_LABELS[value] || value;
}

function numberLabel(row, field) {
  if (!field) return "";
  const value = row[field];
  if (value === undefined || value === null || value === "") return "";
  return String(value).padStart(2, "0");
}

function renderCell(row, column, options) {
  const raw = row[column] ?? "";
  const display = displayValue(raw);
  const numberPrefix =
    column === options.linkColumn ? numberLabel(row, options.numberField) : "";
  const sortValue = attribute(
    [numberPrefix, sortText(display)].filter(Boolean).join(" "),
  );
  const cellAttrs = `data-column="${attribute(column)}" data-sort-value="${sortValue}"`;

  if (column === options.linkColumn && row[options.urlField]) {
    const prefix = numberPrefix ? `${numberPrefix}. ` : "";
    return `<td ${cellAttrs}><a href="${attribute(row[options.urlField])}">${escapeHtml(prefix)}${inlineMarkup(display)}</a></td>`;
  }

  if (column === "type") {
    return `<td ${cellAttrs}><span class="data-table-badge">${escapeHtml(display)}</span></td>`;
  }

  if (
    column === "summary" ||
    String(display).length > 140 ||
    String(display).includes("<br")
  ) {
    return `<td ${cellAttrs}><div class="data-table-rich-cell">${inlineMarkup(display)}</div></td>`;
  }

  return `<td ${cellAttrs}>${inlineMarkup(display)}</td>`;
}

function dataTableScript(id) {
  return `<script>
(() => {
  const root = document.getElementById(${JSON.stringify(id)});
  if (!root || root.dataset.dataTableReady) return;
  root.dataset.dataTableReady = "true";

  const input = root.querySelector("[data-data-table-search]");
  const tbody = root.querySelector("tbody");
  const empty = root.querySelector("[data-data-table-empty]");
  const rows = Array.from(tbody?.querySelectorAll("tr") || []);
  const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });

  rows.forEach((row, index) => {
    row.dataset.dataTableIndex = String(index);
  });

  function applyFilter() {
    const query = (input?.value || "").trim().toLowerCase();
    let visible = 0;

    for (const row of rows) {
      const matches = !query || row.dataset.search.includes(query);
      row.hidden = !matches;
      if (matches) visible += 1;
    }

    if (empty) empty.hidden = visible !== 0;
  }

  input?.addEventListener("input", applyFilter);

  root.querySelectorAll("[data-data-table-sort]").forEach((button) => {
    button.addEventListener("click", () => {
      const column = button.dataset.dataTableSort;
      const previousColumn = root.dataset.dataTableSortColumn || "";
      const previousDirection = root.dataset.dataTableSortDirection || "";
      const direction =
        previousColumn === column && previousDirection === "ascending"
          ? "descending"
          : "ascending";

      root.querySelectorAll("[data-data-table-sort]").forEach((other) => {
        other.removeAttribute("aria-sort");
      });
      root.dataset.dataTableSortColumn = column;
      root.dataset.dataTableSortDirection = direction;
      button.setAttribute("aria-sort", direction);

      rows
        .slice()
        .sort((left, right) => {
          const leftValue = left.querySelector(\`[data-column="\${column}"]\`)?.dataset.sortValue || "";
          const rightValue = right.querySelector(\`[data-column="\${column}"]\`)?.dataset.sortValue || "";
          const result = collator.compare(leftValue, rightValue);
          if (result !== 0) return direction === "ascending" ? result : -result;
          return Number(left.dataset.dataTableIndex || 0) - Number(right.dataset.dataTableIndex || 0);
        })
        .forEach((row) => tbody.appendChild(row));

      applyFilter();
    });
  });
})();
</script>`;
}

function dataTableFromCode(node, file) {
  const attrs = parseFenceInfo(node);
  if (!attrs.classes.includes("data-table")) return null;

  const src = attrs.values.src || attrs.values.source || "";
  if (!src) {
    return semanticError('Data table block missing <code>src="..."</code>.');
  }

  let rows;
  try {
    rows = readDataRows(src, file);
  } catch (error) {
    return semanticError(escapeHtml(error.message));
  }

  const firstRow = rows[0] || {};
  const columns = csv(attrs.values.columns).length
    ? csv(attrs.values.columns)
    : Object.keys(firstRow).filter((key) => key !== "link");
  const headers = keyValueList(attrs.values.headers || attrs.values.labels);
  const linkColumn =
    attrs.values.link || (columns.includes("title") ? "title" : "");
  const urlField = attrs.values.url || attrs.values.href || "link";
  const numberField = attrs.values.number || "";
  const caption = attrs.values.caption || attrs.values.title || "";
  const searchable = booleanAttr(attrs.values.search, true);
  const sortable = booleanAttr(attrs.values.sort, true);
  const placeholder =
    attrs.values["search-placeholder"] || `Search ${caption || "table"}`;
  const id =
    attrs.id ||
    `data-table-${src.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "")}`;
  const tableId = `${id}-table`;
  const searchId = `${id}-search`;

  const headerHtml = columns
    .map((column) => {
      const label = headers[column] || labelize(column);
      const content = sortable
        ? `<button type="button" data-data-table-sort="${attribute(column)}">${escapeHtml(label)}</button>`
        : escapeHtml(label);
      return `<th scope="col">${content}</th>`;
    })
    .join("");

  const bodyHtml = rows
    .map((row) => {
      const searchText = columns
        .map((column) => plainText(displayValue(row[column] ?? "")))
        .join(" ")
        .toLowerCase();
      const cells = columns
        .map((column) =>
          renderCell(row, column, { linkColumn, urlField, numberField }),
        )
        .join("");
      return `<tr data-search="${attribute(searchText)}">${cells}</tr>`;
    })
    .join("");

  const searchHtml = searchable
    ? [
        '<div class="data-table-search">',
        `<label for="${attribute(searchId)}">Search</label>`,
        `<input id="${attribute(searchId)}" type="search" autocomplete="off" placeholder="${attribute(placeholder)}" data-data-table-search>`,
        "</div>",
      ].join("")
    : "";

  return {
    type: "html",
    value: [
      `<section class="semantic-data-table" id="${attribute(id)}" data-data-table-root>`,
      caption || searchable
        ? `<div class="data-table-toolbar">${caption ? `<h2>${escapeHtml(caption)}</h2>` : ""}${searchHtml}</div>`
        : "",
      '<div class="data-table-scroll">',
      `<table id="${attribute(tableId)}">`,
      caption ? `<caption>${escapeHtml(caption)}</caption>` : "",
      `<thead><tr>${headerHtml}</tr></thead>`,
      `<tbody>${bodyHtml}</tbody>`,
      "</table>",
      "</div>",
      `<p class="data-table-empty" data-data-table-empty hidden>No matching rows.</p>`,
      "</section>",
      dataTableScript(id),
    ].join(""),
  };
}

function interactiveFromCode(node) {
  const attrs = parseFenceInfo(node);
  if (!attrs.classes.includes("interactive")) return null;

  const declaredId =
    attrs.id || attrs.values.component || attrs.values.name || "";
  const componentId =
    attrs.values.component ||
    (declaredId.startsWith("int:")
      ? declaredId.slice("int:".length)
      : declaredId);
  const label =
    attrs.values.label ||
    (declaredId.startsWith("int:") ? declaredId : `int:${declaredId}`);
  const caption = attrs.values.caption || attrs.values.title || "";
  const description = node.value.trim();
  const fallback =
    attrs.values.pdf || attrs.values.fallback || description || caption;

  if (!componentId) {
    return {
      type: "html",
      value:
        '<aside class="semantic-error">Interactive block missing an id. Add <code>#int:name</code> or <code>id="name"</code>.</aside>',
    };
  }

  const props = {
    id: componentId,
    label,
    caption,
    description,
    fallback,
    attrs: attrs.values,
  };

  return {
    type: "html",
    value: [
      `<figure class="semantic-interactive" id="${attribute(label)}" data-interactive-label="${attribute(label)}">`,
      `<div class="interactive-mount" data-interactive-id="${attribute(componentId)}" data-interactive-props="${attribute(encodedJson(props))}">`,
      fallback
        ? `<p class="interactive-fallback">${escapeHtml(fallback)}</p>`
        : "",
      "</div>",
      caption ? `<figcaption>${escapeHtml(caption)}</figcaption>` : "",
      "</figure>",
    ].join(""),
  };
}

function envFromCode(node) {
  const attrs = parseFenceInfo(node);
  const shorthandType = firstClass(attrs, new Set(["env", "latex-env"]));
  const explicitType = attrs.values.type || shorthandType;
  const type = ENV_TYPES.has(explicitType) ? explicitType : "";
  const isEnv =
    attrs.classes.includes("env") ||
    attrs.classes.includes("latex-env") ||
    Boolean(type);

  if (!isEnv) return null;

  if (!type) {
    return {
      type: "html",
      value:
        '<aside class="semantic-error">Environment block missing a type. Add <code>.theorem</code>, <code>.definition</code>, or <code>type="theorem"</code>.</aside>',
    };
  }

  const title = attrs.values.title || attrs.values.name || "";
  const label = attrs.id || attrs.values.label || "";
  const renderMode =
    attrs.values.html || (type === "proof" ? "plain" : "callout");
  const heading = title ? `${ENV_LABELS[type]} (${title})` : ENV_LABELS[type];
  const parsed = fromMarkdown(node.value || "");
  const className = [
    "semantic-env",
    `semantic-env-${type}`,
    `semantic-env-${renderMode}`,
    renderMode === "callout" ? "callout" : "",
  ].filter(Boolean);

  return {
    type: "blockquote",
    data: {
      hName: "section",
      hProperties: {
        id: label || undefined,
        className,
        "data-env-type": type,
        "data-env-title": title || undefined,
        "data-env-render": renderMode,
      },
    },
    children: [
      {
        type: "paragraph",
        data: {
          hProperties: {
            className: ["semantic-env-heading"],
          },
        },
        children: [
          {
            type: "strong",
            children: [{ type: "text", value: heading }],
          },
        ],
      },
      ...parsed.children,
    ],
  };
}

function transformTree(parent, file) {
  if (!parent?.children) return;

  for (let index = 0; index < parent.children.length; index += 1) {
    const child = parent.children[index];
    if (child.type === "code") {
      const replacement =
        dataTableFromCode(child, file) ||
        interactiveFromCode(child) ||
        envFromCode(child);
      if (replacement) {
        parent.children[index] = replacement;
        continue;
      }
    }

    transformTree(child, file);
  }
}

export default function remarkSemanticBlocks() {
  return (tree, file) => transformTree(tree, file);
}
