/**
 * Update products with flexible filtering and updates (internal operation)
 */

import { set, merge } from 'lodash';

import { Doc, Id } from '../_generated/dataModel';
import { MutationCtx } from '../_generated/server';
import { UpdateProductsResult, ProductStatus } from './types';

export interface UpdateProductsArgs {
  // Option 1: Update by product ID (safest, most common)
  productId?: Id<'products'>;

  // Option 2: Update by filters (for batch updates)
  organizationId?: string;
  externalId?: string | number | Array<string | number>;
  status?: ProductStatus;
  category?: string;

  // Updates to apply
  updates: {
    name?: string;
    description?: string;
    imageUrl?: string;
    stock?: number;
    price?: number;
    currency?: string;
    category?: string;
    tags?: Array<string>;
    status?: ProductStatus;
    externalId?: string | number;
    metadata?: Record<string, unknown>;
  };
}

export async function updateProducts(
  ctx: MutationCtx,
  args: UpdateProductsArgs,
): Promise<UpdateProductsResult> {
  // Validate: must provide either productId or organizationId
  if (!args.productId && !args.organizationId) {
    throw new Error(
      'Must provide either productId or organizationId for safety',
    );
  }

  // Find products to update
  let productsToUpdate: Array<Doc<'products'>> = [];

  if (args.productId) {
    // Update by ID (most common case)
    const product = await ctx.db.get(args.productId);
    if (!product) {
      throw new Error(`Product not found: ${args.productId}`);
    }
    productsToUpdate = [product];
  } else if (args.organizationId) {
    // Update by filters (batch update)
    const { organizationId } = args;
    let products: Array<Doc<'products'>>;

    if (args.externalId !== undefined) {
      if (Array.isArray(args.externalId)) {
        // For array of externalIds, execute multiple targeted queries using .first()
        const externalIdArray = args.externalId;
        const productPromises = externalIdArray.map((externalId) =>
          ctx.db
            .query('products')
            .withIndex('by_organizationId_and_externalId', (q) =>
              q
                .eq('organizationId', organizationId)
                .eq('externalId', externalId),
            )
            .first(),
        );

        // Execute all queries in parallel and filter out nulls
        const productResults = await Promise.all(productPromises);
        products = productResults.filter(
          (p): p is Doc<'products'> => p !== null,
        );

        // Remove duplicates (in case same product has multiple matching externalIds)
        const seenIds = new Set<string>();
        products = products.filter((product) => {
          if (seenIds.has(product._id)) {
            return false;
          }
          seenIds.add(product._id);
          return true;
        });
      } else {
        // Single externalId - use the specific index with .first()
        const singleExternalId = args.externalId;
        const product = await ctx.db
          .query('products')
          .withIndex('by_organizationId_and_externalId', (q) =>
            q
              .eq('organizationId', organizationId)
              .eq('externalId', singleExternalId),
          )
          .first();
        products = product ? [product] : [];
      }
    } else if (args.status !== undefined) {
      const { status } = args;
      products = [];
      for await (const product of ctx.db
        .query('products')
        .withIndex('by_organizationId_and_status', (q) =>
          q.eq('organizationId', organizationId).eq('status', status),
        )) {
        products.push(product);
      }
    } else if (args.category !== undefined) {
      const { category } = args;
      products = [];
      for await (const product of ctx.db
        .query('products')
        .withIndex('by_organizationId_and_category', (q) =>
          q.eq('organizationId', organizationId).eq('category', category),
        )) {
        products.push(product);
      }
    } else {
      products = [];
      for await (const product of ctx.db
        .query('products')
        .withIndex('by_organizationId', (q) =>
          q.eq('organizationId', organizationId),
        )) {
        products.push(product);
      }
    }

    // Apply additional filters
    productsToUpdate = products.filter((product) => {
      if (args.status && product.status !== args.status) {
        return false;
      }
      if (args.category && product.category !== args.category) {
        return false;
      }
      if (args.externalId) {
        if (Array.isArray(args.externalId)) {
          // For array, check if product's externalId is in the array
          if (
            product.externalId === undefined ||
            !args.externalId.includes(product.externalId)
          ) {
            return false;
          }
        } else {
          // For single value, check exact match
          if (product.externalId !== args.externalId) {
            return false;
          }
        }
      }

      return true;
    });
  }

  // Build patches for each product
  const patches: Array<{ id: Id<'products'>; patch: Record<string, unknown> }> =
    productsToUpdate.map((product) => {
      const patch: Record<string, unknown> = {};

      // Copy direct field updates
      if (args.updates.name !== undefined) patch.name = args.updates.name;
      if (args.updates.description !== undefined)
        patch.description = args.updates.description;
      if (args.updates.imageUrl !== undefined)
        patch.imageUrl = args.updates.imageUrl;
      if (args.updates.stock !== undefined) patch.stock = args.updates.stock;
      if (args.updates.price !== undefined) patch.price = args.updates.price;
      if (args.updates.currency !== undefined)
        patch.currency = args.updates.currency;
      if (args.updates.category !== undefined)
        patch.category = args.updates.category;
      if (args.updates.tags !== undefined) patch.tags = args.updates.tags;
      if (args.updates.status !== undefined) patch.status = args.updates.status;
      if (args.updates.externalId !== undefined)
        patch.externalId = args.updates.externalId;

      // Handle metadata updates with lodash
      if (args.updates.metadata) {
        const existingMetadata: Record<string, unknown> =
          product.metadata ?? {};
        const updatedMetadata: Record<string, unknown> = {
          ...existingMetadata,
        };

        for (const [key, value] of Object.entries(args.updates.metadata)) {
          if (key.includes('.')) {
            // Use lodash.set for dot-notation keys
            set(updatedMetadata, key, value);
          } else {
            // For top-level keys, use merge for objects
            if (
              typeof value === 'object' &&
              value !== null &&
              !Array.isArray(value) &&
              typeof updatedMetadata[key] === 'object' &&
              updatedMetadata[key] !== null &&
              !Array.isArray(updatedMetadata[key])
            ) {
              const existing = updatedMetadata[key];
              updatedMetadata[key] = merge({}, existing, value);
            } else {
              updatedMetadata[key] = value;
            }
          }
        }

        patch.metadata = updatedMetadata;
      }

      return { id: product._id, patch };
    });

  // Apply all patches in parallel
  await Promise.all(patches.map(({ id, patch }) => ctx.db.patch(id, patch)));

  const updatedIds = patches.map(({ id }) => id);

  return {
    success: true,
    updatedCount: updatedIds.length,
    updatedIds,
  };
}
