'use strict';

const { RuntimeDecisionHookBus } = require('./decision-hooks');
const { buildRuntimeToolResultEnvelope } = require('./tool-result-envelope');

class RuntimeToolExecutor {
  constructor({
    registry = null,
    runtimeProfile = null,
    toolSpecs = null,
    executeToolCall = null,
    decisionHooks = null,
    now = () => new Date().toISOString(),
    clock = () => Date.now(),
  } = {}) {
    this.registry = registry;
    this.runtimeProfile = runtimeProfile || {};
    this.toolSpecs = Array.isArray(toolSpecs) ? toolSpecs : null;
    this.executeToolCall = typeof executeToolCall === 'function' ? executeToolCall : null;
    this.decisionHooks = decisionHooks || new RuntimeDecisionHookBus();
    this.now = now;
    this.clock = clock;
  }

  async run(toolCalls = [], ctx = {}) {
    const calls = normalizeToolCalls(toolCalls);
    const specMap = await this._loadSpecMap(ctx);
    const results = new Array(calls.length);
    let batch = [];

    const flush = async () => {
      if (!batch.length) return;
      const current = batch;
      batch = [];
      await Promise.all(current.map(async (item) => {
        results[item.index] = await this._executeOne(item.call, item.index, specMap.get(item.call.name), ctx);
      }));
    };

    for (let index = 0; index < calls.length; index += 1) {
      const call = calls[index];
      const spec = specMap.get(call.name) || normalizeRuntimeToolSpec({ name: call.name });
      if (isRuntimeToolParallelSafe(spec)) {
        batch.push({ call, index });
        continue;
      }
      await flush();
      results[index] = await this._executeOne(call, index, spec, ctx);
    }
    await flush();
    return results;
  }

  async _executeOne(call, index, rawSpec, ctx = {}) {
    const spec = normalizeRuntimeToolSpec(rawSpec || { name: call.name });
    const startedAt = this.clock();
    let finalCall = { ...call, input: call.input || {} };
    const hookCtx = this._ctx(ctx);
    const before = await this.decisionHooks.run('tool.before', {
      call: finalCall,
      index,
      input: finalCall.input,
      spec,
    }, hookCtx);
    if (before.input !== undefined) {
      finalCall = { ...finalCall, input: before.input };
    }
    if (before.denied || before.stop) {
      const result = {
        ok: false,
        denied: before.denied,
        stopped: before.stop,
        error: before.reason || 'Tool execution stopped by runtime hook',
      };
      return this._row({ call: finalCall, spec, result, error: result.error, startedAt });
    }

    try {
      let rawResult = this.executeToolCall
        ? await this.executeToolCall(finalCall, { ...hookCtx, spec, index })
        : await this._executeViaRegistry(finalCall, hookCtx);
      const resultForHook = unwrapToolResult(rawResult);
      const after = await this.decisionHooks.run('tool.after', {
        call: finalCall,
        index,
        input: finalCall.input,
        spec,
        rawResult,
        result: resultForHook,
      }, hookCtx);
      if (after.result !== undefined) {
        rawResult = replaceUnwrappedToolResult(rawResult, after.result);
      }
      const result = unwrapToolResult(rawResult);
      return this._row({ call: finalCall, spec, rawResult, result, startedAt });
    } catch (error) {
      const failed = await this.decisionHooks.run('tool.failed', {
        call: finalCall,
        index,
        input: finalCall.input,
        spec,
        error,
      }, hookCtx);
      const result = failed.result !== undefined
        ? failed.result
        : { ok: false, error: error?.message || String(error) };
      return this._row({ call: finalCall, spec, result, error, startedAt });
    }
  }

  async _executeViaRegistry(call, ctx) {
    if (!this.registry?.execute) {
      return { ok: false, invalid: true, error: `Tool registry unavailable for ${call.name}` };
    }
    return this.registry.execute(call.name, call.input || {}, ctx);
  }

  _row({ call, spec, rawResult, result, error = null, startedAt }) {
    const envelope = buildRuntimeToolResultEnvelope({
      call,
      spec,
      result,
      error,
      startedAt,
      endedAt: this.clock(),
      now: this.now,
    });
    return {
      call,
      spec,
      rawResult: rawResult === undefined ? result : rawResult,
      result,
      envelope,
      ok: envelope.ok,
    };
  }

  async _loadSpecMap(ctx = {}) {
    const specs = [];
    if (this.toolSpecs) specs.push(...this.toolSpecs);
    if (this.registry?.getToolSpecs) {
      try {
        specs.push(...await this.registry.getToolSpecs(this._ctx(ctx)));
      } catch {}
    }
    const map = new Map();
    for (const spec of specs) {
      const normalized = normalizeRuntimeToolSpec(spec);
      if (normalized.name) map.set(normalized.name, normalized);
    }
    return map;
  }

  _ctx(extra = {}) {
    return {
      ...extra,
      runtimeProfile: this.runtimeProfile.profileId || extra.runtimeProfile,
      permissionProfile: this.runtimeProfile.permissionProfile || extra.permissionProfile,
      persistenceProfile: this.runtimeProfile.persistenceProfile || extra.persistenceProfile,
      outputContract: this.runtimeProfile.outputContract || extra.outputContract,
      toolsets: extra.toolsets || this.runtimeProfile.toolsets || [],
      channel: this.runtimeProfile.channel || extra.channel,
      source: this.runtimeProfile.source || extra.source,
      agentMode: this.runtimeProfile.agentMode || extra.agentMode,
      agentKind: this.runtimeProfile.agentKind || extra.agentKind,
      taskType: this.runtimeProfile.taskType || extra.taskType,
      sessionId: extra.sessionId || this.runtimeProfile.sessionId,
      ctmSessionId: extra.ctmSessionId || this.runtimeProfile.ctmSessionId,
      agentSessionId: extra.agentSessionId || this.runtimeProfile.agentSessionId,
      chatSessionId: extra.chatSessionId || this.runtimeProfile.chatSessionId,
      cwd: extra.cwd || this.runtimeProfile.cwd,
    };
  }
}

function normalizeToolCalls(toolCalls = []) {
  return (toolCalls || []).filter(Boolean).map((call, index) => ({
    id: String(call.id || call.toolCallId || call.tool_call_id || `tool-${index + 1}`),
    name: String(call.name || call.tool || ''),
    input: call.input || call.args || call.arguments || {},
    original: call,
  })).filter((call) => call.name);
}

function normalizeRuntimeToolSpec(spec = {}) {
  const name = String(spec.name || spec.tool || '');
  const sideEffect = normalizeSideEffect(spec.sideEffect || spec.side_effect || inferSideEffect(name));
  const readOnly = Boolean(spec.readOnly ?? spec.read_only ?? sideEffect === 'read');
  const approval = normalizeApproval(spec.approval || spec.approvalPolicy || spec.approval_policy || (readOnly ? 'never' : 'required'));
  const supportsParallel = Boolean(spec.supportsParallel ?? spec.supports_parallel ?? (readOnly && approval === 'never'));
  return {
    ...spec,
    name,
    sideEffect,
    readOnly,
    approval,
    supportsParallel,
    interruptBehavior: normalizeInterruptBehavior(spec.interruptBehavior || spec.interrupt_behavior || (readOnly ? 'cancel' : 'block')),
    resultBudgetBytes: Number.isFinite(Number(spec.resultBudgetBytes || spec.result_budget_bytes))
      ? Number(spec.resultBudgetBytes || spec.result_budget_bytes)
      : undefined,
    telemetryPolicy: spec.telemetryPolicy || spec.telemetry_policy || 'safe-summary',
    renderHint: spec.renderHint || spec.render_hint || (readOnly ? 'collapsed-read' : 'expanded-action'),
    sandboxProfile: spec.sandboxProfile || spec.sandbox_profile || (sideEffect === 'read' ? 'readonly' : 'workspace'),
  };
}

function isRuntimeToolParallelSafe(spec = {}) {
  const normalized = normalizeRuntimeToolSpec(spec);
  return normalized.supportsParallel === true
    && normalized.readOnly === true
    && normalized.approval === 'never'
    && normalized.sideEffect === 'read';
}

function normalizeSideEffect(value) {
  const normalized = String(value || '').toLowerCase();
  if (['read', 'write', 'external', 'network', 'process', 'none', 'unknown'].includes(normalized)) return normalized;
  return 'unknown';
}

function normalizeApproval(value) {
  const normalized = String(value || '').toLowerCase();
  if (['never', 'required', 'profile'].includes(normalized)) return normalized;
  if (normalized === 'always') return 'required';
  return 'required';
}

function normalizeInterruptBehavior(value) {
  const normalized = String(value || '').toLowerCase();
  return normalized === 'cancel' ? 'cancel' : 'block';
}

function inferSideEffect(name = '') {
  const text = String(name || '').toLowerCase();
  if (!text) return 'unknown';
  if (/^(read|list|search|grep|glob|find|lookup|get|fetch|check|ctm_context|ctm_remote_access_status|ctm_session_search|think|load|calendar_list|calendar_events|mail_read|mail_search|mail_messages|slack_search|slack_read_channel|clipboard_read|system_info|session_search|session_load|web_fetch)/.test(text)) return 'read';
  if (/(write|edit|patch|delete|remove|send|reply|create|update|apply|run_shell|shell|screenshot|browser|start_|stop_)/.test(text)) return 'write';
  if (/(mcp|skill|slack|gmail|calendar|mail|http|web)/.test(text)) return 'external';
  return 'unknown';
}

function unwrapToolResult(rawResult) {
  if (rawResult && typeof rawResult === 'object' && rawResult.call && Object.prototype.hasOwnProperty.call(rawResult.call, 'result')) {
    return rawResult.call.result;
  }
  if (rawResult && typeof rawResult === 'object' && Object.prototype.hasOwnProperty.call(rawResult, 'result') && Object.keys(rawResult).length <= 4) {
    return rawResult.result;
  }
  return rawResult;
}

function replaceUnwrappedToolResult(rawResult, result) {
  if (rawResult && typeof rawResult === 'object' && rawResult.call && Object.prototype.hasOwnProperty.call(rawResult.call, 'result')) {
    return {
      ...rawResult,
      call: { ...rawResult.call, result },
    };
  }
  return result;
}

module.exports = {
  RuntimeToolExecutor,
  inferSideEffect,
  isRuntimeToolParallelSafe,
  normalizeRuntimeToolSpec,
  normalizeToolCalls,
};
