import type { PageAst, ViewNode, WrnDiagnostic } from "@wrnexus/syntax";
import { normalizeClientRuntime, normalizePackageAsset, validateStyleIds } from "./manifest.ts";
import type {
  ClientRuntimeDefinition,
  PackageAssetDefinition,
  PackageStyleDefinition,
  PackageRouteDefinition,
  PackageMigrationDefinition,
  PluginDevToolbarPanel,
  PluginContributions,
  PluginContext,
  PluginInput,
  PluginOrder,
  PluginPermission,
  PluginRunner,
  PluginDirective,
  PluginCliCommand,
  PluginVirtualModule,
  PluginDeploymentAdapter,
  PluginConfigSchema,
  TransformContext,
  WrnexusPlugin,
} from "./types.ts";

export * from "./types.ts";
export * from "./manifest.ts";
export { discoverPlugins, type DiscoverPluginOptions } from "./discovery.ts";
export { testPluginCompatibility } from "./compatibility.ts";
export type { PluginCompatibilityResult, PluginCompatibilityTarget } from "./compatibility.ts";

export { definePlugin, flattenPlugins } from "./base.ts";
import { flattenPlugins } from "./base.ts";

function rank(plugin: WrnexusPlugin): number {
  return plugin.enforce === "pre" ? 0 : plugin.enforce === "post" ? 2 : 1;
}

/** Resolve plugin order deterministically and reject duplicates/cycles. */
export function resolvePlugins(input: PluginInput): WrnexusPlugin[] {
  const plugins = flattenPlugins(input);
  const byName = new Map<string, WrnexusPlugin>();
  for (const plugin of plugins) {
    if (byName.has(plugin.name)) throw new Error(`WRN-PLUGIN-DUPLICATE: ${plugin.name}`);
    byName.set(plugin.name, plugin);
  }

  const edges = new Map<string, Set<string>>();
  for (const plugin of plugins) edges.set(plugin.name, new Set());
  for (const plugin of plugins) {
    for (const dependency of plugin.after ?? []) {
      if (byName.has(dependency)) edges.get(dependency)!.add(plugin.name);
    }
    for (const dependent of plugin.before ?? []) {
      if (byName.has(dependent)) edges.get(plugin.name)!.add(dependent);
    }
  }
  for (const left of plugins) {
    for (const right of plugins) {
      if (rank(left) < rank(right)) edges.get(left.name)!.add(right.name);
    }
  }

  const indegree = new Map(plugins.map((plugin) => [plugin.name, 0]));
  for (const targets of edges.values()) {
    for (const target of targets) indegree.set(target, (indegree.get(target) ?? 0) + 1);
  }
  const ready = plugins
    .filter((plugin) => indegree.get(plugin.name) === 0)
    .sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
  const resolved: WrnexusPlugin[] = [];
  while (ready.length) {
    const plugin = ready.shift()!;
    resolved.push(plugin);
    for (const target of edges.get(plugin.name) ?? []) {
      indegree.set(target, indegree.get(target)! - 1);
      if (indegree.get(target) === 0) {
        ready.push(byName.get(target)!);
        ready.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
      }
    }
  }
  if (resolved.length !== plugins.length) {
    const cyclic = plugins
      .filter((plugin) => !resolved.includes(plugin))
      .map((plugin) => plugin.name);
    throw new Error(`WRN-PLUGIN-CYCLE: ${cyclic.join(", ")}`);
  }
  return resolved;
}

async function resolveContribution<T>(
  value: T[] | ((context: PluginContext) => T[] | Promise<T[]>) | undefined,
  context: PluginContext,
): Promise<T[]> {
  if (!value) return [];
  return typeof value === "function"
    ? await (value as (context: PluginContext) => T[] | Promise<T[]>)(context)
    : value;
}

function styleRank(style: PackageStyleDefinition): number {
  return style.order === "pre" ? 0 : style.order === "post" ? 2 : 1;
}

function sortStyles(styles: readonly PackageStyleDefinition[]): PackageStyleDefinition[] {
  return styles
    .map((style, index) => ({ style, index }))
    .sort(
      (left, right) => styleRank(left.style) - styleRank(right.style) || left.index - right.index,
    )
    .map(({ style }) => style);
}

function assertUnique<T>(kind: string, entries: readonly T[], key: (entry: T) => string): void {
  const seen = new Set<string>();
  for (const entry of entries) {
    const value = key(entry);
    if (seen.has(value)) throw new Error(`WRN-PLUGIN-${kind}-DUPLICATE: ${value}`);
    seen.add(value);
  }
}

async function transformDirectives(
  nodes: ViewNode[],
  directives: readonly PluginDirective[],
  context: TransformContext,
): Promise<void> {
  const registry = new Map(directives.map((directive) => [directive.name, directive]));
  for (const node of nodes) {
    if (node.type === "element") {
      for (const attribute of node.attrs) {
        if (!attribute.name.startsWith("use:")) continue;
        const name = attribute.name.slice(4);
        const directive = registry.get(name);
        if (!directive) continue;
        const transformed = await directive.transform?.(attribute.value, context);
        if (transformed) {
          attribute.name = transformed.name;
          attribute.value = transformed.value;
        } else attribute.name = `data-wrn-directive-${name}`;
      }
      await transformDirectives(node.children, directives, context);
    } else if (node.type === "each") {
      await transformDirectives(node.body, directives, context);
      await transformDirectives(node.empty, directives, context);
    } else if (node.type === "if") {
      for (const branch of node.branches)
        await transformDirectives(branch.body, directives, context);
    }
  }
}

