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

export interface CreateApprovalRequestAssigneeInput {
  userId?: string;
  roleId?: string;
  required?: boolean;
  roleQuorum?: ApprovalStepAssigneeRoleQuorum;
}

export interface CreateApprovalRequestStepInput {
  stepOrder: number;
  name: string;
  minimumApprovals?: number;
  assignees: CreateApprovalRequestAssigneeInput[];
}

export type CreateApprovalRequestInput = {
  name: string;
  purpose: string;
  targetEntityType: string;
  targetEntityId: string;
} & (
  | { policyId: string; steps?: undefined }
  | { policyId?: undefined; steps: CreateApprovalRequestStepInput[] }
);

export async function run(
  db: Transaction,
  input: CreateApprovalRequestInput,
  ctx: CommandContext,
  deps: Pick<UserManagementQueries, "listUsersByRole">,
) {
  if (input.name.trim() === "") {
    return err(new MissingRequiredFieldError("name"));
  }
  if (input.purpose.trim() === "") {
    return err(new MissingRequiredFieldError("purpose"));
  }
  if (!input.targetEntityType) {
    return err(new MissingRequiredFieldError("targetEntityType"));
  }
  if (!input.targetEntityId) {
    return err(new MissingRequiredFieldError("targetEntityId"));
  }

  if ((input.policyId == null) === (input.steps == null)) {
    return err(new CreationModeAmbiguousError(input.name));
  }

  // Insert the request and its step + assignee rows; the lowest-stepOrder step
  // is activated inline (status = IN_PROGRESS, role assignees expanded). The
  // requestId, the activating step row, and its raw assignee rows are returned
  // so the activation logic below can run uniformly across both modes.
  let request: { id: string } & Record<string, unknown>;
  let activatingStep: {
    id: string;
    minimumApprovals: number;
  };
  let activatingStepAssignees: Array<{
    id: string;
    userId: string | null;
    roleId: string | null;
    required: boolean;
    roleQuorum: ApprovalStepAssigneeRoleQuorum | null;
  }>;

  if (input.policyId != null) {
    const policyId = input.policyId;
    const policy = await db
      .selectFrom("ApprovalPolicy")
      .selectAll()
      .where("id", "=", policyId)
      .executeTakeFirst();
    if (!policy) {
      return err(new PolicyNotFoundError(policyId));
    }
    if (policy.status !== "ACTIVE") {
      return err(new PolicyNotActiveError(policyId));
    }

    const policySteps = await db
      .selectFrom("ApprovalPolicyStep")
      .selectAll()
      .where("approvalPolicyId", "=", policy.id)
      .orderBy("stepOrder", "asc")
      .execute();

    request = await db
      .insertInto("ApprovalRequest")
      .values({
        name: input.name,
        purpose: input.purpose,
        targetEntityType: input.targetEntityType,
        targetEntityId: input.targetEntityId,
        requesterId: ctx.actorId,
        sourcePolicyId: policy.id,
        status: "PENDING",
      })
      .returningAll()
      .executeTakeFirstOrThrow();

    let firstStepId: string | null = null;
    let firstStepMinimumApprovals = 0;
    for (const [index, ps] of policySteps.entries()) {
      const isFirst = index === 0;
      const step = await db
        .insertInto("ApprovalStep")
        .values({
          approvalRequestId: request.id,
          stepOrder: ps.stepOrder,
          name: ps.name,
          minimumApprovals: ps.minimumApprovals,
          status: isFirst ? "IN_PROGRESS" : "PENDING",
        })
        .returningAll()
        .executeTakeFirstOrThrow();
      if (isFirst) {
        firstStepId = step.id;
        firstStepMinimumApprovals = ps.minimumApprovals;
      }

      const policyAssignees = await db
        .selectFrom("ApprovalPolicyStepAssignee")
        .selectAll()
        .where("approvalPolicyStepId", "=", ps.id)
        .execute();

      if (policyAssignees.length > 0) {
        await db
          .insertInto("ApprovalStepAssignee")
          .values(
            policyAssignees.map((a) => ({
              approvalStepId: step.id,
              userId: a.userId,
              roleId: a.roleId,
              required: a.required,
              roleQuorum: a.roleQuorum,
              status: "PENDING",
            })),
          )
          .execute();
      }
    }

    if (firstStepId === null) {
      return err(new PolicyHasNoStepsError(input.name));
    }

    activatingStep = { id: firstStepId, minimumApprovals: firstStepMinimumApprovals };
    activatingStepAssignees = await db
      .selectFrom("ApprovalStepAssignee")
      .selectAll()
      .where("approvalStepId", "=", firstStepId)
      .execute();
  } else {
    const steps = input.steps;
    if (steps.length === 0) {
      return err(new PolicyHasNoStepsError(input.name));
    }

    const stepOrders = new Set<number>();
    for (const s of steps) {
      if (stepOrders.has(s.stepOrder)) {
        return err(new DuplicateStepOrderError(input.name));
      }
      stepOrders.add(s.stepOrder);
      if (s.assignees.length === 0) {
        return err(new StepHasNoAssigneesError(s.name));
      }
      const min = s.minimumApprovals ?? 1;
      if (!Number.isInteger(min) || min < 1) {
        return err(new InvalidMinimumApprovalsError(s.name));
      }
      const seenUserIds = new Set<string>();
      const seenRoleIds = new Set<string>();
      for (const { userId, roleId, required, roleQuorum } of s.assignees) {
        const hasUser = userId !== undefined;
        const hasRole = roleId !== undefined;
        if (hasUser === hasRole) {
          return err(new AssigneeUserRoleXorViolatedError(s.name));
        }
        if (hasUser && roleQuorum !== undefined) {
          return err(new AssigneeFlagsIncompatibleError(s.name));
        }
        if (hasRole && required === true) {
          return err(new AssigneeFlagsIncompatibleError(s.name));
        }
        if (userId !== undefined) {
          if (seenUserIds.has(userId)) {
            return err(new DuplicateUserAssigneeError(s.name));
          }
          seenUserIds.add(userId);
        }
        if (roleId !== undefined) {
          if (seenRoleIds.has(roleId)) {
            return err(new DuplicateRoleAssigneeError(s.name));
          }
          seenRoleIds.add(roleId);
        }
      }
    }

    request = await db
      .insertInto("ApprovalRequest")
      .values({
        name: input.name,
        purpose: input.purpose,
        targetEntityType: input.targetEntityType,
        targetEntityId: input.targetEntityId,
        requesterId: ctx.actorId,
        status: "PENDING",
      })
      .returningAll()
      .executeTakeFirstOrThrow();

    const orderedSteps = [...steps].sort((a, b) => a.stepOrder - b.stepOrder);
    let firstStepId: string | null = null;
    let firstStepMinimumApprovals = 0;
    for (const [index, s] of orderedSteps.entries()) {
      const isFirst = index === 0;
      const minimumApprovals = s.minimumApprovals ?? 1;
      const step = await db
        .insertInto("ApprovalStep")
        .values({
          approvalRequestId: request.id,
          stepOrder: s.stepOrder,
          name: s.name,
          minimumApprovals,
          status: isFirst ? "IN_PROGRESS" : "PENDING",
        })
        .returningAll()
        .executeTakeFirstOrThrow();
      if (isFirst) {
        firstStepId = step.id;
        firstStepMinimumApprovals = minimumApprovals;
      }

      await db
        .insertInto("ApprovalStepAssignee")
        .values(
          s.assignees.map((a) => ({
            approvalStepId: step.id,
            userId: a.userId,
            roleId: a.roleId,
            required: a.required ?? false,
            roleQuorum: a.roleQuorum,
            status: "PENDING",
          })),
        )
        .execute();
    }

    if (firstStepId === null) {
      return err(new PolicyHasNoStepsError(input.name));
    }

    activatingStep = { id: firstStepId, minimumApprovals: firstStepMinimumApprovals };
    activatingStepAssignees = await db
      .selectFrom("ApprovalStepAssignee")
      .selectAll()
      .where("approvalStepId", "=", firstStepId)
      .execute();
  }

  // Activate the lowest-stepOrder step inline: expand role-based assignees and
  // verify the runtime assignee count meets the step's minimumApprovals.
  let runtimeAssigneeCount = 0;
  for (const row of activatingStepAssignees) {
    if (row.userId !== null && row.roleId === null) {
      runtimeAssigneeCount += 1;
      continue;
    }
    if (row.userId === null && row.roleId !== null) {
      const usersResult = await deps.listUsersByRole(db, { roleId: row.roleId }, ctx);
      if (!usersResult.ok) {
        return err(new RoleHasNoActiveMembersError(row.roleId));
      }
      const users = usersResult.value.users;
      if (users.length === 0) {
        return err(new RoleHasNoActiveMembersError(row.roleId));
      }

      await db
        .insertInto("ApprovalStepAssignee")
        .values(
          users.map((user) => ({
            approvalStepId: activatingStep.id,
            userId: user.id,
            roleId: row.roleId,
            required: row.required,
            roleQuorum: row.roleQuorum,
            status: "PENDING",
          })),
        )
        .execute();

      // Drop the role-only seed row
      await db.deleteFrom("ApprovalStepAssignee").where("id", "=", row.id).execute();

      runtimeAssigneeCount += users.length;
    }
  }

  if (runtimeAssigneeCount < activatingStep.minimumApprovals) {
    return err(new StepQuorumUnreachableError(activatingStep.id));
  }

  return ok({ approvalRequest: request });
}
