/**
 * EncryptID WebAuthn Module
 *
 * Handles passkey registration, authentication, and PRF extension
 * for key derivation. This is the foundation layer of EncryptID.
 */

// ============================================================================
// TYPES
// ============================================================================

export interface EncryptIDCredential {
  credentialId: string;
  publicKey: ArrayBuffer;
  userId: string;
  username: string;
  createdAt: number;
  prfSupported: boolean;
  transports?: AuthenticatorTransport[];
}

export interface AuthenticationResult {
  credentialId: string;
  userId: string;
  prfOutput?: ArrayBuffer;  // Only if PRF extension supported
  signature: ArrayBuffer;
  authenticatorData: ArrayBuffer;
}

export interface EncryptIDConfig {
  rpId: string;
  rpName: string;
  origin: string;
  userVerification: UserVerificationRequirement;
  timeout: number;
}

// Default configuration for EncryptID
const DEFAULT_CONFIG: EncryptIDConfig = {
  rpId: 'rspace.online',  // Root domain — all *.rspace.online subdomains are valid
  rpName: 'EncryptID',
  origin: typeof window !== 'undefined' ? window.location.origin : '',
  userVerification: 'required',
  timeout: 60000,
};

// Global abort controller for conditional UI
let conditionalUIAbortController: AbortController | null = null;

/**
 * Abort any pending conditional UI request
 * Call this before starting registration or authentication
 */
export function abortConditionalUI(): void {
  if (conditionalUIAbortController) {
    conditionalUIAbortController.abort();
    conditionalUIAbortController = null;
    console.log('EncryptID: Conditional UI aborted');
  }
}

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

/**
 * Convert ArrayBuffer to base64url string
 */
