import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { channelOrderLifecycle } from "../db/channelOrder.lifecycle.generated";
import type { Transaction, Selectable } from "../generated/kysely-tailordb";
import { decodeJson, encodeJson, type SalesOrderLineInput } from "../lib/domain";
import {
  ChannelOrderMappingFailedError,
  ChannelOrderNotFoundError,
  InvalidChannelOrderStatusError,
} from "../lib/errors.generated";
import type { PrimitivesQueries } from "../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,
  primitivesQueries: Pick<PrimitivesQueries, "getCurrency">,
) {
  const channelOrder = await db
    .selectFrom("ChannelOrder")
    .selectAll()
    .where("id", "=", input.channelOrderId)
    .forUpdate()
    .executeTakeFirst();
  if (!channelOrder) {
    return err(new ChannelOrderNotFoundError(input.channelOrderId));
  }
  const nextStatus = channelOrderLifecycle.tryTransition(channelOrder.status, "convert");
  if (!nextStatus) {
    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 { currency: orderCurrency } = (
      await primitivesQueries.getCurrency(db, { id: channelOrder.currencyId }, ctx)
    ).value;
    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 primitivesQueries.getCurrency(db, { id: line.currencyId }, ctx))
      .value;
    if (!currency) {
      return err(new ChannelOrderMappingFailedError(input.channelOrderId));
    }

    const listingId = line.listingId ?? null;
    const externalIdentifier = line.externalIdentifier ?? line.externalLineId ?? null;
    let listing: Selectable<"Listing"> | undefined | null = 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,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  if (lines.length > 0) {
    await db
      .insertInto("SalesOrderLine")
      .values(
        lines.map((line) => ({
          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,
        })),
      )
      .execute();
  }

  const updated = await db
    .updateTable("ChannelOrder")
    .set({
      salesOrderId: salesOrder.id,
      status: nextStatus,
    })
    .where("id", "=", input.channelOrderId)
    .returningAll()
    .executeTakeFirstOrThrow();
  return ok({ channelOrder: updated, salesOrder });
}
