/**
 * Login Status — central registry of expirable auth credentials.
 *
 * Auto-discovers sources from two places:
 *   1. Connector manifests with a `test:` block → one source per
 *      connector. Browser-side or HTTP, decided by `test.probe`.
 *   2. External hardcoded sources (currently just GitLab 2FA via
 *      `git fetch`).
 *
 * Each source can:
 *   - check()    → runs the test, returns { ok, detail }
 *   - refresh()  → returns either a URL for the user to open, or a
 *                  shell command for them to run interactively
 *
 * Results are cached in-memory and persisted to
 * `<dataDir>/login-status.json` so the panel doesn't re-probe every
 * open (which would slow the UI and disturb the user's Chrome tabs).
 */

import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { getDataDir } from '../dirs';
import { runConnectorTest, runBrowserUrlProbe } from '../connectors/test-runner';
import { listConnectorIds, getConnector, getInstalledConnector } from '../connectors/registry';
import { expandSettingsTokens } from '../plugins/templates';
import { readIdpBlocks, type IdpTemplateBlock } from './idp-login';

export type LoginCategory = 'browser' | 'token' | 'external';

export type RefreshHint =
  | { kind: 'open-url'; url: string; description?: string }
  | { kind: 'show-command'; command: string; description: string }
  | { kind: 'open-settings'; section: string };

export interface LoginSource {
  id: string;
  label: string;
  category: LoginCategory;
  /** Free-form one-line description shown under the label. */
  detail?: string;
  /** Returns true if check is safe to run without prompting the user. */
  refresh: RefreshHint;
}

export interface LoginCheckResult {
  ok: boolean;
  /** Short status message (e.g. "session active", "401 unauthorized"). */
  message?: string;
  /** Optional URL the probe landed on — useful for diagnosing redirects. */
  landed_url?: string;
  /** Timestamp of this check (ms since epoch). */
  checked_at: number;
  /** Duration of the check itself, ms. */
  duration_ms: number;
}

export interface LoginStatusRow {
  source: LoginSource;
  result: LoginCheckResult | null;
}

// ─── In-memory + disk cache ─────────────────────────────────────────

const CACHE_FILE = () => join(getDataDir(), 'login-status.json');
let cache: Record<string, LoginCheckResult> | null = null;

function loadCache(): Record<string, LoginCheckResult> {
  if (cache) return cache;
  try {
    if (existsSync(CACHE_FILE())) {
      cache = JSON.parse(readFileSync(CACHE_FILE(), 'utf-8'));
      return cache!;
    }
  } catch (e) {
    console.warn('[login-status] cache read failed:', (e as Error).message);
  }
  cache = {};
  return cache;
}

function persistCache(): void {
  try {
    writeFileSync(CACHE_FILE(), JSON.stringify(cache || {}, null, 2));
  } catch (e) {
    console.warn('[login-status] cache write failed:', (e as Error).message);
  }
}

export function getCachedResult(sourceId: string): LoginCheckResult | null {
  return loadCache()[sourceId] ?? null;
}

export function setCachedResult(sourceId: string, r: LoginCheckResult): void {
  loadCache()[sourceId] = r;
  persistCache();
}

/**
 * Drop the cached probe result for a single source. Called by
 * `setConnectorConfig` so the panel auto-re-probes after the user
 * (or a wizard apply) just rewrote creds — otherwise a stale 401
 * from the no-token state lingers until the user clicks Refresh.
 */
export function invalidateCachedResult(sourceId: string): void {
  const c = loadCache();
  if (sourceId in c) {
    delete c[sourceId];
    persistCache();
  }
}

// ─── Source enumeration ─────────────────────────────────────────────

/**
 * Build the canonical list of LoginSources from connector manifests.
 * Connector-derived rows are filtered to those with `test:` declared and
 * actually installed (so an uninstalled connector with empty creds
 * doesn't pollute the panel with permanent 401s).
 */
export function listSources(): LoginSource[] {
  const out: LoginSource[] = [];

  for (const id of listConnectorIds()) {
    const def = getConnector(id);
    if (!def || !def.test) continue;
    if (!getInstalledConnector(id)) continue;
    const probe = def.test.probe || 'http';
    const category: LoginCategory = probe === 'browser' ? 'browser' : 'token';

    let refresh: RefreshHint;
    if (probe === 'browser') {
      const rawHm = def.host_match || def.connectors?.[0]?.host_match || '';
      const inst = getInstalledConnector(id);
      const expanded = inst ? expandSettingsTokens(rawHm, inst.config as any) : rawHm;
      const url = expanded.replace(/\/\*$/, '/');
      const stillUnresolved = /\{[^}]+\}/.test(url) || !/^https?:\/\//.test(url);
      refresh = stillUnresolved
        ? { kind: 'open-settings', section: 'connectors' }
        : { kind: 'open-url', url, description: 'Open in a new tab to log in' };
    } else {
      refresh = { kind: 'open-settings', section: 'connectors' };
    }

    out.push({
      id: `connector:${id}`,
      label: def.name || id,
      category,
      detail: def.test.description,
      refresh,
    });
  }

  return out;
}

// ─── Checks ─────────────────────────────────────────────────────────

/**
 * Run the check for one source. Returns the fresh result AND writes it
 * to the cache.
 */
export async function checkSource(source: LoginSource): Promise<LoginCheckResult> {
  const t0 = Date.now();
  let r: Omit<LoginCheckResult, 'checked_at' | 'duration_ms'>;

  if (source.id.startsWith('connector:')) {
    r = await checkConnector(source.id.slice('connector:'.length));
  } else if (source.id.startsWith('idp:')) {
    r = await checkIdp(source.id.slice('idp:'.length));
  } else {
    r = { ok: false, message: `unknown source ${source.id}` };
  }

  const result: LoginCheckResult = {
    ...r,
    checked_at: Date.now(),
    duration_ms: Date.now() - t0,
  };
  setCachedResult(source.id, result);
  return result;
}

async function checkConnector(
  connectorId: string,
): Promise<Omit<LoginCheckResult, 'checked_at' | 'duration_ms'>> {
  try {
    const r = await runConnectorTest(connectorId);
    return {
      ok: !!r.ok,
      message: r.ok ? (r.message || 'ok') : (r.error || `HTTP ${r.status || '?'}`),
      landed_url: r.url,
    };
  } catch (e) {
    return { ok: false, message: `probe error: ${(e as Error).message}` };
  }
}

// ─── IdP-consolidated sources (Enterprise badge "Web sessions") ────
// The right-side panel iterates per-connector via listSources(); the
// Enterprise badge wants a coarser view that asks "is the IdP cookie
// still alive?" — one row per _idp block, not one per SP.

function idpProbeUrl(block: IdpTemplateBlock): string {
  return (block.probe_url && block.probe_url.trim()) || `https://${block.host}/`;
}

export function listIdpSources(): LoginSource[] {
  const out: LoginSource[] = [];
  for (const block of readIdpBlocks()) {
    if (!block.host) continue;
    out.push({
      id: `idp:${block.host}`,
      label: block.display_name || block.host,
      category: 'browser',
      detail: block.saml_sps?.length
        ? `IdP for: ${block.saml_sps.join(', ')}`
        : 'Identity provider',
      refresh: {
        kind: 'open-url',
        url: idpProbeUrl(block),
        description: 'Open IdP portal to re-authenticate',
      },
    });
  }
  return out;
}

export function listIdpStatus(): LoginStatusRow[] {
  return listIdpSources().map((source) => ({ source, result: getCachedResult(source.id) }));
}

async function checkIdp(
  host: string,
): Promise<Omit<LoginCheckResult, 'checked_at' | 'duration_ms'>> {
  const block = readIdpBlocks().find((b) => b.host === host);
  const probe_url = block ? idpProbeUrl(block) : `https://${host}/`;
  try {
    const r = await runBrowserUrlProbe({ id: `idp:${host}`, host, probe_url });
    return {
      ok: !!r.ok,
      message: r.ok ? (r.message || 'ok') : (r.error || `HTTP ${r.status || '?'}`),
      landed_url: r.url,
    };
  } catch (e) {
    return { ok: false, message: `probe error: ${(e as Error).message}` };
  }
}

// ─── Bulk view ──────────────────────────────────────────────────────

export function listStatus(): LoginStatusRow[] {
  return listSources().map((source) => ({
    source,
    result: getCachedResult(source.id),
  }));
}
