'use strict';

const fs = require('node:fs');
const path = require('node:path');
const { normalizeToolCallEvidence } = require('./acceptance-contract');

const FILE_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'multi_edit', 'apply_patch']);
const PATH_INPUT_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'pdf_info', 'pdf_render_pages', 'pdf_read_pages']);
const PROJECT_ROOT_TOOLS = new Set(['read_file', 'write_file', 'edit_file', 'apply_patch', 'multi_edit', 'glob', 'grep_files', 'list_directory', 'pdf_info', 'pdf_render_pages', 'pdf_read_pages', 'make_pdf']);

class ToolExecutionController {
  constructor({
    toolRegistry,
    middleware = null,
    permissionService = null,
    questionManager = null,
    events = null,
    lifecycleHooks = null,
    cwd = process.cwd(),
    projectRoot = '',
    sessionId = '',
    provider = '',
    model = '',
    mode = '',
    runtimeMode = '',
    headless = false,
    benchmark = false,
    handlePermissions = true,
    userHooks = null,
  } = {}) {
    if (!toolRegistry) throw new Error('toolRegistry is required');
    this.toolRegistry = toolRegistry;
    this.middleware = middleware;
    this.permissionService = permissionService;
    this.userHooks = userHooks;
    this.questionManager = questionManager;
    this.events = events;
    this.lifecycleHooks = lifecycleHooks;
    this.cwd = path.resolve(cwd || process.cwd());
    this.projectRoot = path.resolve(projectRoot || this.cwd);
    this.sessionId = sessionId;
    this.provider = provider;
    this.model = model;
    this.mode = mode;
    this.runtimeMode = runtimeMode;
    this.headless = Boolean(headless);
    this.benchmark = Boolean(benchmark);
    this.handlePermissions = handlePermissions !== false;
  }

  async execute(call = {}, ctx = {}) {
    const name = String(call.name || call.tool || '');
    const toolCallId = String(call.id || call.toolCallId || call.tool_call_id || '');
    if (!name) throw new Error('tool name is required');
    const base = this._ctx(ctx);
    let input = this._normalizeInput(name, call.input || {}, base);
    let finalInput = input;
    let result = null;
    let ok = false;

    await this._emit('tool_requested', {
      toolCallId,
      name,
      input: previewInput(input),
    }, base);

    try {
      const pathBlock = this._pathBlock(name, input, base);
      if (pathBlock) {
        result = { error: pathBlock };
        await this._emit('tool_blocked', { toolCallId, name, reason: pathBlock }, base);
        return this._result({ call, input, finalInput, result, ok: false, blocked: true });
      }

      if (this.handlePermissions && toolRequiresPermission(name) && this.permissionService?.authorize) {
        await this._emit('permission_requested', {
          toolCallId,
          tool: name,
          input: previewInput(input),
        }, base);
        const permission = await this.permissionService.authorize({
          sessionId: base.sessionId,
          tool: name,
          input,
          cwd: input.cwd || base.cwd,
          projectRoot: base.projectRoot,
          mode: base.mode,
          headless: this.headless || this.benchmark,
          metadata: { toolCallId },
        });
        await this._emit(permission.decision === 'allow' ? 'permission_resolved' : 'permission_denied', {
          toolCallId,
          tool: name,
          decision: permission.decision,
          reason: permission.reason || permission.message || '',
          source: permission.source || '',
          permissionContextId: permission.permissionContextId || permission.permission_context_id || '',
        }, base);
        if (permission.decision !== 'allow') {
          result = { error: `Permission denied: ${permission.reason || permission.message || permission.decision}` };
          return this._result({ call, input, finalInput, result, ok: false, denied: true });
        }
      }

      // User pre_tool hooks run AFTER the permission gate (hooks cannot
      // bypass permissions) and may deny or rewrite the tool input.
      if (this.userHooks?.hasHooks('pre_tool')) {
        const hookVerdict = await this.userHooks.run('pre_tool', {
          tool: name,
          input,
          sessionId: base.sessionId,
          cwd: base.cwd,
          mode: base.mode,
        });
        if (hookVerdict.decision === 'deny') {
          result = { error: `Blocked by user hook: ${hookVerdict.reason || 'pre_tool hook denied'}` };
          await this._emit('tool_blocked', { toolCallId, name, reason: hookVerdict.reason || 'user hook deny' }, base);
          return this._result({ call, input, finalInput, result, ok: false, blocked: true });
        }
        if (hookVerdict.updatedInput) input = hookVerdict.updatedInput;
      }

      await this._emit('tool_started', { toolCallId, name, input: previewInput(input) }, base);
      finalInput = await this._runMiddleware('tool.before', base.llmCtx, name, input);
      if (!finalInput || typeof finalInput !== 'object') finalInput = input;
      finalInput = this._decorateInput(name, finalInput, base);

      result = await this._executeTool(name, finalInput, base);
      result = await this._runMiddleware('tool.after', base.llmCtx, name, finalInput, result) || result;
      ok = !result?.error;
      if (this.userHooks?.hasHooks('post_tool')) {
        await this.userHooks.runObserved('post_tool', {
          tool: name,
          input: finalInput,
          ok,
          result: previewResult(result),
          sessionId: base.sessionId,
          cwd: base.cwd,
        });
      }
      this._emitFileEvents(name, finalInput, result, base);
      await this._emit(ok ? 'tool_finished' : 'tool_failed', {
        toolCallId,
        name,
        ok,
        result: previewResult(result),
      }, base);
      return this._result({ call, input, finalInput, result, ok });
    } catch (err) {
      result = { error: err.message };
      await this._emit('tool_failed', {
        toolCallId,
        name,
        ok: false,
        error: err.message,
      }, base);
      return this._result({ call, input, finalInput, result, ok: false, error: err });
    }
  }

  _ctx(ctx = {}) {
    return {
      sessionId: ctx.sessionId || this.sessionId,
      cwd: path.resolve(ctx.cwd || this.cwd),
      projectRoot: path.resolve(ctx.projectRoot || this.projectRoot || ctx.cwd || this.cwd),
      provider: ctx.provider || this.provider,
      model: ctx.model || this.model,
      mode: ctx.mode || this.mode,
      runtimeMode: ctx.runtimeMode || this.runtimeMode,
      llmCtx: ctx.llmCtx || {
        sessionId: ctx.sessionId || this.sessionId,
        cwd: ctx.cwd || this.cwd,
        provider: ctx.provider || this.provider,
        model: ctx.model || this.model,
        mode: ctx.mode || this.mode,
        runtimeMode: ctx.runtimeMode || this.runtimeMode,
      },
      interactive: ctx.interactive,
      onTodos: ctx.onTodos,
    };
  }

  _normalizeInput(name, rawInput, base) {
    const input = { ...(rawInput || {}) };
    if (PATH_INPUT_TOOLS.has(name)) {
      if (input.file_path && !path.isAbsolute(input.file_path)) input.file_path = path.join(base.cwd, input.file_path);
      if (!input.file_path && input.path) input.file_path = path.isAbsolute(input.path) ? input.path : path.join(base.cwd, input.path);
    }
    if (name === 'list_directory' && input.directory && !path.isAbsolute(input.directory)) {
      input.directory = path.join(base.cwd, input.directory);
    }
    if (name === 'glob' && !input.directory) input.directory = base.cwd;
    if (name === 'grep_files' && !input.directory) input.directory = base.cwd;
    if (name === 'run_shell') {
      input.timeout_ms = input.timeout_ms || 30000;
      input.cwd = input.cwd || base.cwd;
    }
    return input;
  }

  _pathBlock(name, input, base) {
    if (!PATH_INPUT_TOOLS.has(name) || !input.file_path) return '';
    if (isWithinDirectory(input.file_path, base.projectRoot)) return '';
    return `Path ${input.file_path} is outside allowed directory ${base.projectRoot}`;
  }

  _decorateInput(name, input, base) {
    const finalInput = { ...(input || {}) };
    if (PROJECT_ROOT_TOOLS.has(name)) {
      finalInput.sessionId = base.sessionId;
      finalInput.projectRoot = base.projectRoot;
    }
    // Background processes are registered per session for end-of-run cleanup
    if (name === 'run_shell') {
      finalInput.sessionId = base.sessionId;
    }
    return finalInput;
  }

  async _executeTool(name, finalInput, base) {
    if (name === 'update_todos') {
      if (typeof base.onTodos === 'function') base.onTodos(finalInput.todos || []);
      return { ok: true, todos: finalInput.todos || [] };
    }
    if (name === 'ask_user') {
      if (base.mode === 'build' && !base.interactive) {
        return { dismissed: true, message: 'Running in non-interactive mode. Please proceed with your best judgment based on the code you have read.' };
      }
      if (!this.questionManager?.ask) return { dismissed: true, message: 'No interactive question manager is available.' };
      const answer = await this.questionManager.ask(base.sessionId, {
        question: finalInput.question,
        header: finalInput.header,
        options: finalInput.options,
        multiple: finalInput.multiple,
        preview: finalInput.preview,
      });
      return answer ? { answers: answer } : { dismissed: true, message: 'Question timed out or was dismissed' };
    }
    return this.toolRegistry.execute(name, finalInput, {
      sessionId: base.sessionId,
      cwd: base.cwd,
      projectRoot: base.projectRoot,
      model: base.model,
      provider: base.provider,
      llmCtx: base.llmCtx,
      lifecycleHooks: this.lifecycleHooks,
    });
  }

  async _runMiddleware(hook, llmCtx, name, input, result) {
    if (!this.middleware?.run) return result === undefined ? input : result;
    if (hook === 'tool.before') return this.middleware.run(hook, llmCtx, name, input);
    return this.middleware.run(hook, llmCtx, name, input, result);
  }

  _emitFileEvents(name, finalInput, result, base) {
    if (!this.events?.emit || result?.error) return;
    if (['edit_file', 'write_file', 'multi_edit'].includes(name)) {
      this.events.emit('file.edited', { filePath: finalInput.file_path, sessionId: base.sessionId });
    }
    if (name === 'apply_patch') {
      const patchFiles = [
        ...(result.added || []),
        ...(result.modified || []),
      ];
      for (const filePath of patchFiles) this.events.emit('file.edited', { filePath, sessionId: base.sessionId });
    }
    if (name === 'read_file') {
      this.events.emit('file.read', { filePath: finalInput.file_path, sessionId: base.sessionId });
    }
  }

  async _emit(type, payload, base) {
    if (!this.lifecycleHooks?.emit) return null;
    return this.lifecycleHooks.emit(type, payload, {
      sessionId: base.sessionId,
      cwd: base.cwd,
      provider: base.provider,
      model: base.model,
      actor: 'tool-execution-controller',
    });
  }

  _result({ call, input, finalInput, result, ok, blocked = false, denied = false, error = null }) {
    const evidence = normalizeToolCallEvidence({ name: call.name, input: finalInput || input }, result);
    return {
      ok: ok === true,
      blocked,
      denied,
      error,
      input,
      finalInput,
      evidenceInput: finalInput || input,
      result,
      evidence,
    };
  }
}

function toolRequiresPermission(name) {
  return [
    'run_shell',
    'read_file',
    'write_file',
    'edit_file',
    'apply_patch',
    'multi_edit',
    'browser_screenshot',
    'applescript',
    'claude_code',
    'mail_send',
    'mail_reply',
    'pdf_info',
    'pdf_render_pages',
    'pdf_read_pages',
    'make_pdf',
    'slack_send_message',
    'load_skill',
    'mcp_call',
    'skill',
  ].includes(name) || String(name || '').startsWith('mcp__') || String(name || '').startsWith('skill__');
}

function previewInput(input) {
  return previewJson(input, 2000);
}

function previewResult(result) {
  return previewJson(result, 4000);
}

function previewJson(value, max) {
  try {
    const json = JSON.stringify(value);
    if (!json || json.length <= max) return value;
    return { preview: `${json.slice(0, Math.max(0, max - 24))}\n[preview truncated]`, truncated: true };
  } catch {
    return String(value);
  }
}

function isWithinDirectory(filePath, directory) {
  const resolvedFile = realpathBestEffort(filePath);
  const resolvedDir = realpathBestEffort(directory);
  return resolvedFile === resolvedDir || resolvedFile.startsWith(resolvedDir + path.sep);
}

function realpathBestEffort(filePath) {
  const resolved = path.resolve(filePath);
  try {
    return fs.realpathSync(resolved);
  } catch {
    const parent = path.dirname(resolved);
    if (parent === resolved) return resolved;
    return path.join(realpathBestEffort(parent), path.basename(resolved));
  }
}

module.exports = {
  FILE_TOOLS,
  ToolExecutionController,
  toolRequiresPermission,
  previewInput,
  previewResult,
};
