// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * Docker Environment Analyzer — Pre-analyzes benchmark challenge Docker setups.
 *
 * Parses docker-compose.yml, Dockerfiles, and dependency files to build a
 * structured EnvironmentBrief before solver invocation. Pure functions —
 * reads files and returns data, no side effects.
 *
 * @module benchmark/docker-analyzer
 */

import { readFileSync, existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import YAML from 'yaml';

/** Known database image prefixes → database name */
const DB_IMAGE_MAP = {
  'mysql':     'MySQL',
  'mariadb':   'MariaDB',
  'postgres':  'PostgreSQL',
  'mongo':     'MongoDB',
  'redis':     'Redis',
  'sqlite':    'SQLite',
  'mssql':     'MSSQL',
  'cassandra': 'Cassandra',
};

/** Known database service name substrings */
const DB_SERVICE_NAMES = ['db', 'mysql', 'mariadb', 'postgres', 'mongo', 'redis', 'database', 'mssql'];

/** Environment variable prefixes that signal a database */
const DB_ENV_PREFIXES = ['MYSQL_', 'MONGO_', 'POSTGRES_', 'PGHOST', 'REDIS_', 'MARIADB_', 'MSSQL_'];

/** Framework detection from dependency names */
const FRAMEWORK_MAP = {
  'flask':     { framework: 'Flask',     language: 'Python' },
  'django':    { framework: 'Django',    language: 'Python' },
  'fastapi':   { framework: 'FastAPI',   language: 'Python' },
  'express':   { framework: 'Express',   language: 'Node.js' },
  'koa':       { framework: 'Koa',       language: 'Node.js' },
  'hapi':      { framework: 'Hapi',      language: 'Node.js' },
  'sinatra':   { framework: 'Sinatra',   language: 'Ruby' },
  'rails':     { framework: 'Rails',     language: 'Ruby' },
  'laravel':   { framework: 'Laravel',   language: 'PHP' },
  'symfony':   { framework: 'Symfony',   language: 'PHP' },
  'gin':       { framework: 'Gin',       language: 'Go' },
  'spring':    { framework: 'Spring',    language: 'Java' },
  'starlette': { framework: 'Starlette', language: 'Python' },
  'uvicorn':   { framework: 'Uvicorn',   language: 'Python' },
};

/** Language detection from base image */
const IMAGE_LANGUAGE_MAP = {
  'python': 'Python',
  'node':   'Node.js',
  'ruby':   'Ruby',
  'php':    'PHP',
  'golang': 'Go',
  'go':     'Go',
  'java':   'Java',
  'openjdk':'Java',
  'rust':   'Rust',
  'debian': null,
  'ubuntu': null,
  'alpine': null,
};

/**
 * Read a file as UTF-8, returning null on any error.
 */
function safeReadFile(filePath) {
  try {
    return readFileSync(filePath, 'utf-8');
  } catch {
    return null;
  }
}

/**
 * Parse a docker-compose.yml string into a services map.
 * Returns { services: {}, raw: {} } or empty on failure.
 */
function parseCompose(content) {
  try {
    const doc = YAML.parse(content);
    if (!doc || typeof doc !== 'object') return { services: {}, raw: {} };
    // Compose v2+ uses top-level 'services', v1 puts services at root
    const services = doc.services || doc;
    return { services, raw: doc };
  } catch {
    return { services: {}, raw: {} };
  }
}

/**
 * Extract port numbers from a compose service's ports array.
 * Handles: "80", "8080:80", "0.0.0.0:8080:80/tcp"
 * Returns the container port when host:container mapped, or the lone port.
 */
function extractPorts(portsArray) {
  if (!Array.isArray(portsArray)) return [];
  const ports = [];
  for (const p of portsArray) {
    const s = String(p).replace(/\/\w+$/, ''); // strip /tcp /udp
    const parts = s.split(':');
    // Last segment is always the container port
    const containerPort = parseInt(parts[parts.length - 1], 10);
    if (!isNaN(containerPort)) ports.push(containerPort);
  }
  return [...new Set(ports)];
}

/**
 * Get the build context path from a compose service's build field.
 * Handles: string ("./app") or object ({ context: "./app", args: ... })
 */
function getBuildContext(buildField) {
  if (!buildField) return null;
  if (typeof buildField === 'string') return buildField;
  if (typeof buildField === 'object' && buildField.context) return buildField.context;
  return null;
}

/**
 * Parse a Dockerfile and extract structured info.
 */
function parseDockerfile(content) {
  const result = {
    baseImage: null,
    expose: [],
    cmd: null,
    entrypoint: null,
    user: null,
    hasArgFlag: false,
    flagMethod: null,
    aptPackages: [],
    pipInstall: false,
    npmInstall: false,
    requirementsPath: null,
  };

  if (!content) return result;

  for (const rawLine of content.split('\n')) {
    const line = rawLine.trim();

    // FROM
    if (/^FROM\s+/i.test(line)) {
      result.baseImage = line.replace(/^FROM\s+/i, '').split(/\s+/)[0];
    }

    // EXPOSE
    if (/^EXPOSE\s+/i.test(line)) {
      const nums = line.match(/\d+/g);
      if (nums) result.expose.push(...nums.map(Number));
    }

    // CMD
    if (/^CMD\s+/i.test(line)) {
      result.cmd = line.replace(/^CMD\s+/i, '').trim();
    }

    // ENTRYPOINT
    if (/^ENTRYPOINT\s+/i.test(line)) {
      result.entrypoint = line.replace(/^ENTRYPOINT\s+/i, '').trim();
    }

    // USER
    if (/^USER\s+/i.test(line)) {
      result.user = line.replace(/^USER\s+/i, '').trim();
    }

    // ARG FLAG
    if (/^ARG\s+FLAG\b/i.test(line)) {
      result.hasArgFlag = true;
    }

    // Flag injection method: RUN echo $FLAG > /flag or RUN sed -i ...FLAG...
    if (result.hasArgFlag && /\$FLAG/i.test(line)) {
      if (/echo.*\$FLAG.*>.*\/flag/i.test(line)) {
        result.flagMethod = 'ARG FLAG → /flag file';
      } else if (/sed\s+.*FLAG/i.test(line)) {
        result.flagMethod = 'ARG FLAG → sed substitution';
      } else if (!result.flagMethod) {
        result.flagMethod = 'ARG FLAG → inline';
      }
    }

    // RUN pip install -r <path>
    const pipMatch = line.match(/pip\s+install\s+(?:--no-cache-dir\s+)?-r\s+(\S+)/i);
    if (pipMatch) {
      result.pipInstall = true;
      result.requirementsPath = pipMatch[1];
    }

    // RUN npm install
    if (/npm\s+install/i.test(line)) {
      result.npmInstall = true;
    }

    // RUN apt-get install packages
    const aptMatch = line.match(/apt-get\s+install\s+(?:-y\s+)?(.+)/i);
    if (aptMatch) {
      const pkgs = aptMatch[1]
        .replace(/&&.*/, '')
        .replace(/\\$/, '')
        .split(/\s+/)
        .filter(p => p && !p.startsWith('-'));
      result.aptPackages.push(...pkgs);
    }
  }

  return result;
}

/**
 * Parse requirements.txt → array of { name, version }
 */
function parseRequirementsTxt(content) {
  if (!content) return [];
  const deps = [];
  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
    // Handle ==, >=, <=, ~=, !=
    const match = trimmed.match(/^([a-zA-Z0-9_.-]+)\s*(?:([=<>!~]+)\s*(.+))?/);
    if (match) {
      deps.push({ name: match[1].toLowerCase(), version: match[3] || null });
    }
  }
  return deps;
}

