import type { Transaction } from "../generated/kysely-tailordb";
import {
  ChannelOrderMappingFailedError,
  ChannelOrderNotFoundError,
  InvalidChannelOrderStatusError,
} from "../lib/errors.generated";
import { decodeJson, encodeJson, type SalesOrderLineInput } from "../lib/domain";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/module";

export interface ConvertChannelOrderInput {
  channelOrderId: string;
}

interface NormalizedChannelOrderLine extends SalesOrderLineInput {
  listingId?: string | null;
  externalIdentifier?: string | null;
  externalLineId?: string | null;
}

export async function run(db: Transaction, input: ConvertChannelOrderInput, ctx: CommandContext) {
  void ctx;
  const channelOrder = await db
    .selectFrom("ChannelOrder")
    .selectAll()
    .where("id", "=", input.channelOrderId)
    .forUpdate()
    .executeTakeFirst();
  if (!channelOrder) {
    return err(new ChannelOrderNotFoundError(input.channelOrderId));
  }
  if (channelOrder.status !== "PENDING_CONVERSION") {
    return err(new InvalidChannelOrderStatusError(input.channelOrderId));
  }

  const normalizedPayload = decodeJson<{
    customerId?: string;
    paymentTermId?: string | null;
    lines?: NormalizedChannelOrderLine[];
  } | null>(channelOrder.normalizedPayload, null);

  const mappedCustomerId = channelOrder.customerId ?? normalizedPayload?.customerId;
  if (!mappedCustomerId) {
    return err(new ChannelOrderMappingFailedError(input.channelOrderId));
  }
  const lines = normalizedPayload?.lines ?? [];
  if (!lines.length) {
    return err(new ChannelOrderMappingFailedError(input.channelOrderId));
  }
  if (channelOrder.currencyId) {
    const orderCurrency = await db
      .selectFrom("Currency")
      .selectAll()
      .where("id", "=", channelOrder.currencyId)
      .executeTakeFirst();
    if (!orderCurrency) {
      return err(new ChannelOrderMappingFailedError(input.channelOrderId));
    }
  }
  for (const line of lines) {
    if (!line.itemId || !line.currencyId || line.quantity <= 0) {
      return err(new ChannelOrderMappingFailedError(input.channelOrderId));
    }
    const currency = await db
      .selectFrom("Currency")
      .selectAll()
      .where("id", "=", line.currencyId)
      .executeTakeFirst();
    if (!currency) {
      return err(new ChannelOrderMappingFailedError(input.channelOrderId));
    }

    const listingId = line.listingId ?? null;
    const externalIdentifier = line.externalIdentifier ?? line.externalLineId ?? null;
    let listing = null;
    if (listingId) {
      listing = await db
        .selectFrom("Listing")
        .selectAll()
        .where("id", "=", listingId)
        .executeTakeFirst();
    } else if (externalIdentifier) {
      listing = await db
        .selectFrom("Listing")
        .selectAll()
        .where("channelId", "=", channelOrder.channelId)
        .where("externalIdentifier", "=", externalIdentifier)
        .executeTakeFirst();
    }
    if (listing?.channelId !== channelOrder.channelId || listing?.itemId !== line.itemId) {
      return err(new ChannelOrderMappingFailedError(input.channelOrderId));
    }
  }

  const salesOrder = await db
    .insertInto("SalesOrder")
    .values({
      companyId: channelOrder.companyId,
      customerId: mappedCustomerId,
      channelId: channelOrder.channelId,
      paymentTermId: normalizedPayload?.paymentTermId ?? null,
      status: "DRAFT",
      orderDate: channelOrder.orderDate ?? new Date(),
      requestedShipDate: null,
      shippingAddressSnapshot: null,
      billingAddressSnapshot: null,
      paymentTermSnapshot: null,
      rejectionReason: null,
      holdReason: null,
      closeReason: null,
      createdAt: new Date(),
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  for (const line of lines) {
    await db
      .insertInto("SalesOrderLine")
      .values({
        salesOrderId: salesOrder!.id,
        itemId: line.itemId,
        description: line.description ?? null,
        quantity: line.quantity,
        unitPrice: line.unitPrice,
        currencyId: line.currencyId,
        unitId: line.unitId ?? null,
        matchedPriceRuleId: line.matchedPriceRuleId ?? null,
        billingPolicy: line.billingPolicy ?? "ORDER",
        requestedShipDate: line.requestedShipDate ?? null,
        requiresPhysicalFulfillment: line.requiresPhysicalFulfillment ?? true,
        priceSnapshot: line.priceSnapshot ? encodeJson(line.priceSnapshot) : null,
        shippedQuantity: 0,
        invoicedQuantity: 0,
        createdAt: new Date(),
        updatedAt: null,
      })
      .executeTakeFirst();
  }

  const updated = await db
    .updateTable("ChannelOrder")
    .set({
      salesOrderId: salesOrder!.id,
      status: "CONVERTED",
      updatedAt: new Date(),
    })
    .where("id", "=", input.channelOrderId)
    .returningAll()
    .executeTakeFirst();
  return ok({ channelOrder: updated!, salesOrder: salesOrder! });
}
