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

/**
 * Agent Handoff Engine — Formal transfer protocol for multi-mode chains.
 *
 * Enables mode agents to hand off to each other mid-execution via
 * `transfer_to_<mode>` tool calls, with context filtering and cycle detection.
 *
 * Key exports:
 * - HandoffEngine: manages transfer interception, context filtering, depth tracking
 * - runWithHandoffs: wraps runAutonomous with handoff support
 * - runChain: explicit sequential multi-mode execution
 * - HandoffResult: aggregated results from a chain
 * - HandoffLog: structured event timeline
 *
 * @module execution-engine/handoff
 */

import { ModeAgentResult } from './framework.js';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** Maximum handoff depth before forced halt */
const DEFAULT_MAX_DEPTH = 5;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/**
 * A single handoff event in the timeline.
 */
export class HandoffEvent {
  /**
   * @param {object} opts
   * @param {string} opts.sourceMode - Mode that initiated the transfer
   * @param {string} opts.targetMode - Mode being transferred to
   * @param {number} opts.timestamp - Unix epoch seconds
   * @param {number} opts.depth - Current handoff depth
   * @param {string} [opts.reason] - Why the transfer was requested
   * @param {string} [opts.contextSummary] - Summary of filtered context passed
   * @param {string} [opts.status] - 'completed' | 'blocked_cycle' | 'blocked_depth'
   */
  constructor(opts = {}) {
    this.sourceMode = opts.sourceMode ?? '';
    this.targetMode = opts.targetMode ?? '';
    this.timestamp = opts.timestamp ?? Date.now() / 1000;
    this.depth = opts.depth ?? 0;
    this.reason = opts.reason ?? '';
    this.contextSummary = opts.contextSummary ?? '';
    this.status = opts.status ?? 'completed';
  }
}

/**
 * Aggregated result from a multi-mode handoff chain.
 */
export class HandoffResult {
  /**
   * @param {object} opts
   * @param {ModeAgentResult[]} [opts.results] - Per-mode results in execution order
   * @param {HandoffEvent[]} [opts.events] - Handoff event timeline
   * @param {number} [opts.totalDurationS] - Total wall-clock seconds
   * @param {number} [opts.totalTokensIn] - Aggregate input tokens
   * @param {number} [opts.totalTokensOut] - Aggregate output tokens
   * @param {string|null} [opts.error] - Top-level error if chain failed
   */
  constructor(opts = {}) {
    /** @type {ModeAgentResult[]} */
    this.results = opts.results ?? [];
    /** @type {HandoffEvent[]} */
    this.events = opts.events ?? [];
    this.totalDurationS = opts.totalDurationS ?? 0;
    this.totalTokensIn = opts.totalTokensIn ?? 0;
    this.totalTokensOut = opts.totalTokensOut ?? 0;
    this.error = opts.error ?? null;
  }

  /** Return array of modes executed, in order. */
  get modesExecuted() {
    return this.results.map(r => r.mode);
  }
}

/**
 * @typedef {object} HandoffFilter
 * @property {(context: object, sourceMode: string, targetMode: string) => object} transform
 *   Transform handoff context before passing to target mode.
 * @property {string[]} [acceptsFrom]
 *   Modes this filter accepts transfers from. Empty = accept all.
 */

/**
 * Default pass-through filter — passes context unchanged, accepts from all modes.
 * @type {HandoffFilter}
 */
export const DEFAULT_HANDOFF_FILTER = {
  transform: (context) => context,
  acceptsFrom: [],
};

// ---------------------------------------------------------------------------
// HandoffEngine
// ---------------------------------------------------------------------------

/**
 * Manages agent-to-agent transfers with context filtering and cycle detection.
 */
export class HandoffEngine {
  /**
   * @param {object} opts
   * @param {number} [opts.maxDepth] - Maximum handoff depth
   * @param {Map<string, HandoffFilter>} [opts.filters] - Per-mode handoff filters
   */
  constructor(opts = {}) {
    this._maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
    /** @type {Map<string, HandoffFilter>} */
    this._filters = opts.filters ?? new Map();
    /** @type {HandoffEvent[]} */
    this._events = [];
    this._currentDepth = 0;
    /** @type {Set<string>} */
    this._visited = new Set();
  }

