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

/**
 * runner.js — Mode registry, FlagValidator, and execution engine dispatch.
 *
 * Contains:
 * - FlagValidator: RED mode output validation (FLAG{hex} pattern)
 * - MODE_REGISTRY: Map of mode name → factory function
 * - registerMode / availableModes / initModes
 * - makeAgentClient: auto-detect LLM backend
 * - runAutonomous: main dispatch entry point
 *
 * RED mode tools, schemas, and prompts are in modes/red.js.
 *
 * @module execution-engine/runner
 */

import { ModeAgentConfig, ToolRegistry, ValidationResult } from './framework.js';

const debug = process.env.NERF_DEBUG === '1'
  ? (/** @type {string} */ msg) => process.stderr.write(`[bridge:node] ${msg}\n`)
  : () => {};

// ---------------------------------------------------------------------------
// FlagValidator — RED mode output validation
// ---------------------------------------------------------------------------

/** @type {RegExp} */
export const FLAG_PATTERN = /FLAG\{[a-fA-F0-9]+\}/;

/**
 * Validates that a FLAG{hex} pattern was captured.
 *
 * Checks both result.outputText and result.steps since the flag may
 * appear in tool output (recorded in steps via [complete]) rather
 * than in the LLM's text response.
 */
export class FlagValidator {
  /**
   * @param {import('./framework.js').ModeAgentResult} result
   * @returns {ValidationResult}
   */
  validate(result) {
    // Check outputText first
    const textMatch = FLAG_PATTERN.exec(result.outputText);
    if (textMatch) {
      return new ValidationResult({
        valid: true,
        errors: [],
        warnings: [],
        score: 1.0,
        metadata: { flag: textMatch[0] },
      });
    }

    // Check steps (flag may have triggered completion from tool output)
    for (const step of result.steps) {
      const stepMatch = FLAG_PATTERN.exec(step);
      if (stepMatch) {
        return new ValidationResult({
          valid: true,
          errors: [],
          warnings: [],
          score: 1.0,
          metadata: { flag: stepMatch[0] },
        });
      }
    }

    return new ValidationResult({
      valid: false,
      errors: ['No FLAG{hex} pattern found in output or steps'],
      warnings: [],
      score: 0.0,
    });
  }
}

// ---------------------------------------------------------------------------
// Mode registry
// ---------------------------------------------------------------------------

/** @type {Map<string, Function>} */
const MODE_REGISTRY = new Map();

/**
 * Register a mode config factory for use with runAutonomous().
 *
 * @param {string} mode - Mode name (will be uppercased)
 * @param {Function} factory - Callable that returns a fresh ModeAgentConfig
 * @throws {Error} If the mode is already registered
 */
export function registerMode(mode, factory) {
  const key = mode.toUpperCase();
  if (MODE_REGISTRY.has(key)) {
    throw new Error(
      `Mode already registered: '${key}'. ` +
      `Registered modes: ${[...MODE_REGISTRY.keys()].sort().join(', ')}`
    );
  }
  MODE_REGISTRY.set(key, factory);
  debug(`Registered mode: ${key}`);
}

/**
 * Return sorted array of registered mode names.
 * @returns {string[]}
 */
export function availableModes() {
  return [...MODE_REGISTRY.keys()].sort();
}

/**
 * Get the MODE_REGISTRY map (for runAutonomous and testing).
 * @returns {Map<string, Function>}
 */
export function getModeRegistry() {
  return MODE_REGISTRY;
}

// ---------------------------------------------------------------------------
// Lazy mode initialization
// ---------------------------------------------------------------------------

let _modesInitialized = false;

/**
 * Lazily import all mode files and call their register() functions.
 * Runs only once — subsequent calls are no-ops.
 *
 * @returns {Promise<void>}
 */
export async function initModes() {
  if (_modesInitialized) return;
  _modesInitialized = true;

  debug('Initializing mode agents');

  const modeModules = await Promise.all([
    import('./modes/red.js'),
    import('./modes/blue.js'),
    import('./modes/incident.js'),
    import('./modes/purple.js'),
    import('./modes/recon.js'),
    import('./modes/privacy.js'),
    import('./modes/architect.js'),
    import('./modes/researcher.js'),
    import('./modes/build.js'),
  ]);

  for (const mod of modeModules) {
    mod.register(registerMode);
  }

  debug(`Mode initialization complete: ${availableModes().join(', ')}`);
}

