// wall-e/tools/builtin-middleware.js
// Built-in middleware functions for the coding agent.
// Each function corresponds to a lifecycle hook (see coding-middleware.js).
// Inspired by OpenCode's plugin hooks and prompt patterns.
'use strict';

const fs = require('fs');
const path = require('path');
const { buildCapabilityContext } = require('../coding/prompt-capabilities');
const { isReasoningCapableOllamaModel } = require('../llm/ollama');

function providerId(provider) {
  if (!provider) return '';
  if (typeof provider === 'string') return provider;
  return provider.type || provider.provider || provider.id || '';
}

function isOpenWeightReasoningModel(provider, model) {
  if (providerId(provider) !== 'ollama') return false;
  return isReasoningCapableOllamaModel(model);
}

function isDeepSeekV4ProCodingModel(provider, model) {
  return providerId(provider) === 'deepseek' && /^deepseek-v4-pro\b/i.test(String(model || ''));
}

function applyDeepSeekCodingReasoningDefaults(ctx, provider, model) {
  if (!isDeepSeekV4ProCodingModel(provider, model)) return false;
  if (ctx.params.thinking == null) ctx.params.thinking = 'enabled';
  if (ctx.params.reasoningEffort == null) ctx.params.reasoningEffort = 'max';
  ctx.params.maxTokens = Math.max(ctx.params.maxTokens || 4096, 8192);
  return true;
}

function applyOpenWeightCodingReasoningDefaults(ctx, provider, model, opts = {}) {
  if (!isOpenWeightReasoningModel(provider, model)) return false;
  const isGemma4 = providerId(provider) === 'ollama' && /^gemma4:/i.test(String(model || ''));
  const disableThinking = isGemma4 && (ctx.toolsAvailable === true || opts.benchmark === true);
  if (disableThinking) {
    ctx.params.thinking = false;
  } else if (ctx.params.thinking == null) {
    ctx.params.thinking = true;
  }
  if (!disableThinking && ctx.params.reasoningEffort == null) ctx.params.reasoningEffort = 'max';
  if (opts.benchmark && isGemma4 && ctx.toolsAvailable !== true) {
    ctx.params.maxTokens = Math.min(ctx.params.maxTokens || 1024, 1024);
  } else {
    ctx.params.maxTokens = Math.max(ctx.params.maxTokens || 4096, /26b/i.test(model) ? 6144 : 8192);
  }
  ctx.params.options = {
    ...(ctx.params.options || {}),
    num_ctx: Math.max(ctx.params.options?.num_ctx || 0, 16384),
  };
  return true;
}

// Load workflow docs once at require time (not every LLM call)
let workflowDocs = '';
try {
  workflowDocs = fs.readFileSync(
    path.join(__dirname, '..', 'prompts', 'tool-workflows.md'), 'utf8'
  );
} catch { /* file optional — degrades gracefully */ }

/**
 * Register all built-in middleware functions.
 * @param {CodingMiddleware} mw - The middleware pipeline
 * @param {object} opts - Task configuration
 * @param {string} opts.cwd - Working directory
 * @param {string} [opts.provider] - LLM provider name
 * @param {string} [opts.model] - Model ID
 * @param {string} [opts.claudeMd] - CLAUDE.md content
 * @param {string} [opts.mode] - 'plan' | 'build' | 'review'
 * @param {object} [opts.taskEnv] - Environment variables (not logged)
 */
