import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  ItemNotFoundError,
  ItemNotOpenError,
  SameStageError,
  StageNotFoundError,
  StageNotOnPipelineError,
} from "../lib/errors.generated";

export interface MovePipelineItemInput {
  itemId: string;
  stageId: string;
  position?: number;
}

export async function run(db: Transaction, input: MovePipelineItemInput, ctx: CommandContext) {
  const item = await db
    .selectFrom("PipelineItem")
    .selectAll()
    .where("id", "=", input.itemId)
    .forUpdate()
    .executeTakeFirst();

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

  // movePipelineItem only operates on OPEN items. DRAFT items must be published first
  // and CLOSED items must be reopened.
  if (item.lifecycle !== "OPEN") {
    return err(new ItemNotOpenError(input.itemId));
  }

  const targetStage = await db
    .selectFrom("PipelineStage")
    .selectAll()
    .where("id", "=", input.stageId)
    .executeTakeFirst();

  if (!targetStage) {
    return err(new StageNotFoundError(input.stageId));
  }

  if (targetStage.pipelineId !== item.pipelineId) {
    return err(new StageNotOnPipelineError(input.stageId));
  }

  if (targetStage.id === item.stageId) {
    return err(new SameStageError(input.stageId));
  }

  const itemsInTargetStage = await db
    .selectFrom("PipelineItem")
    .selectAll()
    .where("stageId", "=", targetStage.id)
    .orderBy("position", "asc")
    .execute();

  const now = new Date();

  // `input.position` (when supplied, e.g. dropping a card between two cards in a
  // different lane) is a 1-based slot index among the target lane's items, the
  // same contract reorderPipelineItem uses — not a literal position value. We renormalize
  // the whole target lane to a contiguous 1..N sequence with the moved card spliced
  // in at that slot. Interpreting the slot as a literal position only shifted the
  // siblings at or after it, which assumed the stored positions were already a
  // clean 1..N run; whenever they had gaps or duplicates (createPipelineItem assigns
  // max+1, and lane moves leave gaps behind) the card snapped to the top or
  // bottom instead of the chosen slot. Without a position, append to the end.
  const insertIndex =
    typeof input.position === "number"
      ? Math.max(0, Math.min(input.position - 1, itemsInTargetStage.length))
      : itemsInTargetStage.length;

  // Siblings before the slot keep their order (positions 1..insertIndex); the
  // moved card takes insertIndex + 1; siblings at or after the slot shift down by
  // one to make room. This rewrites the lane to a contiguous sequence regardless
  // of the pre-existing position values.
  const nextPosition = insertIndex + 1;
  for (const [index, sibling] of itemsInTargetStage.entries()) {
    const position = index < insertIndex ? index + 1 : index + 2;
    if (sibling.position !== position) {
      await db
        .updateTable("PipelineItem")
        .set({ position, updatedAt: now })
        .where("id", "=", sibling.id)
        .execute();
    }
  }

  // Lifecycle stays OPEN regardless of target stage. Reaching a DONE/CANCELED
  // stage is no longer sufficient to close the item — closing is always an
  // explicit operator action via ClosePipelineItem (or a consuming module's
  // own close command wrapping it).
  const movedItem = await db
    .updateTable("PipelineItem")
    .set({
      stageId: targetStage.id,
      position: nextPosition,
      updatedAt: now,
    })
    .where("id", "=", input.itemId)
    .returningAll()
    .executeTakeFirst();

  await db
    .insertInto("PipelineStageTransition")
    .values({
      itemId: item.id,
      fromStageId: item.stageId,
      toStageId: targetStage.id,
      movedByUserId: ctx.actorId,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  return ok({
    item: movedItem ?? {
      ...item,
      stageId: targetStage.id,
      position: nextPosition,
      updatedAt: now,
    },
  });
}
