// src/actions/deplint/checkPackage/getUnsatisfiedPeerDependencies.ts
import fs2 from "fs";
import chalk from "chalk";
import semver from "semver";
import sortPackageJson from "sort-package-json";

// src/actions/deplint/getRequiredPeerDependencies.ts
import fs from "fs";
import path from "path";
function findDepPackageJson(location, dep) {
  let dir = location;
  while (true) {
    const candidate = path.join(dir, "node_modules", dep, "package.json");
    if (fs.existsSync(candidate)) return candidate;
    const parent = path.dirname(dir);
    if (parent === dir) return void 0;
    dir = parent;
  }
}

// src/actions/deplint/checkPackage/getUnsatisfiedPeerDependencies.ts
function readDepPackageJson(location, dep) {
  const depPkgPath = findDepPackageJson(location, dep);
  if (!depPkgPath) return void 0;
  try {
    return JSON.parse(fs2.readFileSync(depPkgPath, "utf8"));
  } catch {
    return void 0;
  }
}
function readPeerDeps(location, dep) {
  const pkg = readDepPackageJson(location, dep);
  if (!pkg) return [];
  const peers = pkg.peerDependencies;
  return Object.keys(peers ?? {});
}
function readPeerVersionRange(location, dep, peer) {
  const pkg = readDepPackageJson(location, dep);
  if (!pkg) return void 0;
  const peers = pkg.peerDependencies;
  return peers?.[peer];
}
function readInstalledVersion(location, dep) {
  const pkg = readDepPackageJson(location, dep);
  if (!pkg) return void 0;
  return pkg.version;
}
function isPeerOptionalInAll(location, peer, sources) {
  for (const source of sources) {
    const pkg = readDepPackageJson(location, source.requiredBy);
    if (!pkg) continue;
    const meta = pkg.peerDependenciesMeta?.[peer];
    if (!meta?.optional) return false;
  }
  return true;
}
var MAX_TRANSITIVE_DEPTH = 2;
function collectPeerSources(location, dependencies, devDependencies) {
  const peerSources = /* @__PURE__ */ new Map();
  const visited = /* @__PURE__ */ new Set();
  function addPeerSource(peer, source) {
    const sources = peerSources.get(peer) ?? [];
    sources.push(source);
    peerSources.set(peer, sources);
  }
  function collectFromDeps(deps, isRuntime, chain) {
    for (const dep of deps) {
      const visitKey = `${dep}:${chain.join(">")}`;
      if (visited.has(visitKey)) continue;
      visited.add(visitKey);
      for (const peer of readPeerDeps(location, dep)) {
        addPeerSource(peer, {
          chain: chain.length > 0 ? [...chain, dep] : void 0,
          isRuntimeDep: isRuntime,
          requiredBy: dep
        });
      }
      if (chain.length < MAX_TRANSITIVE_DEPTH) {
        const depPkg = readDepPackageJson(location, dep);
        if (depPkg) {
          const transitiveDeps = Object.keys(depPkg.dependencies ?? {});
          collectFromDeps(transitiveDeps, isRuntime, [...chain, dep]);
        }
      }
    }
  }
  collectFromDeps(dependencies, true, []);
  const devOnly = devDependencies.filter((d) => !dependencies.includes(d));
  collectFromDeps(devOnly, false, []);
  return peerSources;
}
function formatChain(sources) {
  const chainsWithDepth = sources.filter((s) => s.chain && s.chain.length > 0);
  if (chainsWithDepth.length === 0) return "";
  const first = chainsWithDepth[0];
  return ` (via ${chalk.dim(first.chain.join(" \u2192 "))})`;
}
function getRequiredTransitivePeers(location, { dependencies, devDependencies }) {
  const peerSources = collectPeerSources(location, dependencies, devDependencies);
  return new Set(peerSources.keys());
}
function logUnsatisfiedPeer(name, unsatisfied) {
  const {
    peer,
    sources,
    reason,
    hasRuntimeSource,
    installedVersion,
    versionRange
  } = unsatisfied;
  const chainInfo = formatChain(sources);
  if (reason === "version-mismatch") {
    const requiredByList = sources.map((s) => s.requiredBy);
    console.log(
      `[${chalk.blue(name)}] Peer dependency version mismatch ${chalk.red(peer)} \u2014 installed ${chalk.yellow(installedVersion ?? "unknown")} does not satisfy ${chalk.cyan(versionRange)} (required by ${chalk.cyan(requiredByList.join(", "))})` + chainInfo
    );
  } else if (reason === "under-exposed") {
    const requiredByList = sources.filter((s) => s.isRuntimeDep).map((s) => s.requiredBy);
    console.log(
      `[${chalk.blue(name)}] Under-exposed peer dependency ${chalk.red(peer)} (required by ${chalk.cyan(requiredByList.join(", "))})` + chainInfo + " \u2014 present in devDependencies but must also be in dependencies or peerDependencies for consumers"
    );
  } else if (hasRuntimeSource) {
    const requiredByList = sources.filter((s) => s.isRuntimeDep).map((s) => s.requiredBy);
    console.log(
      `[${chalk.blue(name)}] Unsatisfied peer dependency ${chalk.red(peer)} (required by ${chalk.cyan(requiredByList.join(", "))})` + chainInfo + " \u2014 must be in dependencies or peerDependencies"
    );
  } else {
    const requiredByList = sources.map((s) => s.requiredBy);
    console.log(
      `[${chalk.blue(name)}] Unsatisfied peer dependency ${chalk.red(peer)} (required by devDependency ${chalk.cyan(requiredByList.join(", "))})` + chainInfo + " \u2014 must be in dependencies, devDependencies, or peerDependencies"
    );
  }
}
function resolveVersionRange(location, sources, peer) {
  for (const source of sources) {
    const range = readPeerVersionRange(location, source.requiredBy, peer);
    if (range) return range;
  }
  return "*";
}
function checkVersionSatisfaction(location, peer, requestedRange) {
  const installedVersion = readInstalledVersion(location, peer);
  if (!installedVersion) return { satisfied: true };
  if (requestedRange === "*") return { installedVersion, satisfied: true };
  if (requestedRange.startsWith("workspace:")) return { installedVersion, satisfied: true };
  try {
    const satisfied = semver.satisfies(installedVersion, requestedRange, { includePrerelease: true });
    return { installedVersion, satisfied };
  } catch {
    return { installedVersion, satisfied: true };
  }
}
function findDeclaredPeerVersionMismatches(name, location, peerDependencies, peerDependencyVersions, exclude) {
  const results = [];
  for (const peer of peerDependencies) {
    if (exclude?.has(peer) || peer === name) continue;
    const declaredRange = peerDependencyVersions[peer];
    if (!declaredRange || declaredRange === "*") continue;
    const { installedVersion, satisfied } = checkVersionSatisfaction(location, peer, declaredRange);
    if (!satisfied) {
      results.push({
        hasRuntimeSource: true,
        installedVersion,
        peer,
        reason: "version-mismatch",
        sources: [{ isRuntimeDep: true, requiredBy: name }],
        versionRange: declaredRange
      });
    }
  }
  return results;
}
function findUnsatisfiedPeers({ name, location }, {
  dependencies,
  devDependencies,
  peerDependencies,
  peerDependencyVersions
}, exclude, isTerminal) {
  const peerSources = collectPeerSources(location, dependencies, devDependencies);
  const depsSet = new Set(dependencies);
  const devDepsSet = new Set(devDependencies);
  const peerDepsSet = new Set(peerDependencies);
  const isTerminalResolved = isTerminal ?? isTerminalPackage(`${location}/package.json`);
  const unsatisfied = [];
  for (const [peer, sources] of peerSources) {
    if (exclude?.has(peer) || peer === name) continue;
    if (isPeerOptionalInAll(location, peer, sources)) continue;
    const hasRuntimeSource = sources.some((s) => s.isRuntimeDep);
    const isPresent = depsSet.has(peer) || devDepsSet.has(peer);
    if (!isPresent) {
      const versionRange2 = resolveVersionRange(location, sources, peer);
      unsatisfied.push({
        hasRuntimeSource,
        peer,
        reason: "missing",
        sources,
        versionRange: versionRange2
      });
      continue;
    }
    const hasDirectRuntimeSource = sources.some((s) => s.isRuntimeDep && !s.chain);
    if (hasDirectRuntimeSource && !isTerminalResolved && !depsSet.has(peer) && !peerDepsSet.has(peer)) {
      const versionRange2 = resolveVersionRange(location, sources, peer);
      unsatisfied.push({
        hasRuntimeSource,
        peer,
        reason: "under-exposed",
        sources,
        versionRange: versionRange2
      });
      continue;
    }
    const versionRange = resolveVersionRange(location, sources, peer);
    const { installedVersion, satisfied } = checkVersionSatisfaction(location, peer, versionRange);
    if (!satisfied) {
      unsatisfied.push({
        hasRuntimeSource,
        installedVersion,
        peer,
        reason: "version-mismatch",
        sources,
        versionRange
      });
    }
  }
  return [
    ...unsatisfied,
    ...findDeclaredPeerVersionMismatches(name, location, peerDependencies, peerDependencyVersions, exclude)
  ];
}
function getUnsatisfiedPeerDependencies(workspace, packageParams, exclude) {
  const unsatisfied = findUnsatisfiedPeers(workspace, packageParams, exclude);
  for (const entry of unsatisfied) {
    logUnsatisfiedPeer(workspace.name, entry);
  }
  if (unsatisfied.length > 0) {
    console.log(`  ${chalk.yellow(`${workspace.location}/package.json`)}
`);
  }
  return {
    count: unsatisfied.length,
    unsatisfiedPeers: new Set(unsatisfied.map((u) => u.peer))
  };
}
function isTerminalPackage(packageJsonPath) {
  try {
    const pkg = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
    return pkg.private === true;
  } catch {
    return false;
  }
}
function fixVersionMismatch(workspaceName, entry, pkg) {
  const {
    installedVersion,
    peer,
    versionRange
  } = entry;
  if (!installedVersion) {
    logUnsatisfiedPeer(workspaceName, entry);
    console.log(
      `  ${chalk.dim("(version mismatch \u2014 could not resolve installed version)")}`
    );
    return false;
  }
  const match = /^(\d+)\.(\d+)/.exec(installedVersion);
  if (!match) {
    logUnsatisfiedPeer(workspaceName, entry);
    console.log(
      `  ${chalk.dim("(version mismatch \u2014 could not resolve installed version)")}`
    );
    return false;
  }
  const expectedVersion = versionRange.startsWith("^") ? `^${match[1]}` : `~${match[1]}.${match[2]}`;
  if (pkg.peerDependencies?.[peer]) {
    pkg.peerDependencies[peer] = expectedVersion;
  }
  if (pkg.devDependencies?.[peer] && !pkg.devDependencies[peer].startsWith("workspace:")) {
    pkg.devDependencies[peer] = expectedVersion;
  }
  console.log(
    `[${chalk.blue(workspaceName)}] Fixed: updated ${chalk.green(peer)} version to ${chalk.cyan(expectedVersion)} (was ${chalk.yellow(versionRange)})`
  );
  return true;
}
function fixUnsatisfiedPeerDependencies(workspace, packageParams, exclude) {
  const unsatisfied = findUnsatisfiedPeers(workspace, packageParams, exclude);
  if (unsatisfied.length === 0) return { addedPeers: /* @__PURE__ */ new Set(), count: 0 };
  const packageJsonPath = `${workspace.location}/package.json`;
  const pkg = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
  const isTerminal = isTerminalPackage(packageJsonPath);
  const addedPeers = /* @__PURE__ */ new Set();
  let fixedCount = 0;
  for (const entry of unsatisfied) {
    const {
      hasRuntimeSource,
      peer,
      reason,
      sources,
      versionRange
    } = entry;
    if (reason === "version-mismatch") {
      if (fixVersionMismatch(workspace.name, entry, pkg)) fixedCount++;
      continue;
    }
    const sections = reason === "under-exposed" ? ["peerDependencies"] : resolvePeerTargetSections(hasRuntimeSource, isTerminal);
    for (const section of sections) {
      pkg[section] = pkg[section] ?? {};
      pkg[section][peer] = versionRange;
    }
    addedPeers.add(peer);
    const requiredByList = sources.map((s) => s.requiredBy).join(", ");
    console.log(
      `[${chalk.blue(workspace.name)}] Fixed: added ${chalk.green(peer)}@${versionRange} to ${chalk.cyan(sections.join(" + "))} (required by ${requiredByList})`
    );
    fixedCount++;
  }
  if (fixedCount > 0) {
    const sorted = sortPackageJson(JSON.stringify(pkg, null, 2));
    fs2.writeFileSync(packageJsonPath, `${sorted}
`);
  }
  return { addedPeers, count: unsatisfied.length };
}
function resolvePeerTargetSections(hasRuntimeSource, isTerminal) {
  if (!hasRuntimeSource) return ["devDependencies"];
  if (isTerminal) return ["dependencies"];
  return ["peerDependencies", "devDependencies"];
}
export {
  findUnsatisfiedPeers,
  fixUnsatisfiedPeerDependencies,
  getRequiredTransitivePeers,
  getUnsatisfiedPeerDependencies,
  resolvePeerTargetSections
};
//# sourceMappingURL=getUnsatisfiedPeerDependencies.mjs.map