/**
 * Connector Types — independent of the plugin (pipeline-node) subsystem.
 *
 * Connectors expose tools to Forge chat and the browser extension.
 * Manifests live in <dataDir>/connectors/<id>/manifest.yaml, fetched
 * from the remote `forge-connectors` registry. There are no built-in
 * connectors — first launch requires a successful registry sync (which
 * is cached afterwards for offline use).
 *
 * See lib/help-docs/17-connectors.md for the user-facing manifest spec.
 */

/** Where a connector's scripts run inside the extension. */
export type ConnectorRunner = 'main' | 'isolated';

/** Where a tool's execution lives. */
export type ConnectorProtocol = 'browser' | 'http' | 'shell' | 'ssh';

/** One expect rule for `protocol: ssh`: when output matches `match`
 *  (a regex, tested per output chunk), send `send` + Enter. Used to
 *  auto-answer interactive prompts like `(y/N)`.
 *
 *  `send` may be the INTENT `yes`/`no` — the runner then picks the token
 *  the prompt actually offers (`(y/N)` → `y`/`n`, `(yes/no)` → `yes`/`no`)
 *  and always sends it explicitly (never relies on the prompt's default).
 *  Any other value is sent literally. */
export interface SshExpectRule {
  match: string;
  send: string;
}

/** One completion/failure action for an async watch. */
export interface WatchAction {
  /** chat = feed result back to the originating session (assistant replies);
   *  tool = dispatch a tool (chaining); none = just record terminal state. */
  mode?: 'chat' | 'tool' | 'none';
  /** mode=chat: text injected into the session (templated with {poll.*}). */
  message?: string;
  /** mode=tool: `<connector>.<tool>` to dispatch. */
  tool?: string;
  /** mode=tool: args (templated with {poll.*}/{args.*}). */
  args?: Record<string, unknown>;
}

/**
 * `async` block — declares a tool as a long-running task that Forge
 * watches in the background. Templating in poll_args / done_match.equals /
 * action args / progress.message: {args.*} = the trigger's input,
 * {result.*} = the trigger's return value (resolved once at register
 * time), {poll.*} = the latest poll result (resolved per use).
 */
export interface AsyncWatchSpec {
  /** Tool to poll (same connector), bare tool name e.g. `get_version`. */
  poll: string;
  /** Args for each poll call (templated against trigger args/result). */
  poll_args?: Record<string, unknown>;
  /** (A) poll-result path that, when truthy, means done. */
  done_path?: string;
  /** (B) value comparison on a poll-result path. */
  done_match?: { path: string; equals?: string; contains?: string };
  /** poll-result path that, when truthy, means failed. */
  fail_path?: string;
  /** Seconds between polls (default 60, min 30). */
  interval_sec?: number;
  /** Overall deadline in seconds (default 1200). */
  timeout_sec?: number;
  /** Hard cap on poll count (default 40). */
  max_polls?: number;
  /** Completion action (default {mode:'chat'}). */
  on_done?: WatchAction;
  /** Failure/timeout action (default {mode:'chat'}). */
  on_fail?: WatchAction;
  /** Per-poll ambient progress (does not enter the message thread). */
  progress?: { show?: boolean; message?: string };
}

/**
 * `protocol: ssh` spec — drives an interactive SSH session via a PTY
 * (the system `ssh` binary). Built for devices whose CLI needs a
 * password + interactive confirmations (e.g. FortiNAC firmware restore).
 * All string fields are templated with {settings.*}/{args.*}.
 */