  /** Current handoff depth. */
  get depth() { return this._currentDepth; }

  /** Max depth. */
  get maxDepth() { return this._maxDepth; }

  /** Full event timeline. */
  get events() { return [...this._events]; }

  /**
   * Register a handoff filter for a mode.
   * @param {string} mode
   * @param {HandoffFilter} filter
   */
  setFilter(mode, filter) {
    this._filters.set(mode.toUpperCase(), filter);
  }

  /**
   * Get the filter for a mode (returns default if none registered).
   * @param {string} mode
   * @returns {HandoffFilter}
   */
  getFilter(mode) {
    return this._filters.get(mode.toUpperCase()) || DEFAULT_HANDOFF_FILTER;
  }

  /**
   * Check whether a transfer from sourceMode to targetMode is allowed.
   *
   * Returns { allowed: boolean, reason: string }.
   *
   * @param {string} sourceMode
   * @param {string} targetMode
   * @returns {{ allowed: boolean, reason: string }}
   */
  canTransfer(sourceMode, targetMode) {
    const source = sourceMode.toUpperCase();
    const target = targetMode.toUpperCase();

    // Depth check
    if (this._currentDepth >= this._maxDepth) {
      return { allowed: false, reason: `Max handoff depth (${this._maxDepth}) reached` };
    }

    // Cycle check — same mode back-to-back is a cycle
    if (this._visited.has(target)) {
      return { allowed: false, reason: `Cycle detected: ${target} already visited in chain [${[...this._visited].join('→')}]` };
    }

    // Filter acceptance check
    const filter = this.getFilter(target);
    if (filter.acceptsFrom && filter.acceptsFrom.length > 0) {
      if (!filter.acceptsFrom.includes(source)) {
        return { allowed: false, reason: `${target} does not accept transfers from ${source}` };
      }
    }

    return { allowed: true, reason: '' };
  }

  /**
   * Record a transfer event and update tracking state.
   *
   * @param {string} sourceMode
   * @param {string} targetMode
   * @param {object} context - Raw context before filtering
   * @param {string} [status='completed']
   * @returns {HandoffEvent}
   */
  recordTransfer(sourceMode, targetMode, context = {}, status = 'completed') {
    const source = sourceMode.toUpperCase();
    const target = targetMode.toUpperCase();

    const event = new HandoffEvent({
      sourceMode: source,
      targetMode: target,
      timestamp: Date.now() / 1000,
      depth: this._currentDepth,
      reason: context.reason || '',
      contextSummary: this._summarizeContext(context),
      status,
    });

    this._events.push(event);

    if (status === 'completed') {
      this._visited.add(source);
      this._visited.add(target);
      this._currentDepth += 1;
    }

    return event;
  }

  /**
   * Filter context for the target mode using registered filters.
   *
   * @param {object} context
   * @param {string} sourceMode
   * @param {string} targetMode
   * @returns {object}
   */
  filterContext(context, sourceMode, targetMode) {
    const filter = this.getFilter(targetMode);
    return filter.transform(context, sourceMode.toUpperCase(), targetMode.toUpperCase());
  }

  /**
   * Generate transfer_to tool schemas for all available modes except the current one.
   *
   * @param {string[]} availableModes - All registered mode names
   * @param {string} currentMode - The currently active mode
   * @returns {Array<{ name: string, schema: object, targetMode: string }>}
   */
  generateTransferTools(availableModes, currentMode) {
    const current = currentMode.toUpperCase();
    return availableModes
      .filter(m => m.toUpperCase() !== current)
      .map(mode => {
        const modeKey = mode.toLowerCase();
        return {
          name: `transfer_to_${modeKey}`,
          targetMode: mode.toUpperCase(),
          schema: {
            name: `transfer_to_${modeKey}`,
            description: `Transfer execution to ${mode.toUpperCase()} mode agent. Use when the current task requires ${mode} capabilities.`,
            input_schema: {
              type: 'object',
              properties: {
                reason: {
                  type: 'string',
                  description: 'Why this transfer is needed',
                },
                context: {
                  type: 'string',
                  description: 'Key findings or context to pass to the target mode',
                },
                task: {
                  type: 'string',
                  description: 'Specific task for the target mode to accomplish',
                },
              },
              required: ['reason', 'task'],
            },
          },
        };
      });
  }

