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 {
  NotActiveAssigneeError,
  ParentRequestNotPendingError,
  RoleHasNoActiveMembersError,
  SelfDecisionNotAllowedError,
  StepQuorumUnreachableError,
} from "../lib/errors.generated";
import type { UserManagementQueries } from "../module";

export interface ApproveApprovalStepInput {
  approvalStepAssigneeId: string;
  comment?: string;
}

export async function run(
  db: Transaction,
  input: ApproveApprovalStepInput,
  ctx: CommandContext,
  deps: Pick<UserManagementQueries, "listUsersByRole">,
) {
  const now = new Date();

  // 1. Load StepAssignee with row lock
  const assignee = await db
    .selectFrom("ApprovalStepAssignee")
    .selectAll()
    .where("id", "=", input.approvalStepAssigneeId)
    .forUpdate()
    .executeTakeFirst();

  if (!assignee) {
    return err(new NotActiveAssigneeError(input.approvalStepAssigneeId));
  }

  // 2. Load parent Step with row lock
  const step = await db
    .selectFrom("ApprovalStep")
    .selectAll()
    .where("id", "=", assignee.approvalStepId)
    .forUpdate()
    .executeTakeFirst();

  if (!step) {
    return err(new NotActiveAssigneeError(input.approvalStepAssigneeId));
  }

  // 3. Load parent Request with row lock
  const request = await db
    .selectFrom("ApprovalRequest")
    .selectAll()
    .where("id", "=", step.approvalRequestId)
    .forUpdate()
    .executeTakeFirst();

  if (!request) {
    return err(new NotActiveAssigneeError(input.approvalStepAssigneeId));
  }

  // 4. C1a guard: ownership + assignee PENDING + step IN_PROGRESS
  if (
    assignee.userId !== ctx.actorId ||
    assignee.status !== "PENDING" ||
    step.status !== "IN_PROGRESS"
  ) {
    return err(new NotActiveAssigneeError(input.approvalStepAssigneeId));
  }

  // 5. C1b guard: parent request liveness
  if (request.status !== "PENDING") {
    return err(new ParentRequestNotPendingError(request.id));
  }

  // 6. C3 guard: segregation of duties
  if (ctx.actorId === request.requesterId) {
    return err(new SelfDecisionNotAllowedError(request.id));
  }

  // 7. Insert immutable ApprovalDecision row
  await db
    .insertInto("ApprovalDecision")
    .values({
      approvalRequestId: request.id,
      approvalStepId: step.id,
      decision: "APPROVE",
      decidedByUserId: ctx.actorId,
      comment: input.comment,
      decidedAt: now,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // 8. Transition assignee to APPROVED
  await db
    .updateTable("ApprovalStepAssignee")
    .set({ status: "APPROVED", resolvedAt: now })
    .where("id", "=", assignee.id)
    .execute();

  // 9. Voting evaluation: re-load all assignees on this step
  const stepAssignees = await db
    .selectFrom("ApprovalStepAssignee")
    .selectAll()
    .where("approvalStepId", "=", step.id)
    .execute();

  // DELEGATED rows are skipped from voting checks; the delegate row replaces them.
  const activeAssignees = stepAssignees.filter((row) => row.status !== "DELEGATED");

  // Per-user required guard: every roleId-null required=true assignee must be APPROVED
  const userRequiredPending = activeAssignees.some(
    (row) => row.roleId === null && row.required === true && row.status !== "APPROVED",
  );
  if (userRequiredPending) {
    return ok({ approvalRequest: request });
  }

  // Per-role group quorum guard: each distinct roleId group must satisfy its roleQuorum
  const roleGroups = new Map<
    string,
    { quorum: ApprovalStepAssigneeRoleQuorum | null; rows: typeof activeAssignees }
  >();
  for (const row of activeAssignees) {
    if (row.roleId !== null) {
      const existing = roleGroups.get(row.roleId);
      if (existing) {
        existing.rows.push(row);
      } else {
        roleGroups.set(row.roleId, { quorum: row.roleQuorum, rows: [row] });
      }
    }
  }
  for (const { quorum, rows } of roleGroups.values()) {
    if (quorum === "ALL") {
      if (rows.some((r) => r.status !== "APPROVED")) {
        return ok({ approvalRequest: request });
      }
    } else if (quorum === "ANY") {
      if (!rows.some((r) => r.status === "APPROVED")) {
        return ok({ approvalRequest: request });
      }
    }
  }

  // Step threshold guard: total APPROVED count must meet step.minimumApprovals
  const approvedCount = stepAssignees.filter((row) => row.status === "APPROVED").length;
  if (approvedCount < step.minimumApprovals) {
    return ok({ approvalRequest: request });
  }

  // Step completes
  await db
    .updateTable("ApprovalStep")
    .set({ status: "APPROVED", resolvedAt: now })
    .where("id", "=", step.id)
    .execute();

  // 10. Find next PENDING step by stepOrder
  const nextStep = await db
    .selectFrom("ApprovalStep")
    .selectAll()
    .where("approvalRequestId", "=", request.id)
    .where("stepOrder", ">", step.stepOrder)
    .where("status", "=", "PENDING")
    .orderBy("stepOrder", "asc")
    .limit(1)
    .executeTakeFirst();

  if (nextStep) {
    // 11. Activate next step: expand role-based assignees and set IN_PROGRESS
    const nextAssignees = await db
      .selectFrom("ApprovalStepAssignee")
      .selectAll()
      .where("approvalStepId", "=", nextStep.id)
      .execute();

    let nextRuntimeAssigneeCount = 0;
    for (const row of nextAssignees) {
      // Direct assignee, or a row already expanded in a prior round (resubmit restart).
      if (row.userId !== null) {
        nextRuntimeAssigneeCount += 1;
        continue;
      }
      if (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));
        }

        // Insert one StepAssignee per ACTIVE member, preserving the originating role and roleQuorum
        await db
          .insertInto("ApprovalStepAssignee")
          .values(
            users.map((user) => ({
              approvalStepId: nextStep.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();

        nextRuntimeAssigneeCount += users.length;
      }
    }

    // Runtime quorum guard: after role expansion, the assignee count must be
    // sufficient to reach minimumApprovals.
    if (nextRuntimeAssigneeCount < nextStep.minimumApprovals) {
      return err(new StepQuorumUnreachableError(nextStep.id));
    }

    await db
      .updateTable("ApprovalStep")
      .set({ status: "IN_PROGRESS" })
      .where("id", "=", nextStep.id)
      .execute();

    return ok({ approvalRequest: request });
  }

  // 12. No further step — request transitions to APPROVED
  const updatedRequest = await db
    .updateTable("ApprovalRequest")
    .set({ status: "APPROVED", resolvedAt: now })
    .where("id", "=", request.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ approvalRequest: updatedRequest });
}