export function bufferToBase64url(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

/**
 * Convert base64url string to ArrayBuffer
 */
export function base64urlToBuffer(base64url: string): ArrayBuffer {
  const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  const padding = '='.repeat((4 - (base64.length % 4)) % 4);
  const binary = atob(base64 + padding);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

/**
 * Generate a random challenge
 */
export function generateChallenge(): ArrayBuffer {
  return crypto.getRandomValues(new Uint8Array(32)).buffer;
}

/**
 * Generate a deterministic salt for PRF extension
 */
export async function generatePRFSalt(purpose: string): Promise<ArrayBuffer> {
  const encoder = new TextEncoder();
  const data = encoder.encode(`encryptid-prf-salt-${purpose}-v1`);
  return crypto.subtle.digest('SHA-256', data);
}

// ============================================================================
// WEBAUTHN REGISTRATION
// ============================================================================

/**
 * Register a new EncryptID passkey
 *
 * This creates a discoverable credential (passkey) that can be used
 * for authentication across all r-ecosystem apps.
 */
export async function registerPasskey(
  username: string,
  displayName: string,
  config: Partial<EncryptIDConfig> = {}
): Promise<EncryptIDCredential> {
  // Abort any pending conditional UI to prevent "request already pending" error
  abortConditionalUI();

  const cfg = { ...DEFAULT_CONFIG, ...config };

  // Check WebAuthn support
  if (!window.PublicKeyCredential) {
    throw new Error('WebAuthn is not supported in this browser');
  }

  // Generate user ID (random bytes, not PII)
  const userId = crypto.getRandomValues(new Uint8Array(32));

  // Generate challenge (in production, this comes from server)
  const challenge = generateChallenge();

  // Generate PRF salt for key derivation
  const prfSalt = await generatePRFSalt('master-key');

  // Build credential creation options
  const createOptions: CredentialCreationOptions = {
    publicKey: {
      challenge: new Uint8Array(challenge),

      // Relying Party information
      rp: {
        id: cfg.rpId,
        name: cfg.rpName,
      },

      // User information
      user: {
        id: userId,
        name: username,
        displayName: displayName,
      },

      // Supported algorithms (prefer ES256)
      pubKeyCredParams: [
        { alg: -7, type: 'public-key' },    // ES256 (P-256)
        { alg: -257, type: 'public-key' },  // RS256
      ],

      // Authenticator requirements
      authenticatorSelection: {
        // Require discoverable credential (passkey)
        residentKey: 'required',
        requireResidentKey: true,

        // Require user verification (biometric/PIN)
        userVerification: cfg.userVerification,

        // No authenticatorAttachment constraint — let the browser offer ALL options:
        // platform (biometrics, PIN, Windows Hello), security keys, phone-as-authenticator.
        // Previously forced 'platform' when available, which blocked cross-platform
        // authenticators on Firefox and other browsers with limited platform UI.
      },

      // Don't request attestation (privacy)
      attestation: 'none',

      // Timeout
      timeout: cfg.timeout,

      // Extensions
      extensions: {
        // Request PRF extension for key derivation
        // @ts-ignore - PRF extension not in standard types yet
        prf: {
          eval: {
            first: new Uint8Array(prfSalt),
          },
        },
        // Request credential properties
        credProps: true,
      },
    },
  };

  // Create the credential
  const credential = await navigator.credentials.create(createOptions) as PublicKeyCredential;

  if (!credential) {
    throw new Error('Failed to create credential');
  }

  const response = credential.response as AuthenticatorAttestationResponse;

  // Check if PRF is supported
  // @ts-ignore
  const prfSupported = credential.getClientExtensionResults()?.prf?.enabled === true;

  // Extract public key from attestation
  const publicKey = response.getPublicKey();
  if (!publicKey) {
    throw new Error('Failed to get public key from credential');
  }

  // Build credential object
  const encryptIDCredential: EncryptIDCredential = {
    credentialId: bufferToBase64url(credential.rawId),
    publicKey: publicKey,
    userId: bufferToBase64url(userId.buffer),
    username: username,
    createdAt: Date.now(),
    prfSupported: prfSupported,
    transports: response.getTransports?.() as AuthenticatorTransport[],
  };

  console.log('EncryptID: Passkey registered', {
    credentialId: encryptIDCredential.credentialId.slice(0, 20) + '...',
    prfSupported,
  });

  return encryptIDCredential;
}

// ============================================================================
// WEBAUTHN AUTHENTICATION
// ============================================================================

/**
 * Authenticate with an existing EncryptID passkey
 *
 * Returns the authentication result including PRF output for key derivation
 * (if the authenticator supports PRF).
 */
export async function authenticatePasskey(
  credentials?: string | string[] | PublicKeyCredentialDescriptor[],
  config: Partial<EncryptIDConfig> = {}
): Promise<AuthenticationResult> {
  // Abort any pending conditional UI to prevent "request already pending" error
  abortConditionalUI();

  const cfg = { ...DEFAULT_CONFIG, ...config };

  // Check WebAuthn support
  if (!window.PublicKeyCredential) {
    throw new Error('WebAuthn is not supported in this browser');
  }

  // Generate challenge (in production, this comes from server)
  const challenge = generateChallenge();

  // Generate PRF salt for key derivation
  const prfSalt = await generatePRFSalt('master-key');

  // Build allowed credentials list
  let allowCredentials: PublicKeyCredentialDescriptor[] | undefined;
  if (credentials) {
    if (typeof credentials === 'string') {
      allowCredentials = [{ type: 'public-key', id: new Uint8Array(base64urlToBuffer(credentials)) }];
    } else if (credentials.length > 0 && typeof credentials[0] === 'string') {
      allowCredentials = (credentials as string[]).map(id => ({
        type: 'public-key' as const,
        id: new Uint8Array(base64urlToBuffer(id)),
      }));
    } else {
      // Already PublicKeyCredentialDescriptor[]
      allowCredentials = credentials as PublicKeyCredentialDescriptor[];
    }
  }

  // Build base publicKey options (without PRF initially)
  const basePublicKey: PublicKeyCredentialRequestOptions = {
    challenge: new Uint8Array(challenge),
    rpId: cfg.rpId,
    allowCredentials: allowCredentials,
    userVerification: cfg.userVerification,
    timeout: cfg.timeout,
  };

  // Try with PRF first, fall back without it (Safari throws on PRF extension)
  let credential: PublicKeyCredential | null = null;
  let usedPrf = false;
  try {
    credential = await navigator.credentials.get({
      publicKey: {
        ...basePublicKey,
        extensions: {
          // @ts-ignore - PRF extension not in standard types yet
          prf: { eval: { first: new Uint8Array(prfSalt) } },
        },
      },
    }) as PublicKeyCredential;
    usedPrf = true;
  } catch (prfError: any) {
    // Safari and some browsers throw TypeError on unsupported PRF extension.
    // Retry without PRF — user falls back to passphrase-based key derivation.
    console.warn('EncryptID: PRF extension failed, retrying without PRF', prfError?.message);
    credential = await navigator.credentials.get({
      publicKey: basePublicKey,
    }) as PublicKeyCredential;
  }

  if (!credential) {
    throw new Error('Authentication failed');
  }

  const response = credential.response as AuthenticatorAssertionResponse;

  // Extract PRF output if available
  // @ts-ignore
  const prfResults = usedPrf ? credential.getClientExtensionResults()?.prf?.results : undefined;
  const prfOutput = prfResults?.first;

  // Build result
  const result: AuthenticationResult = {
    credentialId: bufferToBase64url(credential.rawId),
    userId: response.userHandle
      ? bufferToBase64url(response.userHandle)
      : '',
    prfOutput: prfOutput as ArrayBuffer | undefined,
    signature: response.signature,
    authenticatorData: response.authenticatorData,
  };

  console.log('EncryptID: Authentication successful', {
    credentialId: result.credentialId.slice(0, 20) + '...',
    prfAvailable: !!prfOutput,
  });

  return result;
}

// ============================================================================
// CONDITIONAL UI (AUTOFILL)
// ============================================================================

/**
 * Check if conditional mediation (passkey autofill) is available
 */
export async function isConditionalMediationAvailable(): Promise<boolean> {
  if (!window.PublicKeyCredential) {
    return false;
  }

  // @ts-ignore
  if (typeof PublicKeyCredential.isConditionalMediationAvailable === 'function') {
    // @ts-ignore
    return PublicKeyCredential.isConditionalMediationAvailable();
  }

  return false;
}

/**
 * Start conditional UI authentication (passkey autofill in input fields)
 *
 * Call this on page load to enable passkey suggestions in username fields.
 * The input field needs `autocomplete="username webauthn"` attribute.
 */
export async function startConditionalUI(
  config: Partial<EncryptIDConfig> = {}
): Promise<AuthenticationResult | null> {
  const available = await isConditionalMediationAvailable();

  if (!available) {
    console.log('EncryptID: Conditional mediation not available');
    return null;
  }

  // Abort any existing conditional UI request
  abortConditionalUI();

  // Create new abort controller for this request
  conditionalUIAbortController = new AbortController();

  const cfg = { ...DEFAULT_CONFIG, ...config };
  const challenge = generateChallenge();
  const prfSalt = await generatePRFSalt('master-key');

  const basePublicKey: PublicKeyCredentialRequestOptions = {
    challenge: new Uint8Array(challenge),
    rpId: cfg.rpId,
    userVerification: cfg.userVerification,
    timeout: cfg.timeout,
  };

  const conditionalOpts = {
    // @ts-ignore - conditional mediation
    mediation: 'conditional' as const,
    signal: conditionalUIAbortController.signal,
  };

  try {
    // Try with PRF first
    let credential: PublicKeyCredential | null = null;
    let usedPrf = false;
    try {
      credential = await navigator.credentials.get({
        publicKey: {
          ...basePublicKey,
          extensions: {
            // @ts-ignore
            prf: { eval: { first: new Uint8Array(prfSalt) } },
          },
        },
        ...conditionalOpts,
      }) as PublicKeyCredential;
      usedPrf = true;
    } catch (prfError: any) {
      // Safari throws on PRF — retry without it
      if (prfError?.name === 'AbortError') throw prfError;
      console.warn('EncryptID: Conditional UI PRF failed, retrying without PRF');
      conditionalUIAbortController = new AbortController();
      credential = await navigator.credentials.get({
        publicKey: basePublicKey,
        ...conditionalOpts,
        signal: conditionalUIAbortController.signal,
      }) as PublicKeyCredential;
    }

    // Clear abort controller on success
    conditionalUIAbortController = null;

    if (!credential) {
      return null;
    }

    const response = credential.response as AuthenticatorAssertionResponse;
    // @ts-ignore
    const prfResults = usedPrf ? credential.getClientExtensionResults()?.prf?.results : undefined;

    return {
      credentialId: bufferToBase64url(credential.rawId),
      userId: response.userHandle
        ? bufferToBase64url(response.userHandle)
        : '',
      prfOutput: prfResults?.first as ArrayBuffer | undefined,
      signature: response.signature,
      authenticatorData: response.authenticatorData,
    };
  } catch (error) {
    // Conditional UI was cancelled or failed
    console.log('EncryptID: Conditional UI cancelled or failed', error);
    return null;
  }
}

// ============================================================================
// FEATURE DETECTION
// ============================================================================

export interface WebAuthnCapabilities {
  webauthn: boolean;
  platformAuthenticator: boolean;
  conditionalUI: boolean;
  prfExtension: boolean;  // Note: Can only be confirmed after credential creation
}

/**
 * Detect WebAuthn capabilities of the current browser/device
 */
export async function detectCapabilities(): Promise<WebAuthnCapabilities> {
  const capabilities: WebAuthnCapabilities = {
    webauthn: false,
    platformAuthenticator: false,
    conditionalUI: false,
    prfExtension: false,
  };

  // Check basic WebAuthn support
  if (!window.PublicKeyCredential) {
    return capabilities;
  }
  capabilities.webauthn = true;

  // Check platform authenticator
  try {
    capabilities.platformAuthenticator =
      await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  } catch {
    capabilities.platformAuthenticator = false;
  }

  // Check conditional UI
  capabilities.conditionalUI = await isConditionalMediationAvailable();

  // PRF support can only be confirmed after credential creation
  // We assume true for modern browsers and verify during registration
  capabilities.prfExtension = true;  // Optimistic, verified at registration

  return capabilities;
}
