import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { fetchEngine } from '@/lib/engine-server';
import { injectTokenIntoRepoUrl, detectVcsProvider } from '@/lib/vcs-token';

const execFileAsync = promisify(execFile);

const MAX_FILES = 5000;
const MAX_TOTAL_BYTES = 500 * 1024 * 1024;
const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'build', 'target', '.next', '__pycache__', '.venv', 'venv']);

function globToRegExp(pattern: string): RegExp {
  const escaped = pattern
    .trim()
    .replace(/[.+^${}()|[\]\\]/g, '\\$&')
    .replace(/\*\*/g, '§§')
    .replace(/\*/g, '[^/]*')
    .replace(/§§/g, '.*')
    .replace(/\?/g, '[^/]');
  return new RegExp(`^${escaped}$`);
}

function matchesAnyGlob(relPath: string, patterns: string): boolean {
  const list = patterns.split(',').map((s) => s.trim()).filter(Boolean);
  if (list.length === 0) return true;
  return list.some((p) => globToRegExp(p).test(relPath));
}

function shouldIncludeFile(relPath: string, includePatterns?: string, excludePatterns?: string): boolean {
  if (excludePatterns && matchesAnyGlob(relPath, excludePatterns)) return false;
  if (includePatterns && includePatterns.trim().length > 0) {
    return matchesAnyGlob(relPath, includePatterns);
  }
  return true;
}

async function walkDir(
  root: string,
  dir: string,
  includePatterns?: string,
  excludePatterns?: string,
  acc: Array<{ relPath: string; absPath: string; size: number }> = [],
): Promise<Array<{ relPath: string; absPath: string; size: number }>> {
  if (acc.length >= MAX_FILES) return acc;

  const entries = await fs.readdir(dir, { withFileTypes: true });
  for (const entry of entries) {
    if (acc.length >= MAX_FILES) break;
    const absPath = path.join(dir, entry.name);
    const relPath = path.relative(root, absPath).split(path.sep).join('/');

    if (entry.isDirectory()) {
      if (SKIP_DIRS.has(entry.name)) continue;
      await walkDir(root, absPath, includePatterns, excludePatterns, acc);
      continue;
    }
    if (!entry.isFile()) continue;
    if (!shouldIncludeFile(relPath, includePatterns, excludePatterns)) continue;

    const stat = await fs.stat(absPath);
    acc.push({ relPath, absPath, size: stat.size });
  }
  return acc;
}

export async function cloneRepoToTemp(repoUrl: string, ref?: string, token?: string): Promise<string> {
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'scriba-clone-'));
  const provider = detectVcsProvider(repoUrl);
  const cloneUrl = token && provider ? injectTokenIntoRepoUrl(repoUrl, token, provider) : repoUrl;

  const args = ['clone', '--depth', '1'];
  if (ref) args.push('--branch', ref);
  args.push(cloneUrl, tmp);

  await execFileAsync('git', args, { timeout: 120_000 });
  return tmp;
}

export async function uploadDirectoryToEngine(
  rootDir: string,
  opts: { includePatterns?: string; excludePatterns?: string } = {},
): Promise<{ uploadId: string; fileCount: number; totalBytes: number }> {
  const files = await walkDir(rootDir, rootDir, opts.includePatterns, opts.excludePatterns);
  if (files.length === 0) {
    throw new Error('No source files found to upload');
  }

  let totalBytes = 0;
  for (const f of files) {
    totalBytes += f.size;
    if (totalBytes > MAX_TOTAL_BYTES) {
      throw new Error(`Source bundle exceeds ${MAX_TOTAL_BYTES / (1024 * 1024)} MB limit`);
    }
  }

  const form = new FormData();
  for (const f of files) {
    const buf = await fs.readFile(f.absPath);
    form.append('files', new Blob([buf]), f.relPath);
  }

  const res = await fetchEngine('uploads', { method: 'POST', body: form });
  const data = (await res.json().catch(() => ({}))) as {
    uploadId?: string;
    fileCount?: number;
    totalBytes?: number;
    error?: string;
  };
  if (!res.ok) {
    throw new Error(data.error ?? `Engine upload failed (${res.status})`);
  }
  if (!data.uploadId) throw new Error('Engine did not return uploadId');
  return {
    uploadId: data.uploadId,
    fileCount: data.fileCount ?? files.length,
    totalBytes: data.totalBytes ?? totalBytes,
  };
}

export async function prepareSourceUpload(opts: {
  repoUrl?: string;
  ref?: string;
  accessToken?: string;
  uploadId?: string;
  force?: boolean;
  includePatterns?: string;
  excludePatterns?: string;
}): Promise<{ uploadId: string; fileCount: number; totalBytes: number; reused: boolean }> {
  if (!opts.force && opts.uploadId) {
    return { uploadId: opts.uploadId, fileCount: 0, totalBytes: 0, reused: true };
  }

  const repoUrl = opts.repoUrl?.trim();
  if (!repoUrl) {
    throw new Error('No repository URL or existing uploadId — upload source files first');
  }

  let tmpDir: string | null = null;
  try {
    tmpDir = await cloneRepoToTemp(repoUrl, opts.ref, opts.accessToken);
    const uploaded = await uploadDirectoryToEngine(tmpDir, {
      includePatterns: opts.includePatterns,
      excludePatterns: opts.excludePatterns,
    });
    return { ...uploaded, reused: false };
  } finally {
    if (tmpDir) {
      await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
    }
  }
}
