// src/services/userService.ts
import * as db from './db/index.db';
import type { Job } from 'bullmq';
import * as bcrypt from 'bcrypt';
import type { Logger } from 'pino';
import { AddressRepository } from './db/address.db';
import { UserRepository } from './db/user.db';
import type { Address, Profile, UserProfile } from '../types';
import { ValidationError, NotFoundError } from './db/errors.db';
import { logger as globalLogger } from './logger.server';
import type { TokenCleanupJobData } from '../types/job-data';

/**
 * Encapsulates user-related business logic that may involve multiple repository calls.
 */
class UserService {
  /**
   * Per ADR-002, this function encapsulates a multi-write operation within a transaction.
   * It creates or updates a user's address and links it to their profile atomically.
   *
   * @param user The user profile object.
   * @param addressData The address data to upsert.
   * @returns The ID of the upserted address.
   */
  async upsertUserAddress(
    userprofile: UserProfile,
    addressData: Partial<Address>,
    logger: Logger,
  ): Promise<number> {
    return db.withTransaction(async (client) => {
      // Instantiate repositories with the transactional client
      const addressRepo = new AddressRepository(client);
      const userRepo = new UserRepository(client);

      const addressId = await addressRepo.upsertAddress(
        { ...addressData, address_id: userprofile.address_id ?? undefined },
        logger,
      );

      // If the user didn't have an address_id before, update their profile to link it.
      if (!userprofile.address_id) {
        await userRepo.updateUserProfile(
          userprofile.user.user_id,
          { address_id: addressId },
          logger,
        );
      }

      return addressId;
    });
  }

  /**
   * Processes a job to clean up expired password reset tokens from the database.
   * @param job The BullMQ job object.
   * @returns An object containing the count of deleted tokens.
   */
  async processTokenCleanupJob(
    job: Job<TokenCleanupJobData>,
  ): Promise<{ deletedCount: number }> {
    const logger = globalLogger.child({
      jobId: job.id,
      jobName: job.name,
    });

    logger.info('Picked up expired token cleanup job.');

    try {
      const deletedCount = await db.userRepo.deleteExpiredResetTokens(logger);
      logger.info(`Successfully deleted ${deletedCount} expired tokens.`);
      return { deletedCount };
    } catch (error) {
      const wrappedError = error instanceof Error ? error : new Error(String(error));
      logger.error(
        { err: wrappedError, attemptsMade: job.attemptsMade },
        'Expired token cleanup job failed.',
      );
      throw wrappedError;
    }
  }

  /**
   * Updates a user's avatar, creating the URL and updating the profile.
   * @param userId The ID of the user to update.
   * @param file The uploaded avatar file.
   * @param logger The logger instance.
   * @returns The updated user profile.
   */
  async updateUserAvatar(userId: string, file: Express.Multer.File, logger: Logger): Promise<Profile> {
    const avatarUrl = `/uploads/avatars/${file.filename}`;
    return db.userRepo.updateUserProfile(
      userId,
      { avatar_url: avatarUrl },
      logger,
    );
  }
  /**
   * Updates a user's password after hashing it.
   * @param userId The ID of the user to update.
   * @param newPassword The new plaintext password.
   * @param logger The logger instance.
   */
  async updateUserPassword(userId: string, newPassword: string, logger: Logger): Promise<void> {
    const saltRounds = 10;
    const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
    await db.userRepo.updateUserPassword(userId, hashedPassword, logger);
  }

  /**
   * Deletes a user's account after verifying their password.
   * @param userId The ID of the user to delete.
   * @param password The user's current password for verification.
   * @param logger The logger instance.
   */
  async deleteUserAccount(userId: string, password: string, logger: Logger): Promise<void> {
    const userWithHash = await db.userRepo.findUserWithPasswordHashById(userId, logger);
    if (!userWithHash || !userWithHash.password_hash) {
      // This case should be rare for a logged-in user but is a good safeguard.
      throw new NotFoundError('User not found or password not set.');
    }

    const isMatch = await bcrypt.compare(password, userWithHash.password_hash);
    if (!isMatch) {
      // Use ValidationError for a 400-level response in the route
      throw new ValidationError([], 'Incorrect password.');
    }

    await db.userRepo.deleteUserById(userId, logger);
  }

  /**
   * Fetches a user's address, ensuring the user is authorized to view it.
   * @param userProfile The profile of the user making the request.
   * @param addressId The ID of the address being requested.
   * @param logger The logger instance.
   * @returns The address object.
   */
  async getUserAddress(
    userProfile: UserProfile,
    addressId: number,
    logger: Logger,
  ): Promise<Address> {
    // Security check: Ensure the requested addressId matches the one on the user's profile.
    if (userProfile.address_id !== addressId) {
      // Use ValidationError to trigger a 403 Forbidden response in the route handler.
      throw new ValidationError([], 'Forbidden: You can only access your own address.');
    }
    // The repo method will throw a NotFoundError if the address doesn't exist.
    return db.addressRepo.getAddressById(addressId, logger);
  }

  /**
   * Encapsulates the business logic for an admin deleting another user's account.
   * This includes preventing an admin from deleting their own account.
   * @param deleterId The ID of the admin performing the deletion.
   * @param userToDeleteId The ID of the user to be deleted.
   * @param log The logger instance.
   */
  public async deleteUserAsAdmin(deleterId: string, userToDeleteId: string, log: Logger) {
    if (deleterId === userToDeleteId) {
      throw new ValidationError([], 'Admins cannot delete their own account.');
    }
    await db.userRepo.deleteUserById(userToDeleteId, log);
  }
}

export const userService = new UserService();
