// Connections — service-level marketplace items that own credentials and
// unlock dependent tools. Wraps marketplace_items (kind='connection') and
// marketplace_installations. Verify dispatching lives in the route handlers,
// not here, so each provider's HTTP call is colocated with its parsing logic.

import { getDb, nowIso, newId } from './db.js';
import { getSecret, listSecrets } from './secrets.js';

function safeParse(json, fallback) {
  if (!json) return fallback;
  try {
    const parsed = JSON.parse(json);
    return parsed == null ? fallback : parsed;
  } catch {
    return fallback;
  }
}

function shapeRow(row, installRow) {
  const manifest = safeParse(row.manifest_json, {});
  const tags = safeParse(row.tags, []);
  const settings = safeParse(installRow?.settings_json, {}) || {};
  return {
    id: row.id,
    kind: 'connection',
    slug: row.slug,
    title: row.title,
    description: row.description || '',
    category: row.category || null,
    author: row.author || 'madarx',
    source: { kind: row.source_kind, url: row.source_url || null },
    version: row.version || null,
    risk_summary: row.risk_summary || null,
    tags: Array.isArray(tags) ? tags : [],
    installed: Boolean(installRow?.installed),
    manifest,
    settings,
    last_verified_at: settings.last_verified_at || null,
    account_label: settings.account_label || null,
  };
}

/**
 * List every connection item with its current installation state.
 *
 * @returns {Array}
 */
export function listConnections() {
  const db = getDb();
  const rows = db.prepare(`
    SELECT i.*, m.installed, m.installed_at, m.settings_json
      FROM marketplace_items i
 LEFT JOIN marketplace_installations m ON m.item_id = i.id
     WHERE i.kind = 'connection'
     ORDER BY i.title ASC
  `).all();
  return rows.map(row => shapeRow(row, {
    installed: row.installed,
    installed_at: row.installed_at,
    settings_json: row.settings_json,
  }));
}

/**
 * Get a single connection by service key (the slug).
 *
 * @param {string} serviceKey
 */
export function getConnection(serviceKey) {
  if (!serviceKey) return null;
  const db = getDb();
  const row = db.prepare(`
    SELECT i.*, m.installed, m.installed_at, m.settings_json
      FROM marketplace_items i
 LEFT JOIN marketplace_installations m ON m.item_id = i.id
     WHERE i.kind = 'connection' AND i.slug = ?
     LIMIT 1
  `).get(String(serviceKey));
  if (!row) return null;
  return shapeRow(row, {
    installed: row.installed,
    installed_at: row.installed_at,
    settings_json: row.settings_json,
  });
}

/**
 * Persist a successful verify: flip installed=1 and write non-secret metadata
 * (account label, last-4 of key, pairing state, etc.) into settings_json.
 *
 * Never accepts secret values — only public metadata derived from the verify
 * response (e.g. provider username, key fingerprint, paired phone number).
 *
 * @param {string} serviceKey
 * @param {{ account_label?: string, last_4?: string, summary?: object }} metadata
 */
export function markConnectionVerified(serviceKey, metadata = {}) {
  const connection = getConnection(serviceKey);
  if (!connection) throw new Error(`Unknown connection: ${serviceKey}`);
  const db = getDb();
  const now = nowIso();

  const previous = connection.settings || {};
  const nextSettings = {
    ...previous,
    last_verified_at: now,
    account_label: metadata.account_label || previous.account_label || null,
    last_4: metadata.last_4 || previous.last_4 || null,
    summary: { ...(previous.summary || {}), ...(metadata.summary || {}) },
  };
  const settingsJson = JSON.stringify(nextSettings);

  const existing = db
    .prepare('SELECT item_id FROM marketplace_installations WHERE item_id = ?')
    .get(connection.id);
  if (existing) {
    db.prepare(`
      UPDATE marketplace_installations
         SET installed = 1, installed_at = ?, settings_json = ?
       WHERE item_id = ?
    `).run(now, settingsJson, connection.id);
  } else {
    db.prepare(`
      INSERT INTO marketplace_installations (item_id, installed, installed_at, settings_json)
      VALUES (?, 1, ?, ?)
    `).run(connection.id, now, settingsJson);
  }

  writeAudit('connection.verified', {
    service_key: serviceKey,
    account_label: nextSettings.account_label,
    last_4: nextSettings.last_4,
  }, `Connection verified: ${serviceKey}`);

  return getConnection(serviceKey);
}

/**
 * Mark a connection disconnected — clears the install row metadata and writes
 * an audit row. Secret revocation is the caller's job (the verify route or a
 * disconnect handler decides which secret names to revoke based on the
 * manifest).
 *
 * @param {string} serviceKey
 */
export function markConnectionDisconnected(serviceKey) {
  const connection = getConnection(serviceKey);
  if (!connection) throw new Error(`Unknown connection: ${serviceKey}`);
  const db = getDb();
  const now = nowIso();

  const existing = db
    .prepare('SELECT item_id FROM marketplace_installations WHERE item_id = ?')
    .get(connection.id);
  if (existing) {
    db.prepare(`
      UPDATE marketplace_installations
         SET installed = 0, installed_at = NULL, settings_json = NULL
       WHERE item_id = ?
    `).run(connection.id);
  }

  writeAudit('connection.disconnected', { service_key: serviceKey }, `Connection disconnected: ${serviceKey}`);

  return getConnection(serviceKey);
}

/**
 * Quick check: do all required credentials for a connection currently resolve
 * via the secrets vault (or env fallback)?
 *
 * Used by summarizeConnection to flag a 'reconnect' state when the install row
 * says installed=1 but the underlying secret was revoked out from under it.
 *
 * @param {object} connection — shape returned by getConnection / listConnections
 */
export function hasAllRequiredSecrets(connection) {
  if (!connection?.manifest?.credentials?.length) return true; // qr_pair / binary_detect have no secrets
  for (const cred of connection.manifest.credentials) {
    if (!cred.required) continue;
    const value = getSecret(cred.name);
    if (!value) return false;
  }
  return true;
}

/**
 * Names of vault secrets that belong to a connection (for revoke on disconnect).
 *
 * @param {object} connection
 * @returns {string[]}
 */
export function credentialNamesFor(connection) {
  if (!connection?.manifest?.credentials?.length) return [];
  return connection.manifest.credentials.map(c => c.name).filter(Boolean);
}

function writeAudit(type, meta, summary) {
  try {
    const db = getDb();
    db.prepare(`
      INSERT INTO audit_logs (id, at, type, summary, status, meta)
      VALUES (?, ?, ?, ?, ?, ?)
    `).run(
      newId('audit'),
      nowIso(),
      type,
      summary,
      'ok',
      JSON.stringify(meta || {}),
    );
  } catch {
    // best-effort; never throw from audit
  }
}

/**
 * For unit tests / debug surfaces: which connections currently have vault
 * entries? Reads listSecrets() and matches against each connection's
 * declared credential names.
 */
export function indexConnectionsBySecretPresence() {
  const secrets = new Set(listSecrets().map(s => s.name));
  const map = {};
  for (const c of listConnections()) {
    map[c.slug] = credentialNamesFor(c).every(name => secrets.has(name));
  }
  return map;
}