/**
 * Parse package.json → array of { name, version }
 */
function parsePackageJson(content) {
  if (!content) return [];
  try {
    const pkg = JSON.parse(content);
    const all = { ...pkg.dependencies, ...pkg.devDependencies };
    return Object.entries(all).map(([name, version]) => ({
      name: name.toLowerCase(),
      version: version.replace(/^[\^~>=<]/, ''),
    }));
  } catch {
    return [];
  }
}

/**
 * Detect framework from a dependency list.
 * Returns { framework, frameworkVersion, language } or partial.
 */
function detectFramework(deps) {
  for (const dep of deps) {
    const key = dep.name.toLowerCase();
    if (FRAMEWORK_MAP[key]) {
      return {
        framework: FRAMEWORK_MAP[key].framework,
        frameworkVersion: dep.version || null,
        language: FRAMEWORK_MAP[key].language,
      };
    }
  }
  return { framework: null, frameworkVersion: null, language: null };
}

/**
 * Detect language from base image when not already identified via deps.
 */
function detectLanguageFromImage(baseImage) {
  if (!baseImage) return null;
  const imageName = baseImage.split(':')[0].split('/').pop().toLowerCase();
  for (const [prefix, lang] of Object.entries(IMAGE_LANGUAGE_MAP)) {
    if (imageName.startsWith(prefix)) return lang;
  }
  return null;
}

/**
 * Detect database from compose services (depends_on, images, env vars).
 */
function detectDatabase(services, appServiceName) {
  const databases = [];

  for (const [name, svc] of Object.entries(services)) {
    if (name === appServiceName) continue;

    // Check if service name matches known DB names
    const lowerName = name.toLowerCase();
    if (DB_SERVICE_NAMES.some(db => lowerName.includes(db))) {
      // Try to identify which DB from image
      const image = svc.image || '';
      const dbFromImage = identifyDbFromImage(image);
      if (dbFromImage) {
        databases.push(dbFromImage);
        continue;
      }
      // If it has a build context, we might know from its Dockerfile later
      databases.push(name);
      continue;
    }

    // Check image field directly
    if (svc.image) {
      const dbFromImage = identifyDbFromImage(svc.image);
      if (dbFromImage) {
        databases.push(dbFromImage);
        continue;
      }
    }

    // Check environment variables
    const envVars = extractEnvKeys(svc);
    for (const key of envVars) {
      for (const prefix of DB_ENV_PREFIXES) {
        if (key.toUpperCase().startsWith(prefix)) {
          const dbName = prefix.replace(/_$/, '');
          databases.push(DB_IMAGE_MAP[dbName.toLowerCase()] || dbName);
        }
      }
    }
  }

  return [...new Set(databases)];
}

/**
 * Identify database type from a Docker image string.
 */
function identifyDbFromImage(image) {
  if (!image) return null;
  const imageName = image.split(':')[0].split('/').pop().toLowerCase();
  for (const [prefix, dbName] of Object.entries(DB_IMAGE_MAP)) {
    if (imageName.startsWith(prefix)) return dbName;
  }
  return null;
}

/**
 * Extract environment variable keys from a compose service.
 * Handles both array ["KEY=val"] and object { KEY: val } forms.
 */
function extractEnvKeys(svc) {
  const env = svc.environment;
  if (!env) return [];
  if (Array.isArray(env)) {
    return env.map(e => String(e).split('=')[0]);
  }
  if (typeof env === 'object') {
    return Object.keys(env);
  }
  return [];
}

/**
 * Find dependency files in a directory tree (shallow search).
 * Looks at contextDir, contextDir/website/, contextDir/app/, etc.
 */
function findDependencyFile(contextDir, filename) {
  const candidates = [
    join(contextDir, filename),
    join(contextDir, 'website', filename),
    join(contextDir, 'app', filename),
    join(contextDir, 'src', filename),
  ];
  for (const path of candidates) {
    const content = safeReadFile(path);
    if (content !== null) return content;
  }
  return null;
}

/**
 * Identify the "main" app service — the one with ports exposed that isn't a known DB.
 */
function identifyAppService(services) {
  const candidates = [];
  for (const [name, svc] of Object.entries(services)) {
    // Skip services that look like databases
    if (svc.image) {
      const dbName = identifyDbFromImage(svc.image);
      if (dbName) continue;
    }
    const lowerName = name.toLowerCase();
    const isDbName = DB_SERVICE_NAMES.some(db => lowerName === db);
    if (isDbName && !svc.ports) continue;

    // Prefer services with ports
    if (svc.ports && svc.ports.length > 0) {
      candidates.unshift(name);
    } else {
      candidates.push(name);
    }
  }
  return candidates[0] || Object.keys(services)[0] || null;
}

/**
 * Analyze a benchmark challenge directory's Docker environment.
 *
 * Reads docker-compose.yml, Dockerfiles, and dependency files to produce
 * a structured EnvironmentBrief. Never throws — missing files produce
 * partial results.
 *
 * @param {string} challengePath - Absolute path to challenge directory
 * @returns {Promise<object>} EnvironmentBrief object
 */
