/**
 * Identity Linking Service
 *
 * Handles cross-instance user identity verification and mapping.
 */

import { Injectable, Logger, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import { OIDCService } from "./oidc.service";
import { SignatureService } from "./signature.service";
import { FederationAuditService } from "./audit.service";
import type {
  IdentityVerificationRequest,
  IdentityVerificationResponse,
  CreateIdentityMappingDto,
  UpdateIdentityMappingDto,
  IdentityMappingValidation,
} from "./types/identity-linking.types";
import type { FederatedIdentity } from "./types/oidc.types";

@Injectable()
export class IdentityLinkingService {
  private readonly logger = new Logger(IdentityLinkingService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly oidcService: OIDCService,
    private readonly signatureService: SignatureService,
    private readonly auditService: FederationAuditService
  ) {}

  /**
   * Verify a user's identity from a remote instance
   *
   * Validates:
   * 1. Timestamp is recent (not expired)
   * 2. Signature is valid (signed by remote instance)
   * 3. OIDC token is valid
   * 4. Identity mapping exists
   */
  async verifyIdentity(
    request: IdentityVerificationRequest
  ): Promise<IdentityVerificationResponse> {
    this.logger.log(`Verifying identity: ${request.localUserId} from ${request.remoteInstanceId}`);

    // Validate timestamp (prevent replay attacks)
    if (!this.signatureService.validateTimestamp(request.timestamp)) {
      this.logger.warn(`Identity verification failed: Request timestamp expired`);
      return {
        verified: false,
        error: "Request timestamp expired",
      };
    }

    // Verify signature
    const { signature, ...messageToVerify } = request;
    const signatureValidation = await this.signatureService.verifyMessage(
      messageToVerify,
      signature,
      request.remoteInstanceId
    );

    if (!signatureValidation.valid) {
      const errorMessage = signatureValidation.error ?? "Invalid signature";
      this.logger.warn(`Identity verification failed: ${errorMessage}`);
      return {
        verified: false,
        error: errorMessage,
      };
    }

    // Validate OIDC token
    const tokenValidation = await this.oidcService.validateToken(
      request.oidcToken,
      request.remoteInstanceId
    );

    if (!tokenValidation.valid) {
      const tokenError = tokenValidation.error ?? "Invalid OIDC token";
      this.logger.warn(`Identity verification failed: ${tokenError}`);
      return {
        verified: false,
        error: tokenError,
      };
    }

    // Check if identity mapping exists
    const identity = await this.oidcService.getFederatedIdentity(
      request.localUserId,
      request.remoteInstanceId
    );

    if (!identity) {
      this.logger.warn(
        `Identity verification failed: Mapping not found for ${request.localUserId}`
      );
      return {
        verified: false,
        error: "Identity mapping not found",
      };
    }

    // Verify that the remote user ID matches
    if (identity.remoteUserId !== request.remoteUserId) {
      this.logger.warn(
        `Identity verification failed: Remote user ID mismatch (expected ${identity.remoteUserId}, got ${request.remoteUserId})`
      );
      return {
        verified: false,
        error: "Remote user ID mismatch",
      };
    }

    // Log successful verification
    this.auditService.logIdentityVerification(request.localUserId, request.remoteInstanceId, true);

    this.logger.log(`Identity verified successfully: ${request.localUserId}`);

    return {
      verified: true,
      localUserId: identity.localUserId,
      remoteUserId: identity.remoteUserId,
      remoteInstanceId: identity.remoteInstanceId,
      email: identity.email,
    };
  }

  /**
   * Resolve a remote user to a local user
   *
   * Looks up the identity mapping by remote instance and user ID.
   */
  async resolveLocalIdentity(
    remoteInstanceId: string,
    remoteUserId: string
  ): Promise<FederatedIdentity | null> {
    this.logger.debug(`Resolving local identity for ${remoteUserId}@${remoteInstanceId}`);

    // Query by remoteInstanceId and remoteUserId
    // Note: Prisma doesn't have a unique constraint for this pair,
    // so we use findFirst
    const identity = await this.prisma.federatedIdentity.findFirst({
      where: {
        remoteInstanceId,
        remoteUserId,
      },
    });

    if (!identity) {
      this.logger.debug(`No local identity found for ${remoteUserId}@${remoteInstanceId}`);
      return null;
    }

    return {
      id: identity.id,
      localUserId: identity.localUserId,
      remoteUserId: identity.remoteUserId,
      remoteInstanceId: identity.remoteInstanceId,
      oidcSubject: identity.oidcSubject,
      email: identity.email,
      metadata: identity.metadata as Record<string, unknown>,
      createdAt: identity.createdAt,
      updatedAt: identity.updatedAt,
    };
  }

  /**
   * Resolve a local user to a remote identity
   *
   * Looks up the identity mapping by local user ID and remote instance.
   */
  async resolveRemoteIdentity(
    localUserId: string,
    remoteInstanceId: string
  ): Promise<FederatedIdentity | null> {
    this.logger.debug(`Resolving remote identity for ${localUserId}@${remoteInstanceId}`);

    const identity = await this.oidcService.getFederatedIdentity(localUserId, remoteInstanceId);

    if (!identity) {
      this.logger.debug(`No remote identity found for ${localUserId}@${remoteInstanceId}`);
      return null;
    }

    return identity;
  }

  /**
   * Create a new identity mapping
   *
   * Optionally validates OIDC token if provided.
   */
  async createIdentityMapping(
    localUserId: string,
    dto: CreateIdentityMappingDto
  ): Promise<FederatedIdentity> {
    this.logger.log(
      `Creating identity mapping: ${localUserId} -> ${dto.remoteUserId}@${dto.remoteInstanceId}`
    );

    // Validate OIDC token if provided
    if (dto.oidcToken) {
      const tokenValidation = await this.oidcService.validateToken(
        dto.oidcToken,
        dto.remoteInstanceId
      );

      if (!tokenValidation.valid) {
        const validationError = tokenValidation.error ?? "Unknown validation error";
        throw new UnauthorizedException(`Invalid OIDC token: ${validationError}`);
      }
    }

    // Create identity mapping via OIDCService
    const identity = await this.oidcService.linkFederatedIdentity(
      localUserId,
      dto.remoteUserId,
      dto.remoteInstanceId,
      dto.oidcSubject,
      dto.email,
      dto.metadata ?? {}
    );

    // Log identity linking
    this.auditService.logIdentityLinking(localUserId, dto.remoteInstanceId, dto.remoteUserId);

    return identity;
  }

  /**
   * Update an existing identity mapping
   */
  async updateIdentityMapping(
    localUserId: string,
    remoteInstanceId: string,
    dto: UpdateIdentityMappingDto
  ): Promise<FederatedIdentity> {
    this.logger.log(`Updating identity mapping: ${localUserId}@${remoteInstanceId}`);

    // Verify mapping exists
    const existing = await this.prisma.federatedIdentity.findUnique({
      where: {
        localUserId_remoteInstanceId: {
          localUserId,
          remoteInstanceId,
        },
      },
    });

    if (!existing) {
      throw new NotFoundException("Identity mapping not found");
    }

    // Update metadata
    const updated = await this.prisma.federatedIdentity.update({
      where: {
        localUserId_remoteInstanceId: {
          localUserId,
          remoteInstanceId,
        },
      },
      data: {
        metadata: (dto.metadata ?? existing.metadata) as Prisma.InputJsonValue,
      },
    });

    return {
      id: updated.id,
      localUserId: updated.localUserId,
      remoteUserId: updated.remoteUserId,
      remoteInstanceId: updated.remoteInstanceId,
      oidcSubject: updated.oidcSubject,
      email: updated.email,
      metadata: updated.metadata as Record<string, unknown>,
      createdAt: updated.createdAt,
      updatedAt: updated.updatedAt,
    };
  }

  /**
   * Validate an identity mapping exists and is valid
   */
  async validateIdentityMapping(
    localUserId: string,
    remoteInstanceId: string
  ): Promise<IdentityMappingValidation> {
    const identity = await this.oidcService.getFederatedIdentity(localUserId, remoteInstanceId);

    if (!identity) {
      return {
        valid: false,
        error: "Identity mapping not found",
      };
    }

    return {
      valid: true,
      localUserId: identity.localUserId,
      remoteUserId: identity.remoteUserId,
      remoteInstanceId: identity.remoteInstanceId,
    };
  }

  /**
   * List all federated identities for a user
   */
  async listUserIdentities(localUserId: string): Promise<FederatedIdentity[]> {
    return this.oidcService.getUserFederatedIdentities(localUserId);
  }

  /**
   * Revoke an identity mapping
   */
  async revokeIdentityMapping(localUserId: string, remoteInstanceId: string): Promise<void> {
    this.logger.log(`Revoking identity mapping: ${localUserId}@${remoteInstanceId}`);

    await this.oidcService.revokeFederatedIdentity(localUserId, remoteInstanceId);

    // Log revocation
    this.auditService.logIdentityRevocation(localUserId, remoteInstanceId);
  }
}
