import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import prettier from "prettier";

const uiRoot = join(import.meta.dirname, "..", "packages", "ui");
const componentsDir = join(uiRoot, "components");
const catalogPath = join(uiRoot, "component-catalog.json");
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
const metadata = new Map(catalog.components.map((entry) => [entry.name, entry]));

function normalizeNewlines(source) {
  return source.replace(/\r\n?/g, "\n");
}

async function writeFormatted(path, source, parser) {
  const formatted = parser
    ? await prettier.format(source, { parser, printWidth: 100, endOfLine: "lf" })
    : source;
  const current = readFileSync(path, "utf8");

  // Do not rewrite a generated file when only its working-tree line endings
  // differ. This keeps release verification clean on Windows CRLF checkouts
  // while still rewriting files whose generated content is genuinely stale.
  if (normalizeNewlines(current) === normalizeNewlines(formatted)) {
    return false;
  }

  writeFileSync(path, formatted);
  return true;
}

function block(source, keyword) {
  const start = source.indexOf(`${keyword} {`);
  if (start < 0) return "";
  const open = source.indexOf("{", start);
  let depth = 0;
  for (let index = open; index < source.length; index++) {
    if (source[index] === "{") depth++;
    if (source[index] === "}" && --depth === 0) return source.slice(open + 1, index);
  }
  return "";
}

function propsOf(source) {
  const props = [];
  for (const line of block(source, "props").split(/\r?\n/)) {
    const match = line
      .trim()
      .match(/^([A-Za-z][A-Za-z0-9_]*)(\?)?(?:\s*:\s*([^=]+?))?\s*(?:=\s*(.+))?$/);
    if (!match) continue;
    const [, name, optional, annotation, rawDefault] = match;
    const value = rawDefault?.trim();
    const type =
      annotation?.trim() ||
      (value === "true" || value === "false"
        ? "boolean"
        : /^-?\d+(?:\.\d+)?$/.test(value ?? "")
          ? "number"
          : "string");
    const options = [...type.matchAll(/["']([^"']+)["']/g)].map((entry) => entry[1]);
    props.push({
      name,
      type,
      required: optional !== "?" && value === undefined,
      default: value ?? null,
      options,
    });
  }
  return props;
}

function outputsOf(source) {
  const body = block(source, "outputs");
  const outputs = [];
  let index = 0;
  while (index < body.length) {
    while (index < body.length && /\s/.test(body[index])) index++;
    if (index >= body.length) break;
    const nameMatch = /^[A-Za-z][A-Za-z0-9_]*/.exec(body.slice(index));
    if (!nameMatch) {
      index++;
      continue;
    }
    const name = nameMatch[0];
    index += name.length;
    while (index < body.length && /\s/.test(body[index])) index++;
    if (body[index] !== "(") {
      index++;
      continue;
    }
    const start = ++index;
    let depth = 1;
    let quote = null;
    for (; index < body.length && depth > 0; index++) {
      const char = body[index];
      if (quote) {
        if (char === "\\") index++;
        else if (char === quote) quote = null;
        continue;
      }
      if (char === '"' || char === "'" || char === "`") quote = char;
      else if (char === "(") depth++;
      else if (char === ")") depth--;
    }
    const parameters = body.slice(start, index - 1).trim();
    const payloadMatch = /^payload\??\s*:\s*([\s\S]+)$/.exec(parameters);
    outputs.push({ name, payloadType: payloadMatch?.[1]?.trim() ?? null });
  }
  return outputs;
}

function words(name) {
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
}

function compareText(left, right) {
  return left < right ? -1 : left > right ? 1 : 0;
}

const sourceEntries = readdirSync(componentsDir)
  .filter((file) => file.endsWith(".wrn"))
  .map((file) => {
    const source = readFileSync(join(componentsDir, file), "utf8");
    const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(source);
    if (!declaration) {
      throw new Error(`UI component source does not declare a component: ${file}`);
    }
    return { file, source, name: declaration[1] };
  });

// Windows checkouts can temporarily contain legacy lowercase files beside the
// canonical component source. Generate one public contract per declaration and
// prefer the exact <ComponentName>.wrn filename when both exist.
const sourceByName = new Map();
for (const candidate of sourceEntries) {
  const current = sourceByName.get(candidate.name);
  const candidateCanonical = candidate.file === `${candidate.name}.wrn`;
  const currentCanonical = current?.file === `${candidate.name}.wrn`;
  if (!current || (candidateCanonical && !currentCanonical)) {
    sourceByName.set(candidate.name, candidate);
  }
}

const components = [...sourceByName.values()]
  .map(({ file, source, name }) => {
    const entry = metadata.get(name);
    const slots = [...source.matchAll(/<slot(?:\s+name="([^"]+)")?/g)].map(
      (match) => match[1] ?? "default",
    );
    const outputs = outputsOf(source);
    const legacyEvents = [
      ...new Set(
        [...source.matchAll(/@event\s+([A-Za-z][A-Za-z0-9_]*)\s*=\s*function/g)].map(
          (match) => match[1],
        ),
      ),
    ];
    for (const name of legacyEvents) {
      if (!outputs.some((output) => output.name === name)) {
        outputs.push({ name, payloadType: "unknown" });
      }
    }
    return {
      name,
      mount: name,
      category: entry?.category ?? "core",
      purpose: entry?.purpose ?? `Reusable ${words(name)} component.`,
      props: propsOf(source),
      slots: [...new Set(slots)],
      outputs,
      events: outputs.map((output) => output.name),
      source: `components/${file}`,
    };
  })
  .sort((left, right) => compareText(left.name, right.name));

const catalogComponents = components.map((component) => ({
  name: component.name,
  category: component.category,
  purpose: component.purpose,
}));
await writeFormatted(
  catalogPath,
  JSON.stringify({ ...catalog, components: catalogComponents }, null, 2) + "\n",
  "json",
);

const referencePath = join(uiRoot, "component-reference.json");
await writeFormatted(
  referencePath,
  JSON.stringify(
    { generatedFrom: "packages/ui/components/*.wrn", count: components.length, components },
    null,
    2,
  ) + "\n",
  "json",
);

const lines = [
  "# WRNexus UI component reference",
  "",
  `This reference is generated from the ${components.length} packaged \`.wrn\` component sources. Props marked required have no default; all others show their runtime default.`,
  "",
];
for (const category of [...new Set(components.map((component) => component.category))].sort(
  compareText,
)) {
  lines.push(`## ${category[0].toUpperCase()}${category.slice(1)}`, "");
  for (const component of components.filter((item) => item.category === category)) {
    const props = component.props.length
      ? component.props
          .map(
            (prop) =>
              `\`${prop.name}: ${prop.type}${prop.required ? " (required)" : ` = ${prop.default}`}\``,
          )
          .join(", ")
      : "None";
    lines.push(
      `### ${component.name}`,
      "",
      component.purpose,
      "",
      `- Mount: \`data-component="${component.mount}"\``,
      `- Props: ${props}`,
      `- Slots: ${component.slots.length ? component.slots.map((slot) => `\`${slot}\``).join(", ") : "None"}`,
      `- Outputs: ${component.outputs.length ? component.outputs.map((output) => `\`${output.name}(${output.payloadType ?? ""})\``).join(", ") : "None"}`,
      "",
    );
  }
}
const componentsPath = join(uiRoot, "COMPONENTS.md");
await writeFormatted(componentsPath, `${lines.join("\n").trimEnd()}\n`, "markdown");
