/**
 * lib/fs.ts — File system helpers for all SmartStack Studio dev CLIs.
 * Ported from SmartStack.cli/src/mcp/utils/fs.ts (legacy V1).
 *
 * All functions are async and throw typed FileSystemError on failure.
 * findFiles / findDirectories use glob with sensible defaults (node_modules/bin/obj excluded).
 */
import { stat, mkdir, readFile, writeFile, cp, rm, readdir } from 'node:fs/promises';
import path from 'node:path';
import { minimatch } from 'minimatch';

export class FileSystemError extends Error {
  constructor(
    message: string,
    public readonly operation: string,
    public readonly path: string,
    public readonly cause?: Error,
  ) {
    super(message);
    this.name = 'FileSystemError';
  }
}

/** Throw if targetPath escapes baseDir (path traversal protection). */
export function validatePathSecurity(targetPath: string, baseDir: string): void {
  const normalizedTarget = path.resolve(targetPath);
  const normalizedBase = path.resolve(baseDir);
  if (
    !normalizedTarget.startsWith(normalizedBase + path.sep) &&
    normalizedTarget !== normalizedBase
  ) {
    throw new FileSystemError(
      `Path traversal detected: "${targetPath}" is outside allowed directory "${baseDir}"`,
      'validatePathSecurity',
      targetPath,
    );
  }
}

export function safeJoinPath(baseDir: string, ...segments: string[]): string {
  const joined = path.join(baseDir, ...segments);
  validatePathSecurity(joined, baseDir);
  return joined;
}

export async function fileExists(filePath: string): Promise<boolean> {
  try {
    const s = await stat(filePath);
    return s.isFile();
  } catch {
    return false;
  }
}

export async function directoryExists(dirPath: string): Promise<boolean> {
  try {
    const s = await stat(dirPath);
    return s.isDirectory();
  } catch {
    return false;
  }
}

export async function ensureDirectory(dirPath: string): Promise<void> {
  await mkdir(dirPath, { recursive: true });
}

export async function readJson<T>(filePath: string): Promise<T> {
  try {
    const content = await readFile(filePath, 'utf-8');
    try {
      return JSON.parse(content) as T;
    } catch (parseError) {
      throw new FileSystemError(
        `Invalid JSON in file: ${filePath}`,
        'readJson',
        filePath,
        parseError instanceof Error ? parseError : undefined,
      );
    }
  } catch (error) {
    if (error instanceof FileSystemError) throw error;
    const err = error instanceof Error ? error : new Error(String(error));
    throw new FileSystemError(
      `Failed to read JSON file: ${filePath} - ${err.message}`,
      'readJson',
      filePath,
      err,
    );
  }
}

export async function writeJson<T>(filePath: string, data: T): Promise<void> {
  try {
    await mkdir(path.dirname(filePath), { recursive: true });
    await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    throw new FileSystemError(
      `Failed to write JSON file: ${filePath} - ${err.message}`,
      'writeJson',
      filePath,
      err,
    );
  }
}

export async function readText(filePath: string): Promise<string> {
  try {
    return await readFile(filePath, 'utf-8');
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    throw new FileSystemError(
      `Failed to read file: ${filePath} - ${err.message}`,
      'readText',
      filePath,
      err,
    );
  }
}

export async function writeText(filePath: string, content: string): Promise<void> {
  try {
    await mkdir(path.dirname(filePath), { recursive: true });
    await writeFile(filePath, content, 'utf-8');
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    throw new FileSystemError(
      `Failed to write file: ${filePath} - ${err.message}`,
      'writeText',
      filePath,
      err,
    );
  }
}

export async function copyFile(src: string, dest: string): Promise<void> {
  await mkdir(path.dirname(dest), { recursive: true });
  await cp(src, dest, { recursive: true });
}

export async function removeFile(filePath: string): Promise<void> {
  await rm(filePath, { force: true });
}

export async function removeDirectory(dirPath: string): Promise<void> {
  await rm(dirPath, { recursive: true, force: true });
}

const DEFAULT_IGNORE = [
  '**/node_modules/**',
  '**/bin/**',
  '**/obj/**',
  '**/dist/**',
  '**/out/**',
  '**/.git/**',
];

/**
 * Recursively walk a directory and yield every entry (files AND subdirs) as
 * absolute paths. Skips any entry whose relative-to-cwd path matches the
 * ignorePatterns (minimatch).
 */
async function walk(
  cwd: string,
  options: { includeDirs: boolean; ignorePatterns: string[] },
): Promise<string[]> {
  const results: string[] = [];
  const { includeDirs, ignorePatterns } = options;

  async function visit(dir: string): Promise<void> {
    let entries;
    try {
      entries = await readdir(dir, { withFileTypes: true });
    } catch {
      return; // unreadable dir — skip
    }
    for (const entry of entries) {
      const absolutePath = path.join(dir, entry.name);
      const relative = path.relative(cwd, absolutePath).replace(/\\/g, '/');

      const ignored = ignorePatterns.some((p) =>
        minimatch(relative, p, { dot: true }),
      );
      if (ignored) continue;

      if (entry.isDirectory()) {
        if (includeDirs) results.push(absolutePath);
        await visit(absolutePath);
      } else if (entry.isFile()) {
        if (!includeDirs) results.push(absolutePath);
      }
    }
  }

  await visit(cwd);
  return results;
}

export async function findFiles(
  pattern: string,
  options: { cwd?: string; ignore?: string[] } = {},
): Promise<string[]> {
  const { cwd = process.cwd(), ignore = [] } = options;
  const ignorePatterns = [...DEFAULT_IGNORE, ...ignore];

  const all = await walk(cwd, { includeDirs: false, ignorePatterns });

  // Match absolute paths against the pattern using the relative form
  // (so patterns like "**/*.csproj" work as expected).
  return all.filter((absolute) => {
    const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
    return minimatch(relative, pattern, { dot: true });
  });
}

export async function findDirectories(
  pattern: string,
  options: { cwd?: string; ignore?: string[] } = {},
): Promise<string[]> {
  const { cwd = process.cwd(), ignore = [] } = options;
  const ignorePatterns = [...DEFAULT_IGNORE, ...ignore];

  const all = await walk(cwd, { includeDirs: true, ignorePatterns });

  return all.filter((absolute) => {
    const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
    return minimatch(relative, pattern, { dot: true });
  });
}

export function relativePath(from: string, to: string): string {
  return path.relative(from, to).replace(/\\/g, '/');
}

export function normalizePath(filePath: string): string {
  return filePath.replace(/\\/g, '/');
}
