import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { basename, extname, join, relative, resolve } from "node:path";
import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
import { checkWrnFile, type WrnTypeDiagnostic } from "@wrnexus/typecheck";
import { parse } from "@wrnexus/syntax";
import { generate, generateTargets } from "@wrnexus/compiler";
import { regenerateRoutes } from "./routes.ts";
import { loadAppConfig } from "@wrnexus/styles";
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";

function files(root: string, predicate: (path: string) => boolean): string[] {
  if (!existsSync(root)) return [];
  const output: string[] = [];
  for (const entry of readdirSync(root, { withFileTypes: true })) {
    const path = join(root, entry.name);
    if (entry.isDirectory()) output.push(...files(path, predicate));
    else if (predicate(path)) output.push(path);
  }
  return output.sort();
}

function literalUnion(values: Iterable<string>): string {
  const unique = [...new Set(values)].sort();
  return unique.length ? unique.map((value) => JSON.stringify(value)).join(" | ") : "never";
}

function generatedContractMap(name: string, members: string): string {
  return members.trim()
    ? `interface ${name} {\n${members}\n  }`
    : `type ${name} = Record<string, never>;`;
}

function typeImport(fromDirectory: string, file: string): string {
  const path = relative(fromDirectory, file).replace(/\\/g, "/");
  return JSON.stringify(path.startsWith(".") ? path : `./${path}`);
}

function exportedHandlers(source: string): string[] {
  return [
    ...new Set(
      [
        ...source.matchAll(
          /\bexport\s+(?:const|async\s+function|function)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g,
        ),
      ].map((match) => match[1]!),
    ),
  ];
}

function flatten(value: unknown, prefix = ""): string[] {
  if (!value || typeof value !== "object" || Array.isArray(value)) return prefix ? [prefix] : [];
  return Object.entries(value).flatMap(([key, child]) =>
    flatten(child, prefix ? `${prefix}.${key}` : key),
  );
}

function environmentKeys(root: string): string[] {
  return files(root, (path) => /^\.env(?:\.[\w-]+)?(?:\.example)?$/.test(basename(path))).flatMap(
    (path) =>
      readFileSync(path, "utf8")
        .split(/\r?\n/)
        .map((line) => line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1])
        .filter((key): key is string => Boolean(key)),
  );
}

export interface GeneratedApplicationTypes {
  file: string;
  routes: number;
  components: number;
}

function writePluginArtifacts(root: string, contributions?: PluginContributions): void {
  if (!contributions) return;
  const typeDir = join(root, "app", "types");
  mkdirSync(typeDir, { recursive: true });
  const references = contributions.typeDefinitions.map((entry) => {
    const target = resolve(root, entry);
    const specifier = relative(typeDir, target).replace(/\\/g, "/");
    return `/// <reference path=${JSON.stringify(specifier.startsWith(".") ? specifier : `./${specifier}`)} />`;
  });
  writeFileSync(
    join(typeDir, "wrnexus.plugins.generated.d.ts"),
    `// AUTO-GENERATED plugin type aggregation - do not edit.\n${references.length > 0 ? `${references.join("\n")}\n` : ""}`,
    "utf8",
  );
  const docsDir = join(root, ".wrnexus", "documentation");
  mkdirSync(docsDir, { recursive: true });
  writeFileSync(
    join(docsDir, "plugins.md"),
    `# Plugin documentation\n\n${contributions.documentation
      .map(
        (entry) => `- [${entry}](${relative(docsDir, resolve(root, entry)).replace(/\\/g, "/")})`,
      )
      .join("\n")}\n`,
    "utf8",
  );
}