/**
 * Reset mode initialization state (for testing only).
 * Clears all modes, resets the initialized flag.
 */
export function _resetModes() {
  _modesInitialized = false;
  MODE_REGISTRY.clear();
}

// ---------------------------------------------------------------------------
// makeAgentClient — auto-detect LLM backend
// ---------------------------------------------------------------------------

/** Default Claude model when using env var directly */
const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';

/** Default Ollama model */
const DEFAULT_OLLAMA_MODEL = 'qwen2.5:32b';

/** Preferred Ollama models in priority order */
const PREFERRED_OLLAMA_MODELS = [
  'qwen2.5:32b',
  'qwen2.5:14b',
  'llama3.1:70b',
  'llama3.1:8b',
  'mistral:7b',
];

/**
 * Try loading NERF gateway config and building a client.
 *
 * @param {string|null} backendOverride
 * @returns {Promise<{client: Object, model: string}|null>}
 */
async function _tryGatewayConfig(backendOverride) {
  try {
    const { loadConfig, configExists } = await import('../config.js');
    if (!configExists()) return null;

    const raw = loadConfig();
    if (backendOverride) {
      raw.llm_backend = backendOverride;
    }

    const { validateConfig } = await import('../gateway/config-validate.js');
    const config = validateConfig(raw);
    const { makeClient } = await import('../gateway/client.js');
    return await makeClient(config);
  } catch (e) {
    debug(`_tryGatewayConfig: ${e.message}`);
    return null;
  }
}

/**
 * Try creating a Claude client from ANTHROPIC_API_KEY env var.
 *
 * @returns {Promise<{client: Object, model: string}|null>}
 */
async function _tryAnthropicEnv() {
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) return null;

  try {
    const { makeClient } = await import('../gateway/client.js');
    const config = {
      backend: 'claude',
      claude_api_key: apiKey,
      claude_model: DEFAULT_CLAUDE_MODEL,
      claude_timeout: 60,
    };
    const result = await makeClient(config);
    debug(`makeAgentClient: using ANTHROPIC_API_KEY env, model=${DEFAULT_CLAUDE_MODEL}`);
    return result;
  } catch (e) {
    debug(`_tryAnthropicEnv: ${e.message}`);
    return null;
  }
}

/**
 * Try using pi-mono's AgentSession for delegated agent execution.
 *
 * Creates a pi-mono agent session that can be used as an Anthropic-compatible
 * client adapter. This works when pi-mono packages are available (installed
 * via pi runtime packages (direct dependencies)).
 *
 * @returns {Promise<{client: Object, model: string}|null>}
 */
async function _tryClaudeCodeSDK() {
  try {
    const { importPiCodingAgent, importPiAi } = await import('../gateway/pi-imports.js');

    // Check if pi-mono is available
    let piCoding;
    try {
      piCoding = await importPiCodingAgent();
    } catch {
      return null; // pi-mono not installed
    }

    const piAi = await importPiAi();

    // Build an Anthropic-SDK-compatible client that delegates to pi-mono Agent
    const client = {
      messages: {
        async create({ model, max_tokens, tools, messages, system }) {
          const { createAgentSession, SessionManager, AuthStorage, ModelRegistry } = piCoding;

          const authStorage = AuthStorage.create();
          const modelRegistry = new ModelRegistry(authStorage);
          const piModel = piAi.getModel('anthropic', model || 'claude-sonnet-4-6');

          const { session } = await createAgentSession({
            model: piModel,
            sessionManager: SessionManager.inMemory(),
            authStorage,
            modelRegistry,
          });

          // Extract the latest user message as the task
          const lastUser = [...messages].reverse().find(m => m.role === 'user');
          const task = typeof lastUser?.content === 'string'
            ? lastUser.content
            : Array.isArray(lastUser?.content)
              ? lastUser.content.map(b => b.text || '').join('\n')
              : 'Continue the task.';

          const fullTask = system ? `${system}\n\n---\n\n${task}` : task;

          const textParts = [];
          session.subscribe((event) => {
            if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
              textParts.push(event.assistantMessageEvent.delta);
            }
          });

          try {
            await session.prompt(fullTask);
          } catch {
            // Collect whatever output we got
          }

          const text = textParts.join('');
          session.dispose();

          return {
            content: [{ type: 'text', text }],
            stop_reason: 'end_turn',
            usage: { input_tokens: 0, output_tokens: 0 },
          };
        },
      },
    };

    debug('makeAgentClient: using pi-mono AgentSession');
    return { client, model: 'claude-sonnet-4-6' };
  } catch (e) {
    debug(`_tryClaudeCodeSDK: ${e.message}`);
    return null;
  }
}

