import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";

/**
 * Consumer-facing Oxlint plugin shipped with `@tailor-platform/erp-kit`.
 *
 * Referenced by the shared configs' `jsPlugins` as:
 *   { name: "erp-kit", specifier: "@tailor-platform/erp-kit/oxlint-plugin" }
 *
 * The rule prefix (`erp-kit/...`) comes from the `name` in that config entry.
 *
 * Distinct from `no-cross-module-import.js`, which is erp-kit's own internal monorepo rule.
 */

// dir -> nearest ancestor dir containing a package.json (or null up to the fs root)
const packageDirCache = new Map();

function findPackageDir(startDir) {
  const visited = [];
  let dir = startDir;
  for (;;) {
    if (packageDirCache.has(dir)) {
      const cached = packageDirCache.get(dir);
      for (const v of visited) packageDirCache.set(v, cached);
      return cached;
    }
    visited.push(dir);
    if (existsSync(join(dir, "package.json"))) {
      for (const v of visited) packageDirCache.set(v, dir);
      return dir;
    }
    const parent = dirname(dir);
    if (parent === dir) {
      for (const v of visited) packageDirCache.set(v, null);
      return null;
    }
    dir = parent;
  }
}

// packageDir -> declared package name (or null)
const packageNameCache = new Map();

function readPackageName(packageDir) {
  if (packageNameCache.has(packageDir)) return packageNameCache.get(packageDir);
  let name = null;
  try {
    const pkg = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
    if (typeof pkg.name === "string") name = pkg.name;
  } catch {
    // unreadable / malformed package.json — fall back to a generic message
  }
  packageNameCache.set(packageDir, name);
  return name;
}

const noRelativePackages = {
  meta: {
    type: "problem",
    docs: {
      description:
        "Disallow relative imports that reach into a different package. Import the package by its name instead.",
    },
    messages: {
      relativePackage:
        'Relative import "{{source}}" reaches into another package. Import it by its package name "{{packageName}}" instead.',
      relativePackageUnknown:
        'Relative import "{{source}}" reaches into another package. Import it by its package name instead of a relative path.',
    },
    schema: [],
  },

  create(context) {
    const filename = context.filename ?? context.getFilename();
    const importerPackageDir = findPackageDir(dirname(filename));
    // Cannot determine the importing file's package — nothing to enforce.
    if (!importerPackageDir) return {};

    const check = (node) => {
      const source = node.source?.value;
      if (typeof source !== "string" || !source.startsWith(".")) return;

      const resolved = resolve(dirname(filename), source);
      const targetPackageDir = findPackageDir(resolved);
      // No package boundary found, or resolves within the same package — allowed.
      if (!targetPackageDir || targetPackageDir === importerPackageDir) return;

      const packageName = readPackageName(targetPackageDir);
      context.report({
        node: node.source,
        messageId: packageName ? "relativePackage" : "relativePackageUnknown",
        data: { source, packageName: packageName ?? "" },
      });
    };

    return {
      ImportDeclaration: check,
      ImportExpression: check,
      ExportNamedDeclaration: (node) => {
        if (node.source) check(node);
      },
      ExportAllDeclaration: check,
    };
  },
};

const plugin = {
  meta: { name: "erp-kit" },
  rules: {
    "no-relative-packages": noRelativePackages,
  },
};

export default plugin;
