import { readFile, readdir } from 'node:fs/promises';
import { join, relative, sep } from 'node:path';
import { createRequire } from 'node:module';
import { type Ignore, type Options as IgnoreOptions } from 'ignore';
import { debugWrite } from '../utils/debug-log.js';

const require = createRequire(import.meta.url);
const ignoreFactory = require('ignore') as (options?: IgnoreOptions) => Ignore;

export type GitignoreEntry = { dir: string; ig: Ignore };

const YGGDRASIL_DIRNAME = '.yggdrasil';

export async function loadRootGitignoreStack(projectRoot: string): Promise<GitignoreEntry[]> {
  try {
    const content = await readFile(join(projectRoot, '.gitignore'), 'utf-8');
    const ig = ignoreFactory();
    ig.add(content);
    return [{ dir: projectRoot, ig }];
  } catch (err) {
    debugWrite(`[repo-scanner] root .gitignore not readable: ${(err as Error).message}`);
    return [];
  }
}

export function isIgnoredByStack(absPath: string, stack: GitignoreEntry[]): boolean {
  for (const entry of stack) {
    const rel = relative(entry.dir, absPath);
    if (rel === '' || rel.startsWith('..')) continue;
    const normalized = rel.split(sep).join('/');
    if (entry.ig.ignores(normalized)) return true;
  }
  return false;
}

async function collectFiles(
  dir: string,
  projectRoot: string,
  stack: GitignoreEntry[],
): Promise<string[]> {
  let localStack = stack;
  try {
    const content = await readFile(join(dir, '.gitignore'), 'utf-8');
    const ig = ignoreFactory();
    ig.add(content);
    localStack = [...stack, { dir, ig }];
  } catch (err) {
    debugWrite(`[repo-scanner] local .gitignore not readable in ${dir}: ${(err as Error).message}`);
  }

  let entries;
  try {
    entries = await readdir(dir, { withFileTypes: true });
  } catch (err) {
    debugWrite(`[repo-scanner] readdir failed for ${dir}: ${(err as Error).message}`);
    return [];
  }

  const results: string[] = [];
  for (const entry of entries) {
    const absPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      if (entry.name === '.git') continue;
      if (entry.name === YGGDRASIL_DIRNAME && dir === projectRoot) continue;
      if (isIgnoredByStack(absPath, localStack)) continue;
      results.push(...(await collectFiles(absPath, projectRoot, localStack)));
    } else if (entry.isFile()) {
      if (isIgnoredByStack(absPath, localStack)) continue;
      results.push(relative(projectRoot, absPath).split(sep).join('/'));
    }
  }
  return results;
}

/**
 * Drop every file under a nested-graph subtree — a directory (below the repo root)
 * that contains its own `.yggdrasil/`. Such a subtree is governed by that graph, so
 * the parent graph's checks must ignore it. The top-level `.yggdrasil/` is NOT a
 * nested root (its paths start with `.yggdrasil/`, with no leading-slash segment).
 */
export function excludeNestedGraphSubtrees(relPaths: string[]): string[] {
  // A nested graph always has files under its own `.yggdrasil/`, so a `/.yggdrasil/`
  // segment (with a non-empty prefix — idx > 0) is the complete, correct signal. The
  // top-level `.yggdrasil/` has no leading-slash segment, so it is never a nested root.
  const seg = `/${YGGDRASIL_DIRNAME}/`;
  const nestedRoots = new Set<string>();
  for (const p of relPaths) {
    const idx = p.indexOf(seg);
    if (idx > 0) nestedRoots.add(p.slice(0, idx));
  }
  if (nestedRoots.size === 0) return relPaths;
  return relPaths.filter((p) => {
    for (const root of nestedRoots) {
      if (p === root || p.startsWith(root + '/')) return false;
    }
    return true;
  });
}

/**
 * Walk all files in the repo, returning repo-relative POSIX paths.
 * Excludes `.yggdrasil/`, `.git/`, symlinks, and gitignore-matched files.
 * Excludes subtrees that contain their own nested `.yggdrasil/` directory.
 */
export async function walkRepoFiles(projectRoot: string): Promise<string[]> {
  const stack = await loadRootGitignoreStack(projectRoot);
  const files = await collectFiles(projectRoot, projectRoot, stack);
  return excludeNestedGraphSubtrees(files);
}
