/**
 * `start_watch` — a builtin tool that lets the LLM register a background
 * watch on the fly, instead of relying on a connector author's static
 * `async` block. Use case: the model just triggered a long job (jenkins
 * build, a test run) and, rather than polling in-conversation (burning
 * tokens, getting stuck), it calls start_watch to have Forge poll a
 * given tool until a done/fail condition, then report back in chat.
 *
 * This is the dynamic counterpart to manifest `async` blocks — same
 * watch backend (store + runner + chip), just driven by the model. The
 * handler closes over the originating session id so completion routes to
 * the right conversation.
 */

import type { BuiltinToolDef } from '../chat/tool-dispatcher';
import { createWatch, countActive } from './watch-store';
import { MAX_ACTIVE_WATCHES, DEFAULT_CHAIN_DEPTH } from './register';

const MIN_INTERVAL_SEC = 30;
const DEFAULT_INTERVAL_SEC = 60;
const DEFAULT_TIMEOUT_SEC = 1800;
const DEFAULT_MAX_POLLS = 40;

export interface StartWatchTool {
  def: BuiltinToolDef;
  handle: (input: unknown) => Promise<string>;
}

function num(v: unknown, dflt: number): number {
  const n = Number(v);
  return Number.isFinite(n) && n > 0 ? n : dflt;
}

