/**
 * Tool dispatcher — routes an LLM tool call to the right executor.
 *
 *   <plugin_id>.<tool_name>  →  connector tool
 *       browser protocol → dispatched to extension via bridge.connector_run
 *       (http/ssh/mcp/etc. → reserved for Phase 3 runtimes)
 *   builtin name              →  in-process function
 *
 * Returns the tool_result block content (string) + is_error flag.
 */

import { bridgeRpc } from './bridge-client';
import { runHttp } from './protocols/http';
import { runShell } from './protocols/shell';
import {
  getConnector,
  getInstalledConnector,
  getConnectorEntries,
} from '../connectors/registry';
import { expandSettingsTokens } from '../plugins/templates';
import type { ConnectorEntry, ConnectorTool } from '../connectors/types';

export interface ToolCall {
  id: string;
  name: string;
  input: unknown;
}

export interface ToolResult {
  content: string;
  is_error?: boolean;
}

// ─── Builtins ─────────────────────────────────────────────

export type BuiltinHandler = (input: unknown) => Promise<string>;

const BUILTINS: Record<string, BuiltinHandler> = {
  get_current_time: async () => new Date().toISOString(),
};

export interface BuiltinToolDef {
  name: string;
  description: string;
  input_schema: { type: 'object'; properties: Record<string, unknown>; required?: string[] };
}

export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
  {
    name: 'get_current_time',
    description: 'Get the current local time as an ISO 8601 string. Use whenever the user asks about "now" or "today".',
    input_schema: { type: 'object', properties: {} },
  },
];

// ─── Connector dispatch ──────────────────────────────────

/** Look up a connector tool by `<connector_id>.<tool_name>` form. */
function findConnectorTool(qualified: string): {
  connectorId: string;
  toolName: string;
  entry: ConnectorEntry;
  tool: ConnectorTool;
  settings: Record<string, any>;
} | null {
  const dot = qualified.indexOf('.');
  if (dot < 1) return null;
  const connectorId = qualified.slice(0, dot);
  const toolName = qualified.slice(dot + 1);
  const def = getConnector(connectorId);
  if (!def) return null;
  const entries = getConnectorEntries(def);
  for (const entry of entries) {
    const tool = entry.tools?.[toolName];
    if (tool) {
      const inst = getInstalledConnector(connectorId);
      return { connectorId, toolName, entry, tool, settings: (inst?.config as Record<string, any>) || {} };
    }
  }
  return null;
}

/**
 * Build the ConnectorPayload-ish object the extension's runner expects.
 * Expands {base_url}/{settings.*} tokens here; {args.*} stays literal —
 * the extension's runner finishes those at execution time.
 */
function buildConnectorPayload(
  def: { id: string; name: string; runner?: 'main' | 'isolated'; host_match?: string; login_redirect?: string },
  entry: ConnectorEntry,
  settings: Record<string, any>,
) {
  const expand = (s: string | undefined) => (s ? expandSettingsTokens(s, settings) : s);
  const hostMatch = expand(entry.host_match || def.host_match);
  const loginRedirect = expand(entry.login_redirect || def.login_redirect);

  // Build entries with expanded page URLs
  const expandedEntry: any = {
    ...entry,
    host_match: hostMatch,
    login_redirect: loginRedirect,
    tools: Object.fromEntries(
      Object.entries(entry.tools || {}).map(([n, t]: [string, any]) => [
        n,
        {
          ...t,
          page: t.page
            ? {
                url: expand(t.page.url),
                ...(t.page.on_target ? { on_target: expand(t.page.on_target) } : {}),
                ...(t.page.settle_after_load_ms != null ? { settle_after_load_ms: t.page.settle_after_load_ms } : {}),
              }
            : undefined,
        },
      ]),
    ),
  };

  return {
    plugin_id: def.id,             // legacy wire-name; extension reads this
    name: def.name,
    mode: 'browser-side',
    installed: true,
    host_match: hostMatch,
    login_redirect: loginRedirect,
    runner: def.runner || entry.runner || 'main',
    entries: [expandedEntry],
  };
}

// ─── Public entry ─────────────────────────────────────────

export interface DispatchOptions {
  /** Per-turn extra builtins (e.g. memory_* when Temper is configured). */
  extraBuiltins?: Record<string, BuiltinHandler>;
  /**
   * Skip the HTTP-protocol 8KB body cap. Set by callers that parse the
   * response programmatically (Jobs scheduler, /api/jobs/preview) and
   * therefore don't need an LLM-friendly truncation.
   */
  noTruncation?: boolean;
}

export async function dispatchTool(
  call: ToolCall,
  optsOrExtraBuiltins?: DispatchOptions | Record<string, BuiltinHandler>,
): Promise<ToolResult> {
  // Back-compat: accept either DispatchOptions or a bare extraBuiltins map
  // so older callers (chat agent-loop etc.) keep working without edits.
  const opts: DispatchOptions = optsOrExtraBuiltins && 'extraBuiltins' in (optsOrExtraBuiltins as any)
    ? (optsOrExtraBuiltins as DispatchOptions)
    : optsOrExtraBuiltins && 'noTruncation' in (optsOrExtraBuiltins as any)
      ? (optsOrExtraBuiltins as DispatchOptions)
      : { extraBuiltins: optsOrExtraBuiltins as Record<string, BuiltinHandler> | undefined };
  const extraBuiltins = opts.extraBuiltins;
  // Per-turn builtins first (memory tools), then global builtins.
  const dynBuiltin = extraBuiltins?.[call.name];
  if (dynBuiltin) {
    try { return { content: await dynBuiltin(call.input) }; }
    catch (e) { return { content: `tool '${call.name}' failed: ${(e as Error).message}`, is_error: true }; }
  }
  const builtin = BUILTINS[call.name];
  if (builtin) {
    try {
      const content = await builtin(call.input);
      return { content };
    } catch (e) {
      return { content: `builtin tool '${call.name}' failed: ${(e as Error).message}`, is_error: true };
    }
  }

  // Connector tool
  const located = findConnectorTool(call.name);
  if (!located) {
    return { content: `unknown tool: ${call.name}`, is_error: true };
  }

  const def = getConnector(located.connectorId)!;
  const protocol = located.tool.protocol || 'browser';
  const argInput = (call.input ?? {}) as Record<string, any>;

  try {
    switch (protocol) {
      case 'http':
        return await runHttp({ tool: located.tool, settings: located.settings, args: argInput, noTruncation: opts.noTruncation });
      case 'shell':
        return await runShell({ tool: located.tool, settings: located.settings, args: argInput });
      case 'browser': {
        // Hand the whole connector + tool spec + input + settings to the
        // extension's runner.ts via the bridge. The extension keeps owning
        // the runner logic (tab acquire, navigate, executeScript).
        const connector = buildConnectorPayload(def, located.entry, located.settings);
        const result = (await bridgeRpc('connector.run', {
          pluginId: located.connectorId,       // wire-name kept for extension
          toolName: located.toolName,
          input: argInput,
          connector,
          settings: located.settings,
        })) as { content?: string; is_error?: boolean } | null;
        return { content: result?.content ?? '(no content returned)', is_error: !!result?.is_error };
      }
      default:
        return { content: `unknown protocol "${protocol}" on tool ${call.name}`, is_error: true };
    }
  } catch (e) {
    return { content: `connector tool failed: ${(e as Error).message}`, is_error: true };
  }
}
