export type SupportedSsoProviderId = 'authentik' | 'workos' | 'keycloak';
export type SsoProtocol = 'oidc' | 'saml';
export type SsoLoginMode = 'oidc' | 'saml' | null;

type EnvMap = Record<string, string | undefined>;

export interface GenericOidcProviderConfig {
  providerId: SupportedSsoProviderId;
  clientId: string;
  clientSecret: string;
  discoveryUrl?: string;
  issuer?: string;
  authorizationUrl?: string;
  tokenUrl?: string;
  userInfoUrl?: string;
  scopes: string[];
  pkce?: boolean;
  requireIssuerValidation?: boolean;
}

export interface SsoTeamSyncConfig {
  enabled: boolean;
  claim: string | null;
}

export interface SsoProviderDiscovery {
  id: SupportedSsoProviderId;
  name: string;
  protocols: SsoProtocol[];
  configured: boolean;
  loginMode: SsoLoginMode;
  callbackPath: string | null;
  teamSync: SsoTeamSyncConfig;
  samlFallback: {
    configured: boolean;
    loginUrl: string | null;
  };
  warnings: string[];
}

const DEFAULT_SCOPES = ['openid', 'email', 'profile'];

function readEnv(env: EnvMap, key: string): string | undefined {
  const value = env[key]?.trim();
  return value ? value : undefined;
}

