import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import config from '../config';

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

export interface JWTPayload {
  userId: string;
  email: string;
  subscription: string;
  type: 'access' | 'refresh';
  sessionId: string;
  iat?: number;
  exp?: number;
}

export interface RefreshTokenData {
  userId: string;
  sessionId: string;
  expiresAt: Date;
  isRevoked: boolean;
}

// In-memory token blacklist (in production, use Redis)
const tokenBlacklist = new Set<string>();
const refreshTokens = new Map<string, RefreshTokenData>();

export class AuthService {
  
  /**
   * Generate access and refresh token pair
   */
  static generateTokenPair(userId: string, email: string, subscription: string): TokenPair {
    const sessionId = crypto.randomUUID();
    
    // Access token (short-lived)
    const accessTokenPayload: Omit<JWTPayload, 'iat' | 'exp'> = {
      userId,
      email,
      subscription,
      type: 'access',
      sessionId
    };
    
    const accessToken = jwt.sign(accessTokenPayload, config.jwtSecret, {
      expiresIn: '15m' // Short-lived access token
    });
    
    // Refresh token (long-lived)
    const refreshTokenPayload: Omit<JWTPayload, 'iat' | 'exp'> = {
      userId,
      email,
      subscription,
      type: 'refresh',
      sessionId
    };
    
    const refreshToken = jwt.sign(refreshTokenPayload, config.jwtSecret, {
      expiresIn: '7d' // Long-lived refresh token
    });
    
    // Store refresh token data
    const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
    refreshTokens.set(refreshToken, {
      userId,
      sessionId,
      expiresAt,
      isRevoked: false
    });
    
    return {
      accessToken,
      refreshToken,
      expiresIn: 15 * 60 // 15 minutes in seconds
    };
  }
  
  /**
   * Verify and decode JWT token
   */
  static verifyToken(token: string): JWTPayload {
    // Check if token is blacklisted
    if (tokenBlacklist.has(token)) {
      throw new jwt.JsonWebTokenError('Token is blacklisted');
    }
    
    return jwt.verify(token, config.jwtSecret) as JWTPayload;
  }
  
  /**
   * Refresh access token using refresh token
   */
  static refreshAccessToken(refreshToken: string): TokenPair {
    try {
      // Verify refresh token
      const payload = this.verifyToken(refreshToken);
      
      if (payload.type !== 'refresh') {
        throw new Error('Invalid token type');
      }
      
      // Check if refresh token exists and is not revoked
      const refreshData = refreshTokens.get(refreshToken);
      if (!refreshData || refreshData.isRevoked) {
        throw new Error('Refresh token is revoked');
      }
      
      if (refreshData.expiresAt < new Date()) {
        throw new Error('Refresh token expired');
      }
      
      // Generate new token pair
      const newTokenPair = this.generateTokenPair(
        payload.userId,
        payload.email,
        payload.subscription
      );
      
      // Revoke old refresh token
      refreshData.isRevoked = true;
      
      return newTokenPair;
    } catch (error) {
      throw new Error('Invalid refresh token');
    }
  }
  
  /**
   * Blacklist a token (logout)
   */
  static blacklistToken(token: string): void {
    tokenBlacklist.add(token);
    
    try {
      const payload = this.verifyToken(token);
      
      // If it's a refresh token, mark as revoked
      if (payload.type === 'refresh') {
        const refreshData = refreshTokens.get(token);
        if (refreshData) {
          refreshData.isRevoked = true;
        }
      }
    } catch (error) {
      // Token might be invalid, but we still blacklist it
    }
  }
  
  /**
   * Revoke all tokens for a user session
   */
  static revokeUserSession(userId: string, sessionId?: string): void {
    // Revoke all refresh tokens for user or specific session
    for (const [token, data] of refreshTokens.entries()) {
      if (data.userId === userId && (!sessionId || data.sessionId === sessionId)) {
        data.isRevoked = true;
      }
    }
  }
  
  /**
   * Revoke all tokens for a user (logout from all devices)
   */
  static revokeAllUserTokens(userId: string): void {
    this.revokeUserSession(userId);
  }
  
  /**
   * Hash password
   */
  static async hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, config.bcryptRounds);
  }
  
  /**
   * Verify password
   */
  static async verifyPassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
  
  /**
   * Generate secure random token
   */
  static generateSecureToken(length: number = 32): string {
    return crypto.randomBytes(length).toString('hex');
  }
  
  /**
   * Clean up expired tokens (should be called periodically)
   */
  static cleanupExpiredTokens(): void {
    const now = new Date();
    
    for (const [token, data] of refreshTokens.entries()) {
      if (data.expiresAt < now || data.isRevoked) {
        refreshTokens.delete(token);
      }
    }
  }
  
  /**
   * Get active sessions for a user
   */
  static getUserActiveSessions(userId: string): Array<{ sessionId: string; expiresAt: Date }> {
    const sessions: Array<{ sessionId: string; expiresAt: Date }> = [];
    
    for (const [token, data] of refreshTokens.entries()) {
      if (data.userId === userId && !data.isRevoked && data.expiresAt > new Date()) {
        sessions.push({
          sessionId: data.sessionId,
          expiresAt: data.expiresAt
        });
      }
    }
    
    return sessions;
  }
  
  /**
   * Validate token format and structure
   */
  static validateTokenFormat(token: string): boolean {
    try {
      const parts = token.split('.');
      return parts.length === 3; // JWT has 3 parts: header.payload.signature
    } catch (error) {
      return false;
    }
  }
  
  /**
   * Extract payload without verification (for debugging)
   */
  static decodeTokenPayload(token: string): any {
    try {
      return jwt.decode(token);
    } catch (error) {
      return null;
    }
  }
}