// src/services/authService.ts
import * as bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { DatabaseError } from './processingErrors';
import type { Logger } from 'pino';
import { withTransaction, userRepo } from './db/index.db';
import { RepositoryError, ValidationError } from './db/errors.db';
import { logger } from './logger.server';
import { sendPasswordResetEmail } from './emailService.server';
import type { UserProfile } from '../types';
import { validatePasswordStrength } from '../utils/authUtils';

const JWT_SECRET = process.env.JWT_SECRET!;

class AuthService {
  async registerUser(
    email: string,
    password: string,
    fullName: string | undefined,
    avatarUrl: string | undefined,
    reqLog: Logger,
  ) {
    const strength = validatePasswordStrength(password);
    if (!strength.isValid) {
      throw new ValidationError([], strength.feedback);
    }

    // Wrap user creation and activity logging in a transaction for atomicity.
    // The `createUser` method is now designed to be composed within other transactions.
    return withTransaction(async (client) => {
      const transactionalUserRepo = new (await import('./db/user.db')).UserRepository(client);
      const adminRepo = new (await import('./db/admin.db')).AdminRepository(client);

      const saltRounds = 10;
      const hashedPassword = await bcrypt.hash(password, saltRounds);
      logger.info(`Hashing password for new user: ${email}`);

      const newUser = await transactionalUserRepo.createUser(
        email,
        hashedPassword,
        { full_name: fullName, avatar_url: avatarUrl },
        reqLog,
      );

      logger.info(
        `Successfully created new user in DB: ${newUser.user.email} (ID: ${newUser.user.user_id})`,
      );

      await adminRepo.logActivity(
        {
          userId: newUser.user.user_id,
          action: 'user_registered',
          displayText: `${email} has registered.`,
          icon: 'user-plus',
        },
        reqLog,
      );

      return newUser;
    }).catch((error: unknown) => {
      // Re-throw known repository errors (like UniqueConstraintError) to allow for specific handling upstream.
      if (error instanceof RepositoryError) {
        throw error;
      }
      // For unknown errors, log them and wrap them in a generic DatabaseError
      // to standardize the error contract of the service layer.
      const message =
        error instanceof Error ? error.message : 'An unknown error occurred during registration.';
      logger.error({ error, email }, `User registration failed with an unexpected error.`);
      throw new DatabaseError(message);
    });
  }

  async registerAndLoginUser(
    email: string,
    password: string,
    fullName: string | undefined,
    avatarUrl: string | undefined,
    reqLog: Logger,
  ): Promise<{ newUserProfile: UserProfile; accessToken: string; refreshToken: string }> {
    const newUserProfile = await this.registerUser(email, password, fullName, avatarUrl, reqLog);
    const { accessToken, refreshToken } = await this.handleSuccessfulLogin(newUserProfile, reqLog);
    return { newUserProfile, accessToken, refreshToken };
  }

  generateAuthTokens(userProfile: UserProfile) {
    const payload = {
      user_id: userProfile.user.user_id,
      email: userProfile.user.email,
      role: userProfile.role,
    };
    const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
    const refreshToken = crypto.randomBytes(64).toString('hex');

    return { accessToken, refreshToken };
  }

  async saveRefreshToken(userId: string, refreshToken: string, reqLog: Logger) {
    // The repository method `saveRefreshToken` already includes robust error handling
    // and logging via `handleDbError`. No need for a redundant try/catch block here.
    await userRepo.saveRefreshToken(userId, refreshToken, reqLog);
  }

  async handleSuccessfulLogin(userProfile: UserProfile, reqLog: Logger) {
    const { accessToken, refreshToken } = this.generateAuthTokens(userProfile);
    await this.saveRefreshToken(userProfile.user.user_id, refreshToken, reqLog);
    return { accessToken, refreshToken };
  }

  async resetPassword(email: string, reqLog: Logger) {
    try {
      logger.debug(`[API /forgot-password] Received request for email: ${email}`);
      const user = await userRepo.findUserByEmail(email, reqLog);
      let token: string | undefined;
      logger.debug(
        { user: user ? { user_id: user.user_id, email: user.email } : 'NOT FOUND' },
        `[API /forgot-password] Database search result for ${email}:`,
      );

      if (user) {
        token = crypto.randomBytes(32).toString('hex');
        const saltRounds = 10;
        const tokenHash = await bcrypt.hash(token, saltRounds);
        const expiresAt = new Date(Date.now() + 3600000); // 1 hour

        // Wrap the token creation in a transaction to ensure atomicity of the DELETE and INSERT operations.
        await withTransaction(async (client) => {
          const transactionalUserRepo = new (await import('./db/user.db')).UserRepository(client);
          await transactionalUserRepo.createPasswordResetToken(
            user.user_id,
            tokenHash,
            expiresAt,
            reqLog,
            client,
          );
        });

        const resetLink = `${process.env.FRONTEND_URL}/reset-password/${token}`;

        try {
          await sendPasswordResetEmail(email, resetLink, reqLog);
        } catch (emailError) {
          logger.error({ emailError }, `Email send failure during password reset for user`);
        }
      } else {
        logger.warn(`Password reset requested for non-existent email: ${email}`);
      }

      return token;
    } catch (error) {
      // Re-throw known repository errors to allow for specific handling upstream.
      if (error instanceof RepositoryError) {
        throw error;
      }
      // For unknown errors, log them and wrap them in a generic DatabaseError.
      const message = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error(
        { error, email },
        `An unexpected error occurred during password reset for email: ${email}`,
      );
      throw new DatabaseError(message);
    }
  }

  async updatePassword(token: string, newPassword: string, reqLog: Logger) {
    const strength = validatePasswordStrength(newPassword);
    if (!strength.isValid) {
      throw new ValidationError([], strength.feedback);
    }

    // Wrap all database operations in a transaction to ensure atomicity.
    return withTransaction(async (client) => {
      const transactionalUserRepo = new (await import('./db/user.db')).UserRepository(client);
      const adminRepo = new (await import('./db/admin.db')).AdminRepository(client);

      // This read can happen outside the transaction if we use the non-transactional repo.
      const validTokens = await userRepo.getValidResetTokens(reqLog);
      let tokenRecord;
      for (const record of validTokens) {
        const isMatch = await bcrypt.compare(token, record.token_hash);
        if (isMatch) {
          tokenRecord = record;
          break;
        }
      }

      if (!tokenRecord) {
        return null; // Token is invalid or expired, not an error.
      }

      const saltRounds = 10;
      const hashedPassword = await bcrypt.hash(newPassword, saltRounds);

      // These three writes are now atomic.
      await transactionalUserRepo.updateUserPassword(tokenRecord.user_id, hashedPassword, reqLog);
      await transactionalUserRepo.deleteResetToken(tokenRecord.token_hash, reqLog);
      await adminRepo.logActivity(
        {
          userId: tokenRecord.user_id,
          action: 'password_reset',
          displayText: `User ID ${tokenRecord.user_id} has reset their password.`,
          icon: 'key',
        },
        reqLog,
      );

      return true;
    }).catch((error) => {
      // Re-throw known repository errors to allow for specific handling upstream.
      if (error instanceof RepositoryError) {
        throw error;
      }
      // For unknown errors, log them and wrap them in a generic DatabaseError.
      const message = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error({ error }, `An unexpected error occurred during password update.`);
      throw new DatabaseError(message);
    });
  }

  async getUserByRefreshToken(refreshToken: string, reqLog: Logger) {
    try {
      const basicUser = await userRepo.findUserByRefreshToken(refreshToken, reqLog);
      if (!basicUser) {
        return null;
      }
      const userProfile = await userRepo.findUserProfileById(basicUser.user_id, reqLog);
      return userProfile;
    } catch (error) {
      // Re-throw known repository errors to allow for specific handling upstream.
      if (error instanceof RepositoryError) {
        throw error;
      }
      // For unknown errors, log them and wrap them in a generic DatabaseError.
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error(
        { error, refreshToken },
        'An unexpected error occurred while fetching user by refresh token.',
      );
      throw new DatabaseError(errorMessage);
    }
  }

  async logout(refreshToken: string, reqLog: Logger) {
    // The repository method `deleteRefreshToken` now includes robust error handling
    // and logging via `handleDbError`. No need for a redundant try/catch block here.
    // The original implementation also swallowed errors, which is now fixed.
    await userRepo.deleteRefreshToken(refreshToken, reqLog);
  }

  async refreshAccessToken(
    refreshToken: string,
    reqLog: Logger,
  ): Promise<{ accessToken: string } | null> {
    const user = await this.getUserByRefreshToken(refreshToken, reqLog);
    if (!user) {
      return null;
    }
    const { accessToken } = this.generateAuthTokens(user);
    return { accessToken };
  }
}

export const authService = new AuthService();
