import fs from "fs";
import path from "path";

const TESTKIT_DIRNAME = "__testkit__";
const DISCOVERY_RULES = [
  { suffix: ".int.testkit.ts", type: "integration", framework: "k6" },
  { suffix: ".e2e.testkit.ts", type: "e2e", framework: "k6" },
  { suffix: ".dal.testkit.ts", type: "dal", framework: "k6" },
  { suffix: ".load.testkit.ts", type: "load", framework: "k6" },
  { suffix: ".pw.testkit.ts", type: "e2e", framework: "playwright" },
];

const IGNORED_DIRS = new Set([
  ".cache",
  ".git",
  ".hg",
  ".next",
  ".nuxt",
  ".playwright-browsers",
  ".svn",
  ".testkit",
  ".turbo",
  "build",
  "coverage",
  "dist",
  "node_modules",
  "playwright-report",
  "test-results",
]);

export function discoverProject(productDir, explicitServices = {}) {
  const { suiteFiles, legacyFiles } = discoverFiles(productDir);
  const groupedByService = {};
  const services = {};
  const unowned = [];
  const ambiguous = [];
  const discoveredSuites = [];

  for (const filePath of suiteFiles) {
    const rule = inferRule(filePath);
    if (!rule) continue;

    const owners = inferOwners(filePath, explicitServices);
    if (owners.length === 0) {
      unowned.push(filePath);
      continue;
    }
    if (owners.length > 1) {
      ambiguous.push({
        filePath,
        serviceNames: owners.map((owner) => owner.name).sort((left, right) => left.localeCompare(right)),
      });
      continue;
    }

    const owner = owners[0];
    services[owner.name] = mergeServiceDiscovery(services[owner.name], owner);
    const relativeToService = relativeToServiceRoot(owner, filePath);
    const suiteRef = deriveSuiteRef(relativeToService, rule.suffix);
    discoveredSuites.push({
      serviceName: owner.name,
      type: rule.type,
      framework: rule.framework,
      filePath,
      suitePath: suiteRef.suitePath,
    });
  }

  if (legacyFiles.length > 0 || unowned.length > 0 || ambiguous.length > 0) {
    throw buildDiscoveryError(legacyFiles, unowned, ambiguous);
  }

  for (const entry of discoveredSuites) {
    const grouped = groupedByService[entry.serviceName] || {};
    const suitesForType = grouped[entry.type] || [];
    const suiteKey = entry.suitePath.join("/");
    let suite = suitesForType.find(
      (candidate) => candidate._suiteKey === suiteKey && candidate.framework === entry.framework
    );

    if (!suite) {
      suite = {
        name: null,
        files: [],
        framework: entry.framework,
        _suiteKey: suiteKey,
        _suitePath: entry.suitePath,
      };
      suitesForType.push(suite);
      grouped[entry.type] = suitesForType;
      groupedByService[entry.serviceName] = grouped;
    }

    suite.files.push(entry.filePath);
  }

  for (const grouped of Object.values(groupedByService)) {
    for (const suites of Object.values(grouped)) {
      const suiteNames = disambiguateSuiteNames(suites);
      for (const suite of suites) {
        suite.name = suiteNames.get(suite._suiteKey);
        suite.files.sort((left, right) => left.localeCompare(right));
        delete suite._suiteKey;
        delete suite._suitePath;
      }
      suites.sort((left, right) => left.name.localeCompare(right.name));
    }
  }

  return {
    services,
    suitesByService: groupedByService,
  };
}

export function discoverSuites(productDir, explicitServices = {}) {
  return discoverProject(productDir, explicitServices).suitesByService;
}

function discoverFiles(productDir) {
  const suiteFiles = [];
  const legacyFiles = [];
  const queue = [productDir];

  while (queue.length > 0) {
    const current = queue.pop();
    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
      const absolutePath = path.join(current, entry.name);

      if (entry.isSymbolicLink()) continue;

      if (entry.isDirectory()) {
        if (IGNORED_DIRS.has(entry.name)) continue;
        queue.push(absolutePath);
        continue;
      }

      if (!entry.isFile()) continue;

      const relativePath = normalizePath(path.relative(productDir, absolutePath));
      if (!inferRule(relativePath)) continue;
      if (relativePath.split("/").includes(TESTKIT_DIRNAME)) {
        suiteFiles.push(relativePath);
      } else {
        legacyFiles.push(relativePath);
      }
    }
  }

  return {
    suiteFiles: suiteFiles.sort((left, right) => left.localeCompare(right)),
    legacyFiles: legacyFiles.sort((left, right) => left.localeCompare(right)),
  };
}

function inferOwners(filePath, explicitServices) {
  const serviceRules = Object.entries(explicitServices).flatMap(([name, config]) =>
    buildServiceRules(name, config)
  );
  if (serviceRules.length === 0) {
    return [
      {
        name: "app",
        source: "implicit-root",
        cwdPrefix: ".",
      },
    ];
  }

  const owningRules = serviceRules.filter((rule) => ownsFile(rule, filePath));
  if (owningRules.length === 0) {
    return [];
  }

  const maxDepth = Math.max(...owningRules.map((rule) => rule.depth));
  return owningRules.filter((rule) => rule.depth === maxDepth);
}