export function createPluginRunner(input: PluginInput, context: PluginContext): PluginRunner {
  const plugins = resolvePlugins(input);
  let contributionCache: PluginContributions | null = null;
  let setupComplete = false;
  const transformContext = (file: string): TransformContext => ({ ...context, file });

  const contributions = async (): Promise<PluginContributions> => {
    if (contributionCache) return contributionCache;
    const componentDirs: string[] = [];
    const clientRuntimes: ClientRuntimeDefinition[] = [];
    const assets: PackageAssetDefinition[] = [];
    const styles: PackageStyleDefinition[] = [];
    const routes: PackageRouteDefinition[] = [];
    const middleware: string[] = [];
    const migrations: PackageMigrationDefinition[] = [];
    const directives: PluginDirective[] = [];
    const cliCommands: PluginCliCommand[] = [];
    const virtualModules: PluginVirtualModule[] = [];
    const deploymentAdapters: PluginDeploymentAdapter[] = [];
    const configSchemas: PluginConfigSchema[] = [];
    const documentation: string[] = [];
    const typeDefinitions: string[] = [];

    for (const plugin of plugins) {
      componentDirs.push(...(await resolveContribution(plugin.componentDirs, context)));
      clientRuntimes.push(...(await resolveContribution(plugin.clientRuntimes, context)));
      assets.push(...(await resolveContribution(plugin.assets, context)));
      styles.push(...(await resolveContribution(plugin.styleSources, context)));
      routes.push(...(await resolveContribution(plugin.routeEntries, context)));
      middleware.push(...(await resolveContribution(plugin.middleware, context)));
      migrations.push(...(await resolveContribution(plugin.migrations, context)));
      directives.push(...(await resolveContribution(plugin.directives, context)));
      cliCommands.push(...(await resolveContribution(plugin.cliCommands, context)));
      virtualModules.push(...(await resolveContribution(plugin.virtualModules, context)));
      deploymentAdapters.push(...(await resolveContribution(plugin.deploymentAdapters, context)));
      configSchemas.push(...(await resolveContribution(plugin.configSchemas, context)));
      documentation.push(...(await resolveContribution(plugin.documentation, context)));
      typeDefinitions.push(...(await resolveContribution(plugin.typeDefinitions, context)));
    }

    const normalizedRuntimes = clientRuntimes.map(normalizeClientRuntime);
    const normalizedAssets = assets.map(normalizePackageAsset);
    validateStyleIds(styles);
    assertUnique("RUNTIME", normalizedRuntimes, (entry) => entry.id);
    assertUnique("RUNTIME-PATH", normalizedRuntimes, (entry) => entry.publicPath!);
    assertUnique("ASSET", normalizedAssets, (entry) => entry.id);
    assertUnique("ASSET-PATH", normalizedAssets, (entry) => entry.publicPath!);
    assertUnique(
      "PUBLIC-PATH",
      [...normalizedRuntimes, ...normalizedAssets],
      (entry) => entry.publicPath!,
    );
    for (const route of routes) {
      if (!route.path.startsWith("/") || route.path.includes("..")) {
        throw new Error(`WRN-PLUGIN-ROUTE-PATH: unsafe ${route.kind} route '${route.path}'.`);
      }
      if (!route.entry) throw new Error(`WRN-PLUGIN-ROUTE-ENTRY: '${route.path}' has no entry.`);
    }
    assertUnique("ROUTE", routes, (entry) => `${entry.kind}:${entry.path}`);
    for (const migration of migrations) {
      if (!migration.id.trim())
        throw new Error("WRN-PLUGIN-MIGRATION-ID: migration id is required.");
      if (!migration.entry && migration.source === undefined) {
        throw new Error(`WRN-PLUGIN-MIGRATION-SOURCE: ${migration.id} needs entry or source.`);
      }
    }
    assertUnique("MIGRATION", migrations, (entry) => `${entry.database ?? "default"}:${entry.id}`);
    assertUnique("DIRECTIVE", directives, (entry) => entry.name);
    assertUnique("CLI-COMMAND", cliCommands, (entry) => entry.name);
    assertUnique("VIRTUAL-MODULE", virtualModules, (entry) => entry.id);
    assertUnique("DEPLOYMENT-ADAPTER", deploymentAdapters, (entry) => entry.name);
    assertUnique("CONFIG-SCHEMA", configSchemas, (entry) => entry.namespace);

    contributionCache = {
      componentDirs: [...new Set(componentDirs)],
      clientRuntimes: normalizedRuntimes,
      assets: normalizedAssets,
      styles: sortStyles(styles),
      routes,
      middleware: [...new Set(middleware)],
      migrations,
      directives,
      cliCommands,
      virtualModules,
      deploymentAdapters,
      configSchemas,
      documentation: [...new Set(documentation)],
      typeDefinitions: [...new Set(typeDefinitions)],
    };
    context.metadata.set("@wrnexus/plugin:contributions", contributionCache);
    return contributionCache;
  };

  return {
    plugins,
    async configure(config) {
      if (!setupComplete) {
        for (const plugin of plugins) await plugin.setup?.(context);
        setupComplete = true;
      }
      for (const plugin of plugins) await plugin.configure?.(config, context);
      contributionCache = null;
    },
    async configResolved(config) {
      for (const plugin of plugins) await plugin.configResolved?.(config, context);
      contributionCache = null;
      const resolved = await contributions();
      for (const schema of resolved.configSchemas)
        await schema.validate(config[schema.namespace], context);
    },
    async transformAst(ast, file) {
      let current = ast;
      for (const plugin of plugins)
        current = (await plugin.transformAst?.(current, transformContext(file))) ?? current;
      await transformDirectives(
        current.view,
        (await contributions()).directives,
        transformContext(file),
      );
      return current;
    },
    async transformCode(code, file) {
      let current = code;
      for (const plugin of plugins)
        current = (await plugin.transformCode?.(current, transformContext(file))) ?? current;
      return current;
    },
    async diagnostics(ast, file) {
      const all: WrnDiagnostic[] = [];
      for (const plugin of plugins)
        all.push(...((await plugin.diagnostics?.(ast, transformContext(file))) ?? []));
      return all;
    },
    contributions,
    async devToolbarPanels() {
      const panels: PluginDevToolbarPanel[] = [];
      for (const plugin of plugins)
        panels.push(...((await plugin.devToolbarPanels?.(context)) ?? []));
      return panels;
    },
    async transformRoutes(routes) {
      let current = routes;
      for (const plugin of plugins) current = (await plugin.routes?.(current, context)) ?? current;
      return current;
    },
    async render(html) {
      let current = html;
      for (const plugin of plugins) current = (await plugin.render?.(current, context)) ?? current;
      return current;
    },
    async hook(name, value) {
      for (const plugin of plugins) {
        if (name === "buildStart") await plugin.buildStart?.(context);
        else if (name === "buildEnd") await plugin.buildEnd?.(value, context);
        else if (name === "configureServer") await plugin.configureServer?.(value, context);
        else if (name === "deploy") await plugin.deploy?.(value, context);
        else if (name === "shutdown") await plugin.shutdown?.(context);
        else await plugin.hmrUpdate?.((value as readonly string[]) ?? [], context);
      }
    },
  };
}

// Preserve direct type imports used by older applications.
export type {
  PageAst,
  WrnDiagnostic,
  PluginOrder,
  PluginContext,
  TransformContext,
  WrnexusPlugin,
  PluginPermission,
  PluginInput,
  PluginRunner,
};
