/**
 * Async-iterable directory walker.
 *
 * Depth-first by default — yields a directory's entries before descending
 * into the next sibling. Memory-bounded: never materializes the whole tree;
 * uses `client.list` page-by-page.
 *
 * Designed for arbitrarily-large remote trees: the consumer can break out
 * of the iterator at any point and the walk halts cleanly.
 */
import { posixJoin } from '../utils/path.js';
import { CancelledError } from '../schemas/errors.js';
import type { FileEntry } from '../schemas/file-entry.js';
import type { FileClient } from './client.js';

export interface WalkOpts {
  /** Hard cap on recursion depth. Default 32. */
  maxDepth?: number;
  /** Cancellation. */
  signal?: AbortSignal;
  /**
   * Apply a per-entry filter. Return `false` to skip an entry (and, for
   * directories, to skip descending into it). Default: include all.
   */
  filter?: (entry: FileEntry, relativePath: string) => boolean;
  /** Page size hint for `client.list`. Default 200. */
  pageSize?: number;
}

export interface WalkItem {
  entry: FileEntry;
  /** Path relative to the walk root. Includes the entry's own name. */
  relativePath: string;
  /** Absolute path on the remote (root + relativePath). */
  absolutePath: string;
  /** Depth from root — 1 for direct children. */
  depth: number;
}

const DEFAULT_MAX_DEPTH = 32;
const DEFAULT_PAGE_SIZE = 200;

export async function* walk(
  client: Pick<FileClient, 'list'>,
  rootPath: string,
  opts: WalkOpts = {},
): AsyncIterable<WalkItem> {
  const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
  const pageSize = opts.pageSize ?? DEFAULT_PAGE_SIZE;

  function checkAbort(): void {
    if (opts.signal?.aborted) {
      throw new CancelledError(
        opts.signal.reason instanceof Error
          ? opts.signal.reason.message
          : 'walk aborted',
      );
    }
  }

  async function* descend(absDir: string, depth: number, relPrefix: string): AsyncIterable<WalkItem> {
    if (depth > maxDepth) return;
    let cursor: string | undefined;
    do {
      checkAbort();
      const page = await client.list(absDir, {
        pageSize,
        ...(cursor !== undefined ? { cursor } : {}),
        ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
      });
      for (const entry of page.entries) {
        checkAbort();
        const relPath = relPrefix === '' ? entry.name : `${relPrefix}/${entry.name}`;
        const absPath = posixJoin(absDir, entry.name);
        if (opts.filter !== undefined && !opts.filter(entry, relPath)) {
          continue;
        }
        const item: WalkItem = { entry, relativePath: relPath, absolutePath: absPath, depth };
        yield item;
        if (entry.kind === 'dir') {
          yield* descend(absPath, depth + 1, relPath);
        }
      }
      cursor = page.hasMore ? page.nextCursor : undefined;
    } while (cursor !== undefined);
  }

  yield* descend(rootPath, 1, '');
}
