import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { serialNumberLifecycle } from "../db/serialNumber.lifecycle.generated";
import { stockAdjustmentLifecycle } from "../db/stockAdjustment.lifecycle.generated";
import type { Selectable, Transaction } from "../generated/kysely-tailordb";
import {
  lockSiteAvailableStockLevels,
  sumOpenStockReservationQuantity,
} from "../internal/stockReservation";
import {
  StockAdjustmentNotFoundError,
  InvalidStatusError,
  ItemNotFoundError,
  InsufficientOnHandError,
  InsufficientAvailableStockError,
  InsufficientBlockedStockError,
  InvalidSerialStateForScrapError,
} from "../lib/errors.generated";
import { applyValuation } from "../lib/valuation";
import type { ItemManagementQueries } from "../module";

export interface ConfirmStockAdjustmentInput {
  id: string;
}

type StockLevel = Selectable<"StockLevel">;
type SerialNumber = Selectable<"SerialNumber">;

type StockEffect = {
  itemId: string;
  storageLocationId: string;
  stockType: "AVAILABLE" | "BLOCKED" | "IN_TRANSIT";
  delta: Decimal;
};

type LotEffect = {
  lotId: string;
  storageLocationId: string;
  onHandDelta: Decimal;
  blockedDelta: Decimal;
};

function stockEffectKey(effect: Pick<StockEffect, "itemId" | "storageLocationId" | "stockType">) {
  return `${effect.itemId}:${effect.storageLocationId}:${effect.stockType}`;
}

function lotEffectKey(effect: Pick<LotEffect, "lotId" | "storageLocationId">) {
  return `${effect.lotId}:${effect.storageLocationId}`;
}

function getDcIndicator(direction: string) {
  return direction === "IN" ? "DR" : "CR";
}

/**
 * Function: confirmStockAdjustment
 *
 * Approves and executes a submitted stock adjustment, creating InventoryLedger
 * entries and updating StockLevel records for each adjustment line.
 * Combines approval and execution into a single step.
 */
export async function run(
  db: Transaction,
  input: ConfirmStockAdjustmentInput,
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { id } = input;

  const stockAdjustment = await db
    .selectFrom("StockAdjustment")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!stockAdjustment) {
    return err(new StockAdjustmentNotFoundError(id));
  }

  const nextAdjustmentStatus = stockAdjustmentLifecycle.tryTransition(
    stockAdjustment.status,
    "confirm",
  );
  if (!nextAdjustmentStatus) {
    return err(new InvalidStatusError(id));
  }

  const inventoryExecution = await db
    .selectFrom("InventoryExecution")
    .selectAll()
    .where("executionType", "=", "STOCK_ADJUSTMENT")
    .where("sourceDocumentType", "=", "STOCK_ADJUSTMENT")
    .where("sourceDocumentId", "=", stockAdjustment.id)
    .forUpdate()
    .executeTakeFirst();

  if (!inventoryExecution || inventoryExecution.status !== "DRAFT") {
    return err(new InvalidStatusError(id));
  }

  const lines = await db
    .selectFrom("StockAdjustmentLine")
    .selectAll()
    .where("stockAdjustmentId", "=", id)
    .execute();

  const linesById = new Map(lines.map((line) => [line.id, line]));
  const executionLines = await db
    .selectFrom("InventoryExecutionLine")
    .selectAll()
    .where("inventoryExecutionId", "=", inventoryExecution.id)
    .execute();

  const stockEffects = new Map<string, StockEffect>();
  const lotEffects = new Map<string, LotEffect>();
  const scrapSerials: SerialNumber[] = [];

  for (const executionLine of executionLines) {
    const { itemId, serialNumberId } = executionLine;
    const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;

    if (!item) {
      return err(new ItemNotFoundError(itemId));
    }

    if (stockAdjustment.adjustmentType === "CORRECTION") {
      continue;
    }

    if (stockAdjustment.adjustmentType === "SCRAP") {
      let serial: SerialNumber | null = null;
      if (serialNumberId) {
        serial =
          (await db
            .selectFrom("SerialNumber")
            .selectAll()
            .where("id", "=", serialNumberId)
            .forUpdate()
            .executeTakeFirst()) ?? null;
      }

      if (serial && !serialNumberLifecycle.tryTransition(serial.status, "scrap")) {
        return err(new InvalidSerialStateForScrapError(serial.id));
      }

      if (serial) {
        scrapSerials.push(serial);
      }
      continue;
    }

    if (stockAdjustment.adjustmentType === "BLOCK") {
      continue;
    }

    if (stockAdjustment.adjustmentType === "UNBLOCK") {
      continue;
    }
  }

  for (const executionLine of executionLines) {
    const quantity = new Decimal(executionLine.quantity);
    const delta = executionLine.direction === "IN" ? quantity : quantity.negated();
    const stockEffect = {
      itemId: executionLine.itemId,
      storageLocationId: executionLine.storageLocationId,
      stockType: executionLine.stockType,
      delta,
    };
    const key = stockEffectKey(stockEffect);
    const current = stockEffects.get(key);
    stockEffects.set(key, {
      ...stockEffect,
      delta: (current?.delta ?? new Decimal(0)).plus(delta),
    });

    if (executionLine.lotId) {
      const lotEffect = {
        lotId: executionLine.lotId,
        storageLocationId: executionLine.storageLocationId,
        onHandDelta: new Decimal(0),
        blockedDelta: new Decimal(0),
      };

      if (stockAdjustment.adjustmentType === "CORRECTION") {
        lotEffect.onHandDelta = delta;
      } else if (stockAdjustment.adjustmentType === "SCRAP" && executionLine.direction === "OUT") {
        lotEffect.onHandDelta = quantity.negated();
        lotEffect.blockedDelta =
          executionLine.stockType === "BLOCKED" ? quantity.negated() : new Decimal(0);
      } else if (
        stockAdjustment.adjustmentType === "BLOCK" &&
        executionLine.stockType === "BLOCKED" &&
        executionLine.direction === "IN"
      ) {
        lotEffect.blockedDelta = quantity;
      } else if (
        stockAdjustment.adjustmentType === "UNBLOCK" &&
        executionLine.stockType === "BLOCKED" &&
        executionLine.direction === "OUT"
      ) {
        lotEffect.blockedDelta = quantity.negated();
      }

      const key = lotEffectKey(lotEffect);
      const current = lotEffects.get(key);
      lotEffects.set(key, {
        ...lotEffect,
        onHandDelta: (current?.onHandDelta ?? new Decimal(0)).plus(lotEffect.onHandDelta),
        blockedDelta: (current?.blockedDelta ?? new Decimal(0)).plus(lotEffect.blockedDelta),
      });
    }
  }

  const lockedStockLevels = new Map<string, StockLevel | undefined>();
  for (const effect of stockEffects.values()) {
    const stockLevel = await db
      .selectFrom("StockLevel")
      .selectAll()
      .where("itemId", "=", effect.itemId)
      .where("storageLocationId", "=", effect.storageLocationId)
      .where("stockType", "=", effect.stockType)
      .forUpdate()
      .executeTakeFirst();
    lockedStockLevels.set(stockEffectKey(effect), stockLevel);

    if (new Decimal(stockLevel?.quantity ?? 0).plus(effect.delta).lt(0)) {
      const stockKey = `${effect.itemId}:${effect.storageLocationId}`;
      if (effect.stockType === "BLOCKED") {
        return err(new InsufficientBlockedStockError(stockKey));
      }
      if (stockAdjustment.adjustmentType === "BLOCK") {
        return err(new InsufficientAvailableStockError(stockKey));
      }
      return err(new InsufficientOnHandError(stockKey));
    }
  }

  if (stockAdjustment.adjustmentType === "BLOCK") {
    const blockRequirements = new Map<
      string,
      { itemId: string; siteId: string; quantity: Decimal }
    >();
    for (const effect of stockEffects.values()) {
      if (effect.stockType !== "AVAILABLE" || !effect.delta.isNegative()) {
        continue;
      }

      const location = await db
        .selectFrom("StorageLocation")
        .selectAll()
        .where("id", "=", effect.storageLocationId)
        .executeTakeFirst();
      if (!location) {
        return err(
          new InsufficientAvailableStockError(`${effect.itemId}:${effect.storageLocationId}`),
        );
      }

      const key = `${effect.itemId}:${location.siteId}`;
      const current = blockRequirements.get(key);
      blockRequirements.set(key, {
        itemId: effect.itemId,
        siteId: location.siteId,
        quantity: (current?.quantity ?? new Decimal(0)).plus(effect.delta.abs()),
      });
    }

    for (const requirement of blockRequirements.values()) {
      const lockedSiteStockLevels = await lockSiteAvailableStockLevels(
        db,
        requirement.itemId,
        requirement.siteId,
      );
      const siteStockLevels = lockedSiteStockLevels?.stockLevels ?? [];
      const openReservationsResult = await db
        .selectFrom("StockReservation")
        .selectAll()
        .where("itemId", "=", requirement.itemId)
        .where("siteId", "=", requirement.siteId)
        .where("status", "=", "OPEN")
        .execute();
      const openReservations = Array.isArray(openReservationsResult) ? openReservationsResult : [];
      const reservedQuantity = sumOpenStockReservationQuantity(openReservations);
      const siteAvailableQuantity = siteStockLevels
        .reduce((total, siteStockLevel) => total.plus(siteStockLevel.quantity), new Decimal(0))
        .minus(reservedQuantity);

      if (siteAvailableQuantity.lt(requirement.quantity)) {
        return err(
          new InsufficientAvailableStockError(`${requirement.itemId}:${requirement.siteId}`),
        );
      }
    }
  }

  const now = new Date();
  const allLedgerEntries: {
    id: string;
    inventoryExecutionId: string;
    dcIndicator: string;
    stockType: string;
  }[] = [];

  for (const executionLine of executionLines) {
    const sourceLine = executionLine.sourceLineId
      ? linesById.get(executionLine.sourceLineId)
      : undefined;
    const { itemId, storageLocationId, quantity, lotId, serialNumberId } = executionLine;
    const unitCost = sourceLine?.unitCost ?? null;

    if (stockAdjustment.adjustmentType === "CORRECTION") {
      const ledgerEntry = await db
        .insertInto("InventoryLedger")
        .values({
          inventoryExecutionId: inventoryExecution.id,
          dcIndicator: getDcIndicator(executionLine.direction),
          itemId,
          storageLocationId,
          stockType: executionLine.stockType,
          quantity,
          executedAt: now,
          lotId: lotId ?? null,
          serialNumberId: serialNumberId ?? null,
        })
        .returningAll()
        .executeTakeFirstOrThrow();
      allLedgerEntries.push(ledgerEntry);

      await applyValuation(db, {
        itemId,
        inventoryLedgerId: ledgerEntry.id,
        dcIndicator: getDcIndicator(executionLine.direction),
        quantity,
        unitCost: unitCost ?? undefined,
      });
    } else if (stockAdjustment.adjustmentType === "SCRAP") {
      const scrapEntry = await db
        .insertInto("InventoryLedger")
        .values({
          inventoryExecutionId: inventoryExecution.id,
          dcIndicator: getDcIndicator(executionLine.direction),
          itemId,
          storageLocationId,
          stockType: executionLine.stockType,
          quantity,
          executedAt: now,
          lotId: lotId ?? null,
          serialNumberId: serialNumberId ?? null,
        })
        .returningAll()
        .executeTakeFirstOrThrow();
      allLedgerEntries.push(scrapEntry);

      await applyValuation(db, {
        itemId,
        inventoryLedgerId: scrapEntry.id,
        dcIndicator: getDcIndicator(executionLine.direction),
        quantity,
      });
    } else {
      const ledgerEntry = await db
        .insertInto("InventoryLedger")
        .values({
          inventoryExecutionId: inventoryExecution.id,
          dcIndicator: getDcIndicator(executionLine.direction),
          itemId,
          storageLocationId,
          stockType: executionLine.stockType,
          quantity,
          executedAt: now,
          lotId: lotId ?? null,
          serialNumberId: serialNumberId ?? null,
        })
        .returningAll()
        .executeTakeFirstOrThrow();
      allLedgerEntries.push(ledgerEntry);
    }
  }

  for (const effect of stockEffects.values()) {
    const stockLevel = lockedStockLevels.get(stockEffectKey(effect));
    const nextQuantity = new Decimal(stockLevel?.quantity ?? 0).plus(effect.delta);

    if (stockLevel) {
      await db
        .updateTable("StockLevel")
        .set({ quantity: nextQuantity.toString() })
        .where("id", "=", stockLevel.id)
        .returningAll()
        .executeTakeFirst();
    } else if (effect.delta.gt(0)) {
      await db
        .insertInto("StockLevel")
        .values({
          itemId: effect.itemId,
          storageLocationId: effect.storageLocationId,
          stockType: effect.stockType,
          quantity: effect.delta.toString(),
        })
        .returningAll()
        .executeTakeFirst();
    }
  }

  for (const effect of lotEffects.values()) {
    const lotStockLevel = await db
      .selectFrom("LotStockLevel")
      .selectAll()
      .where("lotId", "=", effect.lotId)
      .where("storageLocationId", "=", effect.storageLocationId)
      .forUpdate()
      .executeTakeFirst();

    if (lotStockLevel) {
      await db
        .updateTable("LotStockLevel")
        .set({
          onHand: Decimal.max(
            0,
            new Decimal(lotStockLevel.onHand).plus(effect.onHandDelta),
          ).toString(),
          blocked: Decimal.max(
            0,
            new Decimal(lotStockLevel.blocked).plus(effect.blockedDelta),
          ).toString(),
        })
        .where("id", "=", lotStockLevel.id)
        .returningAll()
        .executeTakeFirst();
    } else if (effect.onHandDelta.gt(0) || effect.blockedDelta.gt(0)) {
      await db
        .insertInto("LotStockLevel")
        .values({
          lotId: effect.lotId,
          storageLocationId: effect.storageLocationId,
          onHand: Decimal.max(0, effect.onHandDelta).toString(),
          blocked: Decimal.max(0, effect.blockedDelta).toString(),
        })
        .returningAll()
        .executeTakeFirst();
    }
  }

  for (const serial of scrapSerials) {
    const nextSerialStatus = serialNumberLifecycle.tryTransition(serial.status, "scrap");
    await db
      .updateTable("SerialNumber")
      .set({ status: nextSerialStatus })
      .where("id", "=", serial.id)
      .returningAll()
      .executeTakeFirst();
  }

  const postedInventoryExecution = await db
    .updateTable("InventoryExecution")
    .set({
      status: "POSTED",
      postedAt: now,
    })
    .where("id", "=", inventoryExecution.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  const updatedAdjustment = await db
    .updateTable("StockAdjustment")
    .set({
      status: nextAdjustmentStatus,
    })
    .where("id", "=", id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({
    stockAdjustment: updatedAdjustment,
    inventoryExecution: postedInventoryExecution,
    inventoryLedgerEntries: allLedgerEntries,
  });
}
