import jwt, { SignOptions } from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { Repository } from 'typeorm';
import { AppDataSource } from '../../../config/typeorm.js';
import { config } from '../../../config/index.js';
import { User, Session, SessionStatus } from '../entities/index.js';
import { blacklistToken, isTokenBlacklisted } from '../../../config/redis.js';
import { logger } from '../../../shared/utils/logger.js';
import { UnauthorizedError } from '../../../shared/types/index.js';

// ===== Interfaces =====

/**
 * JWT Payload structure for access and refresh tokens
 */
export interface JwtPayload {
  sub: string;      // User ID
  tid: string;      // Tenant ID
  email: string;
  roles: string[];
  jti: string;      // JWT ID único
  iat: number;
  exp: number;
}

/**
 * Token pair returned after authentication
 */
export interface TokenPair {
  accessToken: string;
  refreshToken: string;
  accessTokenExpiresAt: Date;
  refreshTokenExpiresAt: Date;
  sessionId: string;
}

/**
 * Request metadata for session tracking
 */
export interface RequestMetadata {
  ipAddress: string;
  userAgent: string;
}

// ===== TokenService Class =====

/**
 * Service for managing JWT tokens with blacklist support via Redis
 * and session tracking via TypeORM
 */
class TokenService {
  private sessionRepository: Repository<Session>;

  // Configuration constants
  private readonly ACCESS_TOKEN_EXPIRY = '15m';
  private readonly REFRESH_TOKEN_EXPIRY = '7d';
  private readonly ALGORITHM = 'HS256' as const;

  constructor() {
    this.sessionRepository = AppDataSource.getRepository(Session);
  }

  /**
   * Generates a new token pair (access + refresh) and creates a session
   * @param user - User entity with roles loaded
   * @param metadata - Request metadata (IP, user agent)
   * @returns Promise<TokenPair> - Access and refresh tokens with expiration dates
   */
  async generateTokenPair(user: User, metadata: RequestMetadata): Promise<TokenPair> {
    try {
      logger.debug('Generating token pair', { userId: user.id, tenantId: user.tenantId });

      // Extract role codes from user roles
      const roles = user.roles ? user.roles.map(role => role.code) : [];

      // Calculate expiration dates
      const accessTokenExpiresAt = this.calculateExpiration(this.ACCESS_TOKEN_EXPIRY);
      const refreshTokenExpiresAt = this.calculateExpiration(this.REFRESH_TOKEN_EXPIRY);

      // Generate unique JWT IDs
      const accessJti = this.generateJti();
      const refreshJti = this.generateJti();

      // Generate access token
      const accessToken = this.generateToken({
        sub: user.id,
        tid: user.tenantId,
        email: user.email,
        roles,
        jti: accessJti,
      }, this.ACCESS_TOKEN_EXPIRY);

      // Generate refresh token
      const refreshToken = this.generateToken({
        sub: user.id,
        tid: user.tenantId,
        email: user.email,
        roles,
        jti: refreshJti,
      }, this.REFRESH_TOKEN_EXPIRY);

      // Create session record in database
      const session = this.sessionRepository.create({
        userId: user.id,
        token: accessJti, // Store JTI instead of full token
        refreshToken: refreshJti, // Store JTI instead of full token
        status: SessionStatus.ACTIVE,
        expiresAt: accessTokenExpiresAt,
        refreshExpiresAt: refreshTokenExpiresAt,
        ipAddress: metadata.ipAddress,
        userAgent: metadata.userAgent,
      });

      await this.sessionRepository.save(session);

      logger.info('Token pair generated successfully', {
        userId: user.id,
        sessionId: session.id,
        tenantId: user.tenantId,
      });

      return {
        accessToken,
        refreshToken,
        accessTokenExpiresAt,
        refreshTokenExpiresAt,
        sessionId: session.id,
      };
    } catch (error) {
      logger.error('Error generating token pair', {
        error: (error as Error).message,
        userId: user.id,
      });
      throw error;
    }
  }