/**
 * Try connecting to local Ollama and picking the best model.
 *
 * @returns {Promise<{client: Object, model: string}|null>}
 */
async function _tryOllama() {
  const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
  try {
    // Probe Ollama with a short timeout
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 2000);

    const resp = await fetch(`${baseUrl}/api/tags`, { signal: controller.signal });
    clearTimeout(timeoutId);

    if (!resp.ok) return null;

    const data = await resp.json();
    const models = data.models || [];
    if (models.length === 0) return null;

    // Pick the best available model
    const availableNames = new Set(models.map(m => m.name || ''));
    let selected = '';
    for (const preferred of PREFERRED_OLLAMA_MODELS) {
      if (availableNames.has(preferred)) {
        selected = preferred;
        break;
      }
    }
    if (!selected) {
      selected = models[0].name || '';
    }
    if (!selected) return null;

    const { makeClient } = await import('../gateway/client.js');
    const config = {
      backend: 'ollama',
      ollama_base_url: baseUrl,
      ollama_model: selected,
      ollama_timeout: 120,
    };
    const result = await makeClient(config);
    debug(`makeAgentClient: using Ollama at ${baseUrl}, model=${selected}`);
    return result;
  } catch (e) {
    debug(`_tryOllama: ${e.message}`);
    return null;
  }
}

/**
 * Create an LLM client for execution engine.
 *
 * Auto-detects the best available backend matching Python's
 * benchmark.llm.make_agent_client() precedence:
 *   1. Explicit override → validate and build
 *   2. NERF config file
 *   3. ANTHROPIC_API_KEY env var
 *   4. Local Ollama probe
 *
 * @param {string|null} [backendOverride=null]
 * @returns {Promise<{client: Object, model: string}>}
 * @throws {Error} If no backend is available
 */
export async function makeAgentClient(backendOverride = null) {
  debug(`makeAgentClient: override=${backendOverride || 'none'}`);

  // 1. Explicit override
  if (backendOverride) {
    if (backendOverride === 'ollama') {
      const result = await _tryOllama();
      if (result) return result;
      throw new Error(
        `Ollama not available at ${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}. ` +
        'Start Ollama with: ollama serve'
      );
    }
    if (backendOverride === 'claude') {
      const envResult = await _tryAnthropicEnv();
      if (envResult) return envResult;
      const gwResult = await _tryGatewayConfig('claude');
      if (gwResult) return gwResult;
      throw new Error(
        'Claude API key not found. Set ANTHROPIC_API_KEY or run: nerf setup'
      );
    }
    // For other overrides (openai-compat, etc.), try gateway config
    const gwResult = await _tryGatewayConfig(backendOverride);
    if (gwResult) return gwResult;
    throw new Error(
      `Backend '${backendOverride}' not configured. Run: nerf setup`
    );
  }

  // 2. Try Claude Code SDK (Pro/Max subscription — no API key needed)
  const sdkResult = await _tryClaudeCodeSDK();
  if (sdkResult) return sdkResult;

  // 3. Try existing NERF config
  const gwResult = await _tryGatewayConfig(null);
  if (gwResult) return gwResult;

  // 4. Try ANTHROPIC_API_KEY env var
  const envResult = await _tryAnthropicEnv();
  if (envResult) return envResult;

  // 5. Try local Ollama
  const ollamaResult = await _tryOllama();
  if (ollamaResult) return ollamaResult;

  // 6. Nothing available
  throw new Error(
    'No LLM backend available for execution engine.\n' +
    'Options:\n' +
    '  1. Use Claude Code (Pro/Max plan) — run inside Claude Code, no API key needed\n' +
    '  2. Start Ollama locally: ollama serve && ollama pull qwen2.5:32b\n' +
    '  3. Set ANTHROPIC_API_KEY environment variable\n' +
    '  4. Use 1Password: op run -- nerf <command>\n' +
    '  5. Run nerf setup to configure a backend'
  );
}

// ---------------------------------------------------------------------------
// runAutonomous — main dispatch entry point
// ---------------------------------------------------------------------------