export function generateApplicationTypes(
  appRoot: string,
  pluginContributions?: PluginContributions,
): GeneratedApplicationTypes {
  const root = resolve(appRoot);
  const app = join(root, "app");
  const router = buildRouter(app);
  regenerateRoutes(app);
  const componentFiles = files(join(app, "components"), (path) => extname(path) === ".wrn");
  const components = componentFiles.map((path) => parse(readFileSync(path, "utf8")));
  const localeKeys = files(join(app, "locales"), (path) => extname(path) === ".json").flatMap(
    (path) => {
      try {
        return flatten(JSON.parse(readFileSync(path, "utf8")));
      } catch {
        return [];
      }
    },
  );
  const source = files(app, (path) => /\.(?:ts|tsx|js|wrn)$/.test(path))
    .map((path) => readFileSync(path, "utf8"))
    .join("\n");
  const cacheKeys = [
    ...source.matchAll(/\b(?:cache|invalidate(?:Tag)?)\s*\(\s*["'`]([^"'`]+)["'`]/g),
  ].map((match) => match[1]);
  const queueNames = files(join(app, "queues"), (path) => /\.(?:ts|js)$/.test(path)).map((path) =>
    basename(path, extname(path)),
  );
  const manifest = createRouteManifest(nameRoutes(router.pages));
  const componentMap = components
    .map((component) => {
      const props = component.props
        .map(
          (prop) =>
            `${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"}`,
        )
        .join("; ");
      const outputs = component.outputs
        .map(
          (output) =>
            `${JSON.stringify(output.name)}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`,
        )
        .join("; ");
      return `    ${JSON.stringify(component.name)}: { props: ${props ? `{ ${props} }` : "Record<string, never>"}; outputs: ${outputs ? `{ ${outputs} }` : "Record<string, never>"} };`;
    })
    .join("\n");
  const typeDir = join(app, "types");
  const apiContracts = router.api
    .map((route) => {
      const source = readFileSync(route.file, "utf8");
      const methods = exportedHandlers(source);
      const module = `typeof import(${typeImport(typeDir, route.file)})`;
      const entries = methods
        .map((method) => `${method}: ApiContract<${module}[${JSON.stringify(method)}]>`)
        .join("; ");
      return `    ${JSON.stringify(route.raw)}: { ${entries || `default: ApiContract<${module}["default"]>`} };`;
    })
    .join("\n");
  const middlewareContracts = router.middlewareFiles
    .map((file) => {
      const name = basename(file, extname(file));
      return `    ${JSON.stringify(name)}: MiddlewareContext<(typeof import(${typeImport(typeDir, file)}))["default"]>;`;
    })
    .join("\n");
  const databaseContracts = files(join(app, "db"), (path) => /queries\.gen\.ts$/.test(path))
    .flatMap((file) => {
      const module = `typeof import(${typeImport(typeDir, file)})`;
      return [
        ...readFileSync(file, "utf8").matchAll(/\bexport\s+async\s+function\s+([A-Za-z_$][\w$]*)/g),
      ].map(
        (match) =>
          `    ${JSON.stringify(match[1]!)}: QueryContract<${module}[${JSON.stringify(match[1]!)}]>;`,
      );
    })
    .join("\n");
  const realtimeContracts = router.realtime
    .map(
      (route) =>
        `    ${JSON.stringify(route.raw)}: RealtimeMessage<(typeof import(${typeImport(typeDir, route.file)}))["default"]>;`,
    )
    .join("\n");
  const queueContracts = files(join(app, "queues"), (path) => /\.(?:ts|js)$/.test(path))
    .map((file) => {
      const name = basename(file, extname(file));
      return `    ${JSON.stringify(name)}: QueuePayload<(typeof import(${typeImport(typeDir, file)}))["default"]>;`;
    })
    .join("\n");
  const configFile = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]
    .map((name) => join(root, name))
    .find(existsSync);
  const code = `// AUTO-GENERATED by \`wrnexus generate types\` - do not edit.
declare namespace WRNexusGenerated {
  type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
    ? { input: I; output: O }
    : T extends (...args: infer A) => infer R
      ? { input: A extends [any, infer I, ...any[]] ? I : unknown; output: Awaited<R> }
      : { input: unknown; output: unknown };
  type MiddlewareContext<T> = T extends (ctx: infer C, ...args: any[]) => any ? C : never;
  type QueryContract<T> = T extends (db: any, args: infer A, ...rest: any[]) => infer R
    ? { args: A; result: Awaited<R> }
    : T extends (db: any, ...rest: any[]) => infer R
      ? { args: Record<string, never>; result: Awaited<R> }
      : never;
  type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
  type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
  type RouteName = ${literalUnion(manifest.map((route) => route.name))};
  type ApiRoute = ${literalUnion(router.api.map((route) => route.raw))};
  type RealtimeRoute = ${literalUnion(router.realtime.map((route) => route.raw))};
  type EnvironmentKey = ${literalUnion(environmentKeys(root))};
  type TranslationKey = ${literalUnion(localeKeys)};
  type QueueName = ${literalUnion(queueNames)};
  type CacheKey = ${literalUnion(cacheKeys)};
  ${generatedContractMap("Components", componentMap)}
  ${generatedContractMap("ApiContracts", apiContracts)}
  ${generatedContractMap("MiddlewareContexts", middlewareContracts)}
  ${generatedContractMap("DatabaseQueries", databaseContracts)}
  ${generatedContractMap("RealtimeMessages", realtimeContracts)}
  ${generatedContractMap("QueuePayloads", queueContracts)}
  type ApplicationConfig = ${configFile ? `(typeof import(${typeImport(typeDir, configFile)}))["default"]` : "Record<string, never>"};
}
`;
  mkdirSync(typeDir, { recursive: true });
  const output = join(typeDir, "wrnexus.generated.d.ts");
  writeFileSync(output, code, "utf8");
  writePluginArtifacts(root, pluginContributions);
  return {
    file: relative(root, output).replace(/\\/g, "/"),
    routes: manifest.length,
    components: components.length,
  };
}

/** Discover configured plugins and include their type/docs contributions in generated artifacts. */
export async function generateApplicationTypesWithPlugins(
  appRoot: string,
): Promise<GeneratedApplicationTypes> {
  const root = resolve(appRoot);
  const config = await loadAppConfig(root);
  const input = await discoverPlugins(root, config.plugins, {
    includeDevDependencies: true,
    strict: true,
  });
  const runner = createPluginRunner(input, {
    root,
    mode: "development",
    command: "cli",
    profile: process.env.WRNEXUS_PROFILE,
    metadata: new Map(),
    warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
  });
  await runner.configure(config as Record<string, unknown>);
  await runner.configResolved(config as Readonly<Record<string, unknown>>);
  return generateApplicationTypes(root, await runner.contributions());
}

function codegenDiagnostics(file: string): WrnTypeDiagnostic[] {
  try {
    const ast = parse(readFileSync(file, "utf8"));
    const main = generate(ast);
    const targets = generateTargets(ast);
    const bun = (
      globalThis as unknown as {
        Bun?: {
          Transpiler?: new (options: { loader: string }) => {
            transformSync(code: string): unknown;
          };
        };
      }
    ).Bun;
    if (bun?.Transpiler) {
      const transpiler = new bun.Transpiler({ loader: "ts" });
      transpiler.transformSync(main);
      transpiler.transformSync(targets.browser);
      transpiler.transformSync(targets.server);
    }
    return [];
  } catch (error) {
    return [
      {
        code: "WRN-CODEGEN",
        category: "error",
        message: error instanceof Error ? error.message : String(error),
        file,
        line: 1,
        column: 1,
        length: 1,
        hint: "Fix the reported SSR/client code generation issue before starting or building the app.",
      },
    ];
  }
}

export function checkApplication(appRoot: string): WrnTypeDiagnostic[] {
  const root = resolve(appRoot);
  return files(join(root, "app"), (path) => extname(path) === ".wrn").flatMap((file) => {
    const diagnostics = checkWrnFile(file, { appRoot: root });
    return diagnostics.some((item) => item.category === "error")
      ? diagnostics
      : [...diagnostics, ...codegenDiagnostics(file)];
  });
}

export async function runTypecheck(appRoot: string): Promise<boolean> {
  const root = resolve(appRoot);
  await generateApplicationTypesWithPlugins(root);
  const diagnostics = checkApplication(root);
  for (const item of diagnostics)
    console.error(`${item.file}:${item.line}:${item.column} ${item.code} ${item.message}`);
  let tsOk = true;
  if (existsSync(join(root, "tsconfig.json"))) {
    const process = Bun.spawnSync(["bunx", "tsc", "--noEmit"], {
      cwd: root,
      stdout: "inherit",
      stderr: "inherit",
    });
    tsOk = process.exitCode === 0;
  }
  return diagnostics.every((item) => item.category !== "error") && tsOk;
}
