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,
  InvalidStatusTransitionError,
  MissingRequiredFieldError,
  PolicyHasNoStepsError,
  PolicyNotFoundError,
  StepHasNoAssigneesError,
} from "../lib/errors.generated";

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

export interface UpdateApprovalPolicyStepInput {
  stepOrder: number;
  name: string;
  minimumApprovals?: number;
  assignees: UpdateApprovalPolicyAssigneeInput[];
}

export interface UpdateApprovalPolicyInput {
  id: string;
  name?: string;
  description?: string | null;
  steps?: UpdateApprovalPolicyStepInput[];
}

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

  if (name !== undefined && name.trim() === "") {
    return err(new MissingRequiredFieldError("name"));
  }

  const policy = await db
    .selectFrom("ApprovalPolicy")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!policy) {
    return err(new PolicyNotFoundError(id));
  }

  if (policy.status !== "DRAFT") {
    return err(new InvalidStatusTransitionError(id));
  }

  if (steps !== undefined) {
    if (steps.length === 0) {
      return err(new PolicyHasNoStepsError(id));
    }

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

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

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

      const seenUserIds = new Set<string>();
      const seenRoleIds = 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(id));
        }
        if (hasUser && roleQuorum !== undefined) {
          return err(new AssigneeFlagsIncompatibleError(id));
        }
        if (hasRole && required === true) {
          return err(new AssigneeFlagsIncompatibleError(id));
        }
        if (userId !== undefined) {
          if (seenUserIds.has(userId)) {
            return err(new DuplicateUserAssigneeError(id));
          }
          seenUserIds.add(userId);
        }
        if (roleId !== undefined) {
          if (seenRoleIds.has(roleId)) {
            return err(new DuplicateRoleAssigneeError(id));
          }
          seenRoleIds.add(roleId);
        }
      }
    }
  }

  const updateBody = {
    ...(name !== undefined ? { name } : {}),
    ...(description !== undefined ? { description } : {}),
  };
  const updatedPolicy =
    Object.keys(updateBody).length === 0
      ? policy
      : await db
          .updateTable("ApprovalPolicy")
          .set(updateBody)
          .where("id", "=", id)
          .returningAll()
          .executeTakeFirstOrThrow();

  if (steps !== undefined) {
    // Delete existing assignees first to respect FK ordering, then steps
    const existingSteps = await db
      .selectFrom("ApprovalPolicyStep")
      .select("id")
      .where("approvalPolicyId", "=", id)
      .execute();

    const existingStepIds = existingSteps.map((row) => row.id);
    if (existingStepIds.length > 0) {
      await db
        .deleteFrom("ApprovalPolicyStepAssignee")
        .where("approvalPolicyStepId", "in", existingStepIds)
        .execute();
    }
    await db.deleteFrom("ApprovalPolicyStep").where("approvalPolicyId", "=", id).execute();

    const insertedSteps = await db
      .insertInto("ApprovalPolicyStep")
      .values(
        steps.map((step) => ({
          approvalPolicyId: 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: updatedPolicy });
}
