import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { isItemPriority } from "../db/pipelineItem";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PipelineArchivedError,
  PipelineNotFoundError,
  InvalidPriorityError,
  InvalidTitleError,
  StageNotFoundError,
  UserNotFoundError,
} from "../lib/errors.generated";

export interface CreatePipelineItemDeps {
  /**
   * Optional assignee-existence check, injected via
   * defineModule({ userManagement: { getUser } }). When omitted, assigneeId
   * is stored as-is.
   */
  getUser?: (id: string) => Promise<{ id: string } | null>;
}

export interface CreatePipelineItemInput {
  pipelineId: string;
  title: string;
  description?: string;
  assigneeId?: string;
  priority?: string;
  dueDate?: Date | string;
  asDraft?: boolean;
}

const ITEM_OWN_KEYS = new Set([
  "pipelineId",
  "title",
  "description",
  "assigneeId",
  "priority",
  "dueDate",
  "asDraft",
]);

export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: CreatePipelineItemInput & CF,
  ctx: CommandContext,
  deps: CreatePipelineItemDeps = {},
) {
  void ctx;

  const customFields: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
    if (!ITEM_OWN_KEYS.has(key)) {
      customFields[key] = value;
    }
  }

  const pipeline = await db
    .selectFrom("Pipeline")
    .selectAll()
    .where("id", "=", input.pipelineId)
    .forUpdate()
    .executeTakeFirst();

  if (!pipeline) {
    return err(new PipelineNotFoundError(input.pipelineId));
  }

  if (pipeline.status !== "ACTIVE") {
    return err(new PipelineArchivedError(input.pipelineId));
  }

  if (!input.title.trim()) {
    return err(new InvalidTitleError(input.title));
  }

  if (input.priority !== undefined && !isItemPriority(input.priority)) {
    return err(new InvalidPriorityError(input.priority));
  }

  if (deps.getUser && input.assigneeId !== undefined) {
    const assignee = await deps.getUser(input.assigneeId);
    if (!assignee) {
      return err(new UserNotFoundError(input.assigneeId));
    }
  }

  const firstStage = await db
    .selectFrom("PipelineStage")
    .selectAll()
    .where("pipelineId", "=", input.pipelineId)
    .orderBy("position", "asc")
    .limit(1)
    .executeTakeFirst();

  if (!firstStage) {
    return err(new StageNotFoundError(input.pipelineId));
  }

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

  const nextPosition =
    itemsInStage.length > 0 ? Math.max(...itemsInStage.map((item) => item.position)) + 1 : 1;
  const priority =
    input.priority !== undefined && isItemPriority(input.priority) ? input.priority : "MEDIUM";

  // Use the pipeline's monotonic counter to prevent number reuse after deletion.
  // Fall back to MAX(itemNumber) only on the first call after the field was added.
  let nextItemNumber: number;
  if (pipeline.itemNumberSeq !== null) {
    nextItemNumber = pipeline.itemNumberSeq + 1;
  } else {
    // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely aggregate returns unknown
    const maxRow = await db
      .selectFrom("PipelineItem")
      .select(db.fn.max("itemNumber").as("maxNum"))
      .where("pipelineId", "=", input.pipelineId)
      .executeTakeFirst();
    nextItemNumber = ((maxRow?.maxNum as number | null) ?? 0) + 1;
  }

  await db
    .updateTable("Pipeline")
    .set({ itemNumberSeq: nextItemNumber })
    .where("id", "=", input.pipelineId)
    .execute();

  const item = await db
    .insertInto("PipelineItem")
    .values({
      ...customFields,
      pipelineId: input.pipelineId,
      stageId: firstStage.id,
      title: input.title,
      description: input.description ?? null,
      assigneeId: input.assigneeId ?? null,
      priority,
      dueDate: input.dueDate ?? null,
      position: nextPosition,
      itemNumber: nextItemNumber,
      lifecycle: input.asDraft ? "DRAFT" : "OPEN",
      createdAt: new Date(),
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ item });
}
