/**
 * Microsoft Entra VerifiedID connector
 * Provides integration with Microsoft Entra VerifiedID for verifiable credential issuance and verification
 */

import fetch from 'node-fetch';

export interface EntraVerifiedIDConfig {
  tenantId: string;
  clientId: string;
  clientSecret: string;
  credentialManifestId?: string;
  apiVersion?: string;
  logoUri?: string; // URI to credential logo/image (PNG, JPG, BMP recommended; SVG may work)
  backgroundColor?: string; // Background color for credential card
  textColor?: string; // Text color for credential card
}

/**
 * Supported claim value types
 */
export type ClaimValue = string | number | boolean | null;

/**
 * Verifiable credential request with enhanced claim types
 */
export interface VerifiableCredentialRequest {
  claims: Record<string, ClaimValue>;
  pin?: string;
  callbackUrl?: string;
}

export interface VerifiableCredentialResponse {
  requestId: string;
  url: string;
  expiry: number;
  qrCode?: string;
}

export interface VerifiableCredentialStatus {
  requestId: string;
  state: 'request_created' | 'request_retrieved' | 'issuance_successful' | 'issuance_failed';
  code?: string;
  error?: {
    code: string;
    message: string;
  };
}

export interface VerifiedCredential {
  id: string;
  type: string[];
  issuer: string;
  issuanceDate: string;
  expirationDate?: string;
  credentialSubject: Record<string, unknown>;
  proof: {
    type: string;
    created: string;
    proofPurpose: string;
    verificationMethod: string;
    jws: string;
  };
}

/**
 * Microsoft Entra VerifiedID client
 */
export class EntraVerifiedIDClient {
  private accessToken: string | null = null;
  private tokenExpiry: number = 0;
  private baseUrl: string;

  constructor(private config: EntraVerifiedIDConfig) {
    this.baseUrl = `https://verifiedid.did.msidentity.com/v1.0/${config.tenantId}`;
  }

  /**
   * Get access token for Microsoft Entra VerifiedID API
   */
  private async getAccessToken(): Promise<string> {
    // Check if we have a valid cached token
    if (this.accessToken && Date.now() < this.tokenExpiry) {
      return this.accessToken;
    }

    const tokenUrl = `https://login.microsoftonline.com/${this.config.tenantId}/oauth2/v2.0/token`;
    
    const params = new URLSearchParams({
      client_id: this.config.clientId,
      client_secret: this.config.clientSecret,
      scope: 'https://verifiedid.did.msidentity.com/.default',
      grant_type: 'client_credentials',
    });

    const response = await fetch(tokenUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params.toString(),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Failed to get access token: ${response.status} ${errorText}`);
    }

    const tokenData = (await response.json()) as {
      access_token: string;
      expires_in: number;
    };

    this.accessToken = tokenData.access_token;
    // Set expiry 5 minutes before actual expiry for safety
    const expiresIn = typeof tokenData.expires_in === 'number' ? tokenData.expires_in : 3600;
    this.tokenExpiry = Date.now() + (expiresIn - 300) * 1000;

    return this.accessToken;
  }

  /**
   * Validate credential request
   */
  private validateCredentialRequest(request: VerifiableCredentialRequest): void {
    if (!request.claims || Object.keys(request.claims).length === 0) {
      throw new Error('At least one claim is required');
    }

    // Validate claim keys
    for (const key of Object.keys(request.claims)) {
      if (!key || key.trim().length === 0) {
        throw new Error('Claim keys cannot be empty');
      }
      if (key.length > 100) {
        throw new Error(`Claim key "${key}" exceeds maximum length of 100 characters`);
      }
    }

    // Validate PIN if provided
    if (request.pin) {
      if (request.pin.length < 4 || request.pin.length > 8) {
        throw new Error('PIN must be between 4 and 8 characters');
      }
      if (!/^\d+$/.test(request.pin)) {
        throw new Error('PIN must contain only digits');
      }
    }

    // Validate callback URL if provided
    if (request.callbackUrl) {
      try {
        new URL(request.callbackUrl);
      } catch {
        throw new Error('Invalid callback URL format');
      }
    }
  }

  /**
   * Issue a verifiable credential
   */
  async issueCredential(
    request: VerifiableCredentialRequest
  ): Promise<VerifiableCredentialResponse> {
    // Validate request
    this.validateCredentialRequest(request);

    const token = await this.getAccessToken();
    const manifestId = this.config.credentialManifestId;

    if (!manifestId) {
      throw new Error('Credential manifest ID is required for issuance');
    }

    const issueUrl = `${this.baseUrl}/verifiableCredentials/createIssuanceRequest`;

    // Convert claims to string format (Entra VerifiedID requires string values)
    const stringClaims: Record<string, string> = {};
    for (const [key, value] of Object.entries(request.claims)) {
      if (value === null) {
        stringClaims[key] = '';
      } else if (typeof value === 'boolean') {
        stringClaims[key] = value.toString();
      } else if (typeof value === 'number') {
        stringClaims[key] = value.toString();
      } else {
        stringClaims[key] = value;
      }
    }

    const requestBody: Record<string, unknown> = {
      includeQRCode: true,
      callback: request.callbackUrl
        ? {
            url: request.callbackUrl,
            state: crypto.randomUUID(),
          }
        : undefined,
      authority: `did:web:${this.config.tenantId}.verifiedid.msidentity.com`,
      registration: {
        clientName: 'The Order',
      },
      type: manifestId,
      manifestId,
      pin: request.pin
        ? {
            value: request.pin,
            length: request.pin.length,
          }
        : undefined,
      claims: stringClaims,
    };

    // Add display properties if configured
    if (this.config.logoUri || this.config.backgroundColor || this.config.textColor) {
      requestBody.display = {
        ...(this.config.logoUri && { logo: { uri: this.config.logoUri } }),
        ...(this.config.backgroundColor && { backgroundColor: this.config.backgroundColor }),
        ...(this.config.textColor && { textColor: this.config.textColor }),
      };
    }

    const response = await fetch(issueUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(requestBody),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Failed to issue credential: ${response.status} ${errorText}`);
    }

