import fs from 'fs/promises';
import path from 'path';
import { parse as parseDotenv } from 'dotenv';
import { AgentError } from '../middleware/error-handler';
import { logger } from '../utils/logger';

/**
 * Validate that a resolved path is within the allowed basePath.
 * Prevents path traversal attacks.
 */
function assertWithin(filePath: string, basePath: string): void {
  const resolvedFile = path.resolve(filePath);
  const resolvedBase = path.resolve(basePath);
  if (!resolvedFile.startsWith(resolvedBase + '/') && resolvedFile !== resolvedBase) {
    throw new AgentError(403, `Path ${filePath} is outside allowed directory`, 'PATH_TRAVERSAL');
  }
}

export async function readEnvFile(basePath: string): Promise<Record<string, string>> {
  const envPath = path.join(basePath, '.env');
  const content = await fs.readFile(envPath, 'utf-8');
  return parseDotenv(Buffer.from(content));
}

export async function writeFiles(
  basePath: string,
  files: Array<{ relativePath: string; content: string }>
): Promise<void> {
  for (const file of files) {
    const filePath = path.join(basePath, file.relativePath);
    assertWithin(filePath, basePath);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, file.content, 'utf-8');
    logger.debug(`[files] Wrote ${filePath}`);
  }
}

export async function mkdirp(basePath: string, relativePath: string): Promise<void> {
  const dirPath = path.join(basePath, relativePath);
  assertWithin(dirPath, basePath);
  await fs.mkdir(dirPath, { recursive: true });
}

/** Validate git repo URL and branch name to prevent shell injection. */
const SAFE_BRANCH = /^[a-zA-Z0-9][a-zA-Z0-9_.\/-]{0,99}$/;
const SAFE_REPO = /^[a-zA-Z0-9@:._\/-]+$/;
const SAFE_EXCLUDE = /^[a-zA-Z0-9_.\/-]+$/;

export async function cloneSource(
  basePath: string,
  gitRepo: string,
  gitBranch: string,
  excludes?: string[]
): Promise<void> {
  // SECURITY: Validate inputs before any shell execution
  if (!SAFE_REPO.test(gitRepo)) {
    throw new AgentError(400, 'Invalid git repository URL', 'VALIDATION');
  }
  if (!SAFE_BRANCH.test(gitBranch)) {
    throw new AgentError(400, 'Invalid git branch name', 'VALIDATION');
  }

  const { execFile } = await import('child_process');
  const { promisify } = await import('util');
  const execFileAsync = promisify(execFile);

  // Ensure base directory exists
  await fs.mkdir(basePath, { recursive: true });

  // Clone into a temp directory first, then move contents
  const tmpDir = `${basePath}.tmp-${Date.now()}`;
  try {
    // SECURITY: Use execFile with args array — no shell interpolation
    await execFileAsync('git', ['clone', '--branch', gitBranch, '--depth', '1', gitRepo, tmpDir], {
      timeout: 300_000,
    });

    // Remove git metadata and excluded directories
    const defaultExcludes = excludes || [
      '.git', 'node_modules', 'changemaker-control-panel', '.claude',
      'api/dist', 'admin/dist',
    ];
    for (const exclude of defaultExcludes) {
      // SECURITY: Validate each exclude entry
      if (!SAFE_EXCLUDE.test(exclude)) continue;
      const excludePath = path.join(tmpDir, exclude);
      // SECURITY: Verify exclude path is within tmpDir
      if (!path.resolve(excludePath).startsWith(path.resolve(tmpDir) + '/')) continue;
      try {
        await fs.rm(excludePath, { recursive: true, force: true });
      } catch { /* ignore if doesn't exist */ }
    }

    // Move contents to basePath using execFile (no shell)
    await execFileAsync('rsync', ['-a', `${tmpDir}/`, `${basePath}/`], { timeout: 120_000 });
    await fs.rm(tmpDir, { recursive: true, force: true });

    logger.info(`[files] Cloned ${gitRepo}@${gitBranch} → ${basePath}`);
  } catch (err) {
    // Clean up temp dir on failure
    try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
    throw err;
  }
}
