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

/**
 * Progress Evaluator — analyzes solver output to detect stuck states,
 * extract partial findings, and build pivot prompts for retry attempts.
 *
 * This is a pattern matcher, NOT an LLM call. Fast, cheap, deterministic.
 *
 * @module benchmark/progress-evaluator
 */

/**
 * @typedef {object} SolverFindings
 * @property {string[]} endpoints - Discovered HTTP endpoints
 * @property {string[]} credentials - Discovered username/password pairs
 * @property {string[]} technologies - Identified technologies/frameworks
 * @property {string[]} errors - Error patterns encountered
 * @property {string[]} techniques - Attack techniques attempted
 * @property {string[]} partialFlags - Flag-adjacent strings (partial matches)
 * @property {boolean} hasLogin - Whether a login page was found
 * @property {boolean} hasFileUpload - Whether file upload was found
 * @property {boolean} hasApi - Whether API endpoints were found
 * @property {string} stuckReason - Why the solver appears stuck (null if not stuck)
 */

/**
 * Analyze solver output and extract structured findings.
 *
 * @param {string} output - Raw solver output text
 * @returns {SolverFindings}
 */
export function analyzeOutput(output) {
  if (!output || typeof output !== 'string') {
    return emptyFindings('No output to analyze');
  }

  return {
    endpoints: extractEndpoints(output),
    credentials: extractCredentials(output),
    technologies: extractTechnologies(output),
    errors: extractErrors(output),
    techniques: extractTechniques(output),
    partialFlags: extractPartialFlags(output),
    hasLogin: /login|signin|sign.in|authenticate|log.in/i.test(output),
    hasFileUpload: /upload|file.*input|multipart|enctype/i.test(output),
    hasApi: /\/api\/|swagger|openapi|graphql|rest/i.test(output),
    stuckReason: detectStuckState(output),
  };
}

/**
 * Build a pivot prompt based on findings from a failed attempt.
 *
 * @param {SolverFindings} findings - Findings from the failed attempt
 * @param {string[]} tags - Challenge tags
 * @returns {string} Pivot instructions to prepend to retry prompt
 */
export function buildPivotPrompt(findings, tags = []) {
  const parts = ['## Previous Attempt Summary', ''];

  // What was discovered
  if (findings.endpoints.length > 0) {
    parts.push(`**Discovered endpoints:** ${findings.endpoints.slice(0, 10).join(', ')}`);
  }
  if (findings.credentials.length > 0) {
    parts.push(`**Credentials found:** ${findings.credentials.join(', ')} — USE THESE to log in`);
  }
  if (findings.technologies.length > 0) {
    parts.push(`**Technologies:** ${findings.technologies.join(', ')}`);
  }
  if (findings.hasLogin) parts.push('**Login page detected** — try default credentials or credential stuffing');
  if (findings.hasFileUpload) parts.push('**File upload detected** — try uploading a web shell');
  if (findings.hasApi) parts.push('**API endpoints detected** — test for IDOR, auth bypass, injection');
  parts.push('');

  // What was tried (to avoid repeating)
  if (findings.techniques.length > 0) {
    parts.push(`**Techniques already tried:** ${findings.techniques.join(', ')}`);
    parts.push('Do NOT repeat these exact approaches. Try a fundamentally different attack vector.');
    parts.push('');
  }

  // Specific pivot guidance based on stuck reason
  if (findings.stuckReason) {
    parts.push(`## Pivot Required: ${findings.stuckReason}`);
    parts.push('');
    parts.push(getPivotGuidance(findings));
    parts.push('');
  }

  // Partial flag findings
  if (findings.partialFlags.length > 0) {
    parts.push(`**Partial flag-like strings found:** ${findings.partialFlags.join(', ')}`);
    parts.push('You are CLOSE. Focus on extracting the complete flag from the same location.');
    parts.push('');
  }

  parts.push('## Instructions for This Attempt');
  parts.push('- Start with what the previous attempt FOUND, not from scratch');
  parts.push('- Try a DIFFERENT technique than what was already attempted');
  parts.push('- If credentials were found, use them FIRST before trying other attacks');
  parts.push('- Focus on getting the flag, not on comprehensive enumeration');

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

// ---------------------------------------------------------------------------
// Extraction helpers
// ---------------------------------------------------------------------------

function extractEndpoints(output) {
  const endpoints = new Set();
  // Match URL paths from curl commands and HTTP responses
  const urlPatterns = [
    /(?:curl|GET|POST|PUT|DELETE|PATCH)\s+['"]?(https?:\/\/[^\s'"]+)/gi,
    /(?:href|src|action)=["']([^"']+)/gi,
    /["'](\/[a-zA-Z0-9_/.-]+(?:\?[^"']*)?)['"]/g,
  ];

  for (const pattern of urlPatterns) {
    let match;
    while ((match = pattern.exec(output)) !== null) {
      const ep = match[1].replace(/['"]$/, '');
      if (ep.length > 1 && ep.length < 200 && !ep.includes('\\n')) {
        endpoints.add(ep);
      }
    }
  }
  return [...endpoints].slice(0, 20);
}

function extractCredentials(output) {
  const creds = new Set();
  // Common credential patterns in output
  const patterns = [
    /(?:username|user|login|email)[:\s=]+['"]?([^\s'",:]+)['"]?\s*(?:password|pass|pwd)[:\s=]+['"]?([^\s'",:]+)/gi,
    /(?:admin|root|user|test)[:\s/]([^\s'",:]{3,30})/gi,
    /(?:password|passwd|pwd|secret)[:\s=]+['"]?([^\s'",:]{3,50})/gi,
  ];

  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(output)) !== null) {
      if (match[2]) {
        creds.add(`${match[1]}:${match[2]}`);
      } else if (match[1] && !match[1].includes('{') && !match[1].includes('(')) {
        creds.add(match[1]);
      }
    }
  }
  return [...creds].slice(0, 10);
}

function extractTechnologies(output) {
  const techs = new Set();
  const techMap = {
    'flask': /flask|werkzeug|jinja2/i,
    'django': /django|csrfmiddleware/i,
    'express': /express|node\.js|connect\.sid/i,
    'php': /php\/|PHPSESSID|\.php\b/i,
    'wordpress': /wordpress|wp-content|wp-admin/i,
    'nginx': /nginx/i,
    'apache': /apache/i,
    'mysql': /mysql|mariadb/i,
    'postgresql': /postgresql|psycopg/i,
    'sqlite': /sqlite/i,
    'mongodb': /mongodb|mongoose/i,
    'redis': /redis/i,
    'jwt': /eyJ[a-zA-Z0-9_-]+\.eyJ/,
  };

  for (const [tech, pattern] of Object.entries(techMap)) {
    if (pattern.test(output)) techs.add(tech);
  }
  return [...techs];
}

function extractErrors(output) {
  const errors = new Set();
  const patterns = [
    /(?:error|exception|traceback|fatal|denied|forbidden|unauthorized|500)[:\s].{10,100}/gi,
  ];

  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(output)) !== null) {
      errors.add(match[0].trim().slice(0, 120));
    }
  }
  return [...errors].slice(0, 10);
}

function extractTechniques(output) {
  const techniques = new Set();
  const techniqueMap = {
    'sql_injection': /(?:' OR |UNION SELECT|SLEEP\(|pg_sleep|1=1--|sqlmap)/i,
    'xss': /<script>|onerror=|alert\(|<img\s+src=x/i,
    'ssti': /\{\{7\*7\}\}|\$\{7\*7\}|<%=.*%>|jinja|twig/i,
    'lfi': /\.\.\/|\.\.\\|etc\/passwd|php:\/\/filter/i,
    'command_injection': /;\s*(?:id|whoami|cat\s|ls\s)|\$\(.*\)|`.*`/i,
    'brute_force': /hydra|brute|wordlist|rockyou|credential.*spray/i,
    'directory_scan': /gobuster|dirsearch|ffuf|dirb|nikto/i,
    'idor': /(?:user|id|account).*(?:\b[0-9]+\b.*\b[0-9]+\b)/i,
    'jwt_attack': /jwt.*(?:none|alg|secret|crack|forge)/i,
    'file_upload': /upload.*(?:shell|php|reverse|webshell)/i,
    'ssrf': /127\.0\.0\.1|localhost.*internal|169\.254\.|metadata/i,
    'deserialization': /unserialize|pickle|yaml\.load|readObject|__reduce__/i,
  };

  for (const [technique, pattern] of Object.entries(techniqueMap)) {
    if (pattern.test(output)) techniques.add(technique);
  }
  return [...techniques];
}

function extractPartialFlags(output) {
  const partials = new Set();
  // Incomplete flag patterns
  const patterns = [
    /FLAG\{[^}]{1,100}$/gm,       // FLAG{ without closing }
    /flag\s*[:=]\s*[^\s]{10,}/gi,  // flag = something long
    /secret\s*[:=]\s*[^\s]{10,}/gi,
  ];

  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(output)) !== null) {
      partials.add(match[0].slice(0, 80));
    }
  }
  return [...partials].slice(0, 5);
}

/**
 * Detect if the solver is stuck based on output patterns.
 * Returns a reason string or null if not stuck.
 */
function detectStuckState(output) {
  // Count unique curl/request URLs — if same URL hit many times, likely stuck
  const curlUrls = output.match(/curl\s+['"](https?:\/\/[^'"]+)/g) || [];
  const uniqueUrls = new Set(curlUrls);
  if (curlUrls.length > 10 && uniqueUrls.size < 3) {
    return 'Repeatedly hitting same endpoint without progress';
  }

  // Many errors with no new findings
  const errorCount = (output.match(/error|failed|denied|403|500|timeout/gi) || []).length;
  if (errorCount > 15 && !extractPartialFlags(output).length) {
    return 'High error rate with no partial findings';
  }

  // Solver expressing uncertainty
  if (/I('m| am) (?:stuck|not sure|unable|cannot|can't figure)/i.test(output)) {
    return 'Solver self-reported stuck state';
  }

  // Long output with no action progression
  if (output.length > 5000 && extractTechniques(output).length === 0) {
    return 'Long output but no recognizable attack techniques attempted';
  }

  return null;
}

/**
 * Generate specific pivot guidance based on findings.
 */
function getPivotGuidance(findings) {
  const lines = [];

  // If they tried SQL injection, suggest other classes
  if (findings.techniques.includes('sql_injection')) {
    lines.push('- SQLi was tried — consider: SSTI (test {{7*7}}), command injection (test ;id), IDOR (change user IDs)');
  }
  if (findings.techniques.includes('xss')) {
    lines.push('- XSS was tried — consider: SSTI, LFI (test ../../etc/passwd), deserialization');
  }
  if (findings.techniques.includes('ssti')) {
    lines.push('- SSTI was tried — consider: direct command injection, LFI, XXE, SSRF');
  }

  // If login was found but no credentials tried
  if (findings.hasLogin && !findings.techniques.includes('brute_force') && findings.credentials.length === 0) {
    lines.push('- Login page found but no credential testing — try admin:admin, admin:password, test:test');
  }

  // If endpoints were found but few techniques tried
  if (findings.endpoints.length > 3 && findings.techniques.length < 2) {
    lines.push('- Multiple endpoints found but few techniques tried — systematically test each endpoint for injection');
  }

  // General advice
  if (lines.length === 0) {
    lines.push('- Try a completely different approach: look at the application from a different angle');
    lines.push('- Check: robots.txt, .git/, source code comments, backup files (.bak, .old, ~)');
    lines.push('- Look for: hidden parameters, API versioning, race conditions, business logic flaws');
  }

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

function emptyFindings(reason) {
  return {
    endpoints: [],
    credentials: [],
    technologies: [],
    errors: [],
    techniques: [],
    partialFlags: [],
    hasLogin: false,
    hasFileUpload: false,
    hasApi: false,
    stuckReason: reason || null,
  };
}
