import fs from 'fs/promises';
import path from 'path';
import { promisify } from 'util';
import { parse as parseDotenv } from 'dotenv';
import * as docker from './docker.service';
import type { ExecutionDriver } from './execution-driver';
import { logger } from '../utils/logger';

/**
 * LocalDriver wraps existing docker.service.ts functions and filesystem operations.
 * This is a zero-behavior-change adapter — all existing local instance operations
 * pass through unchanged.
 */
export class LocalDriver implements ExecutionDriver {
  // ─── Docker Compose Operations ──────────────────────────────

  composeUp(projectDir: string, project: string, services?: string[]) {
    return docker.composeUp(projectDir, project, services);
  }

  composeDown(projectDir: string, project: string, removeVolumes?: boolean) {
    return docker.composeDown(projectDir, project, removeVolumes);
  }

  composeStop(projectDir: string, project: string) {
    return docker.composeStop(projectDir, project);
  }

  composeRestart(projectDir: string, project: string, service?: string) {
    return docker.composeRestart(projectDir, project, service);
  }

  composePull(projectDir: string, project: string) {
    return docker.composePull(projectDir, project);
  }

  composeBuild(projectDir: string, project: string) {
    return docker.composeBuild(projectDir, project);
  }

  composePs(projectDir: string, project: string) {
    return docker.composePs(projectDir, project);
  }

  composeLogs(projectDir: string, project: string, service?: string, tail?: number, since?: string) {
    return docker.composeLogs(projectDir, project, service, tail, since);
  }

  composeExec(projectDir: string, project: string, service: string, command: string, timeoutMs?: number, envVars?: Record<string, string>) {
    return docker.composeExec(projectDir, project, service, command, timeoutMs, envVars);
  }

  // ─── Container Health ───────────────────────────────────────

  waitForHealthy(containerName: string, timeoutMs?: number, pollIntervalMs?: number) {
    return docker.waitForHealthy(containerName, timeoutMs, pollIntervalMs);
  }

  waitForHttp(url: string, timeoutMs?: number, pollIntervalMs?: number) {
    return docker.waitForHttp(url, timeoutMs, pollIntervalMs);
  }

  // ─── Filesystem Operations ──────────────────────────────────

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

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

  // Approach C pre-flight diff. Reads current file contents at basePath +
  // relativePath, returns per-file status + diff. Local implementation
  // mirrors the agent-side diffFiles helper.
  async diffFiles(basePath: string, files: Array<{ relativePath: string; content: string }>) {
    const results: Array<{ path: string; status: 'unchanged' | 'modified' | 'created'; diff: string | null; sizeBefore: number; sizeAfter: number }> = [];
    for (const file of files) {
      const filePath = path.join(basePath, file.relativePath);
      const sizeAfter = Buffer.byteLength(file.content, 'utf-8');
      let current: string | null = null;
      try { current = await fs.readFile(filePath, 'utf-8'); } catch { current = null; }
      if (current === null) {
        results.push({ path: file.relativePath, status: 'created', diff: null, sizeBefore: 0, sizeAfter });
      } else if (current === file.content) {
        results.push({ path: file.relativePath, status: 'unchanged', diff: null, sizeBefore: Buffer.byteLength(current), sizeAfter });
      } else {
        // Minimal diff for local: full new content. Local mode is dev-only;
        // detailed diffs come from the agent-side implementation.
        results.push({
          path: file.relativePath,
          status: 'modified',
          diff: `--- a/${file.relativePath}\n+++ b/${file.relativePath}\n(local-driver: showing new content only)\n${file.content}`,
          sizeBefore: Buffer.byteLength(current),
          sizeAfter,
        });
      }
    }
    return results;
  }

  // Approach C: patch specific .env keys in place (local mirror of the
  // agent-side patchEnv helper). Preserves comments and key order.
  async patchEnv(basePath: string, vars: Record<string, string>) {
    const envPath = path.join(basePath, '.env');
    const ENV_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
    for (const [k, v] of Object.entries(vars)) {
      if (!ENV_KEY_RE.test(k)) throw new Error(`Invalid env key: ${k}`);
      if (/[\r\n]/.test(v)) throw new Error(`env value for ${k} contains newline`);
    }
    const current = await fs.readFile(envPath, 'utf-8');
    const lines = current.split('\n');
    const patched: string[] = [];
    const remaining = new Set(Object.keys(vars));
    for (let i = 0; i < lines.length; i++) {
      const m = lines[i].match(/^([A-Z_][A-Z0-9_]*)=/);
      if (m && remaining.has(m[1])) {
        lines[i] = `${m[1]}=${vars[m[1]]}`;
        patched.push(m[1]);
        remaining.delete(m[1]);
      }
    }
    const added: string[] = [];
    if (remaining.size > 0) {
      while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
      lines.push('', '# Added by CCP env-patch');
      for (const k of remaining) { lines.push(`${k}=${vars[k]}`); added.push(k); }
      lines.push('');
    }
    await fs.writeFile(envPath, lines.join('\n'), 'utf-8');
    return { patched, added };
  }

  async mkdir(basePath: string, relativePath: string) {
    await fs.mkdir(path.join(basePath, relativePath), { recursive: true });
  }

  async fileExists(basePath: string, relativePath: string): Promise<boolean> {
    try {
      await fs.access(path.join(basePath, relativePath));
      return true;
    } catch {
      return false;
    }
  }

  async deleteDirectory(dirPath: string) {
    await fs.rm(dirPath, { recursive: true, force: true });
  }

  async cloneSource(basePath: string, _gitRepo: string, _gitBranch: string, excludes?: string[]) {
    // Local provisioning uses rsync from CML_SOURCE_PATH
    const { CML_SOURCE_PATH } = await import('../config/env').then((m) => m.env);
    if (!CML_SOURCE_PATH) {
      throw new Error('CML_SOURCE_PATH not configured — cannot clone source');
    }

    // SECURITY: Validate exclude entries — reject anything with shell metacharacters
    const SAFE_EXCLUDE = /^[a-zA-Z0-9_.\/-]+$/;
    const safeExcludes = (excludes || [
      'node_modules', '.git', '.env', 'changemaker-control-panel', '.claude',
      'api/dist', 'admin/dist', 'uploads', 'data',
    ]).filter((e) => SAFE_EXCLUDE.test(e));

    // SECURITY: Use execFile with args array — no shell interpolation
    const { execFile: execFileCb } = await import('child_process');
    const execFileAsync = promisify(execFileCb);
    const args = ['-a', ...safeExcludes.flatMap((e) => ['--exclude', e]), `${CML_SOURCE_PATH}/`, `${basePath}/`];
    await execFileAsync('rsync', args, { timeout: 120_000 });
  }
}

/** Singleton local driver instance. */
let _localDriver: LocalDriver | null = null;

export function getLocalDriver(): LocalDriver {
  if (!_localDriver) {
    _localDriver = new LocalDriver();
  }
  return _localDriver;
}
