import { stat } from 'node:fs/promises';
import { join } from 'node:path';
import { atomicWrite } from '../utils/atomic-write.js';
import type { Finding, ScanResult } from './scanner.js';

export interface ReportOptions {
  outputDir: string;
  format: 'markdown' | 'json' | 'html';
  includeRemediation: boolean;
  groupBySeverity: boolean;
  maxFindingsPerCategory?: number | undefined;
}

const DEFAULT_REPORT_OPTIONS: ReportOptions = {
  outputDir: 'security-reports',
  format: 'markdown',
  includeRemediation: true,
  groupBySeverity: true,
};

export class ReportGenerator {
  private options: ReportOptions;

  constructor(options: Partial<ReportOptions> = {}) {
    this.options = { ...DEFAULT_REPORT_OPTIONS, ...options };
  }

  async generate(scanResult: ScanResult): Promise<string> {
    await this.ensureOutputDir();

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
    const filename = `security-report-${timestamp}.${this.options.format}`;
    const filepath = join(this.options.outputDir, filename);

    let content: string;

    switch (this.options.format) {
      case 'json':
        content = this.generateJson(scanResult);
        break;
      case 'html':
        content = this.generateHtml(scanResult);
        break;
      default:
        content = this.generateMarkdown(scanResult);
    }

    await atomicWrite(filepath, content);
    return filepath;
  }

  private async ensureOutputDir(): Promise<void> {
    try {
      await stat(this.options.outputDir);
    } catch {
      // Directory doesn't exist, create it
      const { mkdir } = await import('node:fs/promises');
      await mkdir(this.options.outputDir, { recursive: true });
    }
  }