function registerBuiltinMiddleware(mw, opts = {}) {
  // -- llm.before: model-specific params --
  mw.use('llm.before', async (ctx) => {
    if (!ctx.params) ctx.params = {};
    // Deterministic output for coding
    ctx.params.temperature = 0;
    const provider = ctx.provider || opts.provider;
    const model = String(ctx.model || opts.model || '');
    const providerType = providerId(provider);
    const isOllama = providerType === 'ollama';
    const isGemma4 = isOllama && /^gemma4:/i.test(model);
    applyDeepSeekCodingReasoningDefaults(ctx, provider, model);
    const hasOpenWeightThinking = applyOpenWeightCodingReasoningDefaults(ctx, provider, model, opts);
    if (!isGemma4 && isOllama && !hasOpenWeightThinking) {
      // Cap generic local models; many smaller tags degrade if asked for huge
      // coding turns. Gemma4 is handled separately above.
      ctx.params.maxTokens = Math.min(ctx.params.maxTokens || 4096, 2048);
    }
  });

  // -- llm.before: rich system prompt --
  mw.use('llm.before', async (ctx) => {
    const cwd = ctx.cwd || opts.cwd || process.cwd();
    const mode = ctx.mode || opts.mode || 'build';
    const sections = [];

    // When the orchestrator already built the full agent prompt
    // (buildAgentSystemPrompt), keep it as the base and only append the
    // sections it doesn't cover. Replacing it here silently dropped the
    // workflow/quality/frontend/memory discipline from every loop turn.
    const baseSystem = typeof ctx.system === 'string' && ctx.system.trim() ? ctx.system : '';
    if (baseSystem) {
      sections.push(baseSystem);
    } else {
      sections.push(`You are an expert software engineer executing a coding task.
Work in: ${cwd}`);
    }

    // Mode enforcement (belt + suspenders with tool filtering)
    if (mode === 'plan') {
      sections.push(`CRITICAL: You are in PLANNING mode. File edits are STRICTLY FORBIDDEN.
You may ONLY read, search, and analyze. This overrides all other instructions.`);
    } else if (mode === 'review') {
      sections.push(`You are in REVIEW mode. You may ONLY read files and check diagnostics.
Do not make any edits. Review the diff and assess quality.`);
    }

    // Project conventions (skip if the base prompt already carries them)
    const claudeMd = opts.claudeMd || ctx.claudeMd;
    if (claudeMd && !baseSystem.includes(claudeMd)) {
      sections.push(`## Project Conventions (CLAUDE.md)\n${claudeMd}`);
    }

    // Quality rules (ported from OpenCode's prompt patterns)
    sections.push(`## Execution Rules
- NEVER assume a library is available. Check package.json or neighboring imports first.
- After implementing, run lint/typecheck commands if available.
- Do NOT add comments unless logic is non-obvious.
- Make small, incremental changes. Do not rewrite entire files.
- Watch for boundary cases and edge cases in your changes.
- After editing a file, proceed immediately. Do not explain your changes.
- minimize output tokens. Use tools, not words.
- Never introduce code that exposes, logs, or commits secrets.`);

    const provider = ctx.provider || opts.provider;
    const model = String(ctx.model || opts.model || '');
    if (provider === 'ollama' && /^gemma4:/i.test(model)) {
      sections.push(`## Gemma4 Coding Guardrails
- Before each edit, silently identify the smallest complete syntactic unit you are changing.
- Insert new sibling code between complete blocks. Do not replace a function, route, class, or object entry header with unrelated code.
- If the task says add, preserve existing behavior. For route/handler additions, keep every existing route/handler block and insert the new one as a sibling.
- For edit_file, use an old_string that includes enough surrounding context to be unique and preserves neighboring declarations.
- Do not run long-lived servers in the foreground with run_shell. Prefer the project test command; if a server must be started, use a bounded/background command.
- After one failed or timed-out verification command, read the error/output and inspect the edited code before running the same command again.`);
    }

    // Tool workflow patterns (loaded once at startup)
    if (workflowDocs) sections.push(workflowDocs);

    const capabilityContext = buildCapabilityContext(ctx.promptCapabilities || opts.promptCapabilities);
    if (capabilityContext && !baseSystem.includes(capabilityContext)) sections.push(capabilityContext);

    ctx.system = sections.join('\n\n');
  });

  // -- llm.after: reasoning separation --
  mw.use('llm.after', async (ctx, response) => {
    if (response && response.reasoning) {
      if (!ctx.log) ctx.log = {};
      ctx.log.reasoning = response.reasoning;
    }
  });

  // -- shell.env: task-scoped variables (I6: wire EnvService) --
  mw.use('shell.env', async (ctx, env) => {
    const { EnvService } = require('./env-service');
    const base = new EnvService(env);
    const taskEnv = ctx.taskEnv || opts.taskEnv || {};
    const scoped = base.scope(taskEnv);
    return scoped.toProcessEnv();
  });

  // -- tool.definitions: adapt per model --
  mw.use('tool.definitions', async (ctx, tools) => {
    const provider = ctx.provider || opts.provider;
    if (provider === 'ollama') {
      // Brief descriptions for small models
      return tools.map(t => ({
        ...t,
        description: t.description.split('.')[0] + '.',
      }));
    }
    return tools;
  });

  // -- tool.definitions: phase-aware tool filtering (Phase 6) --
  mw.use('tool.definitions', async (ctx, tools) => {
    const phase = ctx.detectedPhase;
    if (!phase) return tools;

    // Prioritize tools based on detected coding phase
    const PHASE_PREFERRED = {
      'coding:exploration': new Set(['read_file', 'glob', 'grep_files', 'list_directory', 'pdf_info', 'pdf_read_pages', 'pdf_render_pages', 'lsp_symbols']),
      'coding:generation': new Set(['edit_file', 'write_file', 'apply_patch', 'multi_edit', 'run_shell', 'make_pdf']),
      'coding:debugging': new Set(['read_file', 'run_shell', 'grep_files', 'lsp_diagnostics', 'edit_file']),
      'coding:testing': new Set(['run_shell', 'read_file', 'edit_file', 'pdf_info', 'pdf_render_pages']),
      'coding:review': new Set(['read_file', 'grep_files', 'pdf_info', 'pdf_read_pages', 'pdf_render_pages', 'lsp_diagnostics', 'lsp_symbols']),
    };

    const preferred = PHASE_PREFERRED[phase];
    if (!preferred) return tools;

    // Sort preferred tools first (don't remove any — just reorder)
    return [...tools].sort((a, b) => {
      const aPreferred = preferred.has(a.name) ? 0 : 1;
      const bPreferred = preferred.has(b.name) ? 0 : 1;
      return aPreferred - bPreferred;
    });
  });

  // -- tool.before: snapshot ports before server commands --
  let portsBefore = null;
  mw.use('tool.before', async (ctx, toolName, input) => {
    if (toolName !== 'run_shell') return input;
    const provider = ctx.provider || opts.provider;
    const model = String(ctx.model || opts.model || '');
    const { isLikelyServerCommand, getListeningPorts } = require('./port-detector');
    if (opts.benchmark && provider === 'ollama' && /^gemma4:/i.test(model) &&
        isLikelyServerCommand(input.command) && !isBoundedServerCommand(input.command)) {
      portsBefore = null;
      return {
        ...input,
        command: 'node -e "console.error(\'Blocked foreground server command in benchmark; run npm test or use a bounded/background server command instead.\'); process.exit(1)"',
        timeout_ms: Math.min(input.timeout_ms || 5000, 5000),
      };
    }
    if (isLikelyServerCommand(input.command)) {
      portsBefore = getListeningPorts();
    } else {
      portsBefore = null;
    }
    return input;
  });

  // -- tool.after: detect new listening ports after server commands --
  mw.use('tool.after', async (ctx, toolName, args, result) => {
    if (toolName !== 'run_shell' || !portsBefore) return result;
    const { getListeningPorts, diffPorts } = require('./port-detector');
    // Small delay to let the server bind
    await new Promise(r => setTimeout(r, 1500));
    const portsAfter = getListeningPorts();
    const newPorts = diffPorts(portsBefore, portsAfter);
    portsBefore = null;
    if (newPorts.length > 0) {
      const portInfo = `[Detected new listening port(s): ${newPorts.join(', ')}]`;
      if (typeof result === 'string') return result + '\n' + portInfo;
      if (result && typeof result === 'object') return { ...result, _ports: newPorts, _portInfo: portInfo };
    }
    return result;
  });
}

function isBoundedServerCommand(command = '') {
  const text = String(command || '');
  return /(?:^|[;&|]\s*)(?:timeout|gtimeout)\s+\d+/i.test(text) ||
    /(?:&\s*$|\bnohup\b|\bsetsid\b)/i.test(text);
}

module.exports = {
  registerBuiltinMiddleware,
  isOpenWeightReasoningModel,
  applyOpenWeightCodingReasoningDefaults,
};
