/**
 * Interview Token Service
 *
 * Enterprise implementation of JWT-based token generation and validation
 * for interview scheduling links with 72-hour expiry.
 *
 * Features:
 * - Secure JWT token generation with HS256
 * - Database-backed token tracking
 * - Comprehensive validation (expiry, usage, revocation)
 * - Rate limiting support
 * - Audit logging integration
 *
 * @module lib/services/interview-tokens.service
 */

import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { prisma } from '@/lib/db';
import { interviewBookingTokensAdapter } from '@/lib/adapters/careers';
import {
  validateToken,
  validateEmailMatch,
  validateRateLimit,
} from '@/lib/validators/interview-token.validator';
import type {
  IInterviewTokenService,
  TokenGenerationConfig,
  TokenGenerationResult,
  TokenValidationResult,
  TokenDetailsResult,
  InterviewTokenPayload,
  InterviewTokenRecord,
} from '@/lib/types/interview-token.types';
import { TOKEN_CONFIG } from '@/lib/types/interview-token.types';

/**
 * Generate a UUID v4
 */
function generateUUID(): string {
  return crypto.randomUUID();
}

/**
 * Get JWT secret from environment
 *
 * @throws Error if secret not configured
 */
function getTokenSecret(): string {
  const secret = process.env.INTERVIEW_TOKEN_SECRET;

  if (!secret) {
    throw new Error(
      `${TOKEN_CONFIG.SECRET_ENV_VAR} environment variable is not set`
    );
  }

  if (secret.length < 32) {
    throw new Error(
      `${TOKEN_CONFIG.SECRET_ENV_VAR} must be at least 32 characters long`
    );
  }

  return secret;
}

/**
 * Interview Token Service Implementation
 *
 * @class InterviewTokenService
 * @implements {IInterviewTokenService}
 */
