/**
 * POST /api/connectors/[id]/test
 *
 * Reachability probe for a connector. Runs the manifest's `test:`
 * block (an HTTP request) against the user's saved settings, returns
 * a structured result for the Settings → Connectors UI to render.
 *
 *   {
 *     ok: true,
 *     status: 200,
 *     message: "Authenticated as zliu (Zhen Liu)",
 *     duration_ms: 152
 *   }
 *
 * On failure:
 *
 *   {
 *     ok: false,
 *     status: 401,
 *     error: "Token was revoked. You have to re-authorize from the user.",
 *     body_preview: "{...}"
 *   }
 *
 * The probe is HTTP-only — connectors that need a different mechanism
 * (browser DOM, shell exec) simply omit `test:` and the UI hides the
 * button.
 */

import { NextResponse } from 'next/server';
import {
  getConnector,
  getConnectorEntries,
  getInstalledConnector,
} from '@/lib/connectors/registry';
import { expandSettingsTokens, expandAllTokens } from '@/lib/plugins/templates';
import { bridgeRpc } from '@/lib/chat/bridge-client';
import type { ConnectorDefinition, ConnectorTest, HttpRequestSpec } from '@/lib/connectors/types';

const DEFAULT_TIMEOUT_MS = 15_000;
const MAX_BODY_PREVIEW = 1024;

function expandString(s: string, settings: Record<string, unknown>): string {
  return expandAllTokens(s, settings as Record<string, any>, {});
}

function buildUrl(spec: HttpRequestSpec, settings: Record<string, unknown>): string {
  let url = expandString(spec.url, settings);
  if (spec.query) {
    const u = new URL(url);
    for (const [k, raw] of Object.entries(spec.query)) {
      u.searchParams.set(k, expandString(String(raw), settings));
    }
    url = u.toString();
  }
  return url;
}

function buildHeaders(spec: HttpRequestSpec, settings: Record<string, unknown>): Headers {
  const h = new Headers();
  if (spec.headers) {
    for (const [k, raw] of Object.entries(spec.headers)) {
      h.set(k, expandString(String(raw), settings));
    }
  }
  return h;
}

function buildBody(
  spec: HttpRequestSpec,
  settings: Record<string, unknown>,
): { body?: string; contentType?: string } {
  if (spec.body == null) return {};
  if (typeof spec.body === 'string') {
    return { body: expandString(spec.body, settings) };
  }
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(spec.body)) {
    out[k] = typeof v === 'string' ? expandString(v, settings) : v;
  }
  return { body: JSON.stringify(out), contentType: 'application/json' };
}

/**
 * Render `{{path.to.value}}` placeholders against a parsed JSON body.
 * Missing paths render as "?", non-string scalars get stringified.
 */
function renderTemplate(template: string, body: unknown): string {
  return template.replace(/\{\{([^{}]+)\}\}/g, (_match, expr) => {
    const path = String(expr).trim().split('.').filter(Boolean);
    let cur: any = body;
    for (const p of path) {
      if (cur && typeof cur === 'object' && p in cur) cur = cur[p];
      else return '?';
    }
    if (cur == null) return '?';
    return typeof cur === 'string' ? cur : JSON.stringify(cur);
  });
}

interface TestResult {
  ok: boolean;
  status?: number;
  message?: string;
  error?: string;
  duration_ms?: number;
  body_preview?: string;
}

async function runHttpProbe(test: ConnectorTest, settings: Record<string, unknown>): Promise<TestResult> {
  const spec = test.request;
  if (!spec?.url) return { ok: false, error: 'test.request.url is required for http probe' };

  const method = (spec.method || 'GET').toUpperCase();
  const url = buildUrl(spec, settings);
  const headers = buildHeaders(spec, settings);
  const { body, contentType } = buildBody(spec, settings);
  if (body != null && contentType && !headers.has('content-type')) {
    headers.set('content-type', contentType);
  }

  const timeoutMs = test.timeout_ms || DEFAULT_TIMEOUT_MS;
  const okStatus = test.ok_status?.length ? test.ok_status : [200];

  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  const t0 = Date.now();
  let res: Response;
  try {
    res = await fetch(url, { method, headers, body, signal: ctrl.signal });
  } catch (e) {
    clearTimeout(timer);
    const err = e as Error & { cause?: unknown };
    const cause = err.cause instanceof Error ? `: ${err.cause.message}` : '';
    return {
      ok: false,
      error: `request failed: ${err.message}${cause}`,
      duration_ms: Date.now() - t0,
    };
  }
  clearTimeout(timer);

  const duration = Date.now() - t0;
  const text = await res.text().catch(() => '');
  const preview = text.length > MAX_BODY_PREVIEW ? text.slice(0, MAX_BODY_PREVIEW) + '…' : text;

  if (!okStatus.includes(res.status)) {
    let errMsg = `HTTP ${res.status} ${res.statusText}`;
    try {
      const j = JSON.parse(text);
      if (typeof j?.error === 'string') errMsg += `: ${j.error}`;
      else if (typeof j?.message === 'string') errMsg += `: ${j.message}`;
      else if (typeof j?.error_description === 'string') errMsg += `: ${j.error_description}`;
    } catch {}
    return {
      ok: false,
      status: res.status,
      error: errMsg,
      duration_ms: duration,
      body_preview: preview,
    };
  }

  let parsedBody: unknown = null;
  try { parsedBody = JSON.parse(text); } catch {}
  const message = test.ok_template
    ? renderTemplate(test.ok_template, parsedBody)
    : `OK (HTTP ${res.status})`;
  return {
    ok: true,
    status: res.status,
    message,
    duration_ms: duration,
  };
}

/**
 * Browser probe: ask the paired extension to land on the connector's
 * host_match URL and report whether `login_redirect` was hit.
 *
 * The extension implements `connector.probe` (see
 * lib/help-docs/21-build-connector.md for the wire contract). On
 * sites the user is logged in to, it returns
 *   { ok: true, url: '<final tab URL>' }
 * On a redirect to the login page it returns
 *   { ok: false, error: 'login required', url: '<login url>' }
 * On extension absence we throw, caller surfaces it.
 */
async function runBrowserProbe(
  def: ConnectorDefinition,
  settings: Record<string, unknown>,
): Promise<TestResult> {
  const entries = getConnectorEntries(def);
  const entry = entries[0];
  const hostMatch = def.host_match || entry?.host_match;
  const loginRedirect = def.login_redirect || entry?.login_redirect;
  if (!hostMatch) {
    return { ok: false, error: 'browser probe requires host_match on the manifest' };
  }
  const expandedHost = expandSettingsTokens(hostMatch, settings as any);
  const expandedLoginRedirect = loginRedirect
    ? expandSettingsTokens(loginRedirect, settings as any)
    : undefined;

  const t0 = Date.now();
  let value: unknown;
  try {
    value = await bridgeRpc('connector.probe', {
      pluginId: def.id,
      host_match: expandedHost,
      login_redirect: expandedLoginRedirect,
      runner: def.runner || entry?.runner || 'main',
      timeout_ms: def.test?.timeout_ms || 30_000,
    });
  } catch (e) {
    const raw = (e as Error).message || String(e);
    let friendly = raw;
    if (raw.includes('unknown method: connector.probe')) {
      friendly =
        'Your Forge browser extension is out of date — it doesn\'t know how to run browser probes yet. ' +
        'Rebuild the extension (pnpm ext in forge-browser-extension), then chrome://extensions → Reload, and try Test again.';
    } else if (raw.includes('bridge') && raw.includes('unreachable')) {
      friendly =
        'Forge browser bridge is unreachable on port 8407. Restart Forge (forge server restart) or check that the browser-bridge standalone is running.';
    } else if (raw.includes('no paired extensions') || raw.includes('no connected')) {
      friendly =
        'Forge browser extension not connected. Install the extension from forge-browser-extension/dist, pin it, and sign in with your Forge URL + admin password.';
    }
    return {
      ok: false,
      error: friendly,
      duration_ms: Date.now() - t0,
    };
  }
  const r = (value || {}) as { ok?: boolean; url?: string; error?: string };
  return {
    ok: !!r.ok,
    message: r.ok ? `Session active${r.url ? ` · ${r.url}` : ''}` : undefined,
    error: r.ok ? undefined : (r.error || 'login required'),
    duration_ms: Date.now() - t0,
  };
}

export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const def = getConnector(id);
  if (!def) return NextResponse.json({ ok: false, error: 'connector not found' }, { status: 404 });
  if (!def.test) {
    return NextResponse.json({ ok: false, error: 'connector has no test block' }, { status: 400 });
  }
  const inst = getInstalledConnector(id);
  if (!inst) {
    return NextResponse.json({ ok: false, error: 'connector not installed' }, { status: 400 });
  }

  const probe = def.test.probe || 'http';
  const r = probe === 'browser'
    ? await runBrowserProbe(def, inst.config)
    : await runHttpProbe(def.test, inst.config);
  return NextResponse.json(r);
}