  private generateMarkdown(result: ScanResult): string {
    const lines: string[] = [];

    // Header
    lines.push('# Security Scan Report');
    lines.push('');
    lines.push(`**Generated:** ${new Date(result.timestamp).toLocaleString()}`);
    lines.push(`**Project:** ${result.projectRoot}`);
    lines.push(`**Tech Stack:** ${result.techStack.stack} (${result.techStack.packageManager})`);
    lines.push(`**Scanned Files:** ${result.scannedFiles}`);
    lines.push(`**Scan Duration:** ${result.scanDurationMs}ms`);
    lines.push('');

    // Summary
    lines.push('## Summary');
    lines.push('');
    lines.push('| Severity | Count |');
    lines.push('|----------|-------|');
    lines.push(`| 🔴 Critical | ${result.summary.critical} |`);
    lines.push(`| 🟠 High | ${result.summary.high} |`);
    lines.push(`| 🟡 Medium | ${result.summary.medium} |`);
    lines.push(`| 🟢 Low | ${result.summary.low} |`);
    lines.push(`| **Total** | **${result.summary.total}** |`);
    lines.push('');

    // Group by severity if requested
    if (this.options.groupBySeverity) {
      const severityGroups = this.groupBySeverity(result.findings);

      for (const severity of ['critical', 'high', 'medium', 'low'] as const) {
        const findings = severityGroups[severity] ?? [];
        if (findings.length === 0) continue;

        const emoji =
          severity === 'critical'
            ? '🔴'
            : severity === 'high'
              ? '🟠'
              : severity === 'medium'
                ? '🟡'
                : '🟢';
        lines.push(`## ${emoji} ${severity.toUpperCase()} (${findings.length})`);
        lines.push('');

        for (const finding of findings) {
          lines.push(this.formatFinding(finding));
        }

        lines.push('');
      }
    } else {
      // Group by category
      const categoryGroups = this.groupByCategory(result.findings);

      for (const [category, findings] of Object.entries(categoryGroups)) {
        lines.push(`## ${category.toUpperCase()} (${findings.length})`);
        lines.push('');

        for (const finding of findings) {
          lines.push(this.formatFinding(finding));
        }

        lines.push('');
      }
    }

    // Errors section
    if (result.errors.length > 0) {
      lines.push('## Scan Errors');
      lines.push('');
      for (const error of result.errors) {
        lines.push(`- ${error}`);
      }
      lines.push('');
    }

    // Footer
    lines.push('---');
    lines.push(`*Report generated by security scanner*`);

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

  private formatFinding(finding: Finding): string {
    const lines: string[] = [];

    lines.push(`### ${finding.title}`);
    lines.push('');
    lines.push(`**File:** \`${finding.file}${finding.line ? `:${finding.line}` : ''}\``);
    lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
    lines.push(`**Category:** ${finding.category}`);
    lines.push('');

    if (finding.snippet) {
      lines.push('```');
      lines.push(finding.snippet);
      lines.push('```');
      lines.push('');
    }

    if (this.options.includeRemediation) {
      lines.push(`**Remediation:** ${finding.remediation}`);
      lines.push('');
    }

    lines.push('---');
    lines.push('');

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

  private groupBySeverity(findings: Finding[]): Record<string, Finding[]> {
    const groups: Record<string, Finding[]> = {
      critical: [],
      high: [],
      medium: [],
      low: [],
    };

    for (const finding of findings) {
      const group = groups[finding.severity];
      if (group) {
        group.push(finding);
      }
    }

    return groups;
  }

  private groupByCategory(findings: Finding[]): Record<string, Finding[]> {
    const groups: Record<string, Finding[]> = {};

    for (const finding of findings) {
      if (groups[finding.category] === undefined) {
        groups[finding.category] = [];
      }
      groups[finding.category]?.push(finding);
    }

    return groups;
  }

  private generateJson(result: ScanResult): string {
    return JSON.stringify(
      {
        meta: {
          timestamp: result.timestamp,
          projectRoot: result.projectRoot,
          techStack: result.techStack,
          scannedFiles: result.scannedFiles,
          scanDurationMs: result.scanDurationMs,
        },
        summary: result.summary,
        findings: result.findings,
        errors: result.errors,
      },
      null,
      2,
    );
  }

  private generateHtml(result: ScanResult): string {
    const severityColors: Record<string, string> = {
      critical: '#dc2626',
      high: '#ea580c',
      medium: '#ca8a04',
      low: '#16a34a',
    };

    // SECURITY: Every finding field below is treated as untrusted text. The
    // security scanner (and its LLM-assisted analysis) is allowed to emit
    // arbitrary strings — including titles, file paths, and remediation
    // text that may contain `<script>`, quotes, or HTML entities. Embedding
    // them directly into the template would create a stored XSS vector
    // when the report is opened in a browser or served by the WebUI.
    // All dynamic content passes through escapeHtml() before interpolation.
    const rows = result.findings
      .map((f) => {
        const sevColor = severityColors[f.severity] ?? '#000000';
        const loc = `${escapeHtml(f.file)}${f.line ? `:${escapeHtml(String(f.line))}` : ''}`;
        return `
      <tr>
        <td style="color: ${escapeHtml(sevColor)}; font-weight: bold;">${escapeHtml(f.severity.toUpperCase())}</td>
        <td>${escapeHtml(f.category)}</td>
        <td>${escapeHtml(f.title)}</td>
        <td><code>${loc}</code></td>
        <td>${escapeHtml(f.remediation)}</td>
      </tr>
    `;
      })
      .join('');

    return `
<!DOCTYPE html>
<html>
<head>
  <title>Security Scan Report - ${escapeHtml(new Date(result.timestamp).toLocaleDateString())}</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 2rem; }
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
    th { background: #f5f5f5; }
    code { background: #f0f0f0; padding: 2px 4px; }
  </style>
</head>
<body>
  <h1>Security Scan Report</h1>
  <p><strong>Generated:</strong> ${escapeHtml(new Date(result.timestamp).toLocaleString())}</p>
  <p><strong>Project:</strong> ${escapeHtml(result.projectRoot)}</p>
  <p><strong>Tech Stack:</strong> ${escapeHtml(result.techStack.stack)} (${escapeHtml(result.techStack.packageManager)})</p>

  <h2>Summary</h2>
  <ul>
    <li>🔴 Critical: ${result.summary.critical}</li>
    <li>🟠 High: ${result.summary.high}</li>
    <li>🟡 Medium: ${result.summary.medium}</li>
    <li>🟢 Low: ${result.summary.low}</li>
  </ul>

  <h2>Findings</h2>
  <table>
    <tr>
      <th>Severity</th>
      <th>Category</th>
      <th>Title</th>
      <th>Location</th>
      <th>Remediation</th>
    </tr>
    ${rows}
  </table>
</body>
</html>
    `.trim();
  }
}

/**
 * Minimal HTML entity escaper. Sufficient for the report's interpolation
 * needs (text content, attribute values via double quotes). Intentionally
 * NOT a full sanitizer — output is well-formed HTML, but dangerous
 * patterns like `javascript:` URLs are still rejected upstream by the
 * scanner's input validation. If richer content (Markdown, links) is
 * ever embedded, swap this for a vetted sanitizer like DOMPurify.
 */
function escapeHtml(value: unknown): string {
  return String(value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

export const defaultReportGenerator = new ReportGenerator();