    const data = (await response.json()) as {
      requestId: string;
      url: string;
      expiry: number;
      qrCode?: string;
    };

    return {
      requestId: data.requestId,
      url: data.url,
      expiry: data.expiry,
      qrCode: data.qrCode,
    };
  }

  /**
   * Check issuance status
   */
  async getIssuanceStatus(requestId: string): Promise<VerifiableCredentialStatus> {
    const token = await this.getAccessToken();
    const statusUrl = `${this.baseUrl}/verifiableCredentials/issuanceRequests/${requestId}`;

    const response = await fetch(statusUrl, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Failed to get issuance status: ${response.status} ${errorText}`);
    }

    return (await response.json()) as VerifiableCredentialStatus;
  }

  /**
   * Validate credential structure
   */
  private validateCredential(credential: VerifiedCredential): void {
    if (!credential.id) {
      throw new Error('Credential ID is required');
    }

    if (!credential.type || !Array.isArray(credential.type) || credential.type.length === 0) {
      throw new Error('Credential type is required and must be an array');
    }

    if (!credential.issuer) {
      throw new Error('Credential issuer is required');
    }

    if (!credential.issuanceDate) {
      throw new Error('Credential issuance date is required');
    }

    if (!credential.credentialSubject || typeof credential.credentialSubject !== 'object') {
      throw new Error('Credential subject is required');
    }

    if (!credential.proof) {
      throw new Error('Credential proof is required');
    }

    // Validate proof structure
    if (!credential.proof.type || !credential.proof.jws) {
      throw new Error('Credential proof must include type and jws');
    }
  }

  /**
   * Verify a verifiable credential
   */
  async verifyCredential(credential: VerifiedCredential): Promise<boolean> {
    // Validate credential structure
    this.validateCredential(credential);

    const token = await this.getAccessToken();
    const verifyUrl = `${this.baseUrl}/verifiableCredentials/verify`;

    try {
      const response = await fetch(verifyUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({
          verifiableCredential: credential,
        }),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Verification failed: ${response.status} ${errorText}`);
      }

      const result = (await response.json()) as { verified: boolean };
      return result.verified ?? false;
    } catch (error) {
      if (error instanceof Error && error.message.includes('Verification failed')) {
        throw error;
      }
      throw new Error(`Failed to verify credential: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Create a presentation request for credential verification
   */
  async createPresentationRequest(
    manifestId: string,
    callbackUrl?: string
  ): Promise<VerifiableCredentialResponse> {
    const token = await this.getAccessToken();
    const requestUrl = `${this.baseUrl}/verifiableCredentials/createPresentationRequest`;

    const requestBody = {
      includeQRCode: true,
      callback: callbackUrl
        ? {
            url: callbackUrl,
            state: crypto.randomUUID(),
          }
        : undefined,
      authority: `did:web:${this.config.tenantId}.verifiedid.msidentity.com`,
      registration: {
        clientName: 'The Order',
      },
      requestedCredentials: [
        {
          type: manifestId,
          manifestId,
          acceptedIssuers: [`did:web:${this.config.tenantId}.verifiedid.msidentity.com`],
        },
      ],
    };

    const response = await fetch(requestUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(requestBody),
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Failed to create presentation request: ${response.status} ${errorText}`);
    }

    const data = (await response.json()) as {
      requestId: string;
      url: string;
      expiry: number;
      qrCode?: string;
    };

    return {
      requestId: data.requestId,
      url: data.url,
      expiry: data.expiry,
      qrCode: data.qrCode,
    };
  }
}

