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

/**
 * NERF Pipeline Hook System
 *
 * EventEmitter-based hooks for pipeline stages. Supports before/after
 * callbacks with context passing, finding transformation, and abort.
 *
 * Zero overhead when no hooks are registered.
 *
 * @module pipeline/hooks
 */

import { EventEmitter } from 'node:events';

// ---------------------------------------------------------------------------
// Hook Events
// ---------------------------------------------------------------------------

/** @enum {string} */
export const HookEvent = Object.freeze({
  PIPELINE_START: 'pipeline:start',
  PIPELINE_COMPLETE: 'pipeline:complete',
  PIPELINE_ERROR: 'pipeline:error',
  BEFORE_CRAWL: 'before:crawl',
  AFTER_CRAWL: 'after:crawl',
  BEFORE_SCAN: 'before:scan',
  AFTER_SCAN: 'after:scan',
  BEFORE_REVIEW: 'before:review',
  AFTER_REVIEW: 'after:review',
  BEFORE_ANALYZE: 'before:analyze',
  AFTER_ANALYZE: 'after:analyze',
});

// ---------------------------------------------------------------------------
// Hook Context
// ---------------------------------------------------------------------------

/**
 * Context object passed to hook callbacks.
 * Mutable — hooks can add metadata or transform findings.
 */
export class HookContext {
  /**
   * @param {object} opts
   * @param {string} opts.stage       - Current pipeline stage
   * @param {string} [opts.target]    - Target being processed
   * @param {object} [opts.options]   - Stage options
   * @param {any[]}  [opts.findings]  - Findings accumulated so far
   * @param {object} [opts.result]    - Stage result (for after hooks)
   * @param {number} [opts.startTime] - Stage start timestamp
   * @param {number} [opts.duration]  - Stage duration ms (for after hooks)
   * @param {object} [opts.meta]      - Arbitrary metadata
   */
  constructor(opts = {}) {
    this.stage = opts.stage ?? '';
    this.target = opts.target ?? '';
    this.options = opts.options ?? {};
    this.findings = opts.findings ?? [];
    this.result = opts.result ?? null;
    this.startTime = opts.startTime ?? Date.now();
    this.duration = opts.duration ?? 0;
    this.meta = opts.meta ?? {};
    this._aborted = false;
    this._abortReason = '';
  }

  /**
   * Signal that the pipeline should abort after this hook.
   * @param {string} [reason]
   */
  abort(reason = '') {
    this._aborted = true;
    this._abortReason = reason;
  }

  get aborted() { return this._aborted; }
  get abortReason() { return this._abortReason; }
}

// ---------------------------------------------------------------------------
// PipelineHooks
// ---------------------------------------------------------------------------

/**
 * Pipeline hook manager. Wraps EventEmitter with typed hook registration
 * and context-aware execution.
 */
export class PipelineHooks extends EventEmitter {
  constructor() {
    super();
    this.setMaxListeners(50); // Allow many hooks
  }

  /**
   * Register a hook callback.
   * @param {string} event   - HookEvent value
   * @param {function} fn    - async (ctx: HookContext) => void
   * @param {object} [opts]
   * @param {string} [opts.name]     - Hook name for logging
   * @param {number} [opts.priority] - Lower runs first (default: 100)
   * @returns {PipelineHooks} this (chainable)
   */
  hook(event, fn, opts = {}) {
    fn._hookName = opts.name ?? fn.name ?? 'anonymous';
    fn._hookPriority = opts.priority ?? 100;
    this.on(event, fn);
    return this;
  }

  /**
   * Execute all hooks for an event, in priority order.
   * Returns the context (possibly mutated by hooks).
   *
   * @param {string} event
   * @param {HookContext} ctx
   * @returns {Promise<HookContext>}
   */
  async run(event, ctx) {
    const listeners = this.listeners(event);
    if (listeners.length === 0) return ctx;

    // Sort by priority (stable sort)
    const sorted = [...listeners].sort(
      (a, b) => (a._hookPriority ?? 100) - (b._hookPriority ?? 100),
    );

    for (const fn of sorted) {
      try {
        await fn(ctx);
      } catch (err) {
        ctx.abort(`Hook "${fn._hookName}" threw: ${err.message}`);
      }
      if (ctx.aborted) break;
    }

    return ctx;
  }

