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 firstClass(attrs, excluded) {
  return attrs.classes.find((className) => !excluded.has(className));
}

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) {
  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 = interactiveFromCode(child) || envFromCode(child);
      if (replacement) {
        parent.children[index] = replacement;
        continue;
      }
    }

    transformTree(child);
  }
}

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