/**
 * AuthService - Servicio de Autenticación
 *
 * Gestiona login, logout, refresh tokens y validación de JWT.
 * Implementa patrón multi-tenant con verificación de tenant_id.
 *
 * @module Auth
 */

import * as jwt from 'jsonwebtoken';
import * as bcrypt from 'bcryptjs';
import { Repository } from 'typeorm';
import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity';
import {
  LoginDto,
  RegisterDto,
  RefreshTokenDto,
  ChangePasswordDto,
  TokenPayload,
  AuthResponse,
  TokenValidationResult,
} from '../dto/auth.dto';

export interface RefreshToken {
  id: string;
  userId: string;
  token: string;
  expiresAt: Date;
  revokedAt?: Date;
}

export class AuthService {
  private readonly jwtSecret: string;
  private readonly jwtExpiresIn: string;
  private readonly jwtRefreshExpiresIn: string;

  constructor(
    private readonly userRepository: Repository<User>,
    private readonly tenantRepository: Repository<Tenant>,
    private readonly refreshTokenRepository: Repository<RefreshToken>
  ) {
    this.jwtSecret = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production-minimum-32-chars';
    this.jwtExpiresIn = process.env.JWT_EXPIRES_IN || '1d';
    this.jwtRefreshExpiresIn = process.env.JWT_REFRESH_EXPIRES_IN || '7d';
  }

  /**
   * Login de usuario
   */
  async login(dto: LoginDto): Promise<AuthResponse> {
    // Buscar usuario por email
    const user = await this.userRepository.findOne({
      where: { email: dto.email, deletedAt: null } as any,
      relations: ['userRoles', 'userRoles.role'],
    });

    if (!user) {
      throw new Error('Invalid credentials');
    }

    // Verificar password
    const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash);
    if (!isPasswordValid) {
      throw new Error('Invalid credentials');
    }

    // Verificar que el usuario esté activo
    if (!user.isActive) {
      throw new Error('User is not active');
    }

    // Obtener tenant
    const tenantId = dto.tenantId || user.defaultTenantId;
    if (!tenantId) {
      throw new Error('No tenant specified');
    }

    const tenant = await this.tenantRepository.findOne({
      where: { id: tenantId, isActive: true, deletedAt: null } as any,
    });

    if (!tenant) {
      throw new Error('Tenant not found or inactive');
    }

    // Obtener roles del usuario
    const roles = user.userRoles?.map((ur) => ur.role.code) || [];

    // Generar tokens
    const accessToken = this.generateAccessToken(user, tenantId, roles);
    const refreshToken = await this.generateRefreshToken(user.id);

    // Actualizar último login
    await this.userRepository.update(user.id, { lastLoginAt: new Date() });

    return {
      accessToken,
      refreshToken,
      expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn),
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        roles,
      },
      tenant: {
        id: tenant.id,
        name: tenant.name,
      },
    };
  }

  /**
   * Registro de usuario
   */
  async register(dto: RegisterDto): Promise<AuthResponse> {
    // Verificar si el email ya existe
    const existingUser = await this.userRepository.findOne({
      where: { email: dto.email } as any,
    });

    if (existingUser) {
      throw new Error('Email already registered');
    }

    // Verificar que el tenant existe
    const tenant = await this.tenantRepository.findOne({
      where: { id: dto.tenantId, isActive: true } as any,
    });

    if (!tenant) {
      throw new Error('Tenant not found');
    }

    // Hash del password
    const passwordHash = await bcrypt.hash(dto.password, 12);

    // Crear usuario
    const user = await this.userRepository.save(
      this.userRepository.create({
        email: dto.email,
        passwordHash,
        firstName: dto.firstName,
        lastName: dto.lastName,
        defaultTenantId: dto.tenantId,
        isActive: true,
      })
    );

    // Generar tokens (rol default: user)
    const roles = ['user'];
    const accessToken = this.generateAccessToken(user, dto.tenantId, roles);
    const refreshToken = await this.generateRefreshToken(user.id);

    return {
      accessToken,
      refreshToken,
      expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn),
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        roles,
      },
      tenant: {
        id: tenant.id,
        name: tenant.name,
      },
    };
  }

  /**
   * Refresh de token
   */
  async refresh(dto: RefreshTokenDto): Promise<AuthResponse> {
    // Validar refresh token
    const validation = this.validateToken(dto.refreshToken, 'refresh');
    if (!validation.valid || !validation.payload) {
      throw new Error('Invalid refresh token');
    }

    // Verificar que el token no está revocado
    const storedToken = await this.refreshTokenRepository.findOne({
      where: { token: dto.refreshToken, revokedAt: null } as any,
    });

    if (!storedToken || storedToken.expiresAt < new Date()) {
      throw new Error('Refresh token expired or revoked');
    }

    // Obtener usuario
    const user = await this.userRepository.findOne({
      where: { id: validation.payload.sub, deletedAt: null } as any,
      relations: ['userRoles', 'userRoles.role'],
    });

    if (!user || !user.isActive) {
      throw new Error('User not found or inactive');
    }

    // Obtener tenant
    const tenant = await this.tenantRepository.findOne({
      where: { id: validation.payload.tenantId, isActive: true } as any,
    });

    if (!tenant) {
      throw new Error('Tenant not found or inactive');
    }

    const roles = user.userRoles?.map((ur) => ur.role.code) || [];

    // Revocar token anterior
    await this.refreshTokenRepository.update(storedToken.id, { revokedAt: new Date() });

    // Generar nuevos tokens
    const accessToken = this.generateAccessToken(user, tenant.id, roles);
    const refreshToken = await this.generateRefreshToken(user.id);

    return {
      accessToken,
      refreshToken,
      expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn),
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        roles,
      },
      tenant: {
        id: tenant.id,
        name: tenant.name,
      },
    };
  }

  /**
   * Logout - Revocar refresh token
   */
  async logout(refreshToken: string): Promise<void> {
    await this.refreshTokenRepository.update(
      { token: refreshToken } as any,
      { revokedAt: new Date() }
    );
  }

  /**
   * Cambiar password
   */
  async changePassword(userId: string, dto: ChangePasswordDto): Promise<void> {
    const user = await this.userRepository.findOne({
      where: { id: userId } as any,
    });

    if (!user) {
      throw new Error('User not found');
    }

    const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash);
    if (!isCurrentValid) {
      throw new Error('Current password is incorrect');
    }

    const newPasswordHash = await bcrypt.hash(dto.newPassword, 12);
    await this.userRepository.update(userId, { passwordHash: newPasswordHash });

    // Revocar todos los refresh tokens del usuario
    await this.refreshTokenRepository.update(
      { userId } as any,
      { revokedAt: new Date() }
    );
  }

  /**
   * Validar access token
   */
  validateAccessToken(token: string): TokenValidationResult {
    return this.validateToken(token, 'access');
  }

  /**
   * Validar token
   */
  private validateToken(token: string, expectedType: 'access' | 'refresh'): TokenValidationResult {
    try {
      const payload = jwt.verify(token, this.jwtSecret) as TokenPayload;

      if (payload.type !== expectedType) {
        return { valid: false, error: 'Invalid token type' };
      }

      return { valid: true, payload };
    } catch (error) {
      if (error instanceof jwt.TokenExpiredError) {
        return { valid: false, error: 'Token expired' };
      }
      if (error instanceof jwt.JsonWebTokenError) {
        return { valid: false, error: 'Invalid token' };
      }
      return { valid: false, error: 'Token validation failed' };
    }
  }

  /**
   * Generar access token
   */
  private generateAccessToken(user: User, tenantId: string, roles: string[]): string {
    const payload: TokenPayload = {
      sub: user.id,
      email: user.email,
      tenantId,
      roles,
      type: 'access',
    };

    return jwt.sign(payload, this.jwtSecret, {
      expiresIn: this.jwtExpiresIn as jwt.SignOptions['expiresIn'],
    });
  }

  /**
   * Generar refresh token
   */
  private async generateRefreshToken(userId: string): Promise<string> {
    const payload: Partial<TokenPayload> = {
      sub: userId,
      type: 'refresh',
    };

    const token = jwt.sign(payload, this.jwtSecret, {
      expiresIn: this.jwtRefreshExpiresIn as jwt.SignOptions['expiresIn'],
    });

    // Almacenar en DB
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7); // 7 días

    await this.refreshTokenRepository.save(
      this.refreshTokenRepository.create({
        userId,
        token,
        expiresAt,
      })
    );

    return token;
  }

  /**
   * Convertir expiresIn a segundos
   */
  private getExpiresInSeconds(expiresIn: string): number {
    const match = expiresIn.match(/^(\d+)([dhms])$/);
    if (!match) return 86400; // default 1 día

    const value = parseInt(match[1]);
    const unit = match[2];

    switch (unit) {
      case 'd': return value * 86400;
      case 'h': return value * 3600;
      case 'm': return value * 60;
      case 's': return value;
      default: return 86400;
    }
  }
}
