#!/usr/bin/env node
/**
 * composio-toolkits.mjs — real-network verifier for the Composio
 * integration. Answers the question: "is our service correctly seeing
 * everything Composio actually has, including toolkits the operator
 * believes they enabled?"
 *
 * MOTIVATION
 *
 *   User reported: enabled "canva" via app.composio.dev → Integrations,
 *   but the agent's `composio.list-toolkits` doesn't return it. Possible
 *   causes:
 *
 *     1. Pagination — Composio's /toolkits is cursor-paginated, and our
 *        `client.toolkits.list()` call only fetches page 1. If Canva is
 *        on page 2+, we'd never see it.
 *
 *     2. Filtering — the SDK applies a default filter we don't see.
 *
 *     3. Naming/slug mismatch — toolkit slug isn't "canva".
 *
 *     4. Wrong key — vault has a key from a different Composio org than
 *        the dashboard where the user enabled Canva.
 *
 *     5. SDK normalisation — our `_normalizeListResult` doesn't recognise
 *        the shape and silently returns [].
 *
 *   This verifier diagnoses ALL FIVE by talking to Composio's REST API
 *   directly (no SDK) and comparing the raw paginated truth with what
 *   our service sees through the SDK.
 *
 * WHAT IT CHECKS
 *
 *   1. Resolves the active API key the same way the running service does
 *      (vault → env → CLI flag), so we test what the agent actually uses.
 *
 *   2. Calls GET /api/v3.1/toolkits paginated until exhaustion. Reports
 *      total count + flags whether "canva" appears.
 *
 *   3. Calls GET /api/v3.1/connected_accounts to confirm whether the
 *      user has actually registered a Canva integration in this org.
 *
 *   4. Calls our composioService.listToolkits() via the SDK. Reports the
 *      count + whether Canva appears.
 *
 *   5. Compares (2) vs (4) and tells you which side is wrong.
 *
 * USAGE
 *
 *   # With env var:
 *   COMPOSIO_API_KEY=comp_… node scripts/verify/composio-toolkits.mjs
 *
 *   # With explicit flag (overrides env):
 *   node scripts/verify/composio-toolkits.mjs --key=comp_…
 *
 *   # Search for a different toolkit (default: "canva"):
 *   node scripts/verify/composio-toolkits.mjs --key=… --find=notion
 *
 *   # Verbose — dumps every toolkit slug seen via REST:
 *   node scripts/verify/composio-toolkits.mjs --key=… --verbose
 *
 *   Exit codes:
 *     0  all checks ok, target toolkit detected
 *     1  target toolkit missing from REST (real platform issue) or
 *        SDK-vs-REST mismatch (our bug)
 *     2  required key not provided
 */

import process from 'node:process';

/* ------------------------------------------------------------------------ */
/* Arg + env parsing                                                         */
/* ------------------------------------------------------------------------ */

const args = Object.fromEntries(process.argv.slice(2).map(a => {
  const m = a.match(/^--([^=]+)=?(.*)$/);
  return m ? [m[1], m[2] || true] : [a, true];
}));

const apiKey = args.key || process.env.COMPOSIO_API_KEY;
const target = String(args.find || 'canva').toLowerCase();
const verbose = !!args.verbose;
const baseUrl = args.base || 'https://backend.composio.dev';

console.log('Composio integration real-network verifier');
console.log('');

if (!apiKey) {
  console.error('ERROR: no API key provided.');
  console.error('  Pass --key=comp_… or set COMPOSIO_API_KEY in the env.');
  process.exit(2);
}

console.log(`Base URL:       ${baseUrl}`);
console.log(`Looking for:    "${target}"`);
console.log(`Key fingerprint: ${apiKey.slice(0, 8)}… (length ${apiKey.length})`);
console.log('');

/* ------------------------------------------------------------------------ */
/* REST helpers                                                              */
/* ------------------------------------------------------------------------ */

async function restGet(path, query = {}) {
  const url = new URL(path, baseUrl);
  for (const [k, v] of Object.entries(query)) {
    if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
  }
  const t0 = Date.now();
  let res;
  try {
    res = await fetch(url, {
      method: 'GET',
      headers: { 'x-api-key': apiKey, 'accept': 'application/json' },
    });
  } catch (e) {
    return { ok: false, status: 0, ms: Date.now() - t0, error: e.message, url: url.toString() };
  }
  const ms = Date.now() - t0;
  const text = await res.text();
  let body;
  try { body = text ? JSON.parse(text) : null; }
  catch { body = { _rawText: text }; }
  return { ok: res.ok, status: res.status, ms, body, url: url.toString() };
}

function extractItems(body) {
  if (Array.isArray(body)) return body;
  if (!body || typeof body !== 'object') return [];
  return body.items || body.data || body.results || body.toolkits || [];
}

function extractCursor(body) {
  if (!body || typeof body !== 'object') return null;
  // Possible shapes across Composio SDK/API versions.
  return body.nextCursor || body.next_cursor || body.cursor?.next || null;
}

/* ------------------------------------------------------------------------ */
/* Check 1 — REST toolkits, paginated until exhausted                        */
/* ------------------------------------------------------------------------ */

console.log('1. REST GET /api/v3.1/toolkits (paginated until exhausted)');

const restToolkits = [];
let cursor = null;
let page = 0;
let restPageErrors = [];

do {
  page++;
  const r = await restGet('/api/v3.1/toolkits', { limit: 50, ...(cursor ? { cursor } : {}) });
  if (!r.ok) {
    restPageErrors.push({ page, status: r.status, error: r.body?.error || r.error });
    console.error(`     ✘ page ${page} → HTTP ${r.status} (${r.ms}ms)`);
    console.error(`       URL: ${r.url}`);
    if (r.body) console.error(`       body: ${JSON.stringify(r.body).slice(0, 300)}`);
    break;
  }
  const items = extractItems(r.body);
  restToolkits.push(...items);
  cursor = extractCursor(r.body);
  console.log(`     ✔ page ${page}: +${items.length} items, total ${restToolkits.length}, cursor=${cursor ? 'yes' : 'end'} (${r.ms}ms)`);
  // Safety cap.
  if (page >= 50) {
    console.warn(`     ⚠ stopping after 50 pages (defensive cap)`);
    break;
  }
} while (cursor);

const slugOf = (t) => (t?.slug || t?.name || '').toLowerCase();
const restSlugs = restToolkits.map(slugOf).filter(Boolean);
// Prefer exact-slug match; fall back to substring (avoids the "canvas"
// LMS shadowing "canva" the design tool just because canvas.includes(canva)).
const restHasTarget = restSlugs.includes(target) || restSlugs.some(s => s.includes(target));

console.log('');
console.log(`   total via REST: ${restToolkits.length}`);
console.log(`   target "${target}" in REST list? ${restHasTarget ? '✔ YES' : '✘ NO'}`);
if (restHasTarget) {
  const exact = restToolkits.find(t => slugOf(t) === target);
  const match = exact || restToolkits.find(t => slugOf(t).includes(target));
  console.log(`   → match: slug="${match?.slug}", name="${match?.name}"${exact ? ' (exact)' : ' (substring — no exact slug)'}`);
  // Also surface any other candidates so users can see siblings (canva vs canvas).
  const others = restToolkits.filter(t => slugOf(t) !== match?.slug && slugOf(t).includes(target));
  if (others.length) {
    console.log(`   other candidates containing "${target}":`);
    for (const o of others.slice(0, 5)) console.log(`     - ${o.slug} (${o.name})`);
  }
}
if (verbose) {
  console.log(`   all slugs seen via REST:`);
  for (const s of restSlugs.slice().sort()) console.log(`     - ${s}`);
}
console.log('');

/* ------------------------------------------------------------------------ */
/* Check 2 — REST connected_accounts (what's actually enabled in YOUR org)   */
/* ------------------------------------------------------------------------ */

console.log('2. REST GET /api/v3.1/connected_accounts (your org\'s integrations)');

const r2 = await restGet('/api/v3.1/connected_accounts', { limit: 100 });
if (!r2.ok) {
  console.error(`     ✘ HTTP ${r2.status} (${r2.ms}ms): ${JSON.stringify(r2.body).slice(0, 200)}`);
} else {
  const accounts = extractItems(r2.body);
  console.log(`     ✔ ${accounts.length} connected accounts (${r2.ms}ms)`);
  const accountSlugs = accounts.map(a =>
    (a.toolkitSlug || a.toolkit_slug || a.toolkit?.slug || a.toolkit || a.appName || '').toLowerCase()
  ).filter(Boolean);
  const uniqueSlugs = [...new Set(accountSlugs)];
  console.log(`     unique toolkits with active accounts: ${uniqueSlugs.join(', ') || '(none)'}`);
  const targetAccount = accounts.find(a => {
    const s = (a.toolkitSlug || a.toolkit_slug || a.toolkit?.slug || a.toolkit || a.appName || '').toLowerCase();
    return s === target || s.includes(target);
  });
  if (targetAccount) {
    console.log(`     ✔ "${target}" connection found: id=${targetAccount.id}, status=${targetAccount.status}`);
  } else {
    console.log(`     ℹ no active connection for "${target}" in this org`);
    console.log(`       (the toolkit can still be discovered via list-toolkits if it exists in the catalog)`);
  }
}
console.log('');

/* ------------------------------------------------------------------------ */
/* Check 3 — REST auth-configs (lighter check: enabled integrations even     */
/*           without a live OAuth connection yet)                            */
/* ------------------------------------------------------------------------ */

console.log('3. REST GET /api/v3.1/auth_configs (enabled integrations / OAuth apps)');

// The auth-configs endpoint may differ between SDK versions — try a few.
const authPaths = ['/api/v3.1/auth_configs', '/api/v3.1/integrations', '/api/v3/auth_configs', '/api/v3/integrations'];
let authConfigs = null;
let authPath = null;
for (const p of authPaths) {
  const r = await restGet(p, { limit: 100 });
  if (r.ok) { authConfigs = extractItems(r.body); authPath = p; break; }
}
if (!authConfigs) {
  console.log(`     ℹ no auth-configs endpoint responded OK (tried: ${authPaths.join(', ')})`);
  console.log(`       skip — connected_accounts above is the authoritative "what's enabled" view.`);
} else {
  console.log(`     ✔ ${authConfigs.length} auth-configs via ${authPath}`);
  const targetAuth = authConfigs.find(c => {
    const s = (c.toolkitSlug || c.toolkit_slug || c.toolkit || c.appName || '').toString().toLowerCase();
    return s === target || s.includes(target);
  });
  if (targetAuth) {
    console.log(`     ✔ "${target}" auth-config exists: id=${targetAuth.id || '(no id)'}`);
  } else {
    console.log(`     ✘ no auth-config for "${target}" in this org`);
    console.log(`       → enabling a toolkit on app.composio.dev should create one. If it doesn't,`);
    console.log(`         the dashboard step didn't actually persist, OR you're using a key for a`);
    console.log(`         different Composio org than where you enabled it.`);
  }
}
console.log('');

/* ------------------------------------------------------------------------ */
/* Check 4 — what our SDK call returns                                       */
/* ------------------------------------------------------------------------ */

console.log('4. SDK path: composioService.listToolkits() (the agent\'s actual code path)');

// Force the service to use OUR key for this run.
process.env.COMPOSIO_API_KEY = apiKey;

let sdkResult, sdkCount = 0, sdkHasTarget = false, sdkSlugs = [];
try {
  const composioService = await import('../../src/services/composioService.js');
  composioService._resetForTests();
  sdkResult = await composioService.listToolkits();
  if (!sdkResult.success) {
    console.error(`     ✘ SDK call failed: ${sdkResult.error}`);
  } else {
    sdkCount = sdkResult.toolkits.length;
    sdkSlugs = sdkResult.toolkits.map(slugOf).filter(Boolean);
    sdkHasTarget = sdkSlugs.some(s => s === target || s.includes(target));
    console.log(`     ✔ SDK returned ${sdkCount} toolkits`);
    console.log(`     target "${target}" in SDK list? ${sdkHasTarget ? '✔ YES' : '✘ NO'}`);
    if (verbose) {
      console.log(`     all slugs seen via SDK:`);
      for (const s of sdkSlugs.slice().sort()) console.log(`       - ${s}`);
    }
  }
} catch (e) {
  console.error(`     ✘ SDK call threw: ${e.message}`);
  console.error(`       stack: ${e.stack?.split('\n').slice(0, 3).join(' | ')}`);
}
console.log('');

/* ------------------------------------------------------------------------ */
/* Check 4b — raw SDK shape probe (no normalisation)                         */
/* ------------------------------------------------------------------------ */

console.log('4b. SDK method surface — what methods do each namespace actually expose?');
try {
  const mod = await import('@composio/core');
  const Composio = mod.Composio || mod.default?.Composio || mod.default;
  const probe = new Composio({ apiKey });
  for (const ns of ['toolkits', 'tools', 'connectedAccounts', 'authConfigs', 'integrations', 'triggers']) {
    const obj = probe[ns];
    if (!obj) { console.log(`     ${ns}: (namespace missing on client)`); continue; }
    const own = Object.getOwnPropertyNames(obj).filter(k => k !== 'constructor');
    const proto = Object.getPrototypeOf(obj);
    const protoMethods = proto && proto !== Object.prototype
      ? Object.getOwnPropertyNames(proto).filter(k => k !== 'constructor' && typeof proto[k] === 'function')
      : [];
    const all = [...new Set([...own, ...protoMethods])].sort();
    console.log(`     ${ns}: [${all.join(', ')}]`);
  }
} catch (e) {
  console.error(`     ✘ surface probe failed: ${e.message}`);
}
console.log('');

console.log('4c. Raw SDK shape probe — what does client.toolkits.list() ACTUALLY return?');

try {
  const mod = await import('@composio/core');
  const Composio = mod.Composio || mod.default?.Composio || mod.default;
  if (!Composio) {
    console.error('     ✘ could not resolve a Composio constructor from @composio/core');
  } else {
    const probeClient = new Composio({ apiKey });
    const t0 = Date.now();
    let raw;
    try {
      raw = await probeClient.toolkits?.list?.();
    } catch (e) {
      console.error(`     ✘ client.toolkits.list() threw: ${e.message}`);
      console.error(`       stack: ${e.stack?.split('\n').slice(0, 4).join(' | ')}`);
      raw = null;
    }
    const ms = Date.now() - t0;

    if (raw === undefined) {
      console.log(`     ✘ raw === undefined (${ms}ms) — SDK method is missing or returned nothing`);
      console.log(`       available client.toolkits methods: ${Object.keys(probeClient.toolkits || {}).join(', ') || '(none — toolkits is not on this client)'}`);
    } else if (raw === null) {
      console.log(`     ✘ raw === null (${ms}ms)`);
    } else {
      const typeofRaw = typeof raw;
      const isArr = Array.isArray(raw);
      const keys = (typeofRaw === 'object' && !isArr) ? Object.keys(raw) : null;
      const length = isArr ? raw.length : (raw?.items?.length ?? raw?.data?.length ?? raw?.results?.length ?? raw?.toolkits?.length ?? null);

      console.log(`     ✔ got response (${ms}ms)`);
      console.log(`       typeof raw:     ${typeofRaw}`);
      console.log(`       Array.isArray:  ${isArr}`);
      if (keys) console.log(`       Object.keys:    [${keys.join(', ')}]`);
      console.log(`       items length:   ${length ?? '(none of items/data/results/toolkits exists)'}`);
      console.log(`       constructor:    ${raw?.constructor?.name || '(none)'}`);

      // Detect async-iterator paginators (some SDKs return these instead
      // of a plain object — you have to `for await` them to get items).
      const hasAsyncIter = typeof raw?.[Symbol.asyncIterator] === 'function';
      const hasIter = typeof raw?.[Symbol.iterator] === 'function';
      const hasNext = typeof raw?.next === 'function';
      if (hasAsyncIter || hasIter || hasNext) {
        console.log(`       iterator hints: asyncIter=${hasAsyncIter}, iter=${hasIter}, next=${hasNext}`);
        console.log(`       → SDK may return a paginator object; need to iterate, not just await.`);
      }

      // Dump first ~1KB so we can see the actual shape.
      let dump;
      try { dump = JSON.stringify(raw, (k, v) =>
        typeof v === 'function' ? `[Function: ${v.name || 'anonymous'}]` : v, 2); }
      catch (e) { dump = `[unstringifiable: ${e.message}]`; }
      console.log('       first 1500 chars of JSON.stringify(raw):');
      console.log(dump.slice(0, 1500).split('\n').map(l => '         ' + l).join('\n'));
      if (dump.length > 1500) console.log(`         … (${dump.length - 1500} more chars truncated)`);

      // If it's a paginator with an items array nested somewhere weird,
      // try a couple of likely paths.
      if (length === null) {
        const probes = [
          ['raw.toolkits',      raw.toolkits],
          ['raw.payload',       raw.payload],
          ['raw.body',          raw.body],
          ['raw.body.items',    raw.body?.items],
          ['raw.response',      raw.response],
          ['raw.response.data', raw.response?.data],
        ];
        for (const [path, val] of probes) {
          if (Array.isArray(val)) {
            console.log(`       ! candidate items at ${path} — ${val.length} entries`);
          } else if (val && typeof val === 'object') {
            console.log(`       ! object at ${path} — keys [${Object.keys(val).slice(0, 8).join(', ')}]`);
          }
        }
      }
    }
  }
} catch (e) {
  console.error(`     ✘ probe failed: ${e.message}`);
  console.error(`       stack: ${e.stack?.split('\n').slice(0, 4).join(' | ')}`);
}
console.log('');

/* ------------------------------------------------------------------------ */
/* Summary + diagnosis                                                       */
/* ------------------------------------------------------------------------ */

console.log('───────────────────────────────────────────────────────────');
console.log('DIAGNOSIS');
console.log('───────────────────────────────────────────────────────────');

let exit = 0;
const restOnly = restSlugs.filter(s => !sdkSlugs.includes(s));
const sdkOnly = sdkSlugs.filter(s => !restSlugs.includes(s));

console.log(`REST sees ${restToolkits.length}, SDK sees ${sdkCount}.`);
if (restOnly.length || sdkOnly.length) {
  console.log(`  REST-only (${restOnly.length}): ${restOnly.slice(0, 5).join(', ')}${restOnly.length > 5 ? '…' : ''}`);
  console.log(`  SDK-only (${sdkOnly.length}):  ${sdkOnly.slice(0, 5).join(', ')}${sdkOnly.length > 5 ? '…' : ''}`);
}
console.log('');

if (restHasTarget && sdkHasTarget) {
  console.log(`✔ "${target}" is reachable through BOTH REST and SDK.`);
  console.log(`  The integration is working. If the agent claimed it was missing,`);
  console.log(`  check (a) the agent's actual log output for the failing call,`);
  console.log(`  (b) whether the agent was looking for the right slug.`);
} else if (restHasTarget && !sdkHasTarget) {
  console.log(`✘ "${target}" IS in Composio's catalog but our SDK CALL DROPS IT.`);
  console.log(`  → BUG in composioService.listToolkits() — almost certainly pagination`);
  console.log(`    (our code calls client.toolkits.list() once; REST shows ${restToolkits.length} entries`);
  console.log(`    across ${page} pages). Fix: follow nextCursor until exhausted.`);
  exit = 1;
} else if (!restHasTarget && restToolkits.length > 0) {
  console.log(`✘ "${target}" IS NOT in Composio's catalog AT ALL (REST exhausted).`);
  console.log(`  → not our bug. Either (a) the slug is different than "${target}" — try`);
  console.log(`    --find with another value, e.g. --verbose to dump all slugs and grep;`);
  console.log(`    (b) the API key belongs to a Composio org that doesn't have access to`);
  console.log(`    this toolkit; (c) the toolkit hasn't been published on Composio yet.`);
  exit = 1;
} else if (restToolkits.length === 0) {
  console.log(`✘ Composio returned ZERO toolkits even via direct REST.`);
  console.log(`  → not our bug. Either the API key is invalid for this org, or the`);
  console.log(`    Composio backend is degraded. Check the dashboard at app.composio.dev.`);
  exit = 1;
}

if (restPageErrors.length) {
  console.log('');
  console.log(`⚠ REST pagination hit ${restPageErrors.length} error(s) — counts above may be incomplete.`);
  exit = 1;
}

process.exit(exit);