  /**
   * Refreshes an access token using a valid refresh token
   * Implements token replay detection for enhanced security
   * @param refreshToken - Valid refresh token
   * @param metadata - Request metadata (IP, user agent)
   * @returns Promise<TokenPair> - New access and refresh tokens
   * @throws UnauthorizedError if token is invalid or replay detected
   */
  async refreshTokens(refreshToken: string, metadata: RequestMetadata): Promise<TokenPair> {
    try {
      logger.debug('Refreshing tokens');

      // Verify refresh token
      const payload = this.verifyRefreshToken(refreshToken);

      // Find active session with this refresh token JTI
      const session = await this.sessionRepository.findOne({
        where: {
          refreshToken: payload.jti,
          status: SessionStatus.ACTIVE,
        },
        relations: ['user', 'user.roles'],
      });

      if (!session) {
        logger.warn('Refresh token not found or session inactive', {
          jti: payload.jti,
        });
        throw new UnauthorizedError('Refresh token inválido o expirado');
      }

      // Check if session has already been used (token replay detection)
      if (session.revokedAt !== null) {
        logger.error('TOKEN REPLAY DETECTED - Session was already used', {
          sessionId: session.id,
          userId: session.userId,
          jti: payload.jti,
        });

        // SECURITY: Revoke ALL user sessions on replay detection
        const revokedCount = await this.revokeAllUserSessions(
          session.userId,
          'Token replay detected'
        );

        logger.error('All user sessions revoked due to token replay', {
          userId: session.userId,
          revokedCount,
        });

        throw new UnauthorizedError('Replay de token detectado. Todas las sesiones han sido revocadas por seguridad.');
      }

      // Verify session hasn't expired
      if (session.refreshExpiresAt && new Date() > session.refreshExpiresAt) {
        logger.warn('Refresh token expired', {
          sessionId: session.id,
          expiredAt: session.refreshExpiresAt,
        });

        await this.revokeSession(session.id, 'Token expired');
        throw new UnauthorizedError('Refresh token expirado');
      }

      // Mark current session as used (revoke it)
      session.status = SessionStatus.REVOKED;
      session.revokedAt = new Date();
      session.revokedReason = 'Used for refresh';
      await this.sessionRepository.save(session);

      // Generate new token pair
      const newTokenPair = await this.generateTokenPair(session.user, metadata);

      logger.info('Tokens refreshed successfully', {
        userId: session.userId,
        oldSessionId: session.id,
        newSessionId: newTokenPair.sessionId,
      });

      return newTokenPair;
    } catch (error) {
      logger.error('Error refreshing tokens', {
        error: (error as Error).message,
      });
      throw error;
    }
  }

  /**
   * Revokes a session and blacklists its access token
   * @param sessionId - Session ID to revoke
   * @param reason - Reason for revocation
   */
  async revokeSession(sessionId: string, reason: string): Promise<void> {
    try {
      logger.debug('Revoking session', { sessionId, reason });

      const session = await this.sessionRepository.findOne({
        where: { id: sessionId },
      });

      if (!session) {
        logger.warn('Session not found for revocation', { sessionId });
        return;
      }

      // Mark session as revoked
      session.status = SessionStatus.REVOKED;
      session.revokedAt = new Date();
      session.revokedReason = reason;
      await this.sessionRepository.save(session);

      // Blacklist the access token (JTI) in Redis
      const remainingTTL = this.calculateRemainingTTL(session.expiresAt);
      if (remainingTTL > 0) {
        await this.blacklistAccessToken(session.token, remainingTTL);
      }

      logger.info('Session revoked successfully', { sessionId, reason });
    } catch (error) {
      logger.error('Error revoking session', {
        error: (error as Error).message,
        sessionId,
      });
      throw error;
    }
  }

  /**
   * Revokes all active sessions for a user
   * Used for security events like password change or token replay detection
   * @param userId - User ID whose sessions to revoke
   * @param reason - Reason for revocation
   * @returns Promise<number> - Number of sessions revoked
   */
  async revokeAllUserSessions(userId: string, reason: string): Promise<number> {
    try {
      logger.debug('Revoking all user sessions', { userId, reason });

      const sessions = await this.sessionRepository.find({
        where: {
          userId,
          status: SessionStatus.ACTIVE,
        },
      });

      if (sessions.length === 0) {
        logger.debug('No active sessions found for user', { userId });
        return 0;
      }

      // Revoke each session
      for (const session of sessions) {
        session.status = SessionStatus.REVOKED;
        session.revokedAt = new Date();
        session.revokedReason = reason;

        // Blacklist access token
        const remainingTTL = this.calculateRemainingTTL(session.expiresAt);
        if (remainingTTL > 0) {
          await this.blacklistAccessToken(session.token, remainingTTL);
        }
      }

      await this.sessionRepository.save(sessions);

      logger.info('All user sessions revoked', {
        userId,
        count: sessions.length,
        reason,
      });

      return sessions.length;
    } catch (error) {
      logger.error('Error revoking all user sessions', {
        error: (error as Error).message,
        userId,
      });
      throw error;
    }
  }

