import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import type {
  PackagePluginManifest,
  PluginInput,
  WrnexusPackageManifest,
  WrnexusPlugin,
  ClientRuntimeDefinition,
  PackageAssetDefinition,
  PackageStyleDefinition,
  PackageRouteDefinition,
  PackageMigrationDefinition,
  PluginPermission,
} from "./types.ts";
import { definePlugin, flattenPlugins } from "./base.ts";
import { testPluginCompatibility } from "./compatibility.ts";

interface PackageJson {
  name?: string;
  version?: string;
  dependencies?: Record<string, string>;
  optionalDependencies?: Record<string, string>;
  devDependencies?: Record<string, string>;
  peerDependencies?: Record<string, string>;
  workspaces?: string[] | { packages?: string[] };
  wrnexus?: WrnexusPackageManifest;
}

export interface DiscoverPluginOptions {
  includeDevDependencies?: boolean;
  /** Fail package discovery immediately instead of degrading to a warning. */
  strict?: boolean;
  includePeerDependencies?: boolean;
  warn?: (message: string) => void;
  runtime?: string;
  capabilities?: readonly string[];
  runtimeVersion?: string;
  os?: string;
  /** When true, every requested capability must be declared and granted by the application. */
  enforcePermissions?: boolean;
  grantedPermissions?: Readonly<Record<string, readonly PluginPermission[]>>;
}

function manifestPermissions(manifest: WrnexusPackageManifest): PluginPermission[] {
  const permissions: PluginPermission[] = [];
  if (manifest.components?.length) permissions.push("components");
  if (manifest.clientRuntimes?.length) permissions.push("client-runtime");
  if (manifest.assets?.length) permissions.push("assets");
  if (manifest.styles?.length) permissions.push("styles");
  if (manifest.routes?.length) permissions.push("routes");
  if (manifest.middleware?.length) permissions.push("middleware");
  if (manifest.migrations?.length) permissions.push("migrations");
  return permissions;
}

function modulePermissions(plugin: WrnexusPlugin): PluginPermission[] {
  const permissions: PluginPermission[] = [];
  if (plugin.componentDirs) permissions.push("components");
  if (plugin.clientRuntimes) permissions.push("client-runtime");
  if (plugin.assets) permissions.push("assets");
  if (plugin.styleSources) permissions.push("styles");
  if (plugin.routeEntries || plugin.routes) permissions.push("routes");
  if (plugin.middleware) permissions.push("middleware");
  if (plugin.migrations) permissions.push("migrations");
  if (plugin.configure || plugin.configResolved) permissions.push("config");
  if (plugin.transformAst || plugin.transformCode) permissions.push("transform");
  if (plugin.diagnostics || plugin.devToolbarPanels) permissions.push("diagnostics");
  if (plugin.configureServer || plugin.buildStart || plugin.buildEnd) permissions.push("server");
  if (plugin.cliCommands) permissions.push("cli");
  if (plugin.directives) permissions.push("directives");
  if (plugin.virtualModules) permissions.push("virtual-modules");
  if (plugin.deploymentAdapters || plugin.deploy) permissions.push("deployment");
  if (plugin.documentation) permissions.push("documentation");
  if (plugin.typeDefinitions) permissions.push("types");
  return [...new Set(permissions)];
}

function enforcePermissions(
  packageName: string,
  manifest: WrnexusPackageManifest,
  required: readonly PluginPermission[],
  options: DiscoverPluginOptions,
): void {
  if (!options.enforcePermissions) return;
  const declared = new Set(manifest.permissions ?? []);
  const undeclared = required.filter((permission) => !declared.has(permission));
  if (undeclared.length) {
    throw new Error(
      `WRN-PLUGIN-PERMISSION-UNDECLARED: ${packageName} uses ${undeclared.join(", ")} without declaring them.`,
    );
  }
  const grants = new Set(options.grantedPermissions?.[packageName] ?? []);
  const denied = required.filter((permission) => !grants.has(permission));
  if (denied.length) {
    throw new Error(
      `WRN-PLUGIN-PERMISSION-DENIED: ${packageName} is not granted ${denied.join(", ")}.`,
    );
  }
}

function readJson(path: string): PackageJson | null {
  try {
    return JSON.parse(readFileSync(path, "utf8")) as PackageJson;
  } catch {
    return null;
  }
}

function nearestWorkspaceRoot(start: string): string | null {
  let current = resolve(start);
  while (true) {
    const packageJson = readJson(join(current, "package.json"));
    if (packageJson?.workspaces) return current;
    const parent = dirname(current);
    if (parent === current) return null;
    current = parent;
  }
}

function workspacePatterns(pkg: PackageJson): string[] {
  if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
  return pkg.workspaces?.packages ?? [];
}