export function analyzeDockerEnvironment(challengePath) {
  const brief = {
    framework: null,
    frameworkVersion: null,
    language: null,
    baseImage: null,
    database: null,
    ports: [],
    flagMethod: null,
    services: [],
    packages: [],
    runAsRoot: true,
    cmd: null,
    analysisErrors: [],
  };

  if (!challengePath || !existsSync(challengePath)) {
    brief.analysisErrors.push(`Challenge path not found: ${challengePath}`);
    return brief;
  }

  // 1. Parse docker-compose.yml
  const composePath = join(challengePath, 'docker-compose.yml');
  const composeContent = safeReadFile(composePath);
  if (!composeContent) {
    // Try docker-compose.yaml
    const altContent = safeReadFile(join(challengePath, 'docker-compose.yaml'));
    if (!altContent) {
      brief.analysisErrors.push('No docker-compose.yml found');
      // Still try to find a standalone Dockerfile
      const standaloneDockerfile = safeReadFile(join(challengePath, 'Dockerfile'));
      if (standaloneDockerfile) {
        const df = parseDockerfile(standaloneDockerfile);
        Object.assign(brief, extractDockerfileInfo(df));
        const deps = loadDeps(challengePath);
        applyFrameworkDetection(brief, deps, df);
      }
      return brief;
    }
  }

  const { services } = parseCompose(composeContent || safeReadFile(join(challengePath, 'docker-compose.yaml')));
  if (!services || Object.keys(services).length === 0) {
    brief.analysisErrors.push('docker-compose.yml has no services');
    return brief;
  }

  brief.services = Object.keys(services);

  // 2. Identify the main app service
  const appServiceName = identifyAppService(services);
  const appService = services[appServiceName];

  // 3. Extract ports from the main app service
  if (appService) {
    brief.ports = extractPorts(appService.ports);
  }

  // 4. Detect database from compose structure
  const detectedDbs = detectDatabase(services, appServiceName);

  // Also check DB service Dockerfiles for image identification
  for (const [name, svc] of Object.entries(services)) {
    if (name === appServiceName) continue;
    const buildCtx = getBuildContext(svc.build);
    if (buildCtx) {
      const dbDockerfilePath = join(challengePath, buildCtx, 'Dockerfile');
      const dbDockerfile = safeReadFile(dbDockerfilePath);
      if (dbDockerfile) {
        const dbDf = parseDockerfile(dbDockerfile);
        if (dbDf.baseImage) {
          const dbFromImage = identifyDbFromImage(dbDf.baseImage);
          if (dbFromImage && !detectedDbs.includes(dbFromImage)) {
            detectedDbs.push(dbFromImage);
          }
        }
      }
    }
  }

  brief.database = detectedDbs.length > 0 ? detectedDbs.join(', ') : null;

  // 5. Parse the main app service's Dockerfile
  if (appService) {
    const buildCtx = getBuildContext(appService.build);
    if (buildCtx) {
      const contextDir = resolve(challengePath, buildCtx);
      const dockerfileContent = safeReadFile(join(contextDir, 'Dockerfile'));
      const df = parseDockerfile(dockerfileContent);

      // Apply Dockerfile findings
      Object.assign(brief, extractDockerfileInfo(df));

      // Also check EXPOSE ports if compose didn't specify
      if (brief.ports.length === 0 && df.expose.length > 0) {
        brief.ports = df.expose;
      }

      // 6. Load and parse dependency files
      const deps = loadDeps(contextDir);
      applyFrameworkDetection(brief, deps, df);
    }
  }

  // 7. Check all services for FLAG injection (it might be in a non-app service)
  if (!brief.flagMethod) {
    for (const [name, svc] of Object.entries(services)) {
      if (name === appServiceName) continue;
      const buildCtx = getBuildContext(svc.build);
      if (buildCtx) {
        const dockerfileContent = safeReadFile(join(resolve(challengePath, buildCtx), 'Dockerfile'));
        const df = parseDockerfile(dockerfileContent);
        if (df.flagMethod) {
          brief.flagMethod = `${df.flagMethod} (in ${name} service)`;
          break;
        }
      }
    }
  }

  return brief;
}

/**
 * Extract brief-relevant fields from parsed Dockerfile info.
 */
function extractDockerfileInfo(df) {
  const info = {};
  if (df.baseImage) info.baseImage = df.baseImage;
  if (df.cmd) info.cmd = df.cmd;
  if (df.entrypoint) info.cmd = df.entrypoint;
  if (df.user) info.runAsRoot = df.user === 'root';
  else info.runAsRoot = true; // no USER directive = runs as root
  if (df.flagMethod) info.flagMethod = df.flagMethod;
  return info;
}

/**
 * Load dependency files from a context directory.
 */
function loadDeps(contextDir) {
  let deps = [];

  // requirements.txt
  const reqContent = findDependencyFile(contextDir, 'requirements.txt');
  if (reqContent) {
    deps = parseRequirementsTxt(reqContent);
  }

  // package.json
  const pkgContent = findDependencyFile(contextDir, 'package.json');
  if (pkgContent) {
    deps = [...deps, ...parsePackageJson(pkgContent)];
  }

  // Gemfile (basic)
  const gemContent = findDependencyFile(contextDir, 'Gemfile');
  if (gemContent) {
    for (const line of gemContent.split('\n')) {
      const match = line.match(/gem\s+['"]([^'"]+)['"]/);
      if (match) deps.push({ name: match[1].toLowerCase(), version: null });
    }
  }

  return deps;
}

/**
 * Apply framework and language detection from dependencies and Dockerfile.
 */
function applyFrameworkDetection(brief, deps, df) {
  if (deps.length > 0) {
    const detected = detectFramework(deps);
    if (detected.framework) {
      brief.framework = detected.framework;
      brief.frameworkVersion = detected.frameworkVersion;
      brief.language = detected.language;
    }

    // Store notable packages (frameworks, ORMs, auth libs — skip utility packages)
    const notablePackages = deps
      .filter(d => FRAMEWORK_MAP[d.name] || /sql|auth|jwt|session|crypto|jinja|template|upload|serialize|pickle/i.test(d.name))
      .map(d => d.version ? `${d.name}==${d.version}` : d.name);
    if (notablePackages.length > 0) {
      brief.packages = notablePackages;
    }
  }

  // Fallback language detection from base image
  if (!brief.language && df.baseImage) {
    brief.language = detectLanguageFromImage(df.baseImage);
  }

  // PHP detection from base image
  if (!brief.framework && df.baseImage && df.baseImage.toLowerCase().includes('php')) {
    brief.framework = 'PHP';
    brief.language = 'PHP';
  }
}

/**
 * Format an EnvironmentBrief into a concise human-readable text block
 * suitable for prompt injection (~300-500 chars).
 *
 * @param {object} brief - EnvironmentBrief from analyzeDockerEnvironment()
 * @returns {string} Formatted multi-line summary
 */
export function formatEnvironmentBrief(brief) {
  if (!brief) return '';

  const lines = [];

  // Stack line
  const stack = [];
  if (brief.framework) {
    let fw = brief.framework;
    if (brief.frameworkVersion) fw += ` ${brief.frameworkVersion}`;
    stack.push(fw);
  }
  if (brief.language && !(brief.framework || '').toLowerCase().includes(brief.language.toLowerCase())) {
    stack.push(`(${brief.language})`);
  }
  if (brief.baseImage) {
    stack.push(`on ${brief.baseImage}`);
  }
  if (stack.length > 0) {
    lines.push(`Stack: ${stack.join(' ')}`);
  }

  // Database
  if (brief.database) {
    const dbServices = brief.services?.length > 1 ? ' (separate service)' : '';
    lines.push(`Database: ${brief.database}${dbServices}`);
  }

  // Ports
  if (brief.ports && brief.ports.length > 0) {
    lines.push(`Ports: ${brief.ports.join(', ')}`);
  }

  // Flag method
  if (brief.flagMethod) {
    lines.push(`Flag: ${brief.flagMethod}`);
  }

  // Key packages
  if (brief.packages && brief.packages.length > 0) {
    lines.push(`Key packages: ${brief.packages.join(', ')}`);
  }

  // Running as
  if (brief.runAsRoot !== undefined) {
    lines.push(`Running as: ${brief.runAsRoot ? 'root' : brief.user || 'non-root'}`);
  }

  // Services overview (if multiple)
  if (brief.services && brief.services.length > 1) {
    lines.push(`Services: ${brief.services.join(', ')}`);
  }

  return lines.join('\n');
}