export interface SshSpec {
  // Connection params are resolved by the runner with this precedence:
  //   tool arg (host/port/username/password) > connector setting
  //   (host/port/username/password) > the literal here > built-in default.
  // So chat can pass them per-call and the connector holds defaults; the
  // IP typically comes from chat only (no setting). All optional here.
  host?: string;
  /** Default 22. */
  port?: string | number;
  user?: string;
  /** Password fed when a `password:` prompt appears (sent silently). */
  password?: string;
  /** Commands sent one-per-shell-prompt, in order (e.g. the upgrade cmd, then exit). */
  commands?: string[];
  /** Auto-answers applied throughout the session (e.g. `(y/N)` → `y`). */
  auto_answer?: SshExpectRule[];
  /** Shell-prompt regex that gates sending the next command. Default `[#$>]\s*$`. */
  prompt_regex?: string;
  /** Success marker regex — when seen, the session resolves ok and ssh is closed. */
  done_when?: string;
  /** Failure marker regex — when seen, resolves is_error. */
  fail_when?: string;
  /** name → regex(1 capture group) pulled from the full transcript into the result. */
  capture?: Record<string, string>;
  /** Overall timeout. Default 120s, max 280s. */
  timeout_sec?: number;
  /** Treat the remote closing the connection as success (e.g. after `exit`). */
  success_on_close?: boolean;
}

/** Schema for one settings or parameter field. */
export interface ConnectorFieldSchema {
  type: 'string' | 'number' | 'boolean' | 'secret' | 'json' | 'select' | 'instances';
  label?: string;
  description?: string;
  required?: boolean;
  default?: unknown;
  /** select-type options */
  options?: string[];
  /**
   * For `type: 'instances'` — schema for each row's sub-fields. The UI
   * renders one collapsible group per row, with these as inner inputs;
   * one field MUST be named `name` and serves as the row label / the
   * `instance` key referenced by tools. Stored on disk as a
   * JSON-stringified array; the tool-dispatcher overlay parses it
   * before template expansion.
   */
  fields?: Record<string, ConnectorFieldSchema>;
  /**
   * How this parameter's value is encoded when expanded into a URL path
   * (`{args.X}` inside an HTTP tool's `request.url`). Default
   * `uri_component` matches encodeURIComponent (slashes encoded). Use
   * `none` when the value is a pre-formatted path that must stay literal
   * (e.g. a Jenkins folder path `job/team/job/build`). `path_segments`
   * encodes each `/`-separated segment individually but preserves the
   * slashes (good for human-readable paths with spaces / unicode).
   *
   * Has no effect on values used in headers, query strings, or bodies —
   * those go through token expansion without URL encoding either way.
   */
  url_encoding?: 'uri_component' | 'none' | 'path_segments';
}

/**
 * How a connector authenticates its HTTP tools. Declared at the
 * connector level (manifest top) and inherited by every `protocol: http`
 * tool; an individual tool can override (`tool.auth: { type: none }`)
 * to skip auth for a public endpoint.
 *
 * Forge resolves the scheme into the right header / query param at
 * dispatch time so manifests don't have to hand-craft Authorization
 * headers (and so secrets like `password` can be base64-encoded
 * server-side without leaking to manifests).
 */
export type ConnectorAuth =
  | { type: 'none' }
  | { type: 'basic'; username: string; password: string }
  | { type: 'bearer'; token: string }
  | { type: 'header'; name: string; value: string }
  | { type: 'query'; name: string; value: string }
  /**
   * Two-step bearer-token exchange, e.g. Black Duck:
   *   POST {exchange_url}  Authorization: {exchange_auth_header}
   *     → { bearerToken, expiresInMilliseconds }
   *   ...then every subsequent request gets `Authorization: Bearer <jwt>`.
   * Forge caches the bearer in-memory keyed by (api_token, exchange_url)
   * and refreshes when within 60s of expiry. Tool calls stay fast — only
   * the first call per ~2h pays the exchange round-trip.
   */
  | {
      type: 'bearer-token-exchange';
      /**
       * Long-lived API token sent to the exchange endpoint (BD-style
       * auth via Authorization header). Optional — omit when using
       * username/password via `exchange_body` (NCM-style).
       */
      api_token?: string;
      /**
       * Exchange URL. Templated against settings + args, so `{args.host}`
       * works for connectors that take host per call. Cache key includes
       * the resolved URL so multi-host installs cache per host.
       */
      exchange_url: string;
      /** HTTP method for the exchange. Default POST. */
      exchange_method?: 'POST' | 'GET';
      /**
       * Authorization header VALUE sent to the exchange endpoint.
       * Default `token {api_token}` (Black Duck style). Ignored when
       * `exchange_body` is set. Templated.
       */
      exchange_auth_header?: string;
      /** Extra headers (Accept etc.) for the exchange. Values templated. */
      exchange_headers?: Record<string, string>;
      /**
       * JSON body for the exchange POST (NCM-style username/password
       * login). String values templated against settings + args. When
       * present, content-type is application/json and
       * `exchange_auth_header` is skipped.
       */
      exchange_body?: Record<string, unknown>;
      /** JSON path to the bearer in the exchange response. Default `bearerToken`. */
      bearer_path?: string;
      /** JSON path to expiry ms in the exchange response. Default `expiresInMilliseconds`. */
      expires_path?: string;
      /**
       * Fallback TTL in seconds when `expires_path` is missing from the
       * response. Useful for APIs whose JWT carries `exp` inside the
       * payload but isn't echoed at the response top level. Default 300.
       */
      expires_ttl_sec?: number;
      /**
       * How to attach the bearer to subsequent requests:
       *   `bearer` (default) → `Authorization: Bearer <token>`
       *   `bare`             → `Authorization: <token>` (NCM, NAC,
       *                         some legacy APIs)
       */
      bearer_format?: 'bearer' | 'bare';
    };

/**
 * Server-side HTTP request shape, used by `protocol: http` tools.
 * Template tokens {base_url}, {settings.*}, {args.*} are expanded at run time.
 */
export interface HttpRequestSpec {
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
  /** Full URL. Templated. */
  url: string;
  /** Header values are templated. */
  headers?: Record<string, string>;
  /** Query params. Values templated. Appended after any existing ? on url. */
  query?: Record<string, string>;
  /** Body. string = sent as-is (templated); object = JSON.stringify'd (string values templated). */
  body?: string | Record<string, unknown>;
  /**
   * Alternative body form: when set, Forge serialises the referenced
   * value as `application/x-www-form-urlencoded` (URLSearchParams). Use
   * the literal placeholder string `{args.NAME}` to point at an object-
   * valued parameter — typical for triggering Jenkins builds with a
   * dynamic `params` map, or any old-school form-POST API. Ignored if
   * `body` is also set.
   */
  body_form?: string | Record<string, unknown>;
  /**
   * Extra form-urlencoded keys merged into `body_form` server-side.
   * Each value is a template (`{settings.gitlab_pat}` typical). Both
   * the KEY and the VALUE are templated against settings. Keys with an
   * empty / unsubstituted value are dropped (don't post blank
   * secrets). The LLM never sees these — used to inject credentials a
   * tool needs from settings without exposing them in the tool's
   * declared parameters. Takes precedence over `body_form` if the
   * same key appears in both.
   */
  body_form_inject?: Record<string, string>;
  /**
   * Name of a `type: instances` settings field whose every row is
   * injected as one form-urlencoded key/value pair. Each row must have
   * `name` (Jenkins build-param name) and `value` (the value to send).
   * Server-side resolution — LLM never sees these. Empty rows are
   * dropped. Use alongside or instead of `body_form_inject` when the
   * user needs to configure arbitrary key/value pairs at runtime
   * rather than baking them into the manifest.
   */
  body_form_inject_from?: string;
  /**
   * Names (case-insensitive) of response headers to surface in the
   * truncated preamble. Default empty. Use when the connector needs
   * a value the upstream returns via headers — typically `set-cookie`
   * for session bootstrap, `location` for create-redirect APIs, or
   * `x-ratelimit-remaining` for backoff logic.
   *
   * Headers appear between the status line and the blank line that
   * separates preamble from body, one per line as `name: value`.
   * Pipeline scripts can grep them with sed/awk before the body parse:
   *   JS=$(echo "$RESP" | jq -r '.content' | sed -n 's/^set-cookie: //p' | …)
   *
   * Ignored when `noTruncation: true` (raw-body mode for Jobs scheduler).
   */
  capture_response_headers?: string[];
}

/**
 * Where the extension's generic runner should land the active tab before
 * executing a tool's script.
 */
export interface ConnectorPage {
  /** Template URL; supports {base_url}, {settings.*}, {args.*}. */
  url: string;
  /**
   * Substring; if the current tab URL already contains it, skip
   * navigation. Templated values are expanded the same way as `url`
   * (settings server-side, args at runtime). Omit to always navigate.
   */
  on_target?: string;
}

/**
 * A single callable a connector exposes to the LLM.
 *
 * For `protocol: 'browser'` the script body runs in the user's tab via
 * chrome.scripting.executeScript (page context, has document/fetch/etc,
 * no chrome.* APIs). The extension is a generic runner — it doesn't
 * know about specific connectors. Adding a new connector means writing
 * a manifest with `page` + `script` per tool; no extension rebuild.
 */
export interface ConnectorTool {
  description: string;
  parameters?: Record<string, ConnectorFieldSchema>;
  /** Requires user confirmation before invoking (e.g. add_comment, close_issue). */
  destructive?: boolean;
  /** Free-form description of the return shape — surfaced to the LLM. */
  returns?: string;

  /** Where this tool runs. Default 'browser'. */
  protocol?: ConnectorProtocol;

  // ── protocol: 'browser' ───────────────────────────────────
  /** Page to navigate to before running the script. */
  page?: ConnectorPage;
  /**
   * JavaScript function body. Receives `args` (the LLM's parsed
   * parameters). Returns a JSON-serializable value. Runs in the user's
   * tab — must be self-contained (no closures over Forge/extension code).
   */
  script?: string;

  // ── protocol: 'http' ──────────────────────────────────────
  /** Request shape. Templates {base_url}, {settings.*}, {args.*}. */
  request?: HttpRequestSpec;

  // ── protocol: 'shell' ─────────────────────────────────────
  /** Command + args (each templated independently — no shell injection). */
  command?: string[];
  /** Working directory (templated). */
  cwd?: string;
  /** Extra env vars (values templated). */
  env?: Record<string, string>;

  // ── protocol: 'ssh' ───────────────────────────────────────
  /** Interactive SSH session spec (PTY-driven). See SshSpec. */
  ssh?: SshSpec;

  // ── async (long-task watch) ───────────────────────────────
  /**
   * Marks this tool as a long-running task. After it runs (and detaches
   * quickly), Forge registers a background **watch** that periodically
   * polls `async.poll` until done/failed/timeout, then feeds the result
   * back into the originating chat session (or chains a tool). See
   * AsyncWatchSpec + lib/watch/. The lightweight async-callback primitive
   * for chat-driven background tasks (NAC upgrade, pytest runs, …).
   */
  async?: AsyncWatchSpec;

  /**
   * Timeout in milliseconds.
   *  - shell/http: request timeout (default 30000, max 300000).
   *  - browser: how long the bridge waits for the extension to return the
   *    RPC result (default 60000, capped at 900000). Raise it for tools
   *    whose script issues a long synchronous backend call (e.g. a NAC
   *    upgrade that blocks for minutes).
   */
  timeout_ms?: number;

  /**
   * http: per-tool auth override. Falls back to the connector's
   * top-level `auth` when omitted. Set `{ type: 'none' }` to skip
   * auth on a public endpoint.
   */
  auth?: ConnectorAuth;
}

/**
 * One adapter inside a connector. Most connectors are 1:1 (single tool
 * set, single auth) — the manifest's top-level `tools`/`settings`/
 * `host_match` fields define that single entry. Use `connectors[]`
 * only when one auth covers multiple sibling adapters (Atlassian,
 * Google Workspace, M365).
 */
export interface ConnectorEntry {
  id: string;
  host_permissions?: string[];
  tools: Record<string, ConnectorTool>;
  /** Per-user settings (host URL, default project, etc.). */
  settings?: Record<string, ConnectorFieldSchema>;
  /**
   * Chrome match pattern. Runner uses this to find/open authenticated
   * tabs for this connector. Supports {base_url}, {settings.*}.
   */
  host_match?: string;
  /**
   * Substring. If the tab URL contains this after navigation, treat as
   * "user not logged in" and abort with loginRequired: true.
   */
  login_redirect?: string;
  /** Inherits from ConnectorDefinition.runner if unset. */
  runner?: ConnectorRunner;
}

/**
 * Optional reachability probe shipped inside a connector manifest.
 * Used by the Settings → Connectors "Test" button — the UI hits
 * `POST /api/connectors/<id>/test`, which dispatches to one of:
 *
 *   probe: 'http' (default)  — server issues a one-shot HTTP request
 *                              with {settings.*} expanded. Use this
 *                              when the service has a clean REST
 *                              endpoint (`/api/v4/user`, `/rate_limit`).
 *
 *   probe: 'browser'         — forwarded to the browser extension via
 *                              the bridge. Extension acquires/opens a
 *                              tab matching `host_match`, navigates,
 *                              and checks the resulting URL against
 *                              `login_redirect`. Verifies the user is
 *                              actually logged in (cookie-based auth)
 *                              — no server-side credentials involved.
 */
export interface ConnectorTest {
  /** Human-readable hint shown next to the Test button. */
  description?: string;

  /**
   * Probe kind. Default 'http'. Browser probes require an installed +
   * paired Forge browser extension.
   */
  probe?: 'http' | 'browser';

  // ── probe: 'http' ────────────────────────────────────────
  /** HTTP request to issue. Same template tokens as a tool request. */
  request?: HttpRequestSpec;
  /**
   * Status codes that count as success. Default [200].
   * (Some APIs return 204 for a successful auth check.)
   */
  ok_status?: number[];
  /**
   * Template for the success message. Tokens `{{<json-path>}}` against
   * the parsed response body. Empty → "OK (HTTP <status>)".
   * Example: "Authenticated as {{username}} ({{name}})".
   */
  ok_template?: string;
  /**
   * Optional regex (JS syntax) that the response body must match for
   * the test to pass. Use for version gates or feature flags — e.g.
   * `^(8|9|[1-9]\\d)\\.` to require version 8.x or newer. On mismatch
   * the test fails with a descriptive error including the actual body
   * snippet, so users see immediately why the connector won't work
   * against their server build.
   */
  body_match?: string;
  /**
   * Custom error message shown when `body_match` fails. Defaults to
   * "body did not match expected pattern <regex>". Use when you want
   * to point users at a specific upgrade / config requirement.
   */
  body_match_error?: string;

  // ── probe: 'browser' ─────────────────────────────────────
  // Extension reuses the connector's top-level host_match +
  // login_redirect (1:1) or the first entry's (1:N) to navigate
  // and detect login redirects.
  /**
   * CSS selector that, if present on the loaded page, indicates the
   * user is NOT logged in (e.g. `form#login`, `a[href*="/sign_in"]`).
   * Used by sites that render a login form inline instead of
   * redirecting to `/login` — without this, the URL-based probe
   * passes even when the user can't see real content. Extension v0.2.15+
   * runs `document.querySelector(selector)` after navigation; non-null
   * match → probe fails with "login required".
   */
  auth_required_selector?: string;

  /** Override the default timeout (http: 15s, browser: 30s). */
  timeout_ms?: number;
}

/**
 * The on-disk manifest for a connector, loaded from
 * `<dataDir>/connectors/<id>/manifest.yaml`.
 */
export interface ConnectorDefinition {
  id: string;
  name: string;
  version: string;
  /** Emoji or short string for UI badges. */
  icon?: string;
  author?: string;
  description?: string;

  /**
   * 2-4 line English summary of what this connector can do, shown in the
   * chat system prompt's "Connector catalog" block. Drives the LLM's
   * decision to call connector_open(<id>). When absent, the agent falls
   * back to listing the first few tool names — informative but flat.
   */
  catalog_summary?: string;

  /**
   * Minimum Forge version this manifest expects. The registry filter
   * hides newer-than-supported manifests so users on older Forge
   * versions don't try to install something that depends on newer APIs.
   */
  min_forge_version?: string;

  /**
   * Which extension execution context runs the connector's scripts.
   * Default 'main' (permissive-CSP sites). Use 'isolated' for strict
   * CSP sites (Teams, github.com). Per-connector, not per-tool — a
   * host has one CSP profile.
   */
  runner?: ConnectorRunner;

  /**
   * How the runner acquires a tab to execute scripts in:
   *   - 'reuse' (default): query existing tabs matching host_match;
   *     navigate the first match. Saves tab churn but hijacks whatever
   *     page the user was viewing.
   *   - 'ephemeral': always open a fresh background tab; close it after
   *     the tool returns. User's own tabs are untouched. Slightly slower
   *     (5–10s per call on cold load) but invisible to the user. Use for
   *     connectors hit frequently by background pipelines (Mantis, etc.).
   */
  tab_strategy?: 'reuse' | 'ephemeral';

  // ─── 1:1 connector — top-level fields ──────────────────
  /** Most connectors expose tools directly here. */
  tools?: Record<string, ConnectorTool>;
  settings?: Record<string, ConnectorFieldSchema>;
  host_match?: string;
  login_redirect?: string;

  /**
   * Default HTTP auth scheme applied to every `protocol: http` tool
   * in this connector. Tools can override via their own `auth` field.
   * Omitting it = no auth applied by Forge (tools may still hand-craft
   * an Authorization header in `request.headers`, like the v0.1 gitlab
   * and github-api manifests).
   */
  auth?: ConnectorAuth;

  /**
   * Connector-level HTTP runtime knobs. Apply to every `protocol: http`
   * tool in this connector unless overridden per-tool.
   *
   * `verify_tls: false` disables TLS cert verification — necessary for
   * appliances that ship self-signed certs (FortiNAC, ESXi, internal
   * services). Set this ONLY for hosts you trust by IP; turning it off
   * means an attacker on-path could impersonate the server.
   */
  http?: {
    verify_tls?: boolean;
  };

  // ─── 1:N suite — list of sibling entries sharing auth ──
  connectors?: ConnectorEntry[];

  /** Optional reachability probe — wired to the Settings "Test" button. */
  test?: ConnectorTest;
}

/**
 * A reference to a connector definition without its tools/scripts —
 * used by the marketplace listing so we don't pay the YAML parse cost
 * for connectors the user hasn't installed.
 */
export interface ConnectorMarketEntry {
  id: string;
  name: string;
  version: string;
  icon?: string;
  description?: string;
  author?: string;
  /** present if locally installed */
  installed_version?: string;
  /** true when registry has a newer version than installed_version */
  update_available?: boolean;
  /** true when min_forge_version is satisfied */
  compatible: boolean;
  /**
   * Where this entry originates:
   *   'registry' — listed in a remote registry (may also be installed)
   *   'local'    — installed via /api/connectors/install-local, no
   *                registry counterpart (no Update path)
   */
  source: 'registry' | 'local';
  /**
   * Which registry source provided the winning entry. `public` = the
   * default forge-connectors repo; `enterprise-<tenant>` = a private
   * enterprise repo. Absent for `source: 'local'`. UI uses this for
   * the source badge (e.g. `🔒 Fortinet`).
   */
  source_id?: string;
  /**
   * When `update_available` is true, this is the source that *publishes*
   * the new version (the winning registry entry). Often differs from
   * source_id, which reflects the *installed* manifest's origin — e.g.
   * installed from public, enterprise just added with a newer version.
   * UI uses this to show "Update from enterprise-fortinet" so users
   * know what they're about to pull before they click.
   */
  update_source_id?: string;
  /**
   * Other sources that also list this id but were outranked. Surfaced
   * in the marketplace detail view so users can see "this `mantis`
   * comes from fortinet — public also has one (hidden)".
   */
  hidden_sources?: { source_id: string; display_name: string; version: string }[];
}