class InterviewTokenService implements IInterviewTokenService {
  /**
   * Generate a new interview scheduling token
   *
   * Creates a JWT token with 72-hour expiry and stores metadata in database.
   * Includes rate limiting to prevent abuse.
   *
   * @param config - Token generation configuration
   * @returns Promise resolving to generation result
   *
   * @example
   * ```typescript
   * const result = await tokenService.generateInterviewToken({
   *   applicationId: '123e4567-e89b-12d3-a456-426614174000',
   *   jobId: '123e4567-e89b-12d3-a456-426614174001',
   *   applicantEmail: 'applicant@example.com'
   * });
   *
   * if (result.success) {
   *   console.log('Token:', result.token);
   *   console.log('Expires:', result.expiresAt);
   * }
   * ```
   */
  async generateInterviewToken(
    config: TokenGenerationConfig
  ): Promise<TokenGenerationResult> {
    try {
      // Validate input
      if (!config.applicationId || !config.jobId || !config.applicantEmail) {
        return {
          success: false,
          error: 'Missing required fields: applicationId, jobId, or applicantEmail',
        };
      }

      // Check rate limit: count tokens generated in last hour for this application
      const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
      const recentTokens = await interviewBookingTokensAdapter.count({
        where: {
          applicationId: config.applicationId,
          createdAt: {
            gte: oneHourAgo,
          },
        },
      });

      const rateLimitCheck = validateRateLimit(
        recentTokens,
        TOKEN_CONFIG.RATE_LIMIT_PER_HOUR
      );

      if (!rateLimitCheck.valid) {
        return {
          success: false,
          error: rateLimitCheck.error,
        };
      }

      // Generate unique token ID (jti)
      const tokenId = generateUUID();

      // Calculate expiry
      const expiryHours = config.expiryHours || TOKEN_CONFIG.EXPIRY_HOURS;
      const expirySeconds = expiryHours * 60 * 60;
      const now = Math.floor(Date.now() / 1000);
      const exp = now + expirySeconds;
      const expiresAt = new Date(exp * 1000);

      // Create JWT payload
      const payload: InterviewTokenPayload = {
        applicationId: config.applicationId,
        jobId: config.jobId,
        applicantEmail: config.applicantEmail,
        iat: now,
        exp,
        jti: tokenId,
      };

      // Sign JWT token
      const secret = getTokenSecret();
      const token = jwt.sign(payload, secret, {
        algorithm: TOKEN_CONFIG.ALGORITHM,
      });

      // Store token metadata in database
      await interviewBookingTokensAdapter.create({
        data: {
          id: tokenId,
          token,
          applicationId: config.applicationId,
          jobId: config.jobId,
          applicantEmail: config.applicantEmail,
          expiresAt,
          isRevoked: false,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      });

      return {
        success: true,
        token,
        tokenId,
        expiresAt,
      };
    } catch (error) {
      console.error('[InterviewTokenService] Token generation error:', error);

      // Check for specific database errors
      if (error instanceof Error) {
        if (error.message.includes('Unique constraint')) {
          return {
            success: false,
            error: 'Token generation failed due to duplicate. Please try again.',
          };
        }

        if (error.message.includes('Foreign key constraint')) {
          return {
            success: false,
            error: 'Invalid application or job ID',
          };
        }
      }

      return {
        success: false,
        error: 'Failed to generate interview token. Please try again.',
      };
    }
  }

  /**
   * Validate an interview token
   *
   * Performs comprehensive validation including:
   * - JWT signature verification
   * - Expiry check
   * - Database record lookup
   * - Usage status check
   * - Revocation status check
   *
   * @param token - JWT token string
   * @returns Promise resolving to validation result
   *
   * @example
   * ```typescript
   * const result = await tokenService.validateInterviewToken(token);
   *
   * if (result.valid) {
   *   console.log('Application ID:', result.payload.applicationId);
   *   console.log('Job ID:', result.payload.jobId);
   * } else {
   *   console.error('Invalid token:', result.error);
   * }
   * ```
   */
  async validateInterviewToken(token: string): Promise<TokenValidationResult> {
    try {
      // Verify and decode JWT
      const secret = getTokenSecret();
      let payload: InterviewTokenPayload;

      try {
        payload = jwt.verify(token, secret, {
          algorithms: [TOKEN_CONFIG.ALGORITHM],
        }) as InterviewTokenPayload;
      } catch (jwtError) {
        if (jwtError instanceof jwt.TokenExpiredError) {
          return {
            valid: false,
            error: 'Token has expired',
            errorCode: 'EXPIRED',
          };
        }

        if (jwtError instanceof jwt.JsonWebTokenError) {
          return {
            valid: false,
            error: 'Invalid token signature',
            errorCode: 'INVALID',
          };
        }

        throw jwtError;
      }

      // Lookup token in database
      const tokenRecord = await interviewBookingTokensAdapter.findUnique({
        where: { id: payload.jti },
      });

      if (!tokenRecord) {
        return {
          valid: false,
          error: 'Token not found in database',
          errorCode: 'NOT_FOUND',
        };
      }

      // Verify token string matches
      if (tokenRecord.token !== token) {
        return {
          valid: false,
          error: 'Token mismatch',
          errorCode: 'INVALID',
        };
      }

      // Validate using comprehensive validator
      const validationResult = validateToken({
        token,
        payload,
        tokenRecord: tokenRecord as InterviewTokenRecord,
      });

      if (!validationResult.valid) {
        return validationResult;
      }

      // Verify email matches application record
      const application = await prisma.job_applications.findUnique({
        where: { id: payload.applicationId },
        select: { applicantEmail: true },
      });

      if (!application) {
        return {
          valid: false,
          error: 'Application not found',
          errorCode: 'INVALID',
        };
      }

      const emailMatch = validateEmailMatch(
        payload.applicantEmail,
        application.applicantEmail
      );

      if (!emailMatch.valid) {
        return {
          valid: false,
          error: emailMatch.error,
          errorCode: 'INVALID',
        };
      }

      return {
        valid: true,
        payload,
        tokenRecord: tokenRecord as InterviewTokenRecord,
      };
    } catch (error) {
      console.error('[InterviewTokenService] Token validation error:', error);

      return {
        valid: false,
        error: 'Failed to validate token. Please try again.',
        errorCode: 'INVALID',
      };
    }
  }

  /**
   * Mark token as used after successful booking
   *
   * Prevents token reuse by setting usedAt timestamp.
   *
   * @param tokenId - Token ID (jti)
   * @returns Promise resolving to success status
   *
   * @example
   * ```typescript
   * const success = await tokenService.markTokenAsUsed(tokenId);
   * if (success) {
   *   console.log('Token marked as used');
   * }
   * ```
   */
  async markTokenAsUsed(tokenId: string): Promise<boolean> {
    try {
      await interviewBookingTokensAdapter.update({
        where: { id: tokenId },
        data: {
          usedAt: new Date(),
          updatedAt: new Date(),
        },
      });

      return true;
    } catch (error) {
      console.error('[InterviewTokenService] Mark token used error:', error);
      return false;
    }
  }

  /**
   * Revoke a token (for staff cancellation)
   *
   * Marks token as revoked, preventing further use.
   *
   * @param tokenId - Token ID (jti)
   * @returns Promise resolving to success status
   *
   * @example
   * ```typescript
   * const success = await tokenService.revokeToken(tokenId);
   * if (success) {
   *   console.log('Token revoked');
   * }
   * ```
   */
  async revokeToken(tokenId: string): Promise<boolean> {
    try {
      await interviewBookingTokensAdapter.update({
        where: { id: tokenId },
        data: {
          isRevoked: true,
          updatedAt: new Date(),
        },
      });

      return true;
    } catch (error) {
      console.error('[InterviewTokenService] Revoke token error:', error);
      return false;
    }
  }

  /**
   * Extract and validate token payload
   *
   * Decodes token without full validation, useful for displaying
   * token details or debugging.
   *
   * @param token - JWT token string
   * @returns Promise resolving to token details
   *
   * @example
   * ```typescript
   * const details = await tokenService.getTokenDetails(token);
   * if (details.success) {
   *   console.log('Application:', details.applicationId);
   *   console.log('Expires:', details.expiresAt);
   * }
   * ```
   */
  async getTokenDetails(token: string): Promise<TokenDetailsResult> {
    try {
      // Decode without verification for details extraction
      const decoded = jwt.decode(token) as InterviewTokenPayload | null;

      if (!decoded) {
        return {
          success: false,
          error: 'Failed to decode token',
        };
      }

      return {
        success: true,
        applicationId: decoded.applicationId,
        jobId: decoded.jobId,
        applicantEmail: decoded.applicantEmail,
        tokenId: decoded.jti,
        expiresAt: new Date(decoded.exp * 1000),
        issuedAt: new Date(decoded.iat * 1000),
      };
    } catch (error) {
      console.error('[InterviewTokenService] Get token details error:', error);

      return {
        success: false,
        error: 'Failed to extract token details',
      };
    }
  }
}

// Export singleton instance
export const interviewTokenService = new InterviewTokenService();

// Export class for testing
export { InterviewTokenService };