/**
 * Run an execution agent for the given mode.
 *
 * Looks up the mode config from MODE_REGISTRY, creates an LLM client,
 * and runs a BaseAgent loop.
 *
 * @param {string} mode - Mode name (case-insensitive)
 * @param {Object} taskInput - Dict of task parameters passed to BaseAgent.run()
 * @param {string|null} [backend=null] - LLM backend override
 * @param {*} [context=null] - Pre-created context (e.g. SandboxContainer)
 * @returns {Promise<import('./framework.js').ModeAgentResult>}
 * @throws {Error} If mode is unknown or required context is missing
 */
export async function runAutonomous(mode, taskInput, backend = null, context = null) {
  const modeKey = mode.toUpperCase();
  debug(`runAutonomous: mode=${modeKey}, backend=${backend || 'auto-detect'}`);

  // Ensure all modes are registered
  await initModes();

  // Look up mode factory
  const factory = MODE_REGISTRY.get(modeKey);
  if (!factory) {
    throw new Error(
      `Unknown mode: '${modeKey}'. ` +
      `Available: ${availableModes().join(', ')}`
    );
  }

  // Create fresh config
  const config = factory();

  // Enrich system prompt with mode-relevant knowledge and skills
  try {
    const { loadKnowledgeForMode, loadSkill } = await import('../gateway/prompt.js');
    const knowledge = loadKnowledgeForMode(modeKey);
    const skill = loadSkill(modeKey);
    const enrichments = [];
    if (skill) enrichments.push(`\n\n## NERF ${modeKey} Skill Reference\n${skill.slice(0, 4000)}`);
    if (knowledge) enrichments.push(`\n\n## NERF Knowledge Reference\n${knowledge}`);
    if (enrichments.length > 0) {
      config.systemPromptTemplate += enrichments.join('');
      debug(`runAutonomous: enriched system prompt with ${enrichments.length} knowledge/skill sections`);
    }
  } catch (e) {
    debug(`runAutonomous: knowledge enrichment skipped (${e.message})`);
  }

  // Validate context requirement
  if (config.requiresSandbox && !context) {
    throw new Error(
      `Mode '${modeKey}' requires a sandbox context. ` +
      `Pass a pre-created context via the 'context' parameter.`
    );
  }

  // Try pi SDK path first (when running inside pi)
  if (backend === 'pi-ai' || backend === 'pi' || !backend) {
    try {
      const { isPiSdkAvailable, runWithPiSdk } = await import('./pi-sdk-runner.js');
      if (await isPiSdkAvailable()) {
        debug('runAutonomous: using pi SDK path');
        return await runWithPiSdk(config, taskInput, {
          provider: taskInput.pi_provider || undefined,
          model: taskInput.pi_model || undefined,
        });
      }
    } catch (e) {
      debug(`runAutonomous: pi SDK path failed (${e.message}), falling back to BaseAgent`);
    }
  }

  // Fallback: BaseAgent with direct LLM client
  const { client, model } = await makeAgentClient(backend);
  debug(`runAutonomous: using BaseAgent, model=${model}`);

  // Register operational tools filtered by mode policy
  // Each mode gets only the tools its policy allows (A-002/A-004 fix)
  try {
    const { registerOperationalTools } = await import('./operational-tools.js');
    registerOperationalTools(config.toolRegistry, {
      mode: modeKey,
      hasSandbox: !!context,
    });
    debug(`runAutonomous: operational tools registered (mode=${modeKey}, sandbox=${!!context})`);
  } catch (e) {
    debug(`runAutonomous: operational tools registration skipped (${e.message})`);
  }

  // Run the agent
  const { BaseAgent } = await import('./framework.js');
  const agent = new BaseAgent({ client, model, config, context });
  const result = await agent.run(taskInput);

  debug(
    `runAutonomous: mode=${modeKey} completed — ` +
    `turns=${result.turnsUsed}, tools=${result.toolCalls}, ` +
    `tokens_in=${result.tokensIn}, tokens_out=${result.tokensOut}, ` +
    `valid=${result.validation ? result.validation.valid : 'N/A'}`
  );

  return result;
}

// ---------------------------------------------------------------------------
// Exports for testing
// ---------------------------------------------------------------------------

export {
  _tryClaudeCodeSDK,
  _tryGatewayConfig,
  _tryAnthropicEnv,
  _tryOllama,
};
