'use strict';

/**
 * Fabius Remediation Engine
 * 
 * Proposes, validates, and applies fixes for detected threats.
 * Always creates backups before modifying files.
 */

const fs = require('fs');
const path = require('path');

class RemediationEngine {
  /**
   * @param {object} options
   * @param {import('../registry/db')} options.db
   * @param {import('../ai/provider')} options.ai
   * @param {import('../config/config')} options.config
   */
  constructor(options) {
    this.db = options.db;
    this.ai = options.ai;
    this.config = options.config;
  }

  /**
   * Generate remediation proposals for scan findings
   * @param {number} scanId
   * @returns {Promise<Array>} Array of proposed remediations
   */
  async proposeForScan(scanId) {
    const scan = this.db.getScan(scanId);
    if (!scan) throw new Error(`Scan not found: ${scanId}`);
    if (!scan.findings.length) return [];

    const proposals = [];

    // Group findings by threat
    const byThreat = {};
    for (const finding of scan.findings) {
      const key = finding.threat || 'unknown';
      if (!byThreat[key]) byThreat[key] = [];
      byThreat[key].push(finding);
    }

    for (const [threatKey, findings] of Object.entries(byThreat)) {
      try {
        // Get AI-generated remediation
        const remediation = await this.ai.generateRemediation(
          {
            threat: threatKey,
            findings: findings.map(f => ({
              type: f.type,
              file: f.file,
              severity: f.severity,
              detail: f.detail,
            })),
          },
          {
            repoPath: scan.repo_id ? this.db.getRepo(scan.repo_id)?.path : null,
            scanPath: findings[0]?.file ? path.dirname(findings[0].file) : null,
          }
        );

        const remId = this.db.proposeRemediation(
          scanId,
          threatKey,
          scan.repo_id,
          remediation.summary || `Fix ${findings.length} ${threatKey} findings`,
          remediation.steps || []
        );

        proposals.push({
          id: remId,
          threatKey,
          summary: remediation.summary,
          steps: remediation.steps || [],
          warnings: remediation.warnings || [],
          findingCount: findings.length,
        });
      } catch (err) {
        console.warn(`[Fabius Remediation] Failed to generate remediation for ${threatKey}: ${err.message}`);
      }
    }

    return proposals;
  }

  /**
   * Apply a specific remediation
   * @param {number} remediationId
   * @param {string} appliedBy - 'user' or 'auto'
   */
  async apply(remediationId, appliedBy = 'user') {
    const rem = this.db.getRemediation(remediationId);
    if (!rem) throw new Error(`Remediation not found: ${remediationId}`);

    if (rem.status !== 'proposed' && rem.status !== 'approved') {
      throw new Error(`Cannot apply remediation in status: ${rem.status}`);
    }

    const createBackup = this.config?.get('remediation.createBackup', true);
    const repo = rem.repo_id ? this.db.getRepo(rem.repo_id) : null;

    const appliedSteps = [];
    const errors = [];

    for (const step of rem.patches) {
      try {
        switch (step.type) {
          case 'package-pin':
          case 'file-patch':
            if (step.file && step.before && step.after) {
              const filePath = repo ? path.resolve(repo.path, step.file) : step.file;
              await this._patchFile(filePath, step.before, step.after, createBackup);
              appliedSteps.push(step);
            }
            break;

          case 'package-remove':
            if (step.file) {
              const filePath = repo ? path.resolve(repo.path, step.file) : step.file;
              await this._removeFromPackageJson(filePath, step.packageName, createBackup);
              appliedSteps.push(step);
            }
            break;

          case 'lockfile-regen':
            if (step.file) {
              const lockPath = repo ? path.resolve(repo.path, step.file) : step.file;
              if (fs.existsSync(lockPath)) {
                if (createBackup) await this._backup(lockPath);
                fs.unlinkSync(lockPath);
                appliedSteps.push(step);
              }
            }
            break;

          case 'node-modules-clean':
            if (repo) {
              const nmPath = path.join(repo.path, 'node_modules');
              if (step.packageName) {
                const pkgPath = path.join(nmPath, step.packageName);
                if (fs.existsSync(pkgPath)) {
                  fs.rmSync(pkgPath, { recursive: true, force: true });
                  appliedSteps.push(step);
                }
              }
            }
            break;

          case 'hook-install':
            if (repo) {
              await this._installHook(repo.path, createBackup);
              appliedSteps.push(step);
            }
            break;

          default:
            console.warn(`[Fabius Remediation] Unknown step type: ${step.type}`);
        }
      } catch (err) {
        errors.push({ step, error: err.message });
      }
    }

    if (errors.length === 0) {
      this.db.applyRemediation(remediationId, appliedBy);
    } else if (appliedSteps.length > 0) {
      // Partial success
      this.db.applyRemediation(remediationId, appliedBy);
      this.db.audit('remediation_partial', 'remediation', remediationId.toString(), { errors });
    }

    return { applied: appliedSteps, errors };
  }

