import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Transaction } from "../generated/kysely-tailordb";
import {
  ProductNotFoundError,
  InvalidProductStatusError,
  NoVariantAxesError,
  AttributeNotFoundError,
  AttributeValueNotFoundError,
  ItemCreationFailedError,
} from "../lib/errors.generated";
import type { ItemManagementCommands, SkuStrategy } from "../module";

export interface GenerateVariantsInput {
  productId: string;
}

/**
 * Function: generateVariants
 *
 * Generates product variants from cartesian product of attribute values.
 * All attributes are variant axes.
 */
export async function run(
  db: Transaction,
  input: GenerateVariantsInput,
  ctx: CommandContext,
  deps: {
    itemManagement: Pick<ItemManagementCommands, "createItem">;
    skuStrategy: SkuStrategy;
  },
) {
  const { productId } = input;
  const { itemManagement, skuStrategy } = deps;

  // Check product exists
  const product = await db
    .selectFrom("Product")
    .selectAll()
    .where("id", "=", productId)
    .executeTakeFirst();
  if (!product) {
    return err(new ProductNotFoundError(productId));
  }

  // Must be ACTIVE
  if (product.status !== "ACTIVE") {
    return err(new InvalidProductStatusError(productId));
  }

  // Get all attribute assignments for this product
  const assignments = await db
    .selectFrom("ProductAttributeAssignment")
    .selectAll()
    .where("productId", "=", productId)
    .execute();

  // Group assignments by attribute (all attributes are variant axes)
  const axisValuesByAttribute = new Map<string, string[]>();
  for (const assignment of assignments) {
    const values = axisValuesByAttribute.get(assignment.attributeId) ?? [];
    values.push(assignment.valueId);
    axisValuesByAttribute.set(assignment.attributeId, values);
  }

  if (axisValuesByAttribute.size === 0) {
    return err(new NoVariantAxesError(productId));
  }

  // Data integrity check: verify all referenced attributes still exist
  for (const attributeId of axisValuesByAttribute.keys()) {
    const attr = await db
      .selectFrom("ProductAttribute")
      .select("id")
      .where("id", "=", attributeId)
      .executeTakeFirst();

    if (!attr) {
      return err(new AttributeNotFoundError(attributeId));
    }
  }

  // Data integrity check: verify all referenced values still exist
  const valueIds = Array.from(new Set(Array.from(axisValuesByAttribute.values()).flat()));
  const values = await db
    .selectFrom("ProductAttributeValue")
    .select(["id", "label"])
    .where("id", "in", valueIds)
    .execute();
  const labelsByValueId = new Map<string, string>();
  for (const value of values) {
    labelsByValueId.set(value.id, value.label);
  }
  for (const valueId of valueIds) {
    if (!labelsByValueId.has(valueId)) {
      return err(new AttributeValueNotFoundError(valueId));
    }
  }

  // Compute cartesian product
  const axisEntries = Array.from(axisValuesByAttribute.values());
  const combinations = cartesianProduct(axisEntries);

  // Get existing variants (with lock for axisValueKey uniqueness)
  const existingVariants = await db
    .selectFrom("ProductVariant")
    .selectAll()
    .where("productId", "=", productId)
    .forUpdate()
    .execute();
  const existingKeys = new Set(existingVariants.map((v) => v.axisValueKey));

  const newVariants = [];
  let variantCount = existingVariants.length;

  for (const combination of combinations) {
    const sortedValues = [...combination].sort();
    const axisValueKey = sortedValues.join("|");

    if (existingKeys.has(axisValueKey)) {
      continue;
    }

    // Generate SKU
    const axisLabels = combination.map((valueId) => labelsByValueId.get(valueId) ?? valueId);
    const sku = skuStrategy(product.code, combination, variantCount, axisLabels);
    variantCount++;

    // Create Item in item-management
    const itemResult = await itemManagement.createItem(
      db,
      { sku, name: `${product.name} - ${sku}`, unitId: product.unitId },
      ctx,
    );

    if (!itemResult.ok) {
      return err(new ItemCreationFailedError(sku));
    }

    const itemId = (itemResult.value as { item: { id: string } }).item.id;

    // Create ProductVariant record
    const variant = await db
      .insertInto("ProductVariant")
      .values({
        productId,
        axisValueKey,
        itemId,
      })
      .returningAll()
      .executeTakeFirstOrThrow();

    newVariants.push(variant);
  }

  return ok({ variants: newVariants });
}

function cartesianProduct(arrays: string[][]): string[][] {
  if (arrays.length === 0) return [[]];
  const [first, ...rest] = arrays;
  const restCombinations = cartesianProduct(rest);
  const result: string[][] = [];
  for (const value of first) {
    for (const combination of restCombinations) {
      result.push([value, ...combination]);
    }
  }
  return result;
}