function toDiscoveryUrl(issuer: string): string {
  return `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
}

function getTeamSyncClaim(env: EnvMap, envKey: string, fallbackClaim?: string): SsoTeamSyncConfig {
  const claim = readEnv(env, envKey) ?? fallbackClaim ?? null;
  return {
    enabled: claim !== null,
    claim,
  };
}

function buildAuthentikConfig(env: EnvMap): GenericOidcProviderConfig | null {
  const issuer = readEnv(env, 'AUTHENTIK_ISSUER');
  const clientId = readEnv(env, 'AUTHENTIK_CLIENT_ID');
  const clientSecret = readEnv(env, 'AUTHENTIK_CLIENT_SECRET');

  const fields = [issuer, clientId, clientSecret];
  const presentCount = fields.filter(Boolean).length;
  if (presentCount > 0 && presentCount < fields.length) {
    throw new Error(
      '@mosaicstack/auth: Authentik SSO requires AUTHENTIK_ISSUER, AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET.',
    );
  }
  if (!issuer || !clientId || !clientSecret) {
    return null;
  }

  const baseIssuer = issuer.replace(/\/$/, '');

  return {
    providerId: 'authentik',
    issuer: baseIssuer,
    clientId,
    clientSecret,
    discoveryUrl: toDiscoveryUrl(baseIssuer),
    authorizationUrl: `${baseIssuer}/application/o/authorize/`,
    tokenUrl: `${baseIssuer}/application/o/token/`,
    userInfoUrl: `${baseIssuer}/application/o/userinfo/`,
    scopes: DEFAULT_SCOPES,
  };
}

function buildWorkosConfig(env: EnvMap): GenericOidcProviderConfig | null {
  const issuer = readEnv(env, 'WORKOS_ISSUER');
  const clientId = readEnv(env, 'WORKOS_CLIENT_ID');
  const clientSecret = readEnv(env, 'WORKOS_CLIENT_SECRET');

  const fields = [issuer, clientId, clientSecret];
  const presentCount = fields.filter(Boolean).length;
  if (presentCount > 0 && presentCount < fields.length) {
    throw new Error(
      '@mosaicstack/auth: WorkOS SSO requires WORKOS_ISSUER, WORKOS_CLIENT_ID, WORKOS_CLIENT_SECRET.',
    );
  }
  if (!issuer || !clientId || !clientSecret) {
    return null;
  }

  const normalizedIssuer = issuer.replace(/\/$/, '');

  return {
    providerId: 'workos',
    issuer: normalizedIssuer,
    clientId,
    clientSecret,
    discoveryUrl: toDiscoveryUrl(normalizedIssuer),
    scopes: DEFAULT_SCOPES,
    pkce: true,
    requireIssuerValidation: true,
  };
}

function buildKeycloakConfig(env: EnvMap): GenericOidcProviderConfig | null {
  const explicitIssuer = readEnv(env, 'KEYCLOAK_ISSUER');
  const keycloakUrl = readEnv(env, 'KEYCLOAK_URL');
  const keycloakRealm = readEnv(env, 'KEYCLOAK_REALM');
  const clientId = readEnv(env, 'KEYCLOAK_CLIENT_ID');
  const clientSecret = readEnv(env, 'KEYCLOAK_CLIENT_SECRET');

  // Derive issuer from KEYCLOAK_URL + KEYCLOAK_REALM if KEYCLOAK_ISSUER not set
  const issuer =
    explicitIssuer ??
    (keycloakUrl && keycloakRealm
      ? `${keycloakUrl.replace(/\/$/, '')}/realms/${keycloakRealm}`
      : undefined);

  const anySet = !!(issuer || clientId || clientSecret);
  if (anySet && (!issuer || !clientId || !clientSecret)) {
    throw new Error(
      '@mosaicstack/auth: Keycloak SSO requires KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER.',
    );
  }
  if (!issuer || !clientId || !clientSecret) {
    return null;
  }

  const normalizedIssuer = issuer.replace(/\/$/, '');

  return {
    providerId: 'keycloak',
    issuer: normalizedIssuer,
    clientId,
    clientSecret,
    discoveryUrl: toDiscoveryUrl(normalizedIssuer),
    scopes: DEFAULT_SCOPES,
    pkce: true,
    requireIssuerValidation: true,
  };
}

function collectWarnings(env: EnvMap, provider: SupportedSsoProviderId): string[] {
  const prefix = provider.toUpperCase();
  const oidcFields = [
    `${prefix}_CLIENT_ID`,
    `${prefix}_CLIENT_SECRET`,
    `${prefix}_ISSUER`,
  ] as const;
  const presentOidcFields = oidcFields.filter((field) => readEnv(env, field));
  const warnings: string[] = [];

  if (presentOidcFields.length > 0 && presentOidcFields.length < oidcFields.length) {
    const missing = oidcFields.filter((field) => !readEnv(env, field));
    warnings.push(`${provider} OIDC is partially configured. Missing: ${missing.join(', ')}`);
  }

  return warnings;
}

export function buildGenericOidcProviderConfigs(
  env: EnvMap = process.env,
): GenericOidcProviderConfig[] {
  return [buildAuthentikConfig(env), buildWorkosConfig(env), buildKeycloakConfig(env)].filter(
    (config): config is GenericOidcProviderConfig => config !== null,
  );
}

export function listSsoStartupWarnings(env: EnvMap = process.env): string[] {
  return ['authentik', 'workos', 'keycloak'].flatMap((provider) =>
    collectWarnings(env, provider as SupportedSsoProviderId),
  );
}

export function buildSsoDiscovery(env: EnvMap = process.env): SsoProviderDiscovery[] {
  const oidcConfigs = new Map(
    buildGenericOidcProviderConfigs(env).map((config) => [config.providerId, config]),
  );
  const keycloakSamlLoginUrl = readEnv(env, 'KEYCLOAK_SAML_LOGIN_URL') ?? null;

  return [
    {
      id: 'authentik',
      name: 'Authentik',
      protocols: ['oidc'],
      configured: oidcConfigs.has('authentik'),
      loginMode: oidcConfigs.has('authentik') ? 'oidc' : null,
      callbackPath: oidcConfigs.has('authentik') ? '/api/auth/oauth2/callback/authentik' : null,
      teamSync: getTeamSyncClaim(env, 'AUTHENTIK_TEAM_SYNC_CLAIM', 'groups'),
      samlFallback: {
        configured: false,
        loginUrl: null,
      },
      warnings: collectWarnings(env, 'authentik'),
    },
    {
      id: 'workos',
      name: 'WorkOS',
      protocols: ['oidc'],
      configured: oidcConfigs.has('workos'),
      loginMode: oidcConfigs.has('workos') ? 'oidc' : null,
      callbackPath: oidcConfigs.has('workos') ? '/api/auth/oauth2/callback/workos' : null,
      teamSync: getTeamSyncClaim(env, 'WORKOS_TEAM_SYNC_CLAIM', 'organization_id'),
      samlFallback: {
        configured: false,
        loginUrl: null,
      },
      warnings: collectWarnings(env, 'workos'),
    },
    {
      id: 'keycloak',
      name: 'Keycloak',
      protocols: ['oidc', 'saml'],
      configured: oidcConfigs.has('keycloak') || keycloakSamlLoginUrl !== null,
      loginMode: oidcConfigs.has('keycloak') ? 'oidc' : keycloakSamlLoginUrl ? 'saml' : null,
      callbackPath: oidcConfigs.has('keycloak') ? '/api/auth/oauth2/callback/keycloak' : null,
      teamSync: getTeamSyncClaim(env, 'KEYCLOAK_TEAM_SYNC_CLAIM', 'groups'),
      samlFallback: {
        configured: keycloakSamlLoginUrl !== null,
        loginUrl: keycloakSamlLoginUrl,
      },
      warnings: collectWarnings(env, 'keycloak'),
    },
  ];
}
