import {
  Injectable,
  BadRequestException,
  UnauthorizedException,
  NotFoundException,
  Inject,
  forwardRef,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
  type VerifiedRegistrationResponse,
  type VerifiedAuthenticationResponse,
  type PublicKeyCredentialCreationOptionsJSON,
  type PublicKeyCredentialRequestOptionsJSON,
} from '@simplewebauthn/server';
import { WebAuthnCredential } from './entities/webauthn-credential.entity';
import { User } from '../../database/entities/user.entity';
import { AuthService } from './auth.service';

interface BiometricRegistrationOptions {
  userId: string;
  friendlyName?: string;
}

interface BiometricAuthenticationOptions {
  email?: string;
}

@Injectable()
export class BiometricAuthService {
  // Replace with your actual domain in production
  private readonly rpName = 'Maternal Care App';
  private readonly rpID = process.env.RP_ID || 'localhost';
  private readonly origin = process.env.ORIGIN || 'http://localhost:3000';

  // Store challenges temporarily (use Redis in production)
  private challenges = new Map<string, string>();

  constructor(
    @InjectRepository(WebAuthnCredential)
    private webauthnCredentialRepository: Repository<WebAuthnCredential>,
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}

  /**
   * Generate registration options for a new biometric credential
   */
  async generateRegistrationOptions(
    options: BiometricRegistrationOptions,
  ): Promise<PublicKeyCredentialCreationOptionsJSON> {
    const { userId, friendlyName } = options;

    const user = await this.userRepository.findOne({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('User not found');
    }

    // Get existing credentials to exclude
    const existingCredentials = await this.webauthnCredentialRepository.find({
      where: { userId },
    });

    const opts = await generateRegistrationOptions({
      rpName: this.rpName,
      rpID: this.rpID,
      userName: user.email,
      userDisplayName: user.name,
      attestationType: 'none',
      excludeCredentials: existingCredentials.map((cred) => ({
        id: cred.credentialId,
        transports: cred.transports as any[],
      })),
      authenticatorSelection: {
        residentKey: 'preferred',
        userVerification: 'preferred',
        authenticatorAttachment: 'platform', // Prefer built-in authenticators (Face ID, Touch ID)
      },
    });

    // Store challenge for verification
    this.challenges.set(userId, opts.challenge);

    return opts;
  }

  /**
   * Verify registration response and save credential
   */
  async verifyRegistrationResponse(
    userId: string,
    response: any,
    friendlyName?: string,
  ): Promise<{ success: boolean; credentialId: string; message: string }> {
    const expectedChallenge = this.challenges.get(userId);
    if (!expectedChallenge) {
      throw new BadRequestException('Challenge not found or expired');
    }

    let verification: VerifiedRegistrationResponse;
    try {
      verification = await verifyRegistrationResponse({
        response,
        expectedChallenge,
        expectedOrigin: this.origin,
        expectedRPID: this.rpID,
      });
    } catch (error) {
      throw new BadRequestException(`Verification failed: ${error.message}`);
    }

    if (!verification.verified || !verification.registrationInfo) {
      throw new BadRequestException('Registration verification failed');
    }

    const { credential, aaguid, credentialDeviceType, credentialBackedUp } =
      verification.registrationInfo;

    // Save credential to database
    const credentialEntity = this.webauthnCredentialRepository.create({
      userId,
      credentialId: credential.id,
      publicKey: Buffer.from(credential.publicKey).toString('base64url'),
      counter: credential.counter,
      deviceType: credentialDeviceType,
      transports: credential.transports,
      backedUp: credentialBackedUp,
      authenticatorAttachment: response.authenticatorAttachment,
      friendlyName:
        friendlyName || this.generateDefaultName(credentialDeviceType),
    });

    await this.webauthnCredentialRepository.save(credentialEntity);

    // Clear challenge
    this.challenges.delete(userId);

    return {
      success: true,
      credentialId: credentialEntity.id,
      message: 'Biometric credential registered successfully',
    };
  }

  /**
   * Generate authentication options for login
   */
  async generateAuthenticationOptions(
    options?: BiometricAuthenticationOptions,
  ): Promise<PublicKeyCredentialRequestOptionsJSON> {
    let allowCredentials: Array<{ id: string; transports?: any[] }> = [];

    // If email provided, get user's credentials
    if (options?.email) {
      const user = await this.userRepository.findOne({
        where: { email: options.email },
      });
      if (user) {
        const credentials = await this.webauthnCredentialRepository.find({
          where: { userId: user.id },
        });

        allowCredentials = credentials.map((cred) => ({
          id: cred.credentialId,
          transports: cred.transports,
        }));
      }
    }

    const opts = await generateAuthenticationOptions({
      rpID: this.rpID,
      allowCredentials:
        allowCredentials.length > 0 ? allowCredentials : undefined,
      userVerification: 'preferred',
    });

    // Store challenge (use credential ID or email as key)
    const challengeKey = options?.email || 'anonymous';
    this.challenges.set(challengeKey, opts.challenge);

    return opts;
  }

  /**
   * Verify authentication response
   */
  async verifyAuthenticationResponse(
    response: any,
    email?: string,
  ): Promise<{ success: boolean; user: User; message: string }> {
    const credentialId = response.id;

    // Find credential
    const credential = await this.webauthnCredentialRepository.findOne({
      where: { credentialId },
      relations: ['user'],
    });

    if (!credential) {
      throw new UnauthorizedException('Credential not found');
    }

    const challengeKey = email || 'anonymous';
    const expectedChallenge = this.challenges.get(challengeKey);
    if (!expectedChallenge) {
      throw new BadRequestException('Challenge not found or expired');
    }

    let verification: VerifiedAuthenticationResponse;
    try {
      verification = await verifyAuthenticationResponse({
        response,
        expectedChallenge,
        expectedOrigin: this.origin,
        expectedRPID: this.rpID,
        credential: {
          id: credential.credentialId,
          publicKey: Uint8Array.from(
            Buffer.from(credential.publicKey, 'base64url'),
          ),
          counter: Number(credential.counter),
          transports: credential.transports as any[],
        },
      });
    } catch (error) {
      throw new UnauthorizedException(
        `Authentication failed: ${error.message}`,
      );
    }

    if (!verification.verified) {
      throw new UnauthorizedException('Authentication verification failed');
    }

    // Update counter and last used
    credential.counter = verification.authenticationInfo.newCounter;
    credential.lastUsed = new Date();
    await this.webauthnCredentialRepository.save(credential);

    // Clear challenge
    this.challenges.delete(challengeKey);

    return {
      success: true,
      user: credential.user,
      message: 'Authentication successful',
    };
  }

  /**
   * Get all credentials for a user
   */
  async getUserCredentials(userId: string): Promise<WebAuthnCredential[]> {
    return this.webauthnCredentialRepository.find({
      where: { userId },
      order: { createdAt: 'DESC' },
    });
  }

  /**
   * Delete a credential
   */
  async deleteCredential(
    userId: string,
    credentialId: string,
  ): Promise<{ success: boolean; message: string }> {
    const credential = await this.webauthnCredentialRepository.findOne({
      where: { id: credentialId, userId },
    });

    if (!credential) {
      throw new NotFoundException('Credential not found');
    }

    await this.webauthnCredentialRepository.remove(credential);

    return {
      success: true,
      message: 'Credential deleted successfully',
    };
  }

  /**
   * Update credential friendly name
   */
  async updateCredentialName(
    userId: string,
    credentialId: string,
    friendlyName: string,
  ): Promise<{ success: boolean; message: string }> {
    const credential = await this.webauthnCredentialRepository.findOne({
      where: { id: credentialId, userId },
    });

    if (!credential) {
      throw new NotFoundException('Credential not found');
    }

    credential.friendlyName = friendlyName;
    await this.webauthnCredentialRepository.save(credential);

    return {
      success: true,
      message: 'Credential name updated successfully',
    };
  }

  /**
   * Check if user has biometric credentials
   */
  async hasCredentials(userId: string): Promise<boolean> {
    const count = await this.webauthnCredentialRepository.count({
      where: { userId },
    });
    return count > 0;
  }

  /**
   * Authenticate with biometric and return full auth response with tokens
   */
  async authenticateWithBiometric(
    response: any,
    deviceInfo: { deviceId: string; platform: string },
    email?: string,
  ): Promise<any> {
    // Verify biometric authentication
    const verifyResult = await this.verifyAuthenticationResponse(
      response,
      email,
    );

    // Use AuthService to complete login (register device, generate tokens)
    return await this.authService.loginWithExternalAuth(
      verifyResult.user,
      deviceInfo,
    );
  }

  /**
   * Generate default credential name based on device type
   */
  private generateDefaultName(deviceType?: string): string {
    const timestamp = new Date().toLocaleDateString();
    if (deviceType === 'singleDevice') {
      return `Device - ${timestamp}`;
    } else if (deviceType === 'multiDevice') {
      return `Synced Device - ${timestamp}`;
    }
    return `Biometric Device - ${timestamp}`;
  }
}
