/**
 * Unified IdP login — one form (user + pass + OTP) in Forge logs the
 * user into every SAML SP that shares the IdP. Backend is just a thin
 * wrapper over the extension's `auth.idpLogin` RPC.
 *
 * Tenants may declare MULTIPLE IdPs in their wizard template — e.g.
 * Fortinet has FAC (fac.corp.fortinet.com) covering mantis/pmdb/tp
 * AND an EMEA SAML (sso.frval.fortinet-emea.com) covering scap. We
 * loop through each entry sequentially, each gets a fresh trigger SP
 * tab, each independently reports ok/error. post_login_terminal is
 * per-entry (e.g. GitLab SSH 2FA hangs off the FAC entry).
 *
 * Assumption: every IdP accepts the same user/pass/OTP. True for
 * environments with a shared TOTP secret (FortiToken). If that ever
 * breaks, swap in per-entry credential lookup here.
 */

import { bridgeRpc } from '../chat/bridge-client';
import { getInstalledConnector, listInstalledConnectors } from '../connectors/registry';
import { resolveWizardTemplate } from '../connectors/wizard-template';
import { runTerminalKeystroke, type TerminalSpec, type TerminalResult } from './terminal-keystroke';
import { expandSettingsTokens } from '../plugins/templates';
import { loadSettings } from '../settings';

export interface IdpLoginRequest {
  username: string;
  password: string;
  otp: string;
}

/** Per-IdP outcome — populated for each entry in the template. */
export interface IdpLoginEntry {
  idp_host: string;
  trigger_url?: string;
  ok: boolean;
  steps_completed?: number;
  final_url?: string;
  error?: string;
  post_login_terminal?: TerminalResult[];
}

export interface IdpLoginResult {
  /** True iff every IdP entry's login succeeded. */
  ok: boolean;
  /** Per-IdP results in template order. */
  idp_logins: IdpLoginEntry[];
  error?: string;
}

export interface IdpTemplateBlock {
  host?: string;
  /** Optional display name override for the Login Status panel.
   *  Falls back to `host` when missing. */
  display_name?: string;
  /** Optional explicit URL the Login Status probe opens. Defaults to
   *  `https://<host>/`. Override when the IdP's logged-in landing page
   *  is at a specific path (e.g. `/saml-idp/portal/` for FAC). */
  probe_url?: string;
  /** Optional alt hostnames the IdP also uses (e.g. regional SAML servers). */
  alt_hosts?: string[];
  saml_sps?: string[];
  /** Explicit trigger URL — wins over saml_sps lookup. Use when the SP
   *  connector has no base_url in its config (only host_match), so the
   *  default pickTriggerUrl can't find a URL to open. */
  trigger_url?: string;
  /** Override the username sent to this IdP. Supports template tokens
   *  like {user.email} / {user.login}. Use when the IdP expects a
   *  different identifier than other entries (e.g. Microsoft federated
   *  login needs full email at a tenant-specific domain). */
  username?: string;
  /** Per-IdP CSS selector overrides (Microsoft, Okta, Auth0 — non-FAC
   *  shapes). Sent as-is to the extension; missing fields fall back to
   *  FAC defaults. See extension's IdpSelectors type. */
  selectors?: {
    form?: string;
    username?: string;
    password?: string;
    token_code?: string;
    submit?: string;
  };
  /** Per-entry timeout override (seconds). Teams / Microsoft login is
   *  slow due to multiple cross-domain redirects. */
  timeout_seconds?: number;
  post_login_terminal?: TerminalSpec[];
}

/**
 * Walk every installed enterprise source's wizard template until we
 * find one with an `_idp` block. The block may be a single object
 * (legacy) or an array of objects (multi-IdP). Returns normalized
 * array of valid blocks.
 */
export function readIdpBlocks(): IdpTemplateBlock[] {
  const tryRead = (sourceId?: string): IdpTemplateBlock[] | null => {
    const t = resolveWizardTemplate(sourceId);
    const raw = (t?.template as { _idp?: IdpTemplateBlock | IdpTemplateBlock[] } | undefined)?._idp;
    if (!raw) return null;
    const arr = Array.isArray(raw) ? raw : [raw];
    const valid = arr.filter((b) => b?.host && b.saml_sps?.length);
    return valid.length > 0 ? valid : null;
  };
  const candidates = new Set<string>();
  for (const c of listInstalledConnectors()) {
    if (c.installed_source_id) candidates.add(c.installed_source_id);
  }
  for (const sourceId of candidates) {
    const blocks = tryRead(sourceId);
    if (blocks) return blocks;
  }
  return tryRead() || [];
}

