// src/services/db/personalization.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool, withTransaction } from './connection.db'; 
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors.db';
import type { Logger } from 'pino';
import {
  MasterGroceryItem,
  PantryRecipe,
  RecommendedRecipe,
  WatchedItemDeal,
  PantryItemConversion,
  DietaryRestriction,
  Appliance,
  UserAppliance,
  Recipe,
} from '../../types';

export class PersonalizationRepository {
  private db: Pool | PoolClient;

  constructor(db: Pool | PoolClient = getPool()) {
    this.db = db;
  }

  /**
   * Retrieves all master grocery items from the database.
   * @returns A promise that resolves to an array of MasterGroceryItem objects.
   */
  async getAllMasterItems(logger: Logger): Promise<MasterGroceryItem[]> {
    try {
      const res = await this.db.query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items ORDER BY name ASC');
      return res.rows;
    } catch (error) {
      logger.error({ err: error }, 'Database error in getAllMasterItems');
      throw new Error('Failed to retrieve master grocery items.');
    }
  }

  /**
   * Retrieves all watched master items for a specific user.
   * @param userId The UUID of the user.
   * @returns A promise that resolves to an array of MasterGroceryItem objects.
   */
  async getWatchedItems(userId: string, logger: Logger): Promise<MasterGroceryItem[]> {
    try {
      const query = `
        SELECT mgi.*
        FROM public.master_grocery_items mgi
        JOIN public.user_watched_items uwi ON mgi.master_grocery_item_id = uwi.master_item_id
        WHERE uwi.user_id = $1
        ORDER BY mgi.name ASC;
      `;
      const res = await this.db.query<MasterGroceryItem>(query, [userId]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in getWatchedItems');
      throw new Error('Failed to retrieve watched items.');
    }
  }

  /**
   * Removes an item from a user's watchlist.
   * @param userId The UUID of the user.
   * @param masterItemId The ID of the master item to remove.
   */
  async removeWatchedItem(userId: string, masterItemId: number, logger: Logger): Promise<void> {
    try {
      await this.db.query('DELETE FROM public.user_watched_items WHERE user_id = $1 AND master_item_id = $2', [userId, masterItemId]);
    } catch (error) {
      logger.error({ err: error, userId, masterItemId }, 'Database error in removeWatchedItem');
      throw new Error('Failed to remove item from watchlist.');
    }
  }

  /**
   * Finds the owner of a specific pantry item.
   * @param pantryItemId The ID of the pantry item.
   * @returns A promise that resolves to an object containing the user_id, or undefined if not found.
   */
  async findPantryItemOwner(pantryItemId: number, logger: Logger): Promise<{ user_id: string } | undefined> {
    try {
      const res = await this.db.query<{ user_id: string }>(
        'SELECT user_id FROM public.pantry_items WHERE pantry_item_id = $1',
        [pantryItemId]
      );
      return res.rows[0];
    } catch (error) {
      logger.error({ err: error, pantryItemId }, 'Database error in findPantryItemOwner');
      throw new Error('Failed to retrieve pantry item owner from database.');
    }
  }

  /**
   * Adds an item to a user's watchlist. If the master item doesn't exist, it creates it.
   * This method should be wrapped in a transaction by the calling service if other operations depend on it.
   * @param userId The UUID of the user.
   * @param itemName The name of the item to watch.
   * @param categoryName The category of the item.
   * @returns A promise that resolves to the MasterGroceryItem that was added to the watchlist.
   */
  async addWatchedItem(userId: string, itemName: string, categoryName: string, logger: Logger): Promise<MasterGroceryItem> {
    try {
      return await withTransaction(async (client) => {
        // Find category ID
        const categoryRes = await client.query<{ category_id: number }>('SELECT category_id FROM public.categories WHERE name = $1', [categoryName]);
        const categoryId = categoryRes.rows[0]?.category_id;
        if (!categoryId) {
          throw new Error(`Category '${categoryName}' not found.`);
        }

        // Find or create master item
        let masterItem: MasterGroceryItem;
        const masterItemRes = await client.query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items WHERE name = $1', [itemName]);
        if (masterItemRes.rows.length > 0) {
          masterItem = masterItemRes.rows[0];
        } else {
          const newMasterItemRes = await client.query<MasterGroceryItem>(
            'INSERT INTO public.master_grocery_items (name, category_id) VALUES ($1, $2) RETURNING *',
            [itemName, categoryId]
          );
          masterItem = newMasterItemRes.rows[0];
        }

        // Add to user's watchlist, ignoring if it's already there.
        await client.query(
          'INSERT INTO public.user_watched_items (user_id, master_item_id) VALUES ($1, $2) ON CONFLICT (user_id, master_item_id) DO NOTHING',
          [userId, masterItem.master_grocery_item_id]
        );

        return masterItem;
      });
    } catch (error) {
      // The withTransaction helper will handle rollback. We just need to handle specific errors.
      if (error instanceof Error && 'code' in error) {
        if (error.code === '23505') { // unique_violation
          // This case is handled by ON CONFLICT, but it's good practice for other functions.
          throw new UniqueConstraintError('This item is already in the watchlist.');
        } else if (error.code === '23503') { // foreign_key_violation
          throw new ForeignKeyConstraintError('The specified user or category does not exist.');
        }
      }
      logger.error({ err: error, userId, itemName, categoryName }, 'Transaction error in addWatchedItem');
      throw new Error('Failed to add item to watchlist.');
    }
  }

  /**
   * Calls a database function to get the best current sale prices for all watched items across all users.
   * This is much more efficient than calling getBestSalePricesForUser for each user individually.
   * @returns A promise that resolves to an array of deals, each augmented with user information.
   */
  async getBestSalePricesForAllUsers(logger: Logger): Promise<(WatchedItemDeal & { user_id: string; email: string; full_name: string | null })[]> {
    try {
      // This function assumes a corresponding PostgreSQL function `get_best_sale_prices_for_all_users` exists.
      const res = await this.db.query<(WatchedItemDeal & { user_id: string; email: string; full_name: string | null })>('SELECT * FROM public.get_best_sale_prices_for_all_users()');
      return res.rows;
    } catch (error) {
      logger.error({ err: error }, 'Database error in getBestSalePricesForAllUsers');
      throw new Error('Failed to get best sale prices for all users.');
    }
  }

  /**
   * Retrieves the master list of all available kitchen appliances.
   * @returns A promise that resolves to an array of Appliance objects.
   */
  async getAppliances(logger: Logger): Promise<Appliance[]> {
    try {
      const res = await this.db.query<Appliance>('SELECT * FROM public.appliances ORDER BY name');
      return res.rows;
    } catch (error) {
      logger.error({ err: error }, 'Database error in getAppliances');
      throw new Error('Failed to get appliances.');
    }
  }

  /**
   * Retrieves the master list of all available dietary restrictions.
   * @returns A promise that resolves to an array of DietaryRestriction objects.
   */
  async getDietaryRestrictions(logger: Logger): Promise<DietaryRestriction[]> {
    try {
      const res = await this.db.query<DietaryRestriction>('SELECT * FROM public.dietary_restrictions ORDER BY type, name');
      return res.rows;
    } catch (error) {
      logger.error({ err: error }, 'Database error in getDietaryRestrictions');
      throw new Error('Failed to get dietary restrictions.');
    }
  }

  /**
   * Retrieves the dietary restrictions for a specific user.
   * @param userId The ID of the user.
   * @returns A promise that resolves to an array of the user's selected DietaryRestriction objects.
   */
  async getUserDietaryRestrictions(userId: string, logger: Logger): Promise<DietaryRestriction[]> {
    try {
      const query = `
            SELECT dr.* FROM public.dietary_restrictions dr
            JOIN public.user_dietary_restrictions udr ON dr.dietary_restriction_id = udr.restriction_id
            WHERE udr.user_id = $1 ORDER BY dr.type, dr.name;
        `;
      const res = await this.db.query<DietaryRestriction>(query, [userId]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in getUserDietaryRestrictions');
      throw new Error('Failed to get user dietary restrictions.');
    }
  }

  /**
   * Sets the dietary restrictions for a user, replacing any existing ones.
   * @param userId The ID of the user.
   * @param restrictionIds An array of IDs for the selected dietary restrictions.
   * @returns A promise that resolves when the operation is complete.
   */
  async setUserDietaryRestrictions(userId: string, restrictionIds: number[], logger: Logger): Promise<void> {
    try {
      await withTransaction(async (client) => {
        // 1. Clear existing restrictions for the user
        await client.query('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', [userId]);
        // 2. Insert new ones if any are provided
        if (restrictionIds.length > 0) {
          // Using unnest is safer than string concatenation and prevents SQL injection.
          const insertQuery = `INSERT INTO public.user_dietary_restrictions (user_id, restriction_id) SELECT $1, unnest($2::int[])`;
          await client.query(insertQuery, [userId, restrictionIds]);
        }
      });
    } catch (error) {
      // Check for a foreign key violation, which would mean an invalid ID was provided.
      if (error instanceof Error && 'code' in error && error.code === '23503') {
        throw new ForeignKeyConstraintError('One or more of the specified restriction IDs are invalid.');
      }
      logger.error({ err: error, userId, restrictionIds }, 'Database error in setUserDietaryRestrictions');
      throw new Error('Failed to set user dietary restrictions.');
    }
  }

  /**
   * Sets the kitchen appliances for a user, replacing any existing ones.
   * @param userId The ID of the user.
   * @param applianceIds An array of IDs for the selected appliances.
   * @returns A promise that resolves when the operation is complete.
   */
  async setUserAppliances(userId: string, applianceIds: number[], logger: Logger): Promise<UserAppliance[]> {
    try {
      return await withTransaction(async (client) => {
        // 1. Clear existing appliances for the user
        await client.query('DELETE FROM public.user_appliances WHERE user_id = $1', [userId]);

        let newAppliances: UserAppliance[] = [];
        // 2. Insert new ones if any are provided
        if (applianceIds.length > 0) {
          const insertQuery = `INSERT INTO public.user_appliances (user_id, appliance_id) SELECT $1, unnest($2::int[]) RETURNING *`;
          const res = await client.query<UserAppliance>(insertQuery, [userId, applianceIds]);
          newAppliances = res.rows;
        }
        return newAppliances;
      });
    } catch (error) {
      // Check for a foreign key violation, which would mean an invalid ID was provided.
      if (error instanceof Error && 'code' in error && error.code === '23503') {
        throw new ForeignKeyConstraintError('Invalid appliance ID');
      }
      logger.error({ err: error, userId, applianceIds }, 'Database error in setUserAppliances');
      throw new Error('Failed to set user appliances.');
    }
  }

  /**
   * Retrieves the kitchen appliances for a specific user.
   * @param userId The ID of the user.
   * @returns A promise that resolves to an array of the user's selected Appliance objects.
   */
  async getUserAppliances(userId: string, logger: Logger): Promise<Appliance[]> {
    try {
      const query = `
        SELECT a.* FROM public.appliances a
        JOIN public.user_appliances ua ON a.appliance_id = ua.appliance_id
        WHERE ua.user_id = $1 ORDER BY a.name;
    `;
      const res = await this.db.query<Appliance>(query, [userId]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in getUserAppliances');
      throw new Error('Failed to get user appliances.');
    }
  }

  /**
   * Calls a database function to find recipes that can be made from a user's pantry.
   * @param userId The ID of the user.
   * @returns A promise that resolves to an array of recipes.
   */
  async findRecipesFromPantry(userId: string, logger: Logger): Promise<PantryRecipe[]> {
      try {
          const res = await this.db.query<PantryRecipe>('SELECT * FROM public.find_recipes_from_pantry($1)', [userId]);
          return res.rows;
      } catch (error) {
          logger.error({ err: error, userId }, 'Database error in findRecipesFromPantry');
          throw new Error('Failed to find recipes from pantry.');
      }
  }

  /**
   * Calls a database function to recommend recipes for a user.
   * @param userId The ID of the user.
   * @param limit The maximum number of recipes to recommend.
   * @returns A promise that resolves to an array of recommended recipes.
   */
  async recommendRecipesForUser(userId: string, limit: number, logger: Logger): Promise<RecommendedRecipe[]> {
      try {
          const res = await this.db.query<RecommendedRecipe>('SELECT * FROM public.recommend_recipes_for_user($1, $2)', [userId, limit]);
          return res.rows;
      } catch (error) {
          logger.error({ err: error, userId, limit }, 'Database error in recommendRecipesForUser');
          throw new Error('Failed to recommend recipes.');
      }
  }

  /**
   * Calls a database function to get the best current sale prices for a user's watched items.
   * @param userId The ID of the user.
   * @returns A promise that resolves to an array of the best deals.
   */
  async getBestSalePricesForUser(userId: string, logger: Logger): Promise<WatchedItemDeal[]> {
      try {
          const res = await this.db.query<WatchedItemDeal>('SELECT * FROM public.get_best_sale_prices_for_user($1)', [userId]);
          return res.rows;
      } catch (error) {
          logger.error({ err: error, userId }, 'Database error in getBestSalePricesForUser');
          throw new Error('Failed to get best sale prices.');
      }
  }

  /**
   * Calls a database function to suggest unit conversions for a pantry item.
   * @param pantryItemId The ID of the pantry item.
   * @returns A promise that resolves to an array of suggested conversions.
   */
  async suggestPantryItemConversions(pantryItemId: number, logger: Logger): Promise<PantryItemConversion[]> {
      try {
          const res = await this.db.query<PantryItemConversion>('SELECT * FROM public.suggest_pantry_item_conversions($1)', [pantryItemId]);
          return res.rows;
      } catch (error) {
          logger.error({ err: error, pantryItemId }, 'Database error in suggestPantryItemConversions');
          throw new Error('Failed to suggest pantry item conversions.');
      }
  }

  /**
   * Calls a database function to get recipes that are compatible with a user's dietary restrictions.
   * @param userId The ID of the user.
   * @returns A promise that resolves to an array of compatible Recipe objects.
   */
  async getRecipesForUserDiets(userId: string, logger: Logger): Promise<Recipe[]> {
      try {
          const res = await this.db.query<Recipe>('SELECT * FROM public.get_recipes_for_user_diets($1)', [userId]); // This is a standalone function, no change needed here.
          return res.rows;
      } catch (error) {
          logger.error({ err: error, userId }, 'Database error in getRecipesForUserDiets');
          throw new Error('Failed to get recipes compatible with user diet.');
      }
  }
}