// src/services/db/flyer.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool, withTransaction } from './connection.db';
import type { Logger } from 'pino';
import { UniqueConstraintError, ForeignKeyConstraintError, NotFoundError } from './errors.db';
import type { Flyer, FlyerItem, FlyerInsert, FlyerItemInsert, Brand, FlyerDbInsert } from '../../types';

export class FlyerRepository {
  private db: Pool | PoolClient;

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

  /**
   * Finds a store by name, or creates it if it doesn't exist.
   * This method is designed to be safe for concurrent calls.
   * @param storeName The name of the store.
   * @returns A promise that resolves to the store's ID.
   */
  async findOrCreateStore(storeName: string, logger: Logger): Promise<number> {
    // Note: This method should be called within a transaction if the caller
    // needs to ensure atomicity with other operations.
    try {
      // First, try to find the store.
      let result = await this.db.query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [storeName]);

      if (result.rows.length > 0) {
        return result.rows[0].store_id;
      } else {
        // If not found, create it.
        result = await this.db.query<{ store_id: number }>('INSERT INTO public.stores (name) VALUES ($1) RETURNING store_id', [storeName]);
        return result.rows[0].store_id;
      }
    } catch (error) {
      // Check for a unique constraint violation on name, which could happen in a race condition
      // if two processes try to create the same store at the same time.
      if (error instanceof Error && 'code' in error && error.code === '23505') {
        try {
          logger.warn({ storeName }, `Race condition avoided: Store was created by another process. Refetching.`);
          const result = await this.db.query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [storeName]);
          if (result.rows.length > 0) return result.rows[0].store_id;
        } catch (recoveryError) {
          // If recovery fails, log a warning and fall through to the generic error handler
          logger.warn({ err: recoveryError, storeName }, 'Race condition recovery failed');
        }
      }
      logger.error({ err: error, storeName }, 'Database error in findOrCreateStore');
      throw new Error('Failed to find or create store in database.');
    }
  }

  /**
   * Inserts a new flyer into the database.
   * @param flyerData - The data for the new flyer.
   * @returns The newly created flyer record with its ID.
   */
  async insertFlyer(flyerData: FlyerDbInsert, logger: Logger): Promise<Flyer> {
    try {
      const query = `
        INSERT INTO flyers (
          file_name, image_url, icon_url, checksum, store_id, valid_from, valid_to,
          store_address, item_count, uploaded_by
        )
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
        RETURNING *;
      `;
      const values = [
        flyerData.file_name, // $1
        flyerData.image_url, // $2
        flyerData.icon_url,  // $3
        flyerData.checksum,  // $4
        flyerData.store_id,  // $5
        flyerData.valid_from,// $6
        flyerData.valid_to,  // $7
        flyerData.store_address, // $8
        flyerData.item_count,    // $9
        flyerData.uploaded_by,   // $10
      ];

      const result = await this.db.query<Flyer>(query, values);
      return result.rows[0];
    } catch (error) {
      logger.error({ err: error, flyerData }, 'Database error in insertFlyer');
      // Check for a unique constraint violation on the 'checksum' column.
      if (error instanceof Error && 'code' in error && error.code === '23505') {
        throw new UniqueConstraintError('A flyer with this checksum already exists.');
      }
      throw new Error('Failed to insert flyer into database.');
    }
  }

  /**
   * Inserts multiple flyer items into the database for a given flyer ID.
   * @param flyerId - The ID of the parent flyer.
   * @param items - An array of item data to insert.
   * @returns An array of the newly created flyer item records.
   */
  async insertFlyerItems(flyerId: number, items: FlyerItemInsert[], logger: Logger): Promise<FlyerItem[]> {
    try {
      if (!items || items.length === 0) {
        return [];
      }

      const values: (string | number | null)[] = [];
      const valueStrings: string[] = [];
      let paramIndex = 1;

      for (const item of items) {
        valueStrings.push(`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++})`);
        values.push(flyerId, item.item, item.price_display, item.price_in_cents, item.quantity, item.category_name ?? null, item.view_count, item.click_count);
      }

      const query = `
        INSERT INTO flyer_items (
          flyer_id, item, price_display, price_in_cents, quantity, category_name, view_count, click_count
        )
        VALUES ${valueStrings.join(', ')}
        RETURNING *;
      `;

      const result = await this.db.query<FlyerItem>(query, values);
      return result.rows;
    } catch (error) {
      logger.error({ err: error, flyerId }, 'Database error in insertFlyerItems');
      // Check for a foreign key violation, which would mean the flyerId is invalid.
      if (error instanceof Error && 'code' in error && error.code === '23503') {
        throw new ForeignKeyConstraintError('The specified flyer does not exist.');
      }
      // Preserve the original error if it's not a foreign key violation,
      // allowing transactional functions to catch and identify the specific failure.
      // This is a higher-level fix for the test failure in `createFlyerAndItems`.
      if (error instanceof Error) throw error;
      throw new Error('An unknown error occurred while inserting flyer items.');
    }
  }

  /**
   * Retrieves all distinct brands from the stores table.
   * @returns A promise that resolves to an array of Brand objects.
   */
  async getAllBrands(logger: Logger): Promise<Brand[]> {
    try {
      const query = `
        SELECT s.store_id as brand_id, s.name, s.logo_url
        FROM public.stores s
        ORDER BY s.name;
      `;
      const res = await this.db.query<Brand>(query);
      return res.rows;
    } catch (error) {
      logger.error({ err: error }, 'Database error in getAllBrands');
      throw new Error('Failed to retrieve brands from database.');
    }
  }

  /**
   * Retrieves a single flyer by its ID.
   * @param flyerId The ID of the flyer to retrieve.
   * @returns A promise that resolves to the Flyer object or undefined if not found.
   */
  async getFlyerById(flyerId: number): Promise<Flyer> {
    const res = await this.db.query<Flyer>('SELECT * FROM public.flyers WHERE flyer_id = $1', [flyerId]);
    if (res.rowCount === 0) throw new NotFoundError(`Flyer with ID ${flyerId} not found.`);
    return res.rows[0];
  }

  /**
   * Retrieves all flyers from the database, ordered by creation date.
   * @param limit The maximum number of flyers to return.
   * @param offset The number of flyers to skip.
   * @returns A promise that resolves to an array of Flyer objects.
   */
  async getFlyers(logger: Logger, limit: number = 20, offset: number = 0): Promise<Flyer[]> {
    try {
      const res = await this.db.query<Flyer>('SELECT * FROM public.flyers ORDER BY created_at DESC LIMIT $1 OFFSET $2', [limit, offset]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, limit, offset }, 'Database error in getFlyers');
      throw new Error('Failed to retrieve flyers from database.');
    }
  }

  /**
   * Retrieves all items for a specific flyer.
   * @param flyerId The ID of the flyer.
   * @returns A promise that resolves to an array of FlyerItem objects.
   */
  async getFlyerItems(flyerId: number, logger: Logger): Promise<FlyerItem[]> {
    try {
      const res = await this.db.query<FlyerItem>('SELECT * FROM public.flyer_items WHERE flyer_id = $1 ORDER BY flyer_item_id ASC', [flyerId]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, flyerId }, 'Database error in getFlyerItems');
      throw new Error('Failed to retrieve flyer items from database.');
    }
  }

  /**
   * Retrieves all flyer items for a given list of flyer IDs.
   * @param flyerIds An array of flyer IDs.
   * @returns A promise that resolves to an array of all matching FlyerItem objects.
   */
  async getFlyerItemsForFlyers(flyerIds: number[], logger: Logger): Promise<FlyerItem[]> {
    try {
      const res = await this.db.query<FlyerItem>('SELECT * FROM public.flyer_items WHERE flyer_id = ANY($1::int[]) ORDER BY flyer_id, flyer_item_id ASC', [flyerIds]);
      return res.rows;
    } catch (error) {
      logger.error({ err: error, flyerIds }, 'Database error in getFlyerItemsForFlyers');
      throw new Error('Failed to retrieve flyer items in batch from database.');
    }
  }

  /**
   * Counts the total number of flyer items for a given list of flyer IDs.
   * @param flyerIds An array of flyer IDs.
   * @returns A promise that resolves to the total count of items.
   */
  async countFlyerItemsForFlyers(flyerIds: number[], logger: Logger): Promise<number> {
    try {
      if (flyerIds.length === 0) {
        return 0;
      }
      const res = await this.db.query<{ count: string }>('SELECT COUNT(*) FROM public.flyer_items WHERE flyer_id = ANY($1::int[])', [flyerIds]);
      return parseInt(res.rows[0].count, 10);
    } catch (error) {
      logger.error({ err: error, flyerIds }, 'Database error in countFlyerItemsForFlyers');
      throw new Error('Failed to count flyer items in batch from database.');
    }
  }

  /**
   * Finds a single flyer by its SHA-256 checksum.
   * @param checksum The checksum of the flyer file to find.
   * @returns A promise that resolves to the Flyer object or undefined if not found.
   */
  async findFlyerByChecksum(checksum: string, logger: Logger): Promise<Flyer | undefined> {
    try {
      const res = await this.db.query<Flyer>('SELECT * FROM public.flyers WHERE checksum = $1', [checksum]);
      return res.rows[0];
    } catch (error) {
      logger.error({ err: error, checksum }, 'Database error in findFlyerByChecksum');
      throw new Error('Failed to find flyer by checksum in database.');
    }
  }

  /**
   * Tracks a user interaction (view or click) for a specific flyer item.
   * This is a "fire-and-forget" operation from the client's perspective, so errors are logged but not re-thrown.
   * @param flyerItemId The ID of the flyer item.
   * @param interactionType The type of interaction, either 'view' or 'click'.
   */
  async trackFlyerItemInteraction(flyerItemId: number, interactionType: 'view' | 'click', logger: Logger): Promise<void> {
    try {
      // Choose the column to increment based on the interaction type.
      // This is safe from SQL injection as the input is strictly controlled to be 'view' or 'click'.
      const columnToIncrement = interactionType === 'view' ? 'view_count' : 'click_count';

      const query = `
        UPDATE public.flyer_items
        SET ${columnToIncrement} = ${columnToIncrement} + 1
        WHERE flyer_item_id = $1;
      `;
      await this.db.query(query, [flyerItemId]);
    } catch (error) {
      logger.error({ err: error, flyerItemId, interactionType }, 'Database error in trackFlyerItemInteraction (non-critical)');
    }
  }

  /**
   * Deletes a flyer and all its associated items in a transaction.
   * This should typically be an admin-only action.
   * @param flyerId The ID of the flyer to delete.
   */
  async deleteFlyer(flyerId: number, logger: Logger): Promise<void> {
    try {
      await withTransaction(async (client) => {
        // The schema is set up with ON DELETE CASCADE for flyer_items,
        // so we only need to delete from the parent 'flyers' table.
        // The database will handle deleting associated flyer_items, unmatched_flyer_items, etc.
        const res = await client.query('DELETE FROM public.flyers WHERE flyer_id = $1', [flyerId]);
        if (res.rowCount === 0) {
          throw new NotFoundError(`Flyer with ID ${flyerId} not found.`);
        }
        logger.info(`Successfully deleted flyer with ID: ${flyerId}`);
      });
    } catch (error) {
      logger.error({ err: error, flyerId }, 'Database transaction error in deleteFlyer');
      throw new Error('Failed to delete flyer.');
    }
  }
}

/**
 * Creates a new flyer and its associated items within a single database transaction.
 *
 * @param flyerData - The data for the flyer.
 * @param itemsForDb - An array of item data to associate with the flyer.
 * @returns An object containing the new flyer and its items.
 */
export async function createFlyerAndItems(flyerData: FlyerInsert, itemsForDb: FlyerItemInsert[], logger: Logger) {
  try {
    return await withTransaction(async (client) => {
      const flyerRepo = new FlyerRepository(client);

      // 1. Find or create the store to get the store_id
      const storeId = await flyerRepo.findOrCreateStore(flyerData.store_name, logger);

      // 2. Prepare the data for the flyer table, replacing store_name with store_id
      const flyerDbData: FlyerDbInsert = { ...flyerData, store_id: storeId };

      // 3. Insert the flyer record
      const newFlyer = await flyerRepo.insertFlyer(flyerDbData, logger);

      // 4. Insert the associated flyer items
      const newItems = await flyerRepo.insertFlyerItems(newFlyer.flyer_id, itemsForDb, logger);

      return { flyer: newFlyer, items: newItems };
    });
  } catch (error) {
    logger.error({ err: error }, 'Database transaction error in createFlyerAndItems');
    throw error; // Re-throw the error to be handled by the calling service.
  }
}