// src/services/db/budget.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool, withTransaction } from './connection.db';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import type { Logger } from 'pino';
import type { Budget, SpendingByCategory } from '../../types';
import { GamificationRepository } from './gamification.db';

export class BudgetRepository {
  private db: Pool | PoolClient;

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

  /**
   * Retrieves all budgets for a specific user.
   * @param userId The UUID of the user.
   * @returns A promise that resolves to an array of Budget objects.
   */
  async getBudgetsForUser(userId: string, logger: Logger): Promise<Budget[]> {
    try {
      const res = await this.db.query<Budget>(
        'SELECT * FROM public.budgets WHERE user_id = $1 ORDER BY start_date DESC',
        [userId]
      );
      return res.rows;
    } catch (error) {
      logger.error({ err: error, userId }, 'Database error in getBudgetsForUser');
      throw new Error('Failed to retrieve budgets.');
    }
  }

  /**
   * Creates a new budget for a user.
   * @param userId The ID of the user creating the budget.
   * @param budgetData The data for the new budget.
   * @returns A promise that resolves to the newly created Budget object.
   */
  async createBudget(userId: string, budgetData: Omit<Budget, 'budget_id' | 'user_id'>, logger: Logger): Promise<Budget> {
    const { name, amount_cents, period, start_date } = budgetData;
    try {
      return await withTransaction(async (client) => {
        const res = await client.query<Budget>(
          'INSERT INTO public.budgets (user_id, name, amount_cents, period, start_date) VALUES ($1, $2, $3, $4, $5) RETURNING *',
          [userId, name, amount_cents, period, start_date]
        );

        // After successfully creating the budget, try to award the 'First Budget Created' achievement.
        // The award_achievement function handles checking if the user already has it.
        const gamificationRepo = new GamificationRepository(client);
        await gamificationRepo.awardAchievement(userId, 'First Budget Created', logger);
        return res.rows[0];
      });
    } catch (error) {
      // The patch requested this specific error handling.
      // Type-safe check for a PostgreSQL error code.
      // This ensures 'error' is an object with a 'code' property before we access it.
      if (error instanceof Error && 'code' in error && error.code === '23503') {
        throw new ForeignKeyConstraintError('The specified user does not exist.');
      }
      logger.error({ err: error, budgetData, userId }, 'Database error in createBudget');
      throw new Error('Failed to create budget.');
    }
  }

  /**
   * Updates an existing budget.
   * @param budgetId The ID of the budget to update.
   * @param userId The ID of the user who owns the budget (for verification).
   * @param budgetData The data to update.
   * @returns A promise that resolves to the updated Budget object.
   */
  async updateBudget(budgetId: number, userId: string, budgetData: Partial<Omit<Budget, 'budget_id' | 'user_id'>>, logger: Logger): Promise<Budget> {
    const { name, amount_cents, period, start_date } = budgetData;
    try {
      const res = await this.db.query<Budget>(
        `UPDATE public.budgets SET 
           name = COALESCE($1, name), 
           amount_cents = COALESCE($2, amount_cents), 
           period = COALESCE($3, period), 
           start_date = COALESCE($4, start_date)
         WHERE budget_id = $5 AND user_id = $6 RETURNING *`,
        [name, amount_cents, period, start_date, budgetId, userId],
      );
      if (res.rowCount === 0) throw new NotFoundError('Budget 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, budgetId, userId }, 'Database error in updateBudget');
      throw new Error('Failed to update budget.');
    }
  }

  /**
   * Deletes a budget.
   * @param budgetId The ID of the budget to delete.
   * @param userId The ID of the user who owns the budget (for verification).
   */
  async deleteBudget(budgetId: number, userId: string, logger: Logger): Promise<void> {
    try {
      const result = await this.db.query('DELETE FROM public.budgets WHERE budget_id = $1 AND user_id = $2', [budgetId, userId]);
      if (result.rowCount === 0) {
        throw new NotFoundError('Budget not found or user does not have permission to delete.');
      }
    } catch (error) {
      if (error instanceof NotFoundError) throw error;
      logger.error({ err: error, budgetId, userId }, 'Database error in deleteBudget');
      throw new Error('Failed to delete budget.');
    }
  }

  /**
   * Calls the database function to get a user's spending breakdown by category.
   * @param userId The ID of the user.
   * @param startDate The start of the date range.
   * @param endDate The end of the date range.
   * @returns A promise that resolves to an array of spending data.
   */
  async getSpendingByCategory(userId: string, startDate: string, endDate: string, logger: Logger): Promise<SpendingByCategory[]> {
    try {
      const res = await this.db.query<SpendingByCategory>('SELECT * FROM public.get_spending_by_category($1, $2, $3)', [userId, startDate, endDate]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, userId, startDate, endDate }, 'Database error in getSpendingByCategory');
      throw new Error('Failed to get spending analysis.');
    }
  }
}