/**
 * Template substitution for connector manifest fields.
 *
 * Expands tokens that depend on user settings ({base_url}, {settings.*}) when
 * Forge serves the manifest to the extension. Tokens that depend on the LLM's
 * tool arguments ({args.*}) are left literal — the extension's runner expands
 * those at execution time.
 *
 * `{connector:<id>.<field>}` cross-references another connector's runtime
 * config — e.g. Jenkins inject_params can pull a GitLab PAT without baking
 * it. Read at dispatch time, never persisted in the dependent connector's
 * own config, so changing the GitLab token automatically reflects in
 * Jenkins.
 *
 * See docs/Connector-DeclarativeExtract-Spec.md §4.
 */

import { getInstalledConnector } from '../connectors/registry';
import { loadSettings } from '../settings';

type SettingsMap = Record<string, any>;

/**
 * `{user.<field>}` resolves to the operator's global identity from
 * settings.yaml. Replaces per-connector username fields so an org
 * has one source of truth.
 *
 *   {user.name}   displayName → fallback: local-part of displayEmail → ''
 *   {user.email}  displayEmail
 *   {user.login}  email local-part (same as {user.name}'s fallback)
 */
function resolveUserRef(ref: string): string | null {
  let s: { displayName?: string; displayEmail?: string };
  try { s = loadSettings() as any; } catch { return null; }
  const email = s.displayEmail || '';
  const localPart = email.includes('@') ? email.split('@')[0] : email;
  switch (ref) {
    case 'name':
      return (s.displayName && s.displayName.trim() && s.displayName.trim() !== 'Forge')
        ? s.displayName.trim()
        : localPart;
    case 'email':
      return email;
    case 'login':
      return localPart;
    default:
      return null;
  }
}

/**
 * Look up `{connector:<id>.<field>}` by reading the target connector's
 * runtime config from connector-configs.json. Secret fields are
 * auto-decrypted by getInstalledConnector. Missing connector / field
 * returns null so the caller leaves the token literal (matches the
 * "unknown token → pass through" behaviour of the other resolvers).
 */
function resolveConnectorRef(ref: string): string | null {
  const dot = ref.indexOf('.');
  if (dot < 0) return null;
  const id = ref.slice(0, dot).trim();
  const field = ref.slice(dot + 1).trim();
  if (!id || !field) return null;
  const inst = getInstalledConnector(id);
  if (!inst) return null;
  const v = (inst.config as Record<string, unknown>)[field];
  if (v == null) return null;
  return typeof v === 'string' ? v : String(v);
}

/**
 * Expand {base_url} and {settings.<key>} tokens in a string. Other tokens
 * (including {args.*}) are passed through unchanged so the extension can
 * finish substitution at run time.
 */
export function expandSettingsTokens(template: string, settings: SettingsMap | undefined): string {
  if (!template) return template;
  return template.replace(/\{([^{}]+)\}/g, (full, raw) => {
    const key = String(raw).trim();
    if (key === 'base_url') {
      const v = settings?.base_url;
      return typeof v === 'string' ? stripTrailingSlashes(v) : full;
    }
    if (key.startsWith('settings.')) {
      const path = key.slice('settings.'.length);
      const v = settings?.[path];
      return typeof v === 'string' ? stripTrailingSlashes(v) : full;
    }
    if (key.startsWith('connector:')) {
      const v = resolveConnectorRef(key.slice('connector:'.length));
      return v == null ? full : v;
    }
    if (key.startsWith('user.')) {
      const v = resolveUserRef(key.slice('user.'.length));
      return v == null ? full : v;
    }
    return full;
  });
}

function stripTrailingSlashes(s: string): string {
  return s.replace(/\/+$/, '');
}

/**
 * Expand all tokens — `{base_url}`, `{settings.*}`, `{args.*}` — in one
 * pass. Used by server-side protocols (http, shell) where Forge has both
 * the settings and the LLM's parsed arguments in hand.
 *
 * Unknown tokens are left literal.
 *
 * `{args.*}` supports dotted paths (e.g. `{args.repo.full_name}`).
 * Non-string values are JSON-encoded for object/array, coerced for
 * number/boolean. base_url and settings get trailing slashes stripped;
 * args do not (an args value is whatever the LLM passed).
 */
export function expandAllTokens(
  template: string,
  settings: SettingsMap | undefined,
  args: SettingsMap | undefined,
): string {
  if (!template) return template;
  return template.replace(/\{([^{}]+)\}/g, (full, raw) => {
    const key = String(raw).trim();
    if (key === 'base_url') {
      const v = settings?.base_url;
      return typeof v === 'string' ? stripTrailingSlashes(v) : full;
    }
    if (key.startsWith('settings.')) {
      const v = settings?.[key.slice('settings.'.length)];
      if (v == null) return full;
      return typeof v === 'string' ? v : String(v);
    }
    if (key.startsWith('args.')) {
      const path = key.slice('args.'.length).split('.');
      let v: any = args;
      for (const p of path) {
        if (v == null || typeof v !== 'object') { v = undefined; break; }
        v = v[p];
      }
      if (v == null) return full;
      if (typeof v === 'string') return v;
      if (typeof v === 'number' || typeof v === 'boolean') return String(v);
      try { return JSON.stringify(v); } catch { return full; }
    }
    if (key.startsWith('connector:')) {
      const v = resolveConnectorRef(key.slice('connector:'.length));
      return v == null ? full : v;
    }
    if (key.startsWith('user.')) {
      const v = resolveUserRef(key.slice('user.'.length));
      return v == null ? full : v;
    }
    return full;
  });
}
