import { readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { normalizePath } from "vite";
import type { Plugin, ResolvedConfig } from "vite";

interface Options {
  sourceDir: string;
  destination: string;
}

type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
type JsonObject = { [key: string]: JsonValue };

function flattenKeys(obj: JsonObject, prefix: string, result: Map<string, Set<string>>): void {
  for (const [key, value] of Object.entries(obj)) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    if (value !== null && typeof value === "object" && !Array.isArray(value)) {
      flattenKeys(value as JsonObject, fullKey, result);
    } else if (typeof value === "string") {
      if (!result.has(fullKey)) {
        result.set(fullKey, new Set());
      }
      result.get(fullKey)!.add(value);
    }
  }
}

function generateTypes(sourceDir: string, destination: string): void {
  const files = readdirSync(sourceDir).filter((f) => f.endsWith(".json"));

  const keyValues = new Map<string, Set<string>>();

  for (const file of files) {
    const content = JSON.parse(readFileSync(join(sourceDir, file), "utf-8")) as JsonObject;

    flattenKeys(content, "", keyValues);
  }

  const sortedKeys = Array.from(keyValues.keys()).sort();

  const lines: string[] = [
    "// Auto-generated by i18nextTypesPlugin — do not edit manually",
    "declare const resources: {",
  ];

  for (const key of sortedKeys) {
    const values = Array.from(keyValues.get(key)!);
    const union = values.map((v) => JSON.stringify(v)).join(" | ");
    lines.push(`  ${JSON.stringify(key)}: ${union};`);
  }

  lines.push("};");
  lines.push("export default resources;");
  lines.push("");

  writeFileSync(destination, lines.join("\n"), "utf-8");
}

export function i18nextTypesPlugin(options: Options): Plugin {
  let resolvedSourceDir: string;
  let resolvedDestination: string;

  return {
    name: "i18next-types",

    configResolved(config: ResolvedConfig) {
      resolvedSourceDir = normalizePath(resolve(config.root, options.sourceDir));
      resolvedDestination = normalizePath(resolve(config.root, options.destination));
    },

    buildStart() {
      generateTypes(resolvedSourceDir, resolvedDestination);
    },

    configureServer(server) {
      server.watcher.add(resolvedSourceDir);

      server.watcher.on("change", (file: string) => {
        if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
          generateTypes(resolvedSourceDir, resolvedDestination);
        }
      });

      server.watcher.on("add", (file: string) => {
        if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
          generateTypes(resolvedSourceDir, resolvedDestination);
        }
      });

      server.watcher.on("unlink", (file: string) => {
        if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) {
          generateTypes(resolvedSourceDir, resolvedDestination);
        }
      });
    },
  };
}
