/**
 * Semantically index a repository that arrived without a push.
 *
 * `indexChangedFiles()` is called from exactly one place: the git post-receive
 * hook. That is correct for the push path and wrong for every other way a
 * repository comes into existence:
 *
 *   - Bulk import clones with `git clone`, which never fires post-receive.
 *   - POST /new + first-repo-scaffold writes through the git plumbing API,
 *     which also never fires post-receive.
 *   - Forks copy the bare repo directly.
 *
 * So the two onboarding paths built in this session produced repositories
 * where `gluecron_semantic_search`, the repo chat, and "ask this repo" all
 * returned nothing — silently, because an empty index is indistinguishable
 * from "no matches". The agent-first experience depends entirely on that
 * index being populated, which is how this was found.
 *
 * post-receive indexes the paths that CHANGED in a push. There is no such
 * delta for a repo that just arrived, so this walks the tree at HEAD and
 * indexes what is there, then delegates to the same indexChangedFiles() the
 * hook uses — same filtering, same embedding model, same cap.
 */

import { mapWithConcurrency, DB_FANOUT_LIMIT } from "./concurrency";

export interface IndexRepoResult {
  indexed: number;
  skipped: number;
  model: string;
  error?: string;
}

export interface IndexRepoDeps {
  getTree: (
    owner: string,
    repo: string,
    ref: string,
    path?: string
  ) => Promise<Array<{ name: string; type: string; path?: string }>>;
  indexChangedFiles: (args: {
    repositoryId: string;
    ownerName: string;
    repoName: string;
    commitSha: string;
    changedPaths: string[];
  }) => Promise<{ indexed: number; skipped: number; model: string }>;
}

async function realDeps(): Promise<IndexRepoDeps> {
  const { getTree } = await import("../git/repository");
  const { indexChangedFiles } = await import("./semantic-index");
  return { getTree: getTree as IndexRepoDeps["getTree"], indexChangedFiles };
}

/** Directories never worth embedding. Keeps the walk cheap on a big import. */
const SKIP_DIRS = new Set([
  ".git", "node_modules", "dist", "build", "out", "target", "vendor",
  ".next", ".cache", "coverage", "__pycache__", ".venv", "venv",
]);

/** Depth cap. A pathological monorepo should not turn one import into a
 *  thousand tree reads; indexChangedFiles caps files independently. */
const MAX_DEPTH = 6;

/**
 * Collect blob paths at `ref`, breadth-first, bounded.
 *
 * Concurrency is bounded for the same reason as everywhere else in the
 * onboarding path: an import of 100 repos must not open an unbounded number
 * of concurrent tree reads against one disk.
 */
async function collectPaths(
  deps: IndexRepoDeps,
  owner: string,
  repo: string,
  ref: string
): Promise<string[]> {
  const files: string[] = [];
  let level: string[] = [""];

  for (let depth = 0; depth < MAX_DEPTH && level.length > 0; depth++) {
    const results = await mapWithConcurrency(level, DB_FANOUT_LIMIT, async (dir) => {
      try {
        return await deps.getTree(owner, repo, ref, dir || undefined);
      } catch {
        // A single unreadable directory must not abandon the whole index.
        return [];
      }
    });

    const next: string[] = [];
    for (let i = 0; i < results.length; i++) {
      const dir = level[i];
      for (const entry of results[i] ?? []) {
        const full = entry.path ?? (dir ? `${dir}/${entry.name}` : entry.name);
        if (entry.type === "tree") {
          if (SKIP_DIRS.has(entry.name)) continue;
          next.push(full);
        } else if (entry.type === "blob") {
          files.push(full);
        }
      }
    }
    level = next;
  }

  return files;
}

/**
 * Index a repository at `commitSha`. Never throws — an unindexed repo is a
 * degraded search experience, not a failed import or a failed repo creation.
 */
export async function indexExistingRepo(
  args: {
    repositoryId: string;
    owner: string;
    repoName: string;
    ref: string;
    commitSha: string;
  },
  injected?: IndexRepoDeps
): Promise<IndexRepoResult> {
  try {
    const deps = injected ?? (await realDeps());
    const paths = await collectPaths(deps, args.owner, args.repoName, args.ref);
    if (paths.length === 0) {
      return { indexed: 0, skipped: 0, model: "none" };
    }
    // indexChangedFiles does the filtering (isCodeFile), the cap, and the
    // embedding — reusing it keeps scaffolded, imported and pushed repos on
    // exactly the same indexing behaviour.
    return await deps.indexChangedFiles({
      repositoryId: args.repositoryId,
      ownerName: args.owner,
      repoName: args.repoName,
      commitSha: args.commitSha,
      changedPaths: paths,
    });
  } catch (err) {
    return {
      indexed: 0,
      skipped: 0,
      model: "none",
      error: err instanceof Error ? err.message : String(err),
    };
  }
}