  /**
   * Reset engine state for a new chain.
   */
  reset() {
    this._events = [];
    this._currentDepth = 0;
    this._visited.clear();
  }

  /**
   * Increment depth without recording a full transfer event.
   * Used by agent-as-tool to share depth tracking.
   */
  incrementDepth() {
    this._currentDepth += 1;
  }

  /**
   * Decrement depth (when agent-as-tool sub-agent completes).
   */
  decrementDepth() {
    if (this._currentDepth > 0) this._currentDepth -= 1;
  }

  // -- internal -----------------------------------------------------------

  /**
   * @param {object} context
   * @returns {string}
   */
  _summarizeContext(context) {
    if (!context || typeof context !== 'object') return '';
    const keys = Object.keys(context);
    if (keys.length === 0) return '(empty)';
    const preview = keys.slice(0, 5).join(', ');
    return `keys: [${preview}]${keys.length > 5 ? `, +${keys.length - 5} more` : ''}`;
  }
}

// ---------------------------------------------------------------------------
// runWithHandoffs — wraps BaseAgent with handoff interception
// ---------------------------------------------------------------------------

/**
 * Run an execution agent with handoff support.
 *
 * Wraps the standard runAutonomous flow with a preToolHook that intercepts
 * transfer_to_<mode> tool calls and spawns the target agent.
 *
 * @param {string} mode - Starting mode
 * @param {object} taskInput - Task parameters
 * @param {object} [opts]
 * @param {string|null} [opts.backend] - LLM backend override
 * @param {*} [opts.context] - Sandbox context
 * @param {number} [opts.maxDepth] - Max handoff depth
 * @param {Map<string, HandoffFilter>} [opts.filters] - Per-mode filters
 * @param {Function} [opts.agentRunner] - Injectable runner for testing (default: runAutonomous)
 * @returns {Promise<HandoffResult>}
 */
export async function runWithHandoffs(mode, taskInput, opts = {}) {
  const engine = new HandoffEngine({
    maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH,
    filters: opts.filters,
  });

  const startTime = performance.now() / 1000;
  const handoffResult = new HandoffResult();

  // Import runner lazily to avoid circular deps
  const runner = opts.agentRunner || (await import('./runner.js')).runAutonomous;

  // Mark starting mode as visited
  engine._visited.add(mode.toUpperCase());

  /**
   * Execute a single mode, potentially recursing on handoff.
   *
   * @param {string} currentMode
   * @param {object} currentTask
   * @returns {Promise<void>}
   */
  async function executeMode(currentMode, currentTask) {
    let result;
    try {
      result = await runner(currentMode, currentTask, opts.backend, opts.context);
    } catch (e) {
      handoffResult.error = `${currentMode} execution failed: ${e.message}`;
      handoffResult.results.push(new ModeAgentResult({
        mode: currentMode,
        error: e.message,
      }));
      return;
    }

    handoffResult.results.push(result);
    handoffResult.totalTokensIn += result.tokensIn;
    handoffResult.totalTokensOut += result.tokensOut;

    // Check if the agent requested a transfer (look in steps for transfer tool calls)
    const transferStep = result.steps.find(s =>
      s.startsWith('[tool] transfer_to_')
    );

    if (!transferStep) return; // No handoff requested

    // Parse transfer target from step log
    const match = transferStep.match(/\[tool\] transfer_to_(\w+)/);
    if (!match) return;

    const targetMode = match[1].toUpperCase();
    const check = engine.canTransfer(currentMode, targetMode);

    if (!check.allowed) {
      engine.recordTransfer(currentMode, targetMode, { reason: check.reason }, 'blocked_cycle');
      result.steps.push(`[handoff:blocked] ${check.reason}`);
      return;
    }

    // Filter context for target
    const handoffContext = {
      reason: `Transfer from ${currentMode}`,
      priorOutput: result.outputText,
      priorFindings: result.outputData,
    };
    const filtered = engine.filterContext(handoffContext, currentMode, targetMode);

    engine.recordTransfer(currentMode, targetMode, handoffContext);

    // Build task for target mode
    const targetTask = {
      ...currentTask,
      user_message: `[Handoff from ${currentMode}] ${filtered.priorOutput || ''}\n\nTask: ${currentTask.user_message || currentTask.task || 'Continue the engagement.'}`,
      handoff_context: filtered,
    };

    await executeMode(targetMode, targetTask);
  }

  await executeMode(mode.toUpperCase(), taskInput);

  handoffResult.events = engine.events;
  handoffResult.totalDurationS = performance.now() / 1000 - startTime;

  return handoffResult;
}