function disambiguateSuiteNames(suites) {
  const used = new Map();
  const resolved = new Map();

  for (const suite of suites) {
    const pathSegments = suite._suitePath;
    let candidate = "";

    for (let depth = 1; depth <= pathSegments.length; depth += 1) {
      candidate = pathSegments.slice(-depth).join("-");
      const existing = used.get(candidate);
      if (!existing) {
        used.set(candidate, suite._suiteKey);
        break;
      }
      if (existing === suite._suiteKey) break;
      candidate = "";
    }

    if (!candidate) {
      candidate = pathSegments.join("-");
    }

    resolved.set(suite._suiteKey, candidate);
  }

  if (resolved.size !== suites.length) {
    for (const suite of suites) {
      if (!resolved.has(suite._suiteKey)) {
        resolved.set(suite._suiteKey, suite._suitePath.join("-"));
      }
    }
  }

  if (new Set(resolved.values()).size !== suites.length) {
    const counts = new Map();
    for (const suite of suites) {
      const base = suite._suitePath.join("-");
      const count = counts.get(base) || 0;
      counts.set(base, count + 1);
      resolved.set(suite._suiteKey, count === 0 ? base : `${base}-${count + 1}`);
    }
  }

  return resolved;
}

function buildServiceRules(serviceName, serviceConfig) {
  const cwd = normalizePath(serviceConfig?.local?.cwd || ".");
  const discoveryRoots = Array.isArray(serviceConfig?.discovery?.roots)
    ? serviceConfig.discovery.roots
    : [cwd];

  return discoveryRoots.map((root) => {
    const normalizedRoot = normalizePath(root || ".");
    return {
      name: serviceName,
      source: "cwd",
      cwdPrefix: cwd === "." ? null : cwd,
      matchPrefix: normalizedRoot === "." ? null : normalizedRoot,
      depth: normalizedRoot === "." ? 0 : normalizedRoot.split("/").filter(Boolean).length,
    };
  });
}

function ownsFile(serviceRule, filePath) {
  if (serviceRule.depth === 0) return true;
  if (serviceRule.matchPrefix && hasPrefix(filePath, serviceRule.matchPrefix)) return true;
  return false;
}

function relativeToServiceRoot(serviceRule, filePath) {
  if (serviceRule.cwdPrefix && hasPrefix(filePath, serviceRule.cwdPrefix)) {
    return path.posix.relative(serviceRule.cwdPrefix, filePath);
  }
  return filePath;
}

function deriveSuiteRef(relativePath, suffix) {
  const parts = relativePath.split("/").filter(Boolean);
  const testkitIndex = parts.indexOf(TESTKIT_DIRNAME);
  if (testkitIndex === -1) {
    const fallback = path.posix.basename(relativePath, suffix);
    return {
      suitePath: [fallback],
    };
  }

  const before = parts.slice(0, testkitIndex);
  const after = parts.slice(testkitIndex + 1);
  if (after.length === 0) {
    const fallback = path.posix.basename(relativePath, suffix);
    return {
      suitePath: [fallback],
    };
  }

  if (after.length === 1) {
    if (before.length === 0) {
      return {
        suitePath: [path.posix.basename(after[0], suffix)],
      };
    }
    return {
      suitePath: before,
    };
  }

  return {
    suitePath: [...before, after[0]],
  };
}

function mergeServiceDiscovery(existing, owner) {
  if (!existing) {
    return {
      name: owner.name,
      inferredLocalCwd: owner.cwdPrefix || ".",
      source: owner.source,
    };
  }

  return {
    ...existing,
    inferredLocalCwd:
      existing.inferredLocalCwd === "." ? owner.cwdPrefix || "." : existing.inferredLocalCwd,
  };
}

function buildDiscoveryError(legacyFiles, unowned, ambiguous) {
  const lines = ["Filesystem discovery failed for one or more .testkit.ts files."];

  if (legacyFiles.length > 0) {
    lines.push("");
    lines.push("Legacy test files outside __testkit__:");
    for (const filePath of legacyFiles) {
      lines.push(`- ${filePath}`);
    }
  }

  if (unowned.length > 0) {
    lines.push("");
    lines.push("Unowned test files:");
    for (const filePath of unowned) {
      lines.push(`- ${filePath}`);
    }
  }

  if (ambiguous.length > 0) {
    lines.push("");
    lines.push("Ambiguous test files:");
    for (const entry of ambiguous) {
      lines.push(`- ${entry.filePath} -> ${entry.serviceNames.join(", ")}`);
    }
  }

  lines.push("");
  lines.push('Expected test files to live under a "__testkit__" directory within a service root.');
  return new Error(lines.join("\n"));
}

function inferRule(filePath) {
  return DISCOVERY_RULES.find((rule) => filePath.endsWith(rule.suffix)) || null;
}

function hasPrefix(filePath, prefix) {
  return filePath === prefix || filePath.startsWith(`${prefix}/`);
}

function normalizePath(value) {
  const normalized = String(value).split(path.sep).join("/");
  if (normalized === "." || normalized === "./") return ".";
  return normalized.replace(/^\.\/+/, "").replace(/\/+$/, "") || ".";
}
