import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { isStageCategory, type StageCategory } from "../db/pipelineStage";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  DuplicatePipelineNameError,
  InvalidCategoryError,
  InvalidNameError,
  InvalidPipelineTypeError,
  UserNotFoundError,
} from "../lib/errors.generated";

export interface CreatePipelineDeps {
  getUser?: (id: string) => Promise<{ id: string } | null>;
  /**
   * Allowed pipelineType values, injected via defineModule({ pipelineTypes }).
   * The module itself is type-agnostic — consumers declare their own vocabulary
   * (e.g. "TASK_BOARD", "SUPPORT_QUEUE"). When omitted, any non-empty string
   * is accepted.
   */
  allowedPipelineTypes?: readonly string[];
}

export interface CreatePipelineInitialStage {
  name: string;
  category: StageCategory;
  color?: string;
}

export interface CreatePipelineInput {
  name: string;
  description?: string;
  pipelineType: string;
  /** User who creates the pipeline. Stored as immutable audit metadata. */
  createdByUserId: string;
  /**
   * Managed-board flag. Locked pipelines keep the lane template seeded via
   * `stages` — stage-mutation commands reject them — and skip the
   * name uniqueness check (identity is owned by the managing module, so
   * cross-owner name collisions must be allowed).
   */
  locked?: boolean;
  /** Initial lanes to seed at creation time, ordered left to right. */
  stages?: ReadonlyArray<CreatePipelineInitialStage>;
}

const PIPELINE_OWN_KEYS = new Set([
  "name",
  "description",
  "pipelineType",
  "createdByUserId",
  "locked",
  "stages",
]);

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

  // Consumer extension fields (declared via defineModule's pipeline.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 (!PIPELINE_OWN_KEYS.has(key)) {
      customFields[key] = value;
    }
  }

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

  if (!input.pipelineType.trim()) {
    return err(new InvalidPipelineTypeError(input.pipelineType));
  }
  if (deps.allowedPipelineTypes && !deps.allowedPipelineTypes.includes(input.pipelineType)) {
    return err(new InvalidPipelineTypeError(input.pipelineType));
  }

  for (const stage of input.stages ?? []) {
    if (!isStageCategory(stage.category)) {
      return err(new InvalidCategoryError(stage.category));
    }
  }

  if (deps.getUser) {
    const creator = await deps.getUser(input.createdByUserId);
    if (!creator) {
      return err(new UserNotFoundError(input.createdByUserId));
    }
  }

  // Locked (managed) pipelines are an implementation detail of their owning
  // module — that module owns the identity, so cross-owner name collisions
  // like "Maintenance" must be allowed. User-configured boards remain unique
  // per pipelineType across the app: every user sees every board (the module
  // imposes no visibility partitioning), so the name's namespace is global.
  const locked = input.locked ?? false;
  if (!locked) {
    const existingPipeline = await db
      .selectFrom("Pipeline")
      .selectAll()
      .where("pipelineType", "=", input.pipelineType)
      .where("name", "=", input.name)
      .executeTakeFirst();

    if (existingPipeline) {
      return err(new DuplicatePipelineNameError(input.name));
    }
  }

  const now = new Date();
  const pipeline = await db
    .insertInto("Pipeline")
    .values({
      ...customFields,
      name: input.name,
      description: input.description ?? null,
      pipelineType: input.pipelineType,
      createdByUserId: input.createdByUserId,
      status: "ACTIVE",
      locked,
      createdAt: now,
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // Seed the initial lane template (when supplied) so the board is renderable
  // the moment the pipeline exists. Consumers that configure stages later
  // (e.g. via pipeline settings) simply pass no stages.
  if (input.stages && input.stages.length > 0) {
    await db
      .insertInto("PipelineStage")
      .values(
        input.stages.map((s, index) => ({
          pipelineId: pipeline.id,
          name: s.name,
          position: index + 1,
          category: s.category,
          color: s.color ?? null,
          createdAt: now,
          updatedAt: null,
        })),
      )
      .execute();
  }

  return ok({ pipeline });
}
