// auth.service
import { envs } from '@/common/config/envs';
import { Env, validateString } from '@/common/utils';
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
import {
  LoginUserInterface,
  Roles,
} from '@/features/auth/interfaces/login-user.interface';
import RefreshTokenInterface from '@/features/auth/interfaces/refresh-token.interface';
import { MailService } from '@/features/mail/mail.service';
import { User } from '@/features/users/entities/user.entity';
import {
  HttpException,
  HttpStatus,
  Inject,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { eq, or } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { roles, sessions, users, usersRole } from 'src/database/index';
import { Session } from './interfaces/session.interface';

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly config: ConfigService<Env>,
    @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
    private readonly mailService: MailService,
  ) { }

  //Decode Tokens
  // Método para decodificar el token y obtener los datos completos
  private decodeToken(token: string): {
    sub: number;
    username?: string;
    iat: number;
    exp: number;
  } {
    try {
      const decoded = this.jwtService.decode(token) as {
        sub: number;
        username?: string;
        iat: number;
        exp: number;
      };

      // Validar que contiene los datos esenciales
      if (!decoded || !decoded.exp || !decoded.iat) {
        throw new Error('Token lacks required fields');
      }

      return decoded;
    } catch (error) {
      // Manejo seguro del tipo unknown
      let errorMessage = 'Failed to decode token';

      if (error instanceof Error) {
        errorMessage = error.message;
        console.error('Error decoding token:', errorMessage);
      } else {
        console.error('Unknown error type:', error);
      }

      throw new HttpException(errorMessage, HttpStatus.UNAUTHORIZED);
    }
  }

  //Generate Tokens
  async generateTokens(user: User): Promise<AuthTokensInterface> {
    const accessTokenSecret = envs.access_token_secret ?? '';
    const accessTokenExp = envs.access_token_expiration ?? '';
    const refreshTokenSecret = envs.refresh_token_secret ?? '';
    const refreshTokenExp = envs.refresh_token_expiration ?? '';

    if (
      !accessTokenSecret ||
      !accessTokenExp ||
      !refreshTokenSecret ||
      !refreshTokenExp
    ) {
      throw new Error('JWT environment variables are missing or invalid');
    }

    interface JwtPayload {
      sub: number;
      username: string;
    }

    const payload: JwtPayload = {
      sub: Number(user?.id),
      username: user.username ?? '',
    };

    const [access_token, refresh_token] = await Promise.all([
      this.jwtService.signAsync(payload, {
        secret: accessTokenSecret,
        expiresIn: accessTokenExp,
      } as JwtSignOptions),

      this.jwtService.signAsync(payload, {
        secret: refreshTokenSecret,
        expiresIn: refreshTokenExp,
      } as JwtSignOptions),
    ]);

    return { access_token, refresh_token };
  }

  //Generate OTP Code For Email Confirmation
  async generateOTP(length = 6): Promise<string> {
    return crypto
      .randomInt(0, 10 ** length)
      .toString()
      .padStart(length, '0');
  }

  // metodo para crear una session
  private async createSession(sessionInput: Session): Promise<string> {
    const { userId } = sessionInput;
    const activeSessionsCount = await this.drizzle
      .select()
      .from(sessions)
      .where(eq(sessions.userId, parseInt(userId)));

    if (activeSessionsCount.length !== 0) {
      // Elimina sessiones viejsas
      await this.drizzle
        .delete(sessions)
        .where(eq(sessions.userId, parseInt(userId)));
    }

    const session = await this.drizzle.insert(sessions).values({
      sessionToken: sessionInput.sessionToken,
      userId: parseInt(userId),
      expiresAt: sessionInput.expiresAt,
    });
    if (session.rowCount === 0)
      throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);

    return 'Session created successfully';
  }

  //Find User
  async findUser(username: string): Promise<User | null> {
    const user = await this.drizzle
      .select()
      .from(users)
      .where(eq(users.username, username));
    return user[0];
  }

  //Find User
  async findUserById(id: number): Promise<User | null> {
    const user = await this.drizzle
      .select()
      .from(users)
      .where(eq(users.id, id));
    return user[0];
  }

  //Check User Is Already Exists
  async validateUser(dto: ValidateUserDto): Promise<User> {
    const user = await this.findUser(dto.username);
    if (!user) throw new NotFoundException('User not found');
    const isValid = await validateString(
      dto.password,
      user?.password as string,
    );
    if (!isValid) throw new UnauthorizedException('Invalid credentials');
    return user;
  }

  //Find rol user
  async findUserRol(id: number): Promise<Roles[]> {
    const roles = await this.drizzle
      .select({
        id: schema.roles.id,
        role: schema.roles.name,
      })
      .from(schema.usersRole)
      .leftJoin(schema.roles, eq(schema.roles.id, schema.usersRole.roleId))
      .where(eq(schema.usersRole.userId, id));

    if (roles.length === 0) {
      throw new NotFoundException('User not found');
    }

    // Aseguramos que no haya valores nulos
    return roles.map((role) => ({
      id: role.id ?? 0, // Asignamos un valor por defecto (0) si es null
      rol: role.role ?? '', // Asignamos un valor por defecto (cadena vacía) si es null
    }));
  }

  //Sign In User Account
  async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
    const user = await this.validateUser(dto);
    const tokens = await this.generateTokens(user);
    const decodeAccess = this.decodeToken(tokens.access_token);
    const decodeRefresh = this.decodeToken(tokens.refresh_token);
    const rol = await this.findUserRol(user?.id as number);

    await this.createSession({
      userId: String(user?.id), // Convert number to string
      sessionToken: tokens.refresh_token,
      expiresAt: decodeRefresh.exp,
    });

    return {
      message: 'User signed in successfully',
      user: {
        id: user?.id as number,
        username: user?.username,
        fullname: user?.fullname,
        email: user?.email,
        rol: rol,
      },
      tokens: {
        access_token: tokens.access_token,
        access_expire_in: decodeAccess.exp,
        refresh_token: tokens.refresh_token,
        refresh_expire_in: decodeRefresh.exp,
      },
    };
  }

  // //Forgot Password
  // async forgotPassword(dto: ForgotPasswordDto): Promise<void> {
  //   const user = await this.findUser(dto.username);
  //   if (!user) throw new NotFoundException('User not found');
  //   const passwordResetToken = await this.generateOTP();
  //   user.passwordResetToken = passwordResetToken;
  //   user.passwordResetTokenExpires = new Date(
  //     Date.now() + 1000 * 60 * 60 * 24, // 1 day
  //   );
  //   await this.UserRepository.save(user);
  //   await this.mailService.sendEmail({
  //     to: [user.email],
  //     subject: 'Reset Password',
  //     html: ForgotPasswordMail({
  //       name: user.name,
  //       code: passwordResetToken,
  //     }),
  //   });
  // }

  //Sign Out User Account
  async signOut(dto: SignOutUserDto): Promise<void> {
    const { user_id } = dto;
    const user = await this.drizzle
      .select()
      .from(users)
      .where(eq(users.id, parseInt(user_id)));
    if (!user) throw new NotFoundException('User not found');
    await this.drizzle
      .delete(sessions)
      .where(eq(sessions.userId, parseInt(user_id)));
  }

  //Refresh User Access Token
  async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
    const { refreshToken } = dto;

    // 1. Validar firma del token (Crypto check)
    let payload: any;
    try {
      payload = await this.jwtService.verifyAsync(refreshToken, {
        secret: envs.refresh_token_secret,
      });
    } catch (e) {
      throw new UnauthorizedException('Invalid Refresh Token Signature');
    }

    const userId = Number(payload.sub); // Es más seguro usar el ID del token que el del DTO

    // 2. Buscar la sesión por UserID (SIN filtrar por token todavía)
    // Esto es clave: traemos la sesión para ver qué está pasando
    const [currentSession] = await this.drizzle
      .select()
      .from(sessions)
      .where(eq(sessions.userId, userId));

    if (!currentSession) throw new NotFoundException('Session not found');

    // CONFIGURACIÓN: Tiempo de gracia en milisegundos (ej: 15 segundos)
    const GRACE_PERIOD_MS = 15000;

    // -------------------------------------------------------------------
    // ESCENARIO A: Rotación Normal (El token coincide con el actual)
    // -------------------------------------------------------------------
    if (currentSession.sessionToken === refreshToken) {
      const user = await this.findUserById(userId);
      if (!user) throw new NotFoundException('User not found');

      // Generar nuevos tokens (A -> B)
      const tokensNew = await this.generateTokens(user);
      const decodeAccess = this.decodeToken(tokensNew.access_token);
      const decodeRefresh = this.decodeToken(tokensNew.refresh_token);

      // Actualizamos DB guardando el token "viejo" como "previous"
      await this.drizzle
        .update(sessions)
        .set({
          sessionToken: tokensNew.refresh_token, // Nuevo (B)
          previousSessionToken: refreshToken, // Viejo (A)
          lastRotatedAt: new Date(), // Marca de tiempo
          expiresAt: decodeRefresh.exp,
        })
        .where(eq(sessions.userId, userId));

      return {
        access_token: tokensNew.access_token,
        access_expire_in: decodeAccess.exp,
        refresh_token: tokensNew.refresh_token,
        refresh_expire_in: decodeRefresh.exp,
      };
    }

    // -------------------------------------------------------------------
    // ESCENARIO B: Periodo de Gracia (Condición de Carrera)
    // -------------------------------------------------------------------
    // El token no coincide con el actual, ¿pero coincide con el anterior?
    const isPreviousToken =
      currentSession.previousSessionToken === refreshToken;

    // Calculamos cuánto tiempo ha pasado desde la rotación
    const timeSinceRotation = currentSession.lastRotatedAt
      ? Date.now() - new Date(currentSession.lastRotatedAt).getTime()
      : Infinity;

    if (isPreviousToken && timeSinceRotation < GRACE_PERIOD_MS) {
      // ¡Es una condición de carrera! El usuario envió 'A' pero ya rotamos a 'B'.
      // Le devolvemos 'B' (el actual en DB) para que se sincronice.

      const user = await this.findUserById(userId);

      if (!user) throw new NotFoundException('User not found');

      // Generamos un access token nuevo fresco (barato)
      const accessTokenPayload = { sub: user.id, username: user.username };
      const newAccessToken = await this.jwtService.signAsync(
        accessTokenPayload,
        {
          secret: envs.access_token_secret,
          expiresIn: envs.access_token_expiration,
        } as JwtSignOptions,
      );
      const decodeAccess = this.decodeToken(newAccessToken);

      // IMPORTANTE: Devolvemos el refresh token QUE YA ESTÁ EN LA BASE DE DATOS
      // No generamos uno nuevo para no romper la cadena de la otra petición que ganó.
      return {
        access_token: newAccessToken,
        access_expire_in: decodeAccess.exp,
        refresh_token: currentSession.sessionToken!, // Devolvemos el token 'B'
        refresh_expire_in: currentSession.expiresAt as number,
      };
    }

    // -------------------------------------------------------------------
    // ESCENARIO C: Robo de Token (Reuse Detection)
    // -------------------------------------------------------------------
    // Si el token no es el actual, ni el anterior válido... ALGUIEN LO ROBÓ.
    // O el usuario está intentando reusar un token muy viejo.

    // Medida de seguridad: Borrar todas las sesiones del usuario
    await this.drizzle.delete(sessions).where(eq(sessions.userId, userId));

    throw new UnauthorizedException(
      'Refresh token reuse detected. Access revoked.',
    );
  }

  async singUp(createUserDto: SingUpUserDto): Promise<User> {
    // Check if username or email exists
    const data = await this.drizzle
      .select({
        id: users.id,
        username: users.username,
        email: users.email,
      })
      .from(users)
      .where(
        or(
          eq(users.username, createUserDto.username),
          eq(users.email, createUserDto.email),
        ),
      );

    if (data.length > 0) {
      if (data[0].username === createUserDto.username) {
        throw new HttpException(
          'Username already exists',
          HttpStatus.BAD_REQUEST,
        );
      }
      if (data[0].email === createUserDto.email) {
        throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
      }
    }

    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    // Start a transaction
    return await this.drizzle.transaction(async (tx) => {
      // Hash the password
      // Create the user
      const [newUser] = await tx
        .insert(users)
        .values({
          username: createUserDto.username,
          email: createUserDto.email,
          password: hashedPassword,
          fullname: createUserDto.fullname,
          isActive: true,
          state: createUserDto.state,
          municipality: createUserDto.municipality,
          parish: createUserDto.parish,
          phone: createUserDto.phone,
          isEmailVerified: false,
          isTwoFactorEnabled: false,
        })
        .returning();

      // check if user role is admin
      const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
      // check if user role is admin

      // Assign role to user
      await tx.insert(usersRole).values({
        userId: newUser.id,
        roleId: role,
      });

      // Return the created user with role
      const [userWithRole] = await tx
        .select({
          id: users.id,
          username: users.username,
          email: users.email,
          fullname: users.fullname,
          phone: users.phone,
          isActive: users.isActive,
          role: roles.name,
        })
        .from(users)
        .leftJoin(usersRole, eq(usersRole.userId, users.id))
        .leftJoin(roles, eq(roles.id, usersRole.roleId))
        .where(eq(users.id, newUser.id));

      return userWithRole;
    });
  }
}
