import { findAll, findById, findByName, findAllReferencing, insert, update, softDelete, type ReferencingColumn } from '../repositories/product.repo.js';
import { findOpenOrdersReferencingProduct } from '../repositories/orderProduct.repo.js';
import { findById as findItemById } from '../repositories/item.repo.js';
import { findById as findCategoryById } from '../repositories/productCategory.repo.js';
import { findById as findPurityById } from '../repositories/purity.repo.js';
import { findById as findSizeById } from '../repositories/size.repo.js';
import { findById as findWeightById } from '../repositories/weight.repo.js';
import { findById as findWireSizeById } from '../repositories/wireSize.repo.js';
import { createWithId } from '../utils/id.js';
import { buildVariantName } from '../utils/variantName.js';
import { NotFoundError, ConflictError } from '../utils/errors.js';
import { emitter } from '../events/emitter.js';
import type { Product, PaginatedResult } from '../types/index.js';

export async function list(search?: string, categoryId?: string, page = 1, limit = 20): Promise<PaginatedResult<Product>> {
  const { rows, total } = await findAll(search, categoryId, page, limit);
  return { data: rows, total, page, limit };
}

export async function get(id: string): Promise<Product> {
  const product = await findById(id);
  if (!product) throw new NotFoundError('Product not found');
  return product;
}

// The variant's display name is always server-generated from its selected
// masters (never taken from the client), so it stays consistent with the
// Item-Category-Purity-ChainSize-Weight-WireSize sequence regardless of what
// the client sends as `name`. `merged` is the full resulting state of the
// product (existing row + incoming patch) so updates that only touch one
// field still regenerate the name correctly.
async function resolveVariantName(merged: Partial<Product>): Promise<string> {
  if (!merged.item_id) throw new ConflictError('Item is required');

  const [item, category, purity, size, weight, wireSize] = await Promise.all([
    findItemById(merged.item_id),
    merged.category_id ? findCategoryById(merged.category_id) : null,
    merged.purity_id ? findPurityById(merged.purity_id) : null,
    merged.size_id ? findSizeById(merged.size_id) : null,
    merged.weight_id ? findWeightById(merged.weight_id) : null,
    merged.wire_size_id ? findWireSizeById(merged.wire_size_id) : null,
  ]);
  if (!item) throw new NotFoundError('Item not found');

  return buildVariantName({
    itemName: item.name,
    categoryName: category?.name,
    purityName: purity?.name,
    chainSizeName: size?.name,
    weightName: weight?.name,
    wireSizeName: wireSize?.name,
  });
}

// Called by item/productCategory/purity/size/weight/wireSize .service.ts
// after renaming a master row — every variant built from that row has its
// name baked in at create/update time (see resolveVariantName above), so a
// rename there wouldn't otherwise reach existing orders/products until each
// variant was individually re-saved. Failures on one variant (e.g. its
// recomputed name now collides with another) are logged and skipped rather
// than aborting the whole rename — the master name change itself already
// succeeded and shouldn't be rolled back over an unrelated variant naming
// clash.
export async function regenerateNamesReferencing(column: ReferencingColumn, id: string, userId: string): Promise<void> {
  const products = await findAllReferencing(column, id);
  for (const product of products) {
    try {
      const name = await resolveVariantName(product);
      if (name !== product.name) await update(product.id, { name }, userId);
    } catch {
      // Skip — see comment above.
    }
  }
}

export async function create(data: Partial<Product>, userId: string): Promise<Product> {
  const name = await resolveVariantName(data);
  const duplicate = await findByName(name);
  if (duplicate) throw new ConflictError(`A variant named "${name}" already exists`);

  const { id } = await createWithId('products', 'PROD', (id) => insert({ ...data, name }, id, userId));
  const product = await findById(id);
  if (!product) throw new NotFoundError('Product not found');
  emitter.emit('activity.create', { entity_type: 'product', entity_id: id, action: 'created', user_id: userId });
  return product;
}

export async function updateProduct(id: string, data: Partial<Product>, userId: string): Promise<Product> {
  const existing = await findById(id);
  if (!existing) throw new NotFoundError('Product not found');
  const merged: Partial<Product> = { ...existing, ...data };
  const name = await resolveVariantName(merged);
  const duplicate = await findByName(name, id);
  if (duplicate) throw new ConflictError(`A variant named "${name}" already exists`);
  await update(id, { ...data, name }, userId);
  emitter.emit('activity.create', { entity_type: 'product', entity_id: id, action: 'updated', user_id: userId });
  return get(id);
}

// A variant used only on dispatched/cancelled ("closed") order lines can
// still be deleted — those lines are historical and order.repo.ts's join
// already resolves a soft-deleted variant's name to undefined, which the
// frontend renders as "Unknown item" instead of breaking. Only variants with
// at least one still-open (not dispatched, not cancelled) line are blocked,
// and the error names exactly which orders need to be resolved first.
export async function remove(id: string, userId: string): Promise<void> {
  const existing = await findById(id);
  if (!existing) throw new NotFoundError('Product not found');

  const openOrders = await findOpenOrdersReferencingProduct(id);
  if (openOrders.length > 0) {
    const labels = openOrders.map((o) => (o.joNo ? `${o.orderNo} (JO ${o.joNo})` : o.orderNo));
    throw new ConflictError(
      `This variant is used in open order(s): ${labels.join(', ')}. Dispatch or cancel those lines before deleting it.`,
    );
  }

  await softDelete(id, userId);
  emitter.emit('activity.create', { entity_type: 'product', entity_id: id, action: 'deleted', user_id: userId });
}