  /**
   * Check if any hooks are registered for an event.
   * @param {string} event
   * @returns {boolean}
   */
  hasHooks(event) {
    return this.listenerCount(event) > 0;
  }

  /**
   * Get all registered hook names grouped by event.
   * @returns {object}
   */
  listHooks() {
    const result = {};
    for (const event of Object.values(HookEvent)) {
      const listeners = this.listeners(event);
      if (listeners.length > 0) {
        result[event] = listeners.map((fn) => fn._hookName ?? 'anonymous');
      }
    }
    return result;
  }
}

// ---------------------------------------------------------------------------
// HookablePipeline — wraps any stage function with hooks
// ---------------------------------------------------------------------------

/**
 * Create a hookable wrapper around a pipeline stage function.
 *
 * @param {PipelineHooks} hooks   - Hook manager
 * @param {string} stageName      - Stage name (e.g. 'crawl', 'scan', 'review')
 * @param {function} stageFn      - async (target, options) => result
 * @returns {function}            - async (target, options) => result (with hooks)
 */
export function hookableStage(hooks, stageName, stageFn) {
  const beforeEvent = `before:${stageName}`;
  const afterEvent = `after:${stageName}`;

  return async function hookedStage(target, options = {}) {
    // Skip hook overhead if none registered
    if (!hooks.hasHooks(beforeEvent) && !hooks.hasHooks(afterEvent)) {
      return stageFn(target, options);
    }

    const t0 = Date.now();

    // Before hook
    const beforeCtx = new HookContext({
      stage: stageName,
      target,
      options,
      startTime: t0,
    });
    await hooks.run(beforeEvent, beforeCtx);

    if (beforeCtx.aborted) {
      throw new Error(`Pipeline aborted before ${stageName}: ${beforeCtx.abortReason}`);
    }

    // Execute stage
    const result = await stageFn(target, beforeCtx.options);
    const duration = Date.now() - t0;

    // After hook
    const afterCtx = new HookContext({
      stage: stageName,
      target,
      options: beforeCtx.options,
      result,
      startTime: t0,
      duration,
      meta: beforeCtx.meta, // Pass through metadata from before hook
    });
    await hooks.run(afterEvent, afterCtx);

    if (afterCtx.aborted) {
      throw new Error(`Pipeline aborted after ${stageName}: ${afterCtx.abortReason}`);
    }

    return afterCtx.result ?? result;
  };
}

// ---------------------------------------------------------------------------
// Built-in hooks
// ---------------------------------------------------------------------------

/**
 * Timing hook — logs stage duration. Attach to after:* events.
 * @param {function} logger - (message: string) => void
 * @returns {function}
 */
export function timingHook(logger = console.log) {
  const fn = async (ctx) => {
    logger(`[hook:timing] ${ctx.stage} completed in ${ctx.duration}ms`);
  };
  fn._hookName = 'timing';
  fn._hookPriority = 999; // Run last
  return fn;
}

/**
 * Finding filter hook — removes findings below a severity threshold.
 * Attach to after:review or after:scan events.
 *
 * @param {string} minSeverity - Minimum severity to keep
 * @returns {function}
 */
export function severityFilterHook(minSeverity) {
  const RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
  const minRank = RANK[minSeverity] ?? 0;

  const fn = async (ctx) => {
    if (ctx.result && ctx.result.findings) {
      ctx.result.findings = ctx.result.findings.filter(
        (f) => (RANK[f.severity] ?? 0) >= minRank,
      );
    }
  };
  fn._hookName = `severity-filter:${minSeverity}`;
  fn._hookPriority = 50;
  return fn;
}

/**
 * Abort hook — aborts pipeline if findings exceed a threshold.
 * Attach to after:review or after:scan events.
 *
 * @param {string} severity - Severity level to count
 * @param {number} max      - Maximum allowed findings of that severity
 * @returns {function}
 */
export function thresholdAbortHook(severity, max) {
  const fn = async (ctx) => {
    if (!ctx.result?.findings) return;
    const count = ctx.result.findings.filter((f) => f.severity === severity).length;
    if (count > max) {
      ctx.abort(`${count} ${severity} findings exceed threshold of ${max}`);
    }
  };
  fn._hookName = `threshold-abort:${severity}>${max}`;
  fn._hookPriority = 80;
  return fn;
}
