// src/services/db/user.db.ts
import { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db'; 
import type { Logger } from 'pino';
import { UniqueConstraintError, ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { Profile, MasterGroceryItem, ShoppingList, ActivityLogItem, UserProfile, SearchQuery } from '../../types';
import { ShoppingRepository } from './shopping.db';
import { PersonalizationRepository } from './personalization.db';
import { withTransaction } from './connection.db';

/**
 * Defines the structure of a user object as returned from the database.
 */
interface DbUser {
    user_id: string;
    email: string;
    // The password_hash can be null for users who signed up via OAuth.
    password_hash: string | null;
    refresh_token?: string | null;
    failed_login_attempts: number;
    last_failed_login: string | null; // This will be a date string from the DB
}

export class UserRepository {
  private db: Pool | PoolClient;

  constructor(db: Pool | PoolClient = getPool()) {
    this.db = db;
  }
  /**
   * Finds a user by their email in the public.users table.
   * @param email The email of the user to find.
   * @returns A promise that resolves to the user object or undefined if not found.
   */
  async findUserByEmail(email: string, logger: Logger): Promise<DbUser | undefined> {
    logger.debug({ email }, `[DB findUserByEmail] Searching for user.`);
    try {
      const res = await this.db.query<DbUser>(
        'SELECT user_id, email, password_hash, refresh_token, failed_login_attempts, last_failed_login FROM public.users WHERE email = $1',
        [email]
      );
      const userFound = res.rows[0];
      logger.debug(`[DB findUserByEmail] Query for ${email} result: ${userFound ? `FOUND user ID ${userFound.user_id}` : 'NOT FOUND'}`);
      return res.rows[0];
    } catch (error) {
      logger.error({ err: error, email }, 'Database error in findUserByEmail');
      throw new Error('Failed to retrieve user from database.');
    }
  }

  /**
   * Creates a new user in the public.users table.
   * This method expects to be run within a transaction, so it requires a PoolClient.
   * @param email The user's email.
   * @param passwordHash The bcrypt hashed password.
   * @param profileData An object containing optional full_name and avatar_url for the profile.
   * @returns A promise that resolves to the newly created user object (id, email).
   */
  async createUser(
    email: string,
    passwordHash: string | null,
    profileData: { full_name?: string; avatar_url?: string },
    logger: Logger
  ): Promise<UserProfile> {
    return withTransaction(async (client: PoolClient) => {
      logger.debug(`[DB createUser] Starting transaction for email: ${email}`);

      // Use 'set_config' to safely pass parameters to a configuration variable.
      await client.query("SELECT set_config('my_app.user_metadata', $1, true)", [JSON.stringify(profileData)]);
      logger.debug(`[DB createUser] Session metadata set for ${email}.`);

      // Insert the new user into the 'users' table. This will fire the trigger.
      const userInsertRes = await client.query<{ user_id: string }>(
        'INSERT INTO public.users (email, password_hash) VALUES ($1, $2) RETURNING user_id, email',
        [email, passwordHash]
      );
      const newUserId = userInsertRes.rows[0].user_id;
      logger.debug(`[DB createUser] Inserted into users table. New user ID: ${newUserId}`);

      // After the trigger has run, fetch the complete profile data.
      const profileQuery = `
        SELECT u.user_id, u.email, p.full_name, p.avatar_url, p.role, p.points, p.preferences
        FROM public.users u
        JOIN public.profiles p ON u.user_id = p.user_id
        WHERE u.user_id = $1;
      `;
      const finalProfileRes = await client.query(profileQuery, [newUserId]);
      const flatProfile = finalProfileRes.rows[0];

      if (!flatProfile) {
        throw new Error('Failed to create or retrieve user profile after registration.');
      }

      // Construct the nested UserProfile object to match the type definition.
      const fullUserProfile: UserProfile = {
        user: {
          user_id: flatProfile.user_id,
          email: flatProfile.email,
        },
        user_id: flatProfile.user_id,
        full_name: flatProfile.full_name,
        avatar_url: flatProfile.avatar_url,
        role: flatProfile.role,
        points: flatProfile.points,
        preferences: flatProfile.preferences,
      };

      logger.debug({ user: fullUserProfile }, `[DB createUser] Fetched full profile for new user:`);
      return fullUserProfile;
    }).catch(error => {
      // Check for specific PostgreSQL error codes
      if (error instanceof Error && 'code' in error && error.code === '23505') {
        logger.warn(`Attempted to create a user with an existing email: ${email}`);
        throw new UniqueConstraintError('A user with this email address already exists.');
      }
      // The withTransaction helper logs the rollback, so we just log the context here.
      logger.error({ err: error, email }, 'Error during createUser transaction');
      throw new Error('Failed to create user in database.');
    });
  }

  /**
   * Finds a user by their email and joins their profile data.
   * This is used by the LocalStrategy to get all necessary data for authentication and session creation in one query.
   * @param email The email of the user to find.
   * @returns A promise that resolves to the combined user and profile object or undefined if not found.
   */
  async findUserWithProfileByEmail(email: string, logger: Logger): Promise<(DbUser & Profile) | undefined> {
    logger.debug({ email }, `[DB findUserWithProfileByEmail] Searching for user.`);
    try {
      const query = `
        SELECT u.*, p.full_name, p.avatar_url, p.role, p.points, p.preferences, p.address_id
        FROM public.users u
        JOIN public.profiles p ON u.user_id = p.user_id
        WHERE u.email = $1;
      `;
      const res = await this.db.query<(DbUser & Profile)>(query, [email]);
      return res.rows[0];
    } catch (error) {
      logger.error({ err: error, email }, 'Database error in findUserWithProfileByEmail');
      throw new Error('Failed to retrieve user with profile from database.');
    }
  }

  /**
   * Finds a user by their ID. Used by the JWT strategy to validate tokens.
   * @param id The UUID of the user to find.
   * @returns A promise that resolves to the user object (id, email) or undefined if not found.
   */
  // prettier-ignore
  async findUserById(userId: string, logger: Logger): Promise<{ user_id: string; email: string; }> {
    try {
      const res = await this.db.query<{ user_id: string; email: string }>(
        'SELECT user_id, email FROM public.users WHERE user_id = $1',
        [userId]
      );
      if (res.rowCount === 0) {
        throw new NotFoundError(`User with ID ${userId} not found.`);
      }
      return res.rows[0];
    } catch (error) {
      if (error instanceof NotFoundError) throw error;
      logger.error({ err: error, userId }, 'Database error in findUserById');
      throw new Error('Failed to retrieve user by ID from database.');
    }
  }

  /**
   * Finds a user by their ID, including their password hash.
   * This should only be used in contexts where password verification is required, like account deletion.
   * @param id The UUID of the user to find.
   * @returns A promise that resolves to the user object (id, email, password_hash) or undefined if not found.
   */
  // prettier-ignore
  async findUserWithPasswordHashById(userId: string, logger: Logger): Promise<{ user_id: string; email: string; password_hash: string | null }> {
    try {
      const res = await this.db.query<{ user_id: string; email: string; password_hash: string | null }>(
        'SELECT user_id, email, password_hash FROM public.users WHERE user_id = $1',
        [userId]
      );
      if ((res.rowCount ?? 0) === 0) {
        throw new NotFoundError(`User with ID ${userId} not found.`);
      }
      return res.rows[0];
    } catch (error) {
      if (error instanceof NotFoundError) throw error;
      logger.error({ err: error, userId }, 'Database error in findUserWithPasswordHashById');
      throw new Error('Failed to retrieve user with sensitive data by ID from database.');
    }
  }

  /**
   * Finds a user's profile by their user ID.
   * @param id The UUID of the user.
   * @returns A promise that resolves to the user's profile object or undefined if not found.
   */
  // prettier-ignore
  async findUserProfileById(userId: string, logger: Logger): Promise<Profile> {
    try {
      const res = await this.db.query<Profile>(
        `SELECT 
           p.user_id, p.full_name, p.avatar_url, p.address_id, p.preferences, p.role, p.points,
           CASE
             WHEN a.address_id IS NOT NULL THEN json_build_object(
               'address_id', a.address_id,
               'address_line_1', a.address_line_1,
               'address_line_2', a.address_line_2,
               'city', a.city,
               'province_state', a.province_state,
               'postal_code', a.postal_code,
               'country', a.country
             )
             ELSE NULL
           END as address
         FROM public.profiles p
         LEFT JOIN public.addresses a ON p.address_id = a.address_id
         WHERE p.user_id = $1`,
        [userId]
      );
      if (res.rowCount === 0) {
        throw new NotFoundError('Profile not found for this user.');
      }
      return res.rows[0];
    } catch (error) {
      if (error instanceof NotFoundError) {
        throw error;
      }
      logger.error({ err: error, userId }, 'Database error in findUserProfileById');
      throw new Error('Failed to retrieve user profile from database.');
    }
  }

  /**
   * Updates the profile for a given user.
   * @param id The UUID of the user.
   * @param profileData The profile data to update (e.g., full_name, avatar_url).
   * @returns A promise that resolves to the updated profile object.
   */
  // prettier-ignore
  async updateUserProfile(userId: string, profileData: Partial<Pick<Profile, 'full_name' | 'avatar_url' | 'address_id'>>, logger: Logger): Promise<Profile> {
    try {
      const { full_name, avatar_url, address_id } = profileData;
      const fieldsToUpdate = [];
      const values = [];
      let paramIndex = 1;

      if (full_name !== undefined) { fieldsToUpdate.push(`full_name = $${paramIndex++}`); values.push(full_name); }
      if (avatar_url !== undefined) { fieldsToUpdate.push(`avatar_url = $${paramIndex++}`); values.push(avatar_url); }
      if (address_id !== undefined) { fieldsToUpdate.push(`address_id = $${paramIndex++}`); values.push(address_id); }

      if (fieldsToUpdate.length === 0) {
        return this.findUserProfileById(userId, logger) as Promise<Profile>;
      }

      values.push(userId);
      const query = `
        UPDATE public.profiles
        SET ${fieldsToUpdate.join(', ')}, updated_at = now()
        WHERE user_id = $${paramIndex}
        RETURNING *;
      `;

      const res = await this.db.query<Profile>(
        query, values
      );
      if (res.rowCount === 0) {
        throw new NotFoundError('User not found or user does not have permission to update.');
      }
      return res.rows[0];
    } catch (error) {
      if (error instanceof NotFoundError) {
        throw error;
      }
      logger.error({ err: error, userId, profileData }, 'Database error in updateUserProfile');
      throw new Error('Failed to update user profile in database.');
    }
  }

  /**
   * Updates the preferences for a given user.
   * @param id The UUID of the user.
   * @param preferences The preferences object to save.
   * @returns A promise that resolves to the updated profile object.
   */
  // prettier-ignore
  async updateUserPreferences(userId: string, preferences: Profile['preferences'], logger: Logger): Promise<Profile> {
    try {
      const res = await this.db.query<Profile>(
        `UPDATE public.profiles
         SET preferences = COALESCE(preferences, '{}'::jsonb) || $1, updated_at = now()
         WHERE user_id = $2
         RETURNING user_id, full_name, avatar_url, preferences, role`,
        [preferences, userId]
      );
      if (res.rowCount === 0) {
        throw new NotFoundError('User not found or user does not have permission to update.');
      }
      return res.rows[0];
    } catch (error) {
      if (error instanceof NotFoundError) {
        throw error;
      }
      logger.error({ err: error, userId, preferences }, 'Database error in updateUserPreferences');
      throw new Error('Failed to update user preferences in database.');
    }
  }

  /**
   * Updates the password hash for a given user.
   * @param id The UUID of the user.
   * @param passwordHash The new bcrypt hashed password.
   */
  // prettier-ignore
  async updateUserPassword(userId: string, passwordHash: string, logger: Logger): Promise<void> {
    try {
      await this.db.query(
        'UPDATE public.users SET password_hash = $1 WHERE user_id = $2',
        [passwordHash, userId]
      );
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in updateUserPassword');
      throw new Error('Failed to update user password in database.');
    }
  }

  /**
   * Deletes a user from the database by their ID.
   * @param id The UUID of the user to delete.
   */
  // prettier-ignore
  async deleteUserById(userId: string, logger: Logger): Promise<void> {
    try {
      await this.db.query('DELETE FROM public.users WHERE user_id = $1', [userId]);
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in deleteUserById');
      throw new Error('Failed to delete user from database.');
    }
  }

  /**
   * Saves or updates a refresh token for a user.
   * @param userId The UUID of the user.
   * @param refreshToken The new refresh token to save.
   */
  // prettier-ignore
  async saveRefreshToken(userId: string, refreshToken: string, logger: Logger): Promise<void> {
    try {
      await this.db.query(
        'UPDATE public.users SET refresh_token = $1 WHERE user_id = $2',
        [refreshToken, userId]
      );
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in saveRefreshToken');
      throw new Error('Failed to save refresh token.');
    }
  }

  /**
   * Finds a user by their refresh token.
   * @param refreshToken The refresh token to look up.
   * @returns A promise that resolves to the user object (id, email) or undefined if not found.
   */
  // prettier-ignore
  async findUserByRefreshToken(refreshToken: string, logger: Logger): Promise<{ user_id: string; email: string; }> {
    try {
      const res = await this.db.query<{ user_id: string; email: string }>(
        'SELECT user_id, email FROM public.users WHERE refresh_token = $1',
        [refreshToken]
      );
      if ((res.rowCount ?? 0) === 0) {
        throw new NotFoundError('User not found for the given refresh token.');
      }
      return res.rows[0];
    } catch (error) {
      if (error instanceof NotFoundError) throw error;
      logger.error({ err: error }, 'Database error in findUserByRefreshToken');
      throw new Error('Failed to find user by refresh token.'); // Generic error for other failures
    }
  }

  /**
   * Deletes a refresh token from the database by setting it to NULL.
   * @param refreshToken The refresh token to delete.
   */
  async deleteRefreshToken(refreshToken: string, logger: Logger): Promise<void> {
    try {
      await this.db.query('UPDATE public.users SET refresh_token = NULL WHERE refresh_token = $1', [refreshToken]);
    } catch (error) {
      logger.error({ err: error }, 'Database error in deleteRefreshToken');
    }
  }

  /**
   * Creates a password reset token for a user.
   * @param userId The UUID of the user.
   * @param tokenHash The hashed version of the reset token.
   * @param expiresAt The timestamp when the token expires.
   */
  // prettier-ignore
  async createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date, logger: Logger): Promise<void> {
    const client = this.db as PoolClient;
    try {
      await client.query('DELETE FROM public.password_reset_tokens WHERE user_id = $1', [userId]);
      await client.query(
        'INSERT INTO public.password_reset_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)',
        [userId, tokenHash, expiresAt]
      );
    } catch (error) {
      if (error instanceof Error && 'code' in error && error.code === '23503') {
        throw new ForeignKeyConstraintError('The specified user does not exist.');
      }
      logger.error({ err: error, userId }, 'Database error in createPasswordResetToken');
      throw new Error('Failed to create password reset token.');
    }
  }

  /**
   * Finds a user and token details by the token hash.
   * @returns A promise that resolves to an array of valid token records.
   */
  // prettier-ignore
  async getValidResetTokens(logger: Logger): Promise<{ user_id: string; token_hash: string; expires_at: Date }[]> {
    try {
      const res = await this.db.query<{ user_id: string; token_hash: string; expires_at: Date }>(
        'SELECT user_id, token_hash, expires_at FROM public.password_reset_tokens WHERE expires_at > NOW()'
      );
      return res.rows;
    } catch (error) {
      logger.error({ err: error }, 'Database error in getValidResetTokens');
      throw new Error('Failed to retrieve valid reset tokens.');
    }
  }

  /**
   * Deletes a password reset token by its hash.
   * @param tokenHash The hashed token to delete.
   */
  // prettier-ignore
  async deleteResetToken(tokenHash: string, logger: Logger): Promise<void> {
    try {
      await this.db.query('DELETE FROM public.password_reset_tokens WHERE token_hash = $1', [tokenHash]);
    } catch (error) {
      logger.error({ err: error, tokenHash }, 'Database error in deleteResetToken');
    }
  }

  /**
   * Creates a following relationship between two users.
   * @param followerId The ID of the user who is following.
   * @param followingId The ID of the user being followed.
   */
  async followUser(followerId: string, followingId: string, logger: Logger): Promise<void> {
    try {
      await this.db.query(
        'INSERT INTO public.user_follows (follower_id, following_id) VALUES ($1, $2) ON CONFLICT (follower_id, following_id) DO NOTHING',
        [followerId, followingId]
      );
    } catch (error) {
      if (error instanceof Error && 'code' in error && error.code === '23503') {
        throw new ForeignKeyConstraintError('One or both users do not exist.');
      }
      logger.error({ err: error, followerId, followingId }, 'Database error in followUser');
      throw new Error('Failed to follow user.');
    }
  }

  /**
   * Removes a following relationship between two users.
   * @param followerId The ID of the user who is unfollowing.
   * @param followingId The ID of the user being unfollowed.
   */
  async unfollowUser(followerId: string, followingId: string, logger: Logger): Promise<void> {
    try {
      await this.db.query('DELETE FROM public.user_follows WHERE follower_id = $1 AND following_id = $2', [followerId, followingId]);
    } catch (error) {
      logger.error({ err: error, followerId, followingId }, 'Database error in unfollowUser');
      throw new Error('Failed to unfollow user.');
    }
  }

  /**
   * Retrieves a personalized activity feed for a user, based on the actions of users they follow.
   * @param userId The ID of the user for whom to generate the feed.
   * @param limit The number of feed items to retrieve.
   * @param offset The number of feed items to skip for pagination.
   * @returns A promise that resolves to an array of ActivityLogItem objects.
   */
  async getUserFeed(userId: string, limit: number, offset: number, logger: Logger): Promise<ActivityLogItem[]> {
    try {
      const query = `
        SELECT al.*, p.full_name as user_full_name, p.avatar_url as user_avatar_url
        FROM public.activity_log al
        JOIN public.user_follows uf ON al.user_id = uf.following_id
        JOIN public.profiles p ON al.user_id = p.user_id
        WHERE uf.follower_id = $1
        ORDER BY al.created_at DESC
        LIMIT $2 OFFSET $3;
      `;
      const res = await this.db.query<ActivityLogItem>(query, [userId, limit, offset]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, userId, limit, offset }, 'Database error in getUserFeed');
      throw new Error('Failed to retrieve user feed.');
    }
  }

  /**
   * Logs a search query made by a user.
   * @param queryData The search query data to log.
   * @returns A promise that resolves to the created SearchQuery object.
   */
  async logSearchQuery(queryData: Omit<SearchQuery, 'search_query_id' | 'created_at'>, logger: Logger): Promise<SearchQuery> {
    const { user_id, query_text, result_count, was_successful } = queryData;
    try {
      const res = await this.db.query<SearchQuery>(
        'INSERT INTO public.search_queries (user_id, query_text, result_count, was_successful) VALUES ($1, $2, $3, $4) RETURNING *',
        [user_id, query_text, result_count, was_successful]
      );
      return res.rows[0];
    } catch (error) {
      logger.error({ err: error, queryData }, 'Database error in logSearchQuery');
      throw new Error('Failed to log search query.');
    }
  }
}

/**
 * Gathers all data associated with a specific user for export.
 * @param userId The UUID of the user.
 * @returns A promise that resolves to an object containing all user data.
 */
// prettier-ignore
export async function exportUserData(userId: string, logger: Logger): Promise<{ profile: Profile; watchedItems: MasterGroceryItem[]; shoppingLists: ShoppingList[] }> {
  try {
    return await withTransaction(async (client) => {
      const userRepo = new UserRepository(client);
      const shoppingRepo = new ShoppingRepository(client);
      const personalizationRepo = new PersonalizationRepository(client);

      // Run queries in parallel for efficiency within the transaction
      const profileQuery = userRepo.findUserProfileById(userId, logger);
      const watchedItemsQuery = personalizationRepo.getWatchedItems(userId, logger);
      const shoppingListsQuery = shoppingRepo.getShoppingLists(userId, logger);

      const [profile, watchedItems, shoppingLists] = await Promise.all([profileQuery, watchedItemsQuery, shoppingListsQuery]);

      if (!profile) {
        // This will be caught by withTransaction and rolled back (though no writes were made)
        throw new NotFoundError('User profile not found for data export.');
      }

      return { profile, watchedItems, shoppingLists };
    });
  } catch (error) {
    logger.error({ err: error, userId }, 'Database error in exportUserData');
    throw new Error('Failed to export user data.');
  }
}