/**
 * Federation OIDC Service
 *
 * Handles federated authentication using OIDC/OAuth2.
 */

import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaService } from "../prisma/prisma.service";
import type { FederatedIdentity, FederatedTokenValidation } from "./types/oidc.types";
import type { Prisma } from "@prisma/client";
import * as jose from "jose";

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService
    // FederationService will be added in future implementation
    // for fetching remote instance OIDC configuration
  ) {}

  /**
   * Link a local user to a remote federated identity
   */
  async linkFederatedIdentity(
    localUserId: string,
    remoteUserId: string,
    remoteInstanceId: string,
    oidcSubject: string,
    email: string,
    metadata: Record<string, unknown> = {}
  ): Promise<FederatedIdentity> {
    this.logger.log(
      `Linking federated identity: ${localUserId} -> ${remoteUserId}@${remoteInstanceId}`
    );

    const identity = await this.prisma.federatedIdentity.create({
      data: {
        localUserId,
        remoteUserId,
        remoteInstanceId,
        oidcSubject,
        email,
        metadata: metadata as Prisma.InputJsonValue,
      },
    });

    return this.mapToFederatedIdentity(identity);
  }

  /**
   * Get federated identity for a user and remote instance
   */
  async getFederatedIdentity(
    localUserId: string,
    remoteInstanceId: string
  ): Promise<FederatedIdentity | null> {
    const identity = await this.prisma.federatedIdentity.findUnique({
      where: {
        localUserId_remoteInstanceId: {
          localUserId,
          remoteInstanceId,
        },
      },
    });

    return identity ? this.mapToFederatedIdentity(identity) : null;
  }

  /**
   * Get all federated identities for a user
   */
  async getUserFederatedIdentities(localUserId: string): Promise<FederatedIdentity[]> {
    const identities = await this.prisma.federatedIdentity.findMany({
      where: { localUserId },
      orderBy: { createdAt: "desc" },
    });

    return identities.map((identity) => this.mapToFederatedIdentity(identity));
  }

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

    await this.prisma.federatedIdentity.delete({
      where: {
        localUserId_remoteInstanceId: {
          localUserId,
          remoteInstanceId,
        },
      },
    });
  }

  /**
   * Validate an OIDC token from a federated instance
   *
   * Verifies JWT signature and validates all standard claims.
   *
   * Current implementation uses a test secret for validation.
   * Production implementation should:
   * 1. Fetch OIDC discovery metadata from the issuer
   * 2. Retrieve and cache JWKS (JSON Web Key Set)
   * 3. Verify JWT signature using the public key from JWKS
   * 4. Handle key rotation and JWKS refresh
   */
  async validateToken(token: string, instanceId: string): Promise<FederatedTokenValidation> {
    try {
      // Validate token format
      if (!token || typeof token !== "string") {
        return {
          valid: false,
          error: "Malformed token: token must be a non-empty string",
        };
      }

      // Check if token looks like a JWT (three parts separated by dots)
      const parts = token.split(".");
      if (parts.length !== 3) {
        return {
          valid: false,
          error: "Malformed token: JWT must have three parts (header.payload.signature)",
        };
      }

      // Get OIDC configuration from environment variables
      // These must be configured for federation token validation to work
      const issuer = this.config.get<string>("OIDC_ISSUER");
      const clientId = this.config.get<string>("OIDC_CLIENT_ID");

      // Fail fast if OIDC configuration is missing
      if (!issuer || issuer.trim() === "") {
        this.logger.error(
          "Federation OIDC validation failed: OIDC_ISSUER environment variable is not configured"
        );
        return {
          valid: false,
          error:
            "Federation OIDC configuration error: OIDC_ISSUER is required for token validation",
        };
      }

      if (!clientId || clientId.trim() === "") {
        this.logger.error(
          "Federation OIDC validation failed: OIDC_CLIENT_ID environment variable is not configured"
        );
        return {
          valid: false,
          error:
            "Federation OIDC configuration error: OIDC_CLIENT_ID is required for token validation",
        };
      }

      // Get validation secret from config (for testing/development)
      // In production, this should fetch JWKS from the remote instance
      const secret =
        this.config.get<string>("OIDC_VALIDATION_SECRET") ?? "test-secret-key-for-jwt-signing";
      const secretKey = new TextEncoder().encode(secret);

      // Remove trailing slash from issuer for JWT validation (jose expects issuer without trailing slash)
      const normalizedIssuer = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;

      // Verify and decode JWT
      const { payload } = await jose.jwtVerify(token, secretKey, {
        issuer: normalizedIssuer,
        audience: clientId,
      });

      // Extract claims
      const sub = payload.sub;
      const email = payload.email as string | undefined;

      if (!sub) {
        return {
          valid: false,
          error: "Token missing required 'sub' claim",
        };
      }

      // Return validation result
      const result: FederatedTokenValidation = {
        valid: true,
        userId: sub,
        subject: sub,
        instanceId,
      };

      // Only include email if present (exactOptionalPropertyTypes compliance)
      if (email) {
        result.email = email;
      }

      return result;
    } catch (error) {
      // Handle specific JWT errors
      if (error instanceof jose.errors.JWTExpired) {
        return {
          valid: false,
          error: "Token has expired",
        };
      }

      if (error instanceof jose.errors.JWTClaimValidationFailed) {
        const claimError = error.message;
        // Check specific claim failures
        if (claimError.includes("iss") || claimError.includes("issuer")) {
          return {
            valid: false,
            error: "Invalid token issuer",
          };
        }
        if (claimError.includes("aud") || claimError.includes("audience")) {
          return {
            valid: false,
            error: "Invalid token audience",
          };
        }
        return {
          valid: false,
          error: `Claim validation failed: ${claimError}`,
        };
      }

      if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
        return {
          valid: false,
          error: "Invalid token signature",
        };
      }

      // Generic error handling
      this.logger.error(
        `Token validation error: ${error instanceof Error ? error.message : "Unknown error"}`,
        error instanceof Error ? error.stack : undefined
      );

      return {
        valid: false,
        error: error instanceof Error ? error.message : "Token validation failed",
      };
    }
  }

  /**
   * Generate authorization URL for federated authentication
   *
   * Creates an OAuth2 authorization URL to redirect the user to
   * the remote instance's OIDC provider.
   */
  generateAuthUrl(remoteInstanceId: string, redirectUrl?: string): string {
    // This would fetch the remote instance's OIDC configuration
    // and generate the authorization URL

    // For now, return a placeholder
    // Real implementation would:
    // 1. Fetch remote instance metadata
    // 2. Get OIDC discovery endpoint
    // 3. Build authorization URL with proper params
    // 4. Include state for CSRF protection
    // 5. Include PKCE parameters

    const baseUrl = this.config.get<string>("INSTANCE_URL") ?? "http://localhost:3001";
    const callbackUrl = redirectUrl ?? `${baseUrl}/api/v1/federation/auth/callback`;

    this.logger.log(`Generating auth URL for instance ${remoteInstanceId}`);

    // Placeholder - real implementation would fetch actual OIDC config
    return `https://auth.example.com/authorize?client_id=placeholder&redirect_uri=${encodeURIComponent(callbackUrl)}&response_type=code&scope=openid+profile+email&state=${remoteInstanceId}`;
  }

  /**
   * Map Prisma FederatedIdentity to type
   */
  private mapToFederatedIdentity(identity: {
    id: string;
    localUserId: string;
    remoteUserId: string;
    remoteInstanceId: string;
    oidcSubject: string;
    email: string;
    metadata: unknown;
    createdAt: Date;
    updatedAt: Date;
  }): FederatedIdentity {
    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,
    };
  }
}
