import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  EmptyIssueLinesError,
  InvalidQuantityError,
  InvalidUnitConversionRateError,
  ItemNotFoundError,
  LotNotFoundError,
  PrimaryUnitMismatchError,
  SerialNotFoundError,
  StorageLocationNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries } from "../module";

export interface GoodsIssueLineInput {
  itemId: string;
  storageLocationId: string;
  quantity: string;
  unitId: string;
  primaryQuantity: string;
  primaryUnitId: string;
  unitConversionRate: string;
  sourceLineId?: string | null;
  lotId?: string | null;
  serialNumberId?: string | null;
}

export interface GoodsIssueHeaderInput {
  effectiveDate: Date;
  sourceDocumentType: "SALES_ORDER" | "TRANSFER_ORDER" | "MANUFACTURING_ORDER";
  sourceDocumentId: string;
}

export interface CreateGoodsIssueInput {
  header: GoodsIssueHeaderInput;
  lines: GoodsIssueLineInput[];
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: {
    header: GoodsIssueHeaderInput & CF;
    lines: (GoodsIssueLineInput & LCF)[];
  },
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { header, lines } = input;
  const { effectiveDate, sourceDocumentType, sourceDocumentId } = header;

  if (lines.length === 0) {
    return err(new EmptyIssueLinesError("createGoodsIssue"));
  }

  const executionLines = [];
  for (const line of lines) {
    const {
      itemId,
      storageLocationId,
      quantity,
      unitId,
      primaryQuantity,
      primaryUnitId,
      unitConversionRate,
      sourceLineId,
      lotId,
      serialNumberId,
      ...lineCustomFields
    } = line;

    if (new Decimal(quantity).lte(0)) {
      return err(new InvalidQuantityError(quantity));
    }
    if (new Decimal(primaryQuantity).lte(0)) {
      return err(new InvalidQuantityError(primaryQuantity));
    }
    if (new Decimal(unitConversionRate).lte(0)) {
      return err(new InvalidUnitConversionRateError(unitConversionRate));
    }

    const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(itemId));
    }
    if (primaryUnitId !== item.unitId) {
      return err(new PrimaryUnitMismatchError(`${primaryUnitId} expected ${item.unitId}`));
    }

    const location = await db
      .selectFrom("StorageLocation")
      .selectAll()
      .where("id", "=", storageLocationId)
      .executeTakeFirst();
    if (!location) {
      return err(new StorageLocationNotFoundError(storageLocationId));
    }

    if (lotId) {
      const lot = await db.selectFrom("Lot").selectAll().where("id", "=", lotId).executeTakeFirst();
      if (!lot) {
        return err(new LotNotFoundError(lotId));
      }
    }

    if (serialNumberId) {
      const serial = await db
        .selectFrom("SerialNumber")
        .selectAll()
        .where("id", "=", serialNumberId)
        .executeTakeFirst();
      if (!serial) {
        return err(new SerialNotFoundError(serialNumberId));
      }
    }

    executionLines.push({
      ...lineCustomFields,
      direction: "OUT" as const,
      itemId,
      quantity,
      unitId,
      primaryQuantity,
      primaryUnitId,
      unitConversionRate,
      unitCost: null,
      storageLocationId,
      stockType: "AVAILABLE" as const,
      sourceLineId: sourceLineId ?? null,
      lotId: lotId ?? null,
      serialNumberId: serialNumberId ?? null,
    });
  }

  const inventoryExecution = await db
    .insertInto("InventoryExecution")
    .values({
      // Header input first (incl. custom fields) so command-set columns always win.
      ...(header as Record<string, unknown>),
      executionType: "GOODS_ISSUE",
      status: "DRAFT",
      effectiveDate,
      postedAt: null,
      sourceDocumentType,
      sourceDocumentId,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  await db
    .insertInto("InventoryExecutionLine")
    .values(
      executionLines.map((line) => ({ ...line, inventoryExecutionId: inventoryExecution.id })),
    )
    .execute();

  return ok({ inventoryExecution });
}
