import {
  and,
  asc,
  eq,
  inArray,
  isNull
} from 'drizzle-orm';
import {
  crmApprovalActions,
  crmApprovalEscalationPolicies,
  crmApprovalReminderPolicies,
  crmApprovalRequests,
  crmApprovalSteps,
  crmApprovalWorkflows,
  crmRoleProfiles,
  crmUserRoleAssignments,
  memberships,
  users
} from '@/db/schema';
import { auditAction } from '@/features/foundation/audit-log/service';
import { publishNotificationEvent } from '@/features/foundation/notifications/server/event-service';
import { syncEntityStatusFromApproval } from '@/features/foundation/approval/server/service';
import { resolveCrmMembershipAccess } from '@/lib/auth/crm-access';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import type { Permission, SystemRole } from '@/lib/auth/rbac';
import type {
  ApprovalEscalationPolicyPayload,
  ApprovalEscalationPolicyRecord,
  ApprovalReminderPolicyPayload,
  ApprovalReminderPolicyRecord,
  PendingApprovalQueueItem,
  PendingApprovalQueueResponse,
  PendingApprovalQueueSummary
} from '../types';

function mapReminderPolicyRecord(
  row: typeof crmApprovalReminderPolicies.$inferSelect,
  workflowName: string
): ApprovalReminderPolicyRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    workflowId: row.workflowId,
    workflowName,
    stepNumber: row.stepNumber,
    slaHours: row.slaHours,
    reminderOffsetsHours: row.reminderOffsetsHours ?? [],
    calendarMode: row.calendarMode as ApprovalReminderPolicyRecord['calendarMode'],
    timeoutAction: row.timeoutAction as ApprovalReminderPolicyRecord['timeoutAction'],
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy
  };
}

function mapEscalationPolicyRecord(
  row: typeof crmApprovalEscalationPolicies.$inferSelect,
  workflowName: string
): ApprovalEscalationPolicyRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    workflowId: row.workflowId,
    workflowName,
    stepNumber: row.stepNumber,
    triggerAfterHours: row.triggerAfterHours,
    targetType: row.targetType as ApprovalEscalationPolicyRecord['targetType'],
    targetValue: row.targetValue ?? null,
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy
  };
}

function normalizeOffsets(values: number[]) {
  return [...new Set(values.map((value) => Math.floor(value)).filter((value) => value > 0))].sort(
    (left, right) => left - right
  );
}

function diffHours(from: Date, to: Date) {
  return Math.floor((to.getTime() - from.getTime()) / 3_600_000);
}

function isSameDate(left: Date, right: Date) {
  return (
    left.getUTCFullYear() === right.getUTCFullYear() &&
    left.getUTCMonth() === right.getUTCMonth() &&
    left.getUTCDate() === right.getUTCDate()
  );
}

async function getWorkflowNameMap(organizationId: string, workflowIds?: string[]) {
  const rows = await db
    .select({ id: crmApprovalWorkflows.id, name: crmApprovalWorkflows.name })
    .from(crmApprovalWorkflows)
    .where(
      and(
        eq(crmApprovalWorkflows.organizationId, organizationId),
        isNull(crmApprovalWorkflows.deletedAt),
        ...(workflowIds?.length ? [inArray(crmApprovalWorkflows.id, workflowIds)] : [])
      )
    );

  return new Map(rows.map((row) => [row.id, row.name]));
}

async function assertWorkflowExists(workflowId: string, organizationId: string) {
  const [workflow] = await db
    .select()
    .from(crmApprovalWorkflows)
    .where(
      and(
        eq(crmApprovalWorkflows.id, workflowId),
        eq(crmApprovalWorkflows.organizationId, organizationId),
        isNull(crmApprovalWorkflows.deletedAt)
      )
    )
    .limit(1);

  if (!workflow) {
    throw new AuthError('Approval workflow not found', 404);
  }

  return workflow;
}

async function assertWorkflowStepExists(
  workflowId: string,
  stepNumber: number,
  organizationId: string
) {
  const [step] = await db
    .select()
    .from(crmApprovalSteps)
    .where(
      and(
        eq(crmApprovalSteps.workflowId, workflowId),
        eq(crmApprovalSteps.organizationId, organizationId),
        eq(crmApprovalSteps.stepNumber, stepNumber),
        isNull(crmApprovalSteps.deletedAt)
      )
    )
    .limit(1);

  if (!step) {
    throw new AuthError('Approval workflow step not found', 404);
  }

  return step;
}

async function resolveRoleTargetUserIds(
  organizationId: string,
  roleCode: string
) {
  const assignmentRows = await db
    .select({ userId: crmUserRoleAssignments.userId })
    .from(crmUserRoleAssignments)
    .innerJoin(
      crmRoleProfiles,
      and(
        eq(crmRoleProfiles.id, crmUserRoleAssignments.roleProfileId),
        eq(crmRoleProfiles.organizationId, organizationId),
        eq(crmRoleProfiles.code, roleCode),
        eq(crmRoleProfiles.isActive, true),
        isNull(crmRoleProfiles.deletedAt)
      )
    )
    .where(
      and(
        eq(crmUserRoleAssignments.organizationId, organizationId),
        eq(crmUserRoleAssignments.isActive, true),
        isNull(crmUserRoleAssignments.deletedAt)
      )
    );

  const fallbackRows = await db
    .select({ userId: memberships.userId })
    .from(memberships)
    .where(
      and(
        eq(memberships.organizationId, organizationId),
        eq(memberships.businessRole, roleCode)
      )
    );

  return [...new Set([...assignmentRows, ...fallbackRows].map((row) => row.userId))];
}

async function resolvePermissionTargetUserIds(
  organizationId: string,
  permission: Permission
) {
  const membershipRows = await db
    .select({
      membership: memberships,
      systemRole: users.systemRole
    })
    .from(memberships)
    .innerJoin(users, eq(users.id, memberships.userId))
    .where(eq(memberships.organizationId, organizationId));

  const matchedUserIds: string[] = [];

  for (const row of membershipRows) {
    const resolved = await resolveCrmMembershipAccess({
      systemRole: row.systemRole as SystemRole,
      organizationId,
      membership: row.membership
    });

    if (resolved.permissions.includes(permission)) {
      matchedUserIds.push(row.membership.userId);
    }
  }

  return [...new Set(matchedUserIds)];
}

async function resolveEscalationTargetUserIds(input: {
  organizationId: string;
  requestedBy: string;
  targetType: ApprovalEscalationPolicyRecord['targetType'];
  targetValue: string | null;
}) {
  switch (input.targetType) {
    case 'requester':
      return [input.requestedBy];
    case 'explicit_user':
      return input.targetValue ? [input.targetValue] : [];
    case 'role':
      return input.targetValue ? resolveRoleTargetUserIds(input.organizationId, input.targetValue) : [];
    case 'permission_group':
      return input.targetValue
        ? resolvePermissionTargetUserIds(input.organizationId, input.targetValue as Permission)
        : [];
    case 'manager':
      return input.targetValue
        ? resolveRoleTargetUserIds(input.organizationId, input.targetValue)
        : (
            await db
              .select({ userId: memberships.userId })
              .from(memberships)
              .where(
                and(
                  eq(memberships.organizationId, input.organizationId),
                  eq(memberships.role, 'admin')
                )
              )
          ).map((row) => row.userId);
    default:
      return [];
  }
}

async function buildReminderPolicyMap(organizationId: string) {
  const rows = await db
    .select()
    .from(crmApprovalReminderPolicies)
    .where(
      and(
        eq(crmApprovalReminderPolicies.organizationId, organizationId),
        eq(crmApprovalReminderPolicies.isActive, true),
        isNull(crmApprovalReminderPolicies.deletedAt)
      )
    );

  return new Map(rows.map((row) => [`${row.workflowId}:${row.stepNumber}`, row]));
}

async function buildEscalationPolicyMap(organizationId: string) {
  const rows = await db
    .select()
    .from(crmApprovalEscalationPolicies)
    .where(
      and(
        eq(crmApprovalEscalationPolicies.organizationId, organizationId),
        eq(crmApprovalEscalationPolicies.isActive, true),
        isNull(crmApprovalEscalationPolicies.deletedAt)
      )
    )
    .orderBy(asc(crmApprovalEscalationPolicies.triggerAfterHours));

  const map = new Map<string, Array<typeof crmApprovalEscalationPolicies.$inferSelect>>();
  for (const row of rows) {
    const key = `${row.workflowId}:${row.stepNumber}`;
    const current = map.get(key) ?? [];
    current.push(row);
    map.set(key, current);
  }

  return map;
}

async function loadPendingApprovalRows(organizationId: string) {
  const [requests, workflows, steps] = await Promise.all([
    db
      .select()
      .from(crmApprovalRequests)
      .where(
        and(
          eq(crmApprovalRequests.organizationId, organizationId),
          eq(crmApprovalRequests.status, 'pending'),
          isNull(crmApprovalRequests.deletedAt)
        )
      )
      .orderBy(asc(crmApprovalRequests.requestedAt)),
    db
      .select()
      .from(crmApprovalWorkflows)
      .where(
        and(
          eq(crmApprovalWorkflows.organizationId, organizationId),
          isNull(crmApprovalWorkflows.deletedAt)
        )
      ),
    db
      .select()
      .from(crmApprovalSteps)
      .where(
        and(
          eq(crmApprovalSteps.organizationId, organizationId),
          isNull(crmApprovalSteps.deletedAt)
        )
      )
  ]);

  const workflowMap = new Map(workflows.map((row) => [row.id, row]));
  const stepMap = new Map(steps.map((row) => [`${row.workflowId}:${row.stepNumber}`, row]));

  return requests.map((request) => ({
    request,
    workflow: workflowMap.get(request.workflowId) ?? null,
    step: stepMap.get(`${request.workflowId}:${request.currentStep}`) ?? null
  }));
}

function buildQueueItem(input: {
  request: typeof crmApprovalRequests.$inferSelect;
  workflow: typeof crmApprovalWorkflows.$inferSelect | null;
  step: typeof crmApprovalSteps.$inferSelect | null;
  reminderPolicy: typeof crmApprovalReminderPolicies.$inferSelect | null;
  escalationPolicies: Array<typeof crmApprovalEscalationPolicies.$inferSelect>;
  now: Date;
}): PendingApprovalQueueItem {
  const startedAt = input.request.currentStepStartedAt;
  const waitingHours = diffHours(startedAt, input.now);
  const slaHours = input.reminderPolicy?.slaHours ?? input.step?.slaHours ?? null;
  const dueAt =
    slaHours !== null ? new Date(startedAt.getTime() + slaHours * 3_600_000) : null;
  const isOverdue = dueAt ? dueAt.getTime() < input.now.getTime() : false;
  const isDueToday = dueAt ? isSameDate(dueAt, input.now) : false;
  const remainingHours = dueAt
    ? Math.max(0, Math.ceil((dueAt.getTime() - input.now.getTime()) / 3_600_000))
    : null;
  const isEscalated = input.escalationPolicies.some(
    (policy) => waitingHours >= policy.triggerAfterHours
  );

  return {
    approvalRequestId: input.request.id,
    workflowId: input.request.workflowId,
    workflowName: input.workflow?.name ?? '-',
    entityType: input.request.entityType,
    entityId: input.request.entityId,
    requestedBy: input.request.requestedBy,
    requestedAt: input.request.requestedAt.toISOString(),
    currentStep: input.request.currentStep,
    currentStepRoleCode: input.step?.roleCode ?? null,
    currentStepRoleName: input.step?.roleName ?? null,
    currentStepStartedAt: startedAt.toISOString(),
    waitingHours,
    slaHours,
    remainingHours,
    dueAt: dueAt?.toISOString() ?? null,
    isDueToday,
    isOverdue,
    isEscalated
  };
}

async function publishApprovalAutomationEvent(input: {
  organizationId: string;
  eventType: 'approval.reminder' | 'approval.escalated' | 'approval.timeout';
  request: typeof crmApprovalRequests.$inferSelect;
  workflowName: string;
  step: typeof crmApprovalSteps.$inferSelect | null;
  dedupeKey: string;
  actorUserId: string;
  explicitUserIds?: string[];
  remark?: string;
}) {
  const [actorRows] = await Promise.all([
    db.select({ name: users.name }).from(users).where(eq(users.id, input.actorUserId)).limit(1)
  ]);

  await publishNotificationEvent({
    organizationId: input.organizationId,
    eventType: input.eventType,
    entityType: input.request.entityType,
    entityId: input.request.entityId,
    actorUserId: input.actorUserId,
    dedupeKey: input.dedupeKey,
    payload: {
      approvalRequestId: input.request.id,
      requestedByUserId: input.request.requestedBy,
      workflowId: input.request.workflowId,
      workflowName: input.workflowName,
      entityType: input.request.entityType,
      entityId: input.request.entityId,
      quotationId: input.request.entityId,
      quotationCode: input.request.entityId,
      actorName: actorRows[0]?.name ?? 'Automation',
      currentStepRoleCode: input.step?.roleCode ?? null,
      currentStepRoleName: input.step?.roleName ?? null,
      explicitUserIds: input.explicitUserIds ?? [],
      remark: input.remark ?? '-',
      entityLink: `/dashboard/crm/approvals/${input.request.id}`
    }
  });
}

async function applyTimeoutAction(input: {
  organizationId: string;
  request: typeof crmApprovalRequests.$inferSelect;
  step: typeof crmApprovalSteps.$inferSelect;
  timeoutAction: ApprovalReminderPolicyRecord['timeoutAction'];
  actorUserId: string;
  workflowName: string;
  explicitUserIds: string[];
}) {
  if (input.timeoutAction === 'none') {
    return;
  }

  if (input.timeoutAction === 'auto_escalate') {
    await publishApprovalAutomationEvent({
      organizationId: input.organizationId,
      eventType: 'approval.timeout',
      request: input.request,
      workflowName: input.workflowName,
      step: input.step,
      actorUserId: input.actorUserId,
      explicitUserIds: input.explicitUserIds,
      dedupeKey: `approval.timeout:${input.request.id}:${input.step.stepNumber}`,
      remark: `Timed out after SLA on step ${input.step.stepNumber}`
    });

    await auditAction({
      organizationId: input.organizationId,
      userId: input.actorUserId,
      entityType: 'crm_approval_request',
      entityId: input.request.id,
      action: 'approval_timeout',
      afterData: {
        timeoutAction: input.timeoutAction,
        stepNumber: input.step.stepNumber,
        explicitUserIds: input.explicitUserIds
      }
    });

    return;
  }

  const nextStatus = input.timeoutAction === 'auto_reject' ? 'rejected' : 'cancelled';
  const action = input.timeoutAction === 'auto_reject' ? 'reject' : 'cancel';
  const remark = `Automatically ${action} by approval automation timeout`;

  const [createdAction] = await db
    .insert(crmApprovalActions)
    .values({
      id: crypto.randomUUID(),
      organizationId: input.organizationId,
      approvalRequestId: input.request.id,
      stepNumber: input.step.stepNumber,
      action,
      remark,
      actedBy: input.actorUserId
    })
    .returning();

  const [updatedRequest] = await db
    .update(crmApprovalRequests)
    .set({
      status: nextStatus,
      completedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmApprovalRequests.id, input.request.id))
    .returning();

  await syncEntityStatusFromApproval(
    input.organizationId,
    input.request.entityType,
    input.request.entityId,
    input.actorUserId,
    nextStatus
  );

  await auditAction({
    organizationId: input.organizationId,
    userId: input.actorUserId,
    entityType: 'crm_approval_request',
    entityId: input.request.id,
    action: 'approval_timeout',
    beforeData: input.request,
    afterData: updatedRequest
  });

  await auditAction({
    organizationId: input.organizationId,
    userId: input.actorUserId,
    entityType: 'crm_approval_action',
    entityId: createdAction.id,
    action,
    afterData: createdAction
  });

  await publishApprovalAutomationEvent({
    organizationId: input.organizationId,
    eventType: 'approval.timeout',
    request: updatedRequest,
    workflowName: input.workflowName,
    step: input.step,
    actorUserId: input.actorUserId,
    explicitUserIds: input.explicitUserIds,
    dedupeKey: `approval.timeout:${input.request.id}:${input.step.stepNumber}`,
    remark
  });
}

export async function listApprovalReminderPolicies(organizationId: string) {
  const rows = await db
    .select()
    .from(crmApprovalReminderPolicies)
    .where(
      and(
        eq(crmApprovalReminderPolicies.organizationId, organizationId),
        isNull(crmApprovalReminderPolicies.deletedAt)
      )
    )
    .orderBy(
      asc(crmApprovalReminderPolicies.workflowId),
      asc(crmApprovalReminderPolicies.stepNumber)
    );

  const workflowNameMap = await getWorkflowNameMap(
    organizationId,
    [...new Set(rows.map((row) => row.workflowId))]
  );

  return rows.map((row) => mapReminderPolicyRecord(row, workflowNameMap.get(row.workflowId) ?? '-'));
}

export async function createApprovalReminderPolicy(
  organizationId: string,
  userId: string,
  payload: ApprovalReminderPolicyPayload
) {
  await assertWorkflowExists(payload.workflowId, organizationId);
  await assertWorkflowStepExists(payload.workflowId, payload.stepNumber, organizationId);

  const [current] = await db
    .select()
    .from(crmApprovalReminderPolicies)
    .where(
      and(
        eq(crmApprovalReminderPolicies.organizationId, organizationId),
        eq(crmApprovalReminderPolicies.workflowId, payload.workflowId),
        eq(crmApprovalReminderPolicies.stepNumber, payload.stepNumber),
        isNull(crmApprovalReminderPolicies.deletedAt)
      )
    )
    .limit(1);

  if (current) {
    throw new AuthError('Reminder policy already exists for this workflow step', 409);
  }

  const [created] = await db
    .insert(crmApprovalReminderPolicies)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      workflowId: payload.workflowId,
      stepNumber: payload.stepNumber,
      slaHours: payload.slaHours,
      reminderOffsetsHours: normalizeOffsets(payload.reminderOffsetsHours),
      calendarMode: payload.calendarMode ?? 'calendar_days',
      timeoutAction: payload.timeoutAction ?? 'none',
      isActive: payload.isActive ?? true,
      createdBy: userId,
      updatedBy: userId
    })
    .returning();

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_approval_reminder_policy',
    entityId: created.id,
    action: 'approval_policy_created',
    afterData: created
  });

  const workflowNameMap = await getWorkflowNameMap(organizationId, [created.workflowId]);
  return mapReminderPolicyRecord(created, workflowNameMap.get(created.workflowId) ?? '-');
}

export async function updateApprovalReminderPolicy(
  id: string,
  organizationId: string,
  userId: string,
  payload: ApprovalReminderPolicyPayload
) {
  const [current] = await db
    .select()
    .from(crmApprovalReminderPolicies)
    .where(
      and(
        eq(crmApprovalReminderPolicies.id, id),
        eq(crmApprovalReminderPolicies.organizationId, organizationId),
        isNull(crmApprovalReminderPolicies.deletedAt)
      )
    )
    .limit(1);

  if (!current) {
    throw new AuthError('Reminder policy not found', 404);
  }

  await assertWorkflowExists(payload.workflowId, organizationId);
  await assertWorkflowStepExists(payload.workflowId, payload.stepNumber, organizationId);

  const [updated] = await db
    .update(crmApprovalReminderPolicies)
    .set({
      workflowId: payload.workflowId,
      stepNumber: payload.stepNumber,
      slaHours: payload.slaHours,
      reminderOffsetsHours: normalizeOffsets(payload.reminderOffsetsHours),
      calendarMode: payload.calendarMode ?? 'calendar_days',
      timeoutAction: payload.timeoutAction ?? 'none',
      isActive: payload.isActive ?? true,
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalReminderPolicies.id, id))
    .returning();

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_approval_reminder_policy',
    entityId: id,
    action: 'approval_policy_updated',
    beforeData: current,
    afterData: updated
  });

  const workflowNameMap = await getWorkflowNameMap(organizationId, [updated.workflowId]);
  return mapReminderPolicyRecord(updated, workflowNameMap.get(updated.workflowId) ?? '-');
}

export async function deleteApprovalReminderPolicy(
  id: string,
  organizationId: string,
  userId: string
) {
  const [current] = await db
    .select()
    .from(crmApprovalReminderPolicies)
    .where(
      and(
        eq(crmApprovalReminderPolicies.id, id),
        eq(crmApprovalReminderPolicies.organizationId, organizationId),
        isNull(crmApprovalReminderPolicies.deletedAt)
      )
    )
    .limit(1);

  if (!current) {
    throw new AuthError('Reminder policy not found', 404);
  }

  await db
    .update(crmApprovalReminderPolicies)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalReminderPolicies.id, id));

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_approval_reminder_policy',
    entityId: id,
    action: 'approval_policy_deleted',
    beforeData: current
  });
}

export async function listApprovalEscalationPolicies(organizationId: string) {
  const rows = await db
    .select()
    .from(crmApprovalEscalationPolicies)
    .where(
      and(
        eq(crmApprovalEscalationPolicies.organizationId, organizationId),
        isNull(crmApprovalEscalationPolicies.deletedAt)
      )
    )
    .orderBy(
      asc(crmApprovalEscalationPolicies.workflowId),
      asc(crmApprovalEscalationPolicies.stepNumber),
      asc(crmApprovalEscalationPolicies.triggerAfterHours)
    );

  const workflowNameMap = await getWorkflowNameMap(
    organizationId,
    [...new Set(rows.map((row) => row.workflowId))]
  );

  return rows.map((row) =>
    mapEscalationPolicyRecord(row, workflowNameMap.get(row.workflowId) ?? '-')
  );
}

export async function createApprovalEscalationPolicy(
  organizationId: string,
  userId: string,
  payload: ApprovalEscalationPolicyPayload
) {
  await assertWorkflowExists(payload.workflowId, organizationId);
  await assertWorkflowStepExists(payload.workflowId, payload.stepNumber, organizationId);

  const [created] = await db
    .insert(crmApprovalEscalationPolicies)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      workflowId: payload.workflowId,
      stepNumber: payload.stepNumber,
      triggerAfterHours: payload.triggerAfterHours,
      targetType: payload.targetType,
      targetValue: payload.targetValue ?? null,
      isActive: payload.isActive ?? true,
      createdBy: userId,
      updatedBy: userId
    })
    .returning();

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_approval_escalation_policy',
    entityId: created.id,
    action: 'approval_policy_created',
    afterData: created
  });

  const workflowNameMap = await getWorkflowNameMap(organizationId, [created.workflowId]);
  return mapEscalationPolicyRecord(created, workflowNameMap.get(created.workflowId) ?? '-');
}

export async function updateApprovalEscalationPolicy(
  id: string,
  organizationId: string,
  userId: string,
  payload: ApprovalEscalationPolicyPayload
) {
  const [current] = await db
    .select()
    .from(crmApprovalEscalationPolicies)
    .where(
      and(
        eq(crmApprovalEscalationPolicies.id, id),
        eq(crmApprovalEscalationPolicies.organizationId, organizationId),
        isNull(crmApprovalEscalationPolicies.deletedAt)
      )
    )
    .limit(1);

  if (!current) {
    throw new AuthError('Escalation policy not found', 404);
  }

  await assertWorkflowExists(payload.workflowId, organizationId);
  await assertWorkflowStepExists(payload.workflowId, payload.stepNumber, organizationId);

  const [updated] = await db
    .update(crmApprovalEscalationPolicies)
    .set({
      workflowId: payload.workflowId,
      stepNumber: payload.stepNumber,
      triggerAfterHours: payload.triggerAfterHours,
      targetType: payload.targetType,
      targetValue: payload.targetValue ?? null,
      isActive: payload.isActive ?? true,
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalEscalationPolicies.id, id))
    .returning();

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_approval_escalation_policy',
    entityId: id,
    action: 'approval_policy_updated',
    beforeData: current,
    afterData: updated
  });

  const workflowNameMap = await getWorkflowNameMap(organizationId, [updated.workflowId]);
  return mapEscalationPolicyRecord(updated, workflowNameMap.get(updated.workflowId) ?? '-');
}

export async function deleteApprovalEscalationPolicy(
  id: string,
  organizationId: string,
  userId: string
) {
  const [current] = await db
    .select()
    .from(crmApprovalEscalationPolicies)
    .where(
      and(
        eq(crmApprovalEscalationPolicies.id, id),
        eq(crmApprovalEscalationPolicies.organizationId, organizationId),
        isNull(crmApprovalEscalationPolicies.deletedAt)
      )
    )
    .limit(1);

  if (!current) {
    throw new AuthError('Escalation policy not found', 404);
  }

  await db
    .update(crmApprovalEscalationPolicies)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date(),
      updatedBy: userId
    })
    .where(eq(crmApprovalEscalationPolicies.id, id));

  await auditAction({
    organizationId,
    userId,
    entityType: 'crm_approval_escalation_policy',
    entityId: id,
    action: 'approval_policy_deleted',
    beforeData: current
  });
}

export async function getPendingApprovalQueue(
  organizationId: string
): Promise<PendingApprovalQueueResponse> {
  const [pendingRows, reminderPolicyMap, escalationPolicyMap] = await Promise.all([
    loadPendingApprovalRows(organizationId),
    buildReminderPolicyMap(organizationId),
    buildEscalationPolicyMap(organizationId)
  ]);

  const now = new Date();
  const items = pendingRows.map(({ request, workflow, step }) =>
    buildQueueItem({
      request,
      workflow,
      step,
      reminderPolicy: reminderPolicyMap.get(`${request.workflowId}:${request.currentStep}`) ?? null,
      escalationPolicies:
        escalationPolicyMap.get(`${request.workflowId}:${request.currentStep}`) ?? [],
      now
    })
  );

  const summary: PendingApprovalQueueSummary = {
    pending: items.length,
    dueToday: items.filter((item) => item.isDueToday).length,
    overdue: items.filter((item) => item.isOverdue).length,
    escalated: items.filter((item) => item.isEscalated).length
  };

  return { items, summary };
}

export async function processApprovalAutomation(
  organizationId: string,
  actorUserId: string
) {
  const [pendingRows, reminderPolicyMap, escalationPolicyMap] = await Promise.all([
    loadPendingApprovalRows(organizationId),
    buildReminderPolicyMap(organizationId),
    buildEscalationPolicyMap(organizationId)
  ]);

  const now = new Date();
  let processedCount = 0;

  for (const { request, workflow, step } of pendingRows) {
    if (!workflow || !step) {
      continue;
    }

    const reminderPolicy =
      reminderPolicyMap.get(`${request.workflowId}:${request.currentStep}`) ?? null;
    const escalationPolicies =
      escalationPolicyMap.get(`${request.workflowId}:${request.currentStep}`) ?? [];
    const startedAt = request.currentStepStartedAt;
    const waitingHours = diffHours(startedAt, now);

    if (reminderPolicy) {
      for (const offset of reminderPolicy.reminderOffsetsHours) {
        if (waitingHours >= offset) {
          await publishApprovalAutomationEvent({
            organizationId,
            eventType: 'approval.reminder',
            request,
            workflowName: workflow.name,
            step,
            actorUserId,
            dedupeKey: `approval.reminder:${request.id}:${request.currentStep}:${offset}`,
            remark: `Reminder after ${offset} hours`
          });

          await auditAction({
            organizationId,
            userId: actorUserId,
            entityType: 'crm_approval_request',
            entityId: request.id,
            action: 'approval_reminder_sent',
            afterData: { stepNumber: request.currentStep, offsetHours: offset }
          });
        }
      }
    }

    for (const escalationPolicy of escalationPolicies) {
      if (waitingHours < escalationPolicy.triggerAfterHours) {
        continue;
      }

      const explicitUserIds = await resolveEscalationTargetUserIds({
        organizationId,
        requestedBy: request.requestedBy,
        targetType: escalationPolicy.targetType as ApprovalEscalationPolicyRecord['targetType'],
        targetValue: escalationPolicy.targetValue ?? null
      });

      await publishApprovalAutomationEvent({
        organizationId,
        eventType: 'approval.escalated',
        request,
        workflowName: workflow.name,
        step,
        actorUserId,
        explicitUserIds,
        dedupeKey: `approval.escalated:${request.id}:${escalationPolicy.id}`,
        remark: `Escalation after ${escalationPolicy.triggerAfterHours} hours`
      });

      await auditAction({
        organizationId,
        userId: actorUserId,
        entityType: 'crm_approval_request',
        entityId: request.id,
        action: 'approval_escalated',
        afterData: {
          escalationPolicyId: escalationPolicy.id,
          explicitUserIds
        }
      });
    }

    const slaHours = reminderPolicy?.slaHours ?? step.slaHours;
    if (slaHours > 0 && waitingHours >= slaHours) {
      const explicitUserIds = escalationPolicies.length
        ? (
            await Promise.all(
              escalationPolicies.map((policy) =>
                resolveEscalationTargetUserIds({
                  organizationId,
                  requestedBy: request.requestedBy,
                  targetType: policy.targetType as ApprovalEscalationPolicyRecord['targetType'],
                  targetValue: policy.targetValue ?? null
                })
              )
            )
          ).flat()
        : [];

      await applyTimeoutAction({
        organizationId,
        request,
        step,
        timeoutAction: (reminderPolicy?.timeoutAction ?? step.timeoutAction) as ApprovalReminderPolicyRecord['timeoutAction'],
        actorUserId,
        workflowName: workflow.name,
        explicitUserIds: [...new Set(explicitUserIds)]
      });
    }

    processedCount += 1;
  }

  return {
    processedCount,
    processedAt: now.toISOString()
  };
}