function expandSimpleWorkspacePattern(root: string, pattern: string): string[] {
  const normalized = pattern.replace(/\\/g, "/").replace(/\/$/, "");
  if (!normalized.includes("*")) return [join(root, normalized)];
  const star = normalized.indexOf("*");
  const before = normalized.slice(0, star).replace(/\/$/, "");
  const after = normalized.slice(star + 1).replace(/^\//, "");
  const base = join(root, before);
  if (!existsSync(base)) return [];
  return readdirSync(base)
    .map((name) => join(base, name, after))
    .filter((path) => existsSync(path) && statSync(path).isDirectory());
}

function workspacePackageRoots(root: string): Map<string, string> {
  const result = new Map<string, string>();
  const rootPackage = readJson(join(root, "package.json"));
  if (!rootPackage) return result;
  for (const pattern of workspacePatterns(rootPackage)) {
    for (const candidate of expandSimpleWorkspacePattern(root, pattern)) {
      const pkg = readJson(join(candidate, "package.json"));
      if (pkg?.name) result.set(pkg.name, candidate);
    }
  }
  return result;
}

function resolvePackageRoot(
  appRoot: string,
  name: string,
  workspaces: Map<string, string>,
): string | null {
  const workspace = workspaces.get(name);
  if (workspace) return workspace;
  const require = createRequire(join(appRoot, "package.json"));
  try {
    return dirname(require.resolve(`${name}/package.json`));
  } catch {
    try {
      const entry = require.resolve(name);
      let current = dirname(entry);
      while (true) {
        const pkg = readJson(join(current, "package.json"));
        if (pkg?.name === name) return current;
        const parent = dirname(current);
        if (parent === current) return null;
        current = parent;
      }
    } catch {
      return null;
    }
  }
}

function resolveRuntime(
  packageRoot: string,
  value: ClientRuntimeDefinition,
): ClientRuntimeDefinition {
  return {
    ...value,
    entry: value.entry ? resolve(packageRoot, value.entry) : undefined,
  };
}

function resolveAsset(packageRoot: string, value: PackageAssetDefinition): PackageAssetDefinition {
  return {
    ...value,
    entry: value.entry ? resolve(packageRoot, value.entry) : undefined,
  };
}

function resolveStyle(packageRoot: string, value: PackageStyleDefinition): PackageStyleDefinition {
  return {
    ...value,
    entry: value.entry ? resolve(packageRoot, value.entry) : undefined,
    source: value.source ? resolve(packageRoot, value.source) : undefined,
  };
}

function resolveRoute(packageRoot: string, value: PackageRouteDefinition): PackageRouteDefinition {
  return { ...value, entry: resolve(packageRoot, value.entry) };
}

function resolveMigration(
  packageRoot: string,
  value: PackageMigrationDefinition,
): PackageMigrationDefinition {
  return { ...value, entry: value.entry ? resolve(packageRoot, value.entry) : undefined };
}

function manifestContributionPlugin(
  packageRoot: string,
  packageName: string,
  packageVersion: string | undefined,
  manifest: WrnexusPackageManifest,
): WrnexusPlugin | null {
  const components = (manifest.components ?? []).map((value) => resolve(packageRoot, value));
  const runtimes = (manifest.clientRuntimes ?? []).map((value) =>
    resolveRuntime(packageRoot, value),
  );
  const assets = (manifest.assets ?? []).map((value) => resolveAsset(packageRoot, value));
  const styles = (manifest.styles ?? []).map((value) => resolveStyle(packageRoot, value));
  const routes = (manifest.routes ?? []).map((value) => resolveRoute(packageRoot, value));
  const middleware = (manifest.middleware ?? []).map((value) => resolve(packageRoot, value));
  const migrations = (manifest.migrations ?? []).map((value) =>
    resolveMigration(packageRoot, value),
  );
  if (
    !components.length &&
    !runtimes.length &&
    !assets.length &&
    !styles.length &&
    !routes.length &&
    !middleware.length &&
    !migrations.length
  )
    return null;
  return definePlugin({
    name: `${packageName}/manifest`,
    version: manifest.version ?? packageVersion,
    componentDirs: components,
    clientRuntimes: runtimes,
    assets,
    styleSources: styles,
    routeEntries: routes,
    middleware,
    migrations,
  });
}

function pluginManifest(value: string | PackagePluginManifest): PackagePluginManifest {
  return typeof value === "string" ? { plugin: value } : value;
}

async function loadPlugin(
  packageRoot: string,
  manifest: PackagePluginManifest,
): Promise<WrnexusPlugin> {
  const modulePath = resolve(packageRoot, manifest.plugin);
  const mod = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
  const exportName = manifest.export;
  let candidate = exportName ? mod[exportName] : (mod.default ?? mod.plugin);
  if (candidate === undefined) {
    throw new Error(`WRN-PLUGIN-DISCOVERY-EXPORT: ${modulePath} has no plugin export.`);
  }
  const shouldInvoke =
    manifest.factory === true || (manifest.factory !== false && typeof candidate === "function");
  if (shouldInvoke) candidate = await (candidate as () => unknown)();
  return definePlugin(candidate as WrnexusPlugin);
}

function dependencyNames(pkg: PackageJson, options: DiscoverPluginOptions): string[] {
  const all = {
    ...(pkg.dependencies ?? {}),
    ...(pkg.optionalDependencies ?? {}),
    ...(options.includeDevDependencies === false ? {} : (pkg.devDependencies ?? {})),
    ...(options.includePeerDependencies ? (pkg.peerDependencies ?? {}) : {}),
  };
  return Object.keys(all).sort();
}

/**
 * Discover package plugins declared through package.json `wrnexus.plugin`.
 * Explicit plugins win; automatic duplicates are skipped instead of failing.
 */
export async function discoverPlugins(
  appRoot: string,
  explicit: PluginInput,
  options: DiscoverPluginOptions = {},
): Promise<PluginInput> {
  const appPackage = readJson(join(appRoot, "package.json"));
  if (!appPackage) return explicit;

  const explicitPlugins = flattenPlugins(explicit);
  const explicitNames = new Set(explicitPlugins.map((plugin) => plugin.name));
  const workspaceRoot = nearestWorkspaceRoot(appRoot);
  const workspaces = workspaceRoot
    ? workspacePackageRoots(workspaceRoot)
    : new Map<string, string>();
  const automatic: WrnexusPlugin[] = [];

  for (const name of dependencyNames(appPackage, options)) {
    const packageRoot = resolvePackageRoot(appRoot, name, workspaces);
    if (!packageRoot) continue;
    const packageJson = readJson(join(packageRoot, "package.json"));
    const packageManifest = packageJson?.wrnexus;
    if (!packageManifest) continue;
    enforcePermissions(name, packageManifest, manifestPermissions(packageManifest), options);
    if (
      options.runtime &&
      packageManifest.runtimes?.length &&
      !packageManifest.runtimes.includes(options.runtime as never)
    ) {
      throw new Error(`WRN-PLUGIN-RUNTIME: ${name} does not support ${options.runtime}.`);
    }
    const missingCapabilities = (packageManifest.requires ?? []).filter(
      (capability) => !options.capabilities?.includes(capability),
    );
    if (options.runtime && missingCapabilities.length) {
      throw new Error(
        `WRN-PLUGIN-CAPABILITY: ${name} requires unavailable capabilities: ${missingCapabilities.join(", ")}.`,
      );
    }
    if (options.runtime) {
      const matrix = testPluginCompatibility(packageManifest, [
        {
          runtime: options.runtime as
            "bun" | "node" | "edge" | "worker" | "service-worker" | "browser",
          version:
            options.runtimeVersion ??
            (options.runtime === "bun" && typeof Bun !== "undefined" ? Bun.version : undefined),
          os: options.os ?? process.platform,
          capabilities: options.capabilities,
        },
      ])[0]!;
      const compatibilityIssue = matrix.issues.find((issue) =>
        ["WRN-PLUGIN-MATRIX-VERSION", "WRN-PLUGIN-MATRIX-OS"].includes(issue.code),
      );
      if (compatibilityIssue)
        throw new Error(`${compatibilityIssue.code}: ${name}: ${compatibilityIssue.message}`);
    }
    const manifestPlugin = manifestContributionPlugin(
      packageRoot,
      packageJson?.name ?? name,
      packageJson?.version,
      packageManifest,
    );
    if (manifestPlugin && !explicitNames.has(manifestPlugin.name)) automatic.push(manifestPlugin);

    const declared = packageManifest.plugin;
    if (!declared) continue;
    const manifest = pluginManifest(declared);
    if (manifest.autoDiscover === false) continue;
    try {
      const plugin = await loadPlugin(packageRoot, manifest);
      enforcePermissions(name, packageManifest, modulePermissions(plugin), options);
      if (!explicitNames.has(plugin.name) && !automatic.some((item) => item.name === plugin.name)) {
        automatic.push(plugin);
      }
    } catch (error) {
      const message = `Failed to auto-load ${name}: ${error instanceof Error ? error.message : String(error)}`;
      if (options.strict) throw new Error(`WRN-PLUGIN-DISCOVERY: ${message}`, { cause: error });
      options.warn?.(message);
    }
  }

  return [automatic, explicitPlugins];
}
