// lib/tools/adapters/composite-run.js — the ONE generic composite interpreter.
//
// composite_tool_run executes a declarative recipe by RE-ENTERING
// executeRegistryTool for every `tool_call` step. This is the single most
// important security invariant of the composite system: a composite NEVER calls
// an adapter directly, so every gate (agent permission, trust mode, irreversible-
// external approval, allowlist, rate limit, egress, audit) fires per sub-step.
// No gate is skippable via composition.
//
// A sub-step that does not return ok:true (denied permission, approval_required,
// allowlist_violation, rate_limited, adapter error) ABORTS the composite with a
// structured error naming the failing step. The composite's own descriptor risk
// is MAX over steps, so the outer gate already reflects the riskiest sub-call.
//
// llm_synthesis steps are non-side-effecting; they call an injectable synthesizer
// (options.__synthesize) or fall back to chatWithClaude, and on failure yield an
// empty result rather than aborting. artifact_write writes a local artifact.

import { executeRegistryTool } from '../registry.js';
import { getCompositeSpec } from '../composite/registry.js';
import { createArtifact } from '../../runtime.js';

const RESERVED_KEYS = new Set(['__composite_slug', 'approval_id']);

function getByPath(root, dottedPath) {
  let cur = root;
  for (const seg of dottedPath.split('.')) {
    if (cur == null) return undefined;
    cur = cur[seg];
  }
  return cur;
}

/**
 * Resolve {{input.x}} / {{stepId.path}} / {{stepId}} placeholders in a value
 * against the run context. Pure string interpolation over DATA — no execution.
 * A whole-string single placeholder returns the referenced value with its native
 * type; otherwise placeholders are stringified into the surrounding text.
 */
function resolveTemplate(value, context) {
  if (typeof value === 'string') {
    const whole = value.match(/^\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}$/);
    if (whole) {
      const resolved = getByPath(context, whole[1]);
      return resolved === undefined ? '' : resolved;
    }
    return value.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_, p) => {
      const r = getByPath(context, p);
      if (r === undefined) return '';
      return typeof r === 'string' ? r : JSON.stringify(r);
    });
  }
  if (Array.isArray(value)) return value.map((v) => resolveTemplate(v, context));
  if (value && typeof value === 'object') {
    const out = {};
    for (const [k, v] of Object.entries(value)) out[k] = resolveTemplate(v, context);
    return out;
  }
  return value;
}

async function runSynthesis(step, context, options) {
  const prompt = resolveTemplate(step.prompt, context);
  if (typeof options.__synthesize === 'function') {
    try {
      const text = await options.__synthesize(prompt);
      return { ok: true, text: String(text ?? '') };
    } catch (err) {
      return { ok: true, text: '', skipped: true, reason: String(err?.message || err) };
    }
  }
  try {
    const { chatWithClaude } = await import('../../claude.js');
    const reply = await chatWithClaude({ messages: [{ role: 'user', content: prompt }] });
    const text = typeof reply === 'string' ? reply : (reply?.text || reply?.content || '');
    return { ok: true, text: String(text || '') };
  } catch (err) {
    // Synthesis is non-side-effecting — a missing LLM must not abort the recipe.
    return { ok: true, text: '', skipped: true, reason: String(err?.message || err) };
  }
}

export async function compositeToolRun(input = {}, options = {}) {
  const slug = input.__composite_slug;
  const spec = slug ? getCompositeSpec(slug) : null;
  if (!spec) {
    return { ok: false, status: 'error', error: 'not_found', message: `Unknown composite tool: ${slug || '(none)'}` };
  }

  // Build the run input (the composite's own params, minus reserved keys).
  const runInput = {};
  for (const [k, v] of Object.entries(input)) {
    if (!RESERVED_KEYS.has(k)) runInput[k] = v;
  }
  const context = { input: runInput };
  const stepResults = [];

  const subOptions = {
    agent_id: options.agent_id,
    project_id: options.project_id,
    org_id: options.org_id,
    actor_role: options.actor_role,
    owner_trusted: options.owner_trusted,
    acting_on_behalf_of_user_id: options.acting_on_behalf_of_user_id || options.user_id || null,
  };

  for (const step of spec.steps) {
    if (step.kind === 'tool_call') {
      const stepInput = resolveTemplate(step.input_template || {}, context);
      // RE-ENTER the full gate pipeline. Never call the adapter directly. A gate
      // that THROWS (e.g. denied agent permission, disabled/unavailable tool) is
      // caught here and surfaced as step_failed — the composite still aborts, so
      // no gate is skippable via composition.
      let result;
      try {
        result = await executeRegistryTool(step.tool, stepInput, subOptions);
      } catch (err) {
        return {
          ok: false,
          status: 'error',
          error: 'step_failed',
          failed_step: step.id,
          failed_tool: step.tool,
          message: `Composite ${slug} aborted at step '${step.id}' (${step.tool}): ${String(err?.message || err)}`,
          completed_steps: stepResults.map((s) => s.id),
        };
      }
      const okStep = result && result.ok !== false && result.status !== 'approval_required'
        && result.status !== 'allowlist_violation' && result.status !== 'rate_limited' && result.status !== 'error';
      if (!okStep) {
        return {
          ok: false,
          status: 'error',
          error: 'step_failed',
          failed_step: step.id,
          failed_tool: step.tool,
          step_result: result,
          message: `Composite ${slug} aborted at step '${step.id}' (${step.tool}): ${result?.status || result?.error || 'failed'}`,
          completed_steps: stepResults.map((s) => s.id),
        };
      }
      context[step.id] = result;
      stepResults.push({ id: step.id, kind: 'tool_call', tool: step.tool, ok: true, status: result.status });
    } else if (step.kind === 'llm_synthesis') {
      const syn = await runSynthesis(step, context, options);
      context[step.id] = syn;
      stepResults.push({ id: step.id, kind: 'llm_synthesis', ok: true, skipped: Boolean(syn.skipped) });
    } else if (step.kind === 'artifact_write') {
      try {
        const title = resolveTemplate(step.title_template || `${slug} output`, context);
        const content = step.content_ref ? resolveTemplate(step.content_ref, context) : '';
        const artifact = createArtifact({
          title: String(title),
          kind: 'markdown',
          content: typeof content === 'string' ? content : JSON.stringify(content, null, 2),
          agent_id: options.agent_id || null,
          project_id: options.project_id || null,
        });
        context[step.id] = artifact;
        stepResults.push({ id: step.id, kind: 'artifact_write', ok: true, artifact_id: artifact?.id || null });
      } catch (err) {
        return {
          ok: false, status: 'error', error: 'step_failed', failed_step: step.id,
          message: `Composite ${slug} aborted at artifact_write '${step.id}': ${String(err?.message || err)}`,
          completed_steps: stepResults.map((s) => s.id),
        };
      }
    }
  }

  return {
    ok: true,
    status: 'complete',
    composite: slug,
    steps: stepResults,
    result: stepResults.length ? context[stepResults[stepResults.length - 1].id] : null,
  };
}
