// 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 { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
import { SignOutUserDto } from '@/features/auth/dto/signOut-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 } from '@nestjs/jwt';
import crypto from 'crypto';
import { and, eq, or } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from 'src/database/index';
import { sessions, users, roles, usersRole } from 'src/database/index';
import { Session } from './interfaces/session.interface';
import * as bcrypt from 'bcryptjs';

@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 [access_token, refresh_token] = await Promise.all([
      this.jwtService.signAsync(
        {
          sub: user.id,
          username: user.username,
        },
        {
          secret: envs.access_token_secret,
          expiresIn: envs.access_token_expiration,
        },
      ),
      this.jwtService.signAsync(
        {
          sub: user.id,
          username: user.username,
        },
        {
          secret: envs.refresh_token_secret,
          expiresIn: envs.refresh_token_expiration,
        },
      ),
    ]);

    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 { user_id, refresh_token } = dto;
    // const user_id = 1;

    const validation = await this.jwtService.verifyAsync(refresh_token, {
        secret: envs.refresh_token_secret,
    });

    if (!validation) throw new UnauthorizedException('Invalid refresh token');

    const session = await this.drizzle
      .select()
      .from(sessions)
      .where(
        and(
          eq(sessions.userId, user_id) &&
            eq(sessions.sessionToken, dto.refresh_token),
        ),
      );

    // console.log(session.length);
    
    if (session.length === 0) throw new NotFoundException('session not found');
    const user = await this.findUserById(user_id);
    if (!user) throw new NotFoundException('User not found');
    
    // Genera token
    const tokens = await this.generateTokens(user);
    const decodeAccess = this.decodeToken(tokens.access_token);
    const decodeRefresh = this.decodeToken(tokens.refresh_token);
    
    // Actualiza session
    await this.drizzle
      .update(sessions)
      .set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp })
      .where(eq(sessions.userId, user_id));

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

  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);
          }
        }

        // Hash the password
        const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
    
        // Start a transaction
        return await this.drizzle.transaction(async (tx) => {
          // 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;

          // 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;
        })
      
  }
}
