import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { isStageCategory } from "../db/pipelineStage";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PipelineArchivedError,
  PipelineNotFoundError,
  DuplicateStageNameError,
  InvalidCategoryError,
  InvalidNameError,
} from "../lib/errors.generated";
import { pipelineLockError } from "../lib/pipelineLock";

export interface CreatePipelineStageInput {
  pipelineId: string;
  name: string;
  position: number;
  category: string;
  color?: string;
}

const STAGE_OWN_KEYS = new Set(["pipelineId", "name", "position", "category", "color"]);

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

  // Consumer extension fields (declared via defineModule's pipelineStage.fields)
  // pass through to the insert untouched.
  const customFields: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
    if (!STAGE_OWN_KEYS.has(key)) {
      customFields[key] = value;
    }
  }

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

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

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

  const lockError = pipelineLockError(pipeline.locked, input.pipelineId);
  if (lockError) {
    return err(lockError);
  }

  if (!input.name.trim()) {
    return err(new InvalidNameError(input.name));
  }

  const duplicateStage = await db
    .selectFrom("PipelineStage")
    .selectAll()
    .where("pipelineId", "=", input.pipelineId)
    .where("name", "=", input.name)
    .executeTakeFirst();

  if (duplicateStage) {
    return err(new DuplicateStageNameError(input.name));
  }

  if (!isStageCategory(input.category)) {
    return err(new InvalidCategoryError(input.category));
  }

  // Position is a 1-based slot index, consistent with ReorderPipelineStage:
  // out-of-range values are clamped and sibling positions are renormalized to
  // stay contiguous, so two stages never share a position.
  const siblings = await db
    .selectFrom("PipelineStage")
    .selectAll()
    .where("pipelineId", "=", input.pipelineId)
    .orderBy("position", "asc")
    .execute();

  const insertIndex = Math.max(0, Math.min(input.position - 1, siblings.length));
  const now = new Date();

  const stage = await db
    .insertInto("PipelineStage")
    .values({
      ...customFields,
      pipelineId: input.pipelineId,
      name: input.name,
      position: insertIndex + 1,
      category: input.category,
      color: input.color ?? null,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  for (const [index, current] of siblings.entries()) {
    const nextPosition = index < insertIndex ? index + 1 : index + 2;
    if (current.position !== nextPosition) {
      await db
        .updateTable("PipelineStage")
        .set({
          position: nextPosition,
          updatedAt: now,
        })
        .where("id", "=", current.id)
        .execute();
    }
  }

  return ok({ stage });
}