/** Pick the SP whose tab we'll open to trigger the SAML flow.
 *
 *  Preference order:
 *   1. A SP whose login-status cache currently shows ✗ — opening it forces
 *      a SAML redirect to the IdP, which is exactly what we want when the
 *      IdP cookie has expired even though some sibling SPs still have a
 *      valid session cookie.
 *   2. Otherwise first installed SP with a base_url.
 */
function pickTriggerUrl(saml_sps: string[]): string | null {
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const { getCachedResult } = require('./login-status') as typeof import('./login-status');
  let fallback: string | null = null;
  for (const id of saml_sps) {
    const c = getInstalledConnector(id);
    const url = (c?.config as { base_url?: string } | undefined)?.base_url;
    if (!url) continue;
    const normalized = url.replace(/\/+$/, '/');
    fallback ||= normalized;
    const cached = getCachedResult(`connector:${id}`);
    if (cached && cached.ok === false) {
      return normalized;
    }
  }
  return fallback;
}

async function runOneIdp(block: IdpTemplateBlock, req: IdpLoginRequest): Promise<IdpLoginEntry> {
  const idp_host = block.host!;
  const trigger_url = block.trigger_url || pickTriggerUrl(block.saml_sps!);
  if (!trigger_url) {
    return {
      idp_host,
      ok: false,
      error: `No trigger URL — SPs (${block.saml_sps!.join(', ')}) have no base_url, and no explicit "trigger_url" in _idp block.`,
    };
  }
  // Template-provided username override (e.g. "{user.login}@fortinet-us.com"
  // for Microsoft federated login). Falls back to caller's input.
  let username = req.username;
  if (block.username && block.username.trim()) {
    try {
      const settings = loadSettings() as unknown as Record<string, unknown>;
      username = expandSettingsTokens(block.username, settings);
    } catch {
      username = block.username;
    }
  }
  // Teams / Microsoft login crosses 3+ domains and can take >60s when
  // the network is loaded; let templates extend the budget per entry.
  const idpTimeout = block.timeout_seconds && block.timeout_seconds > 0 ? block.timeout_seconds : 60;
  try {
    const raw = await bridgeRpc(
      'auth.idpLogin',
      {
        trigger_url,
        idp_host,
        idp_alt_hosts: block.alt_hosts || [],
        selectors: block.selectors || undefined,
        username,
        password: req.password,
        otp: req.otp,
        timeout_seconds: idpTimeout,
      },
      // bridgeRpc timeout = idp timeout + 15s grace for the round-trip.
      (idpTimeout + 15) * 1000,
    );
    const r = (raw || {}) as { ok?: boolean; final_url?: string; steps_completed?: number; error?: string };
    if (!r.ok) {
      return {
        idp_host,
        trigger_url,
        ok: false,
        final_url: r.final_url,
        steps_completed: r.steps_completed,
        error: r.error || 'idp-login failed (no error from extension)',
      };
    }

    const postResults: TerminalResult[] = [];
    if (block.post_login_terminal && block.post_login_terminal.length > 0) {
      for (const spec of block.post_login_terminal) {
        postResults.push(await runTerminalKeystroke(spec, req.otp));
      }
    }
    return {
      idp_host,
      trigger_url,
      ok: true,
      final_url: r.final_url,
      steps_completed: r.steps_completed,
      post_login_terminal: postResults.length > 0 ? postResults : undefined,
    };
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    let friendly = msg;
    if (msg.includes('unknown method') && msg.includes('auth.idpLogin')) {
      friendly =
        'Forge browser extension is out of date — it does not know auth.idpLogin yet. ' +
        'Rebuild the extension (pnpm build), reload it in chrome://extensions, then try again.';
    } else if (msg.includes('no paired extensions') || msg.includes('no connected') || msg.includes('No extension connected')) {
      friendly = 'Forge browser extension is not connected. Open the extension popup → Settings → Login.';
    }
    return { idp_host, trigger_url, ok: false, error: friendly };
  }
}

export async function performIdpLogin(req: IdpLoginRequest): Promise<IdpLoginResult> {
  if (!req.username || !req.password || !req.otp) {
    return { ok: false, idp_logins: [], error: 'username, password, and otp are all required' };
  }
  const blocks = readIdpBlocks();
  if (blocks.length === 0) {
    return {
      ok: false,
      idp_logins: [],
      error:
        'No _idp block in the active wizard template — declare { "_idp": [ { "host": "...", "saml_sps": [...] } ] } to enable unified login.',
    };
  }

  // Sequential — each opens a tab and we don't want to race them.
  const results: IdpLoginEntry[] = [];
  for (const block of blocks) {
    results.push(await runOneIdp(block, req));
  }
  const allOk = results.every((r) => r.ok);
  return {
    ok: allOk,
    idp_logins: results,
    error: allOk ? undefined : results.find((r) => !r.ok)?.error,
  };
}