  /**
   * Adds an access token to the Redis blacklist
   * @param jti - JWT ID to blacklist
   * @param expiresIn - TTL in seconds
   */
  async blacklistAccessToken(jti: string, expiresIn: number): Promise<void> {
    try {
      await blacklistToken(jti, expiresIn);
      logger.debug('Access token blacklisted', { jti, expiresIn });
    } catch (error) {
      logger.error('Error blacklisting access token', {
        error: (error as Error).message,
        jti,
      });
      // Don't throw - blacklist is optional (Redis might be unavailable)
    }
  }

  /**
   * Checks if an access token is blacklisted
   * @param jti - JWT ID to check
   * @returns Promise<boolean> - true if blacklisted
   */
  async isAccessTokenBlacklisted(jti: string): Promise<boolean> {
    try {
      return await isTokenBlacklisted(jti);
    } catch (error) {
      logger.error('Error checking token blacklist', {
        error: (error as Error).message,
        jti,
      });
      // Return false on error - fail open
      return false;
    }
  }

  // ===== Private Helper Methods =====

  /**
   * Generates a JWT token with the specified payload and expiry
   * @param payload - Token payload (without iat/exp)
   * @param expiresIn - Expiration time string (e.g., '15m', '7d')
   * @returns string - Signed JWT token
   */
  private generateToken(payload: Omit<JwtPayload, 'iat' | 'exp'>, expiresIn: string): string {
    return jwt.sign(payload, config.jwt.secret, {
      expiresIn: expiresIn as jwt.SignOptions['expiresIn'],
      algorithm: this.ALGORITHM,
    } as SignOptions);
  }

  /**
   * Verifies an access token and returns its payload
   * @param token - JWT access token
   * @returns JwtPayload - Decoded payload
   * @throws UnauthorizedError if token is invalid
   */
  private verifyAccessToken(token: string): JwtPayload {
    try {
      return jwt.verify(token, config.jwt.secret, {
        algorithms: [this.ALGORITHM],
      }) as JwtPayload;
    } catch (error) {
      logger.warn('Invalid access token', {
        error: (error as Error).message,
      });
      throw new UnauthorizedError('Access token inválido o expirado');
    }
  }

  /**
   * Verifies a refresh token and returns its payload
   * @param token - JWT refresh token
   * @returns JwtPayload - Decoded payload
   * @throws UnauthorizedError if token is invalid
   */
  private verifyRefreshToken(token: string): JwtPayload {
    try {
      return jwt.verify(token, config.jwt.secret, {
        algorithms: [this.ALGORITHM],
      }) as JwtPayload;
    } catch (error) {
      logger.warn('Invalid refresh token', {
        error: (error as Error).message,
      });
      throw new UnauthorizedError('Refresh token inválido o expirado');
    }
  }

  /**
   * Generates a unique JWT ID (JTI) using UUID v4
   * @returns string - Unique identifier
   */
  private generateJti(): string {
    return uuidv4();
  }

  /**
   * Calculates expiration date from a time string
   * @param expiresIn - Time string (e.g., '15m', '7d')
   * @returns Date - Expiration date
   */
  private calculateExpiration(expiresIn: string): Date {
    const unit = expiresIn.slice(-1);
    const value = parseInt(expiresIn.slice(0, -1), 10);

    const now = new Date();

    switch (unit) {
      case 's':
        return new Date(now.getTime() + value * 1000);
      case 'm':
        return new Date(now.getTime() + value * 60 * 1000);
      case 'h':
        return new Date(now.getTime() + value * 60 * 60 * 1000);
      case 'd':
        return new Date(now.getTime() + value * 24 * 60 * 60 * 1000);
      default:
        throw new Error(`Invalid time unit: ${unit}`);
    }
  }

  /**
   * Calculates remaining TTL in seconds for a given expiration date
   * @param expiresAt - Expiration date
   * @returns number - Remaining seconds (0 if already expired)
   */
  private calculateRemainingTTL(expiresAt: Date): number {
    const now = new Date();
    const remainingMs = expiresAt.getTime() - now.getTime();
    return Math.max(0, Math.floor(remainingMs / 1000));
  }
}

// ===== Export Singleton Instance =====

export const tokenService = new TokenService();
