import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { ApprovalPolicyStepAssigneeRoleQuorum } from "../generated/enums";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  AssigneeFlagsIncompatibleError,
  AssigneeUserRoleXorViolatedError,
  DuplicateRoleAssigneeError,
  DuplicateStepOrderError,
  DuplicateUserAssigneeError,
  InvalidMinimumApprovalsError,
  MissingRequiredFieldError,
  PolicyHasNoStepsError,
  StepHasNoAssigneesError,
} from "../lib/errors.generated";

export interface CreateApprovalPolicyAssigneeInput {
  userId?: string;
  roleId?: string;
  required?: boolean;
  roleQuorum?: ApprovalPolicyStepAssigneeRoleQuorum;
}

export interface CreateApprovalPolicyStepInput {
  stepOrder: number;
  name: string;
  minimumApprovals?: number;
  assignees: CreateApprovalPolicyAssigneeInput[];
}

export interface CreateApprovalPolicyInput {
  purpose: string;
  name: string;
  description?: string;
  steps: CreateApprovalPolicyStepInput[];
}

export async function run(db: Transaction, input: CreateApprovalPolicyInput, _ctx: CommandContext) {
  const { purpose, name, description, steps } = input;

  if (purpose.trim() === "") {
    return err(new MissingRequiredFieldError("purpose"));
  }
  if (name.trim() === "") {
    return err(new MissingRequiredFieldError("name"));
  }
  if (steps.length === 0) {
    return err(new PolicyHasNoStepsError("steps"));
  }

  const stepOrders = steps.map((step) => step.stepOrder);
  if (new Set(stepOrders).size !== stepOrders.length) {
    return err(new DuplicateStepOrderError(name));
  }

  for (const step of steps) {
    if (step.assignees.length === 0) {
      return err(new StepHasNoAssigneesError(step.name));
    }

    const minimumApprovals = step.minimumApprovals ?? 1;
    if (!Number.isInteger(minimumApprovals) || minimumApprovals < 1) {
      return err(new InvalidMinimumApprovalsError(step.name));
    }

    const seenRoleIds = new Set<string>();
    const seenUserIds = new Set<string>();
    for (const { userId, roleId, required, roleQuorum } of step.assignees) {
      const hasUser = userId !== undefined;
      const hasRole = roleId !== undefined;
      if (hasUser === hasRole) {
        return err(new AssigneeUserRoleXorViolatedError(step.name));
      }
      if (hasUser && roleQuorum !== undefined) {
        return err(new AssigneeFlagsIncompatibleError(step.name));
      }
      if (hasRole && required === true) {
        return err(new AssigneeFlagsIncompatibleError(step.name));
      }
      if (hasUser) {
        if (seenUserIds.has(userId)) {
          return err(new DuplicateUserAssigneeError(step.name));
        }
        seenUserIds.add(userId);
      }
      if (hasRole) {
        if (seenRoleIds.has(roleId)) {
          return err(new DuplicateRoleAssigneeError(step.name));
        }
        seenRoleIds.add(roleId);
      }
    }
  }

  const policy = await db
    .insertInto("ApprovalPolicy")
    .values({
      purpose,
      name,
      description,
      status: "DRAFT",
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  const insertedSteps = await db
    .insertInto("ApprovalPolicyStep")
    .values(
      steps.map((step) => ({
        approvalPolicyId: policy.id,
        stepOrder: step.stepOrder,
        name: step.name,
        minimumApprovals: step.minimumApprovals ?? 1,
      })),
    )
    .returningAll()
    .execute();

  const assigneeRows = steps.flatMap((step, idx) => {
    const insertedStep = insertedSteps[idx];
    return step.assignees.map((assignee) => ({
      approvalPolicyStepId: insertedStep.id,
      userId: assignee.userId,
      roleId: assignee.roleId,
      required: assignee.required ?? false,
      roleQuorum: assignee.roleQuorum,
    }));
  });

  if (assigneeRows.length > 0) {
    await db.insertInto("ApprovalPolicyStepAssignee").values(assigneeRows).execute();
  }

  return ok({ approvalPolicy: policy });
}