export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
  const def: BuiltinToolDef = {
    name: 'start_watch',
    description:
      'Register a BACKGROUND WATCH that polls a tool until done, then posts the result back here — for long-running jobs you just kicked off (a Forge pipeline, a Jenkins build, a test run, a device upgrade). Use this INSTEAD of polling in conversation: call the trigger tool, then call start_watch and STOP — Forge polls in the background and a completion message arrives in this chat. ' +
      'Pick `poll` = the read tool that reports status. Two forms accepted: (a) connector tool "<connector>.<tool>" e.g. "jenkins.get_build", "gitlab.get_pipeline"; (b) BARE builtin name — "get_pipeline_status" (pair with poll_args={pipeline_id} and done_match={path:"status",equals:"done"}) or "get_task_status" (pair with poll_args={task_id} and done_path="terminal"). Set `poll_args` to call it with (e.g. the build number you predicted via get_next_build_number). ' +
      'Give a done condition: `done_match` {path, equals} on the poll result (e.g. path "result" equals "SUCCESS"), or `done_path` (a result path that becomes truthy). You usually already saw the poll tool\'s output once, so you know the right field. Optional `fail_path` (truthy = failed). ' +
      'If the poll tool returns NON-JSON text (e.g. some shell-protocol tools), the result is wrapped as {_raw: "...full output..."}, so use done_match={path:"_raw",contains:"DONE"} to grep markers in the output. Tune `interval_sec`/`timeout_sec` to the job (pipeline ≈ 30s / 1800s, build ≈ 60s / 1800s).',
    input_schema: {
      type: 'object',
      properties: {
        poll: { type: 'string', description: 'Tool to poll. Either "<connector>.<tool>" e.g. "jenkins.get_build", OR a bare builtin name — "get_pipeline_status" (Forge pipelines) or "get_task_status" (dispatched tasks). Must be a read/status tool.' },
        poll_args: { type: 'object', description: 'Args to call the poll tool with each tick, e.g. {"job_path":"job/foo","build_number":18}. Concrete values, not templates.' },
        done_match: {
          type: 'object',
          description: 'Done when poll-result <path> equals/contains this. e.g. {"path":"result","equals":"SUCCESS"}.',
          properties: {
            path: { type: 'string' },
            equals: { type: 'string' },
            contains: { type: 'string' },
          },
        },
        done_path: { type: 'string', description: 'Alternative to done_match: done when this poll-result path is truthy.' },
        fail_path: { type: 'string', description: 'Optional: poll-result path that, when truthy, means failed.' },
        message: { type: 'string', description: 'Message posted to chat on completion. May reference {poll.<path>}, e.g. "Build 18 finished: {poll.result}".' },
        fail_message: { type: 'string', description: 'Optional message on failure/timeout.' },
        progress_message: { type: 'string', description: 'Optional per-poll status chip text (ambient, not a chat message). e.g. "Build 18 — {poll.result}".' },
        interval_sec: { type: 'number', description: 'Seconds between polls (default 60, min 30).' },
        timeout_sec: { type: 'number', description: 'Overall deadline in seconds (default 1800).' },
        max_polls: { type: 'number', description: 'Hard cap on poll count (default 40).' },
      },
      required: ['poll'],
    },
  };

  const handle = async (input: unknown): Promise<string> => {
    const a = (input ?? {}) as Record<string, any>;
    const poll = String(a.poll || '').trim();
    const dot = poll.indexOf('.');
    // Two forms accepted:
    //   "connector.tool"  → connector_id="connector", poll_tool="tool"
    //   "tool"            → builtin (e.g. get_pipeline_status), connector_id=""
    // The watch-runner detects empty connector_id and dispatches as a
    // bare builtin name instead of "connector.tool".
    const connectorId = dot >= 1 ? poll.slice(0, dot) : '';
    const pollTool = dot >= 1 ? poll.slice(dot + 1) : poll;
    if (!pollTool) {
      return JSON.stringify({ ok: false, error: 'poll is required, e.g. "jenkins.get_build" or builtin "get_pipeline_status"' });
    }

    const doneMatch = a.done_match && typeof a.done_match === 'object' && a.done_match.path
      ? { path: String(a.done_match.path), ...(a.done_match.equals != null ? { equals: String(a.done_match.equals) } : {}), ...(a.done_match.contains != null ? { contains: String(a.done_match.contains) } : {}) }
      : null;
    const donePath = typeof a.done_path === 'string' && a.done_path ? a.done_path : null;
    if (!doneMatch && !donePath) {
      return JSON.stringify({ ok: false, error: 'give a done condition: done_match {path,equals} or done_path' });
    }
    if (countActive() >= MAX_ACTIVE_WATCHES) {
      return JSON.stringify({ ok: false, error: `active watch limit reached (${MAX_ACTIVE_WATCHES})` });
    }

    const hint = ['build_number', 'host', 'ip', 'lab', 'id', 'name', 'pipeline_id'].map((k) => a.poll_args?.[k]).find((v) => v != null && v !== '');
    const label = `${connectorId ? `${connectorId}.${pollTool}` : pollTool}${hint != null ? ` ${hint}` : ''}`;

    const w = createWatch({
      session_id: sessionId,
      label,
      connector_id: connectorId,
      poll_tool: pollTool,
      poll_args: (a.poll_args && typeof a.poll_args === 'object') ? a.poll_args : {},
      done_path: donePath,
      done_match: doneMatch,
      fail_path: typeof a.fail_path === 'string' && a.fail_path ? a.fail_path : null,
      on_done: { mode: 'chat', message: String(a.message || `${label}: done.`) },
      on_fail: { mode: 'chat', message: String(a.fail_message || `${label}: did not complete in time — please check.`) },
      progress: { show: true, ...(a.progress_message ? { message: String(a.progress_message) } : {}) },
      interval_sec: Math.max(MIN_INTERVAL_SEC, num(a.interval_sec, DEFAULT_INTERVAL_SEC)),
      timeout_sec: num(a.timeout_sec, DEFAULT_TIMEOUT_SEC),
      max_polls: num(a.max_polls, DEFAULT_MAX_POLLS),
      chain_depth: DEFAULT_CHAIN_DEPTH,
      now: Date.now(),
    });
    return JSON.stringify({
      ok: true,
      watch_id: w.id,
      polling: poll,
      note: 'Background watch registered. STOP polling in conversation — a completion message will arrive in this chat. The user can see/cancel it in the watch list.',
    });
  };

  return { def, handle };
}