// ---------------------------------------------------------------------------
// runChain — explicit sequential multi-mode execution
// ---------------------------------------------------------------------------

/**
 * Run multiple modes in explicit sequence, passing results forward.
 *
 * Unlike runWithHandoffs (where the LLM decides to transfer), runChain
 * executes a predetermined sequence of modes.
 *
 * @param {string[]} modes - Ordered list of mode names to execute
 * @param {object} taskInput - Initial task parameters
 * @param {object} [opts]
 * @param {string|null} [opts.backend] - LLM backend override
 * @param {*} [opts.context] - Sandbox context
 * @param {Map<string, HandoffFilter>} [opts.filters] - Per-mode filters
 * @param {Function} [opts.agentRunner] - Injectable runner for testing
 * @returns {Promise<HandoffResult>}
 */
export async function runChain(modes, taskInput, opts = {}) {
  if (!modes || modes.length === 0) {
    return new HandoffResult({ error: 'No modes specified for chain' });
  }

  const engine = new HandoffEngine({ filters: opts.filters });
  const runner = opts.agentRunner || (await import('./runner.js')).runAutonomous;
  const startTime = performance.now() / 1000;
  const handoffResult = new HandoffResult();

  let priorOutput = '';
  let priorData = {};

  for (let i = 0; i < modes.length; i++) {
    const currentMode = modes[i].toUpperCase();
    const isFirst = i === 0;
    const prevMode = isFirst ? null : modes[i - 1].toUpperCase();

    // Build task with prior context
    let currentTask = { ...taskInput };
    if (!isFirst && prevMode) {
      const handoffContext = {
        reason: `Chain step ${i + 1}/${modes.length}: ${prevMode}→${currentMode}`,
        priorOutput,
        priorFindings: priorData,
      };
      const filtered = engine.filterContext(handoffContext, prevMode, currentMode);

      engine.recordTransfer(prevMode, currentMode, handoffContext);

      currentTask = {
        ...taskInput,
        user_message: `[Chain: ${prevMode}→${currentMode}, step ${i + 1}/${modes.length}]\n\nPrior results:\n${filtered.priorOutput || '(none)'}\n\nTask: ${taskInput.user_message || taskInput.task || 'Continue.'}`,
        handoff_context: filtered,
      };
    }

    let result;
    try {
      result = await runner(currentMode, currentTask, opts.backend, opts.context);
    } catch (e) {
      handoffResult.error = `Chain failed at ${currentMode} (step ${i + 1}/${modes.length}): ${e.message}`;
      handoffResult.results.push(new ModeAgentResult({
        mode: currentMode,
        error: e.message,
      }));
      break;
    }

    handoffResult.results.push(result);
    handoffResult.totalTokensIn += result.tokensIn;
    handoffResult.totalTokensOut += result.tokensOut;

    priorOutput = result.outputText;
    priorData = result.outputData;
  }

  handoffResult.events = engine.events;
  handoffResult.totalDurationS = performance.now() / 1000 - startTime;

  return handoffResult;
}
