// 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 { DatabaseError } from './processingErrors';
import { logger as globalLogger } from './logger.server';
import type { TokenCleanupJobData } from '../types/job-data';
import { getBaseUrl } from '../utils/serverUtils';

/**
 * 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) => {
        const addressRepo = new AddressRepository(client);
        const userRepo = new UserRepository(client);
        const addressId = await addressRepo.upsertAddress(
          { ...addressData, address_id: userprofile.address_id ?? undefined },
          logger,
        );
        if (!userprofile.address_id) {
          await userRepo.updateUserProfile(userprofile.user.user_id, { address_id: addressId }, logger);
        }
        return addressId;
      })
      .catch((error) => {
        const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
        logger.error({ err: error, userId: userprofile.user.user_id }, `Transaction to upsert user address failed: ${errorMessage}`);
        // Wrap the original error in a service-level DatabaseError to standardize the error contract,
        // as this is an unexpected failure within the transaction boundary.
        throw new DatabaseError(errorMessage);
      });
  }

  /**
   * 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 errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error({ err: error, attemptsMade: job.attemptsMade }, `Expired token cleanup job failed: ${errorMessage}`);
      // This is a background job, but wrapping in a standard error type is good practice.
      throw new DatabaseError(errorMessage);
    }
  }

  /**
   * 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> {
    try {
      const baseUrl = getBaseUrl(logger);
      const avatarUrl = `${baseUrl}/uploads/avatars/${file.filename}`;
      return await db.userRepo.updateUserProfile(userId, { avatar_url: avatarUrl }, logger);
    } catch (error) {
      // Re-throw known application errors without logging them as system errors.
      if (error instanceof NotFoundError) {
        throw error;
      }
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error({ err: error, userId }, `Failed to update user avatar: ${errorMessage}`);
      // Wrap unexpected errors.
      throw new DatabaseError(errorMessage);
    }
  }
  /**
   * 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> {
    try {
      const saltRounds = 10;
      const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
      await db.userRepo.updateUserPassword(userId, hashedPassword, logger);
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error({ err: error, userId }, `Failed to update user password: ${errorMessage}`);
      // Wrap unexpected errors.
      throw new DatabaseError(errorMessage);
    }
  }

  /**
   * 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> {
    try {
      const userWithHash = await db.userRepo.findUserWithPasswordHashById(userId, logger);
      if (!userWithHash || !userWithHash.password_hash) {
        throw new NotFoundError('User not found or password not set.');
      }
      const isMatch = await bcrypt.compare(password, userWithHash.password_hash);
      if (!isMatch) {
        throw new ValidationError([], 'Incorrect password.');
      }
      await db.userRepo.deleteUserById(userId, logger);
    } catch (error) {
      if (error instanceof NotFoundError || error instanceof ValidationError) {
        throw error;
      }
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error({ err: error, userId }, `Failed to delete user account: ${errorMessage}`);
      // Wrap unexpected errors.
      throw new DatabaseError(errorMessage);
    }
  }

  /**
   * 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> {
    if (userProfile.address_id !== addressId) {
      throw new ValidationError([], 'Forbidden: You can only access your own address.');
    }
    try {
      return await db.addressRepo.getAddressById(addressId, logger);
    } catch (error) {
      if (error instanceof NotFoundError) {
        throw error;
      }
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      logger.error({ err: error, userId: userProfile.user.user_id, addressId }, `Failed to get user address: ${errorMessage}`);
      // Wrap unexpected errors.
      throw new DatabaseError(errorMessage);
    }
  }

  /**
   * 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.');
    }
    try {
      await db.userRepo.deleteUserById(userToDeleteId, log);
    } catch (error) {
      // Rethrow known errors so they are handled correctly by the API layer (e.g. 404 for NotFound)
      if (error instanceof ValidationError || error instanceof NotFoundError) {
        throw error;
      }
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
      log.error({ err: error, deleterId, userToDeleteId }, `Admin failed to delete user account: ${errorMessage}`);
      // Wrap unexpected errors.
      throw new DatabaseError(errorMessage);
    }
  }
}

export const userService = new UserService();