  /**
   * Rollback a remediation by restoring backups
   * @param {number} remediationId
   */
  async rollback(remediationId) {
    const rem = this.db.getRemediation(remediationId);
    if (!rem || rem.status !== 'applied') {
      throw new Error('Can only rollback applied remediations');
    }

    const repo = rem.repo_id ? this.db.getRepo(rem.repo_id) : null;
    const backupDir = repo
      ? path.join(repo.path, '.fabius', 'backups')
      : null;

    let rolledBack = 0;
    if (backupDir && fs.existsSync(backupDir)) {
      const backups = fs.readdirSync(backupDir);
      for (const backup of backups) {
        const backupPath = path.join(backupDir, backup);
        // Backup filenames include timestamp, we restore the most recent
        const originalName = backup.replace(/\.\d+$/, '');
        const originalPath = repo ? path.join(repo.path, originalName) : null;
        if (originalPath) {
          try {
            fs.copyFileSync(backupPath, originalPath);
            rolledBack++;
          } catch { /* ok */ }
        }
      }
    }

    this.db.db.prepare(`UPDATE remediations SET status = 'rolled_back' WHERE id = ?`).run(remediationId);
    this.db.audit('remediation_rolled_back', 'remediation', remediationId.toString());

    return { rolledBack };
  }

  // ─── File Operations ──────────────────────────────────────────────────

  async _patchFile(filePath, before, after, backup = true) {
    if (!fs.existsSync(filePath)) {
      throw new Error(`File not found: ${filePath}`);
    }

    let content = fs.readFileSync(filePath, 'utf-8');
    if (!content.includes(before)) {
      throw new Error(`Pattern not found in ${filePath}`);
    }

    if (backup) await this._backup(filePath);

    content = content.replace(before, after);
    fs.writeFileSync(filePath, content, 'utf-8');
  }

  async _removeFromPackageJson(filePath, packageName, backup = true) {
    if (!fs.existsSync(filePath)) return;

    if (backup) await this._backup(filePath);

    const pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
    let modified = false;

    for (const depType of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
      if (pkg[depType] && pkg[depType][packageName]) {
        delete pkg[depType][packageName];
        modified = true;
      }
    }

    if (modified) {
      fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
    }
  }

  async _installHook(repoPath, backup = true) {
    const pkgPath = path.join(repoPath, 'package.json');
    if (!fs.existsSync(pkgPath)) return;

    if (backup) await this._backup(pkgPath);

    const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
    if (!pkg.scripts) pkg.scripts = {};

    const hookCmd = 'node C:/Work/antivirus/glassworm-preinstall-hook.js --post';
    if (pkg.scripts.postinstall) {
      if (!pkg.scripts.postinstall.includes('glassworm')) {
        pkg.scripts.postinstall += ` && ${hookCmd}`;
      }
    } else {
      pkg.scripts.postinstall = hookCmd;
    }

    fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
  }

  async _backup(filePath) {
    const dir = path.join(path.dirname(filePath), '.fabius', 'backups');
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }

    const basename = path.basename(filePath);
    const timestamp = Date.now();
    const backupPath = path.join(dir, `${basename}.${timestamp}`);
    fs.copyFileSync(filePath, backupPath);
  }
}

module.exports = RemediationEngine;
