import { and, asc, count, desc, eq, ilike, inArray, isNull, or, type SQL } from 'drizzle-orm';
import {
  crmApprovalActions,
  crmApprovalRequests,
  crmApprovalSteps,
  crmApprovalWorkflows,
  crmCustomers,
  crmQuotationItems,
  crmQuotations,
  memberships,
  users
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { auditAction } from '@/features/foundation/audit-log/service';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import type {
  ApprovalActionRecord,
  ApprovalDetailRecord,
  ApprovalFilters,
  ApprovalListItem,
  ApprovalStepMutationPayload,
  ApprovalRequestRecord,
  ApprovalStepRecord,
  ApprovalTimelineItem,
  ApprovalWorkflowDetail,
  ApprovalWorkflowListItem,
  ApprovalWorkflowMutationPayload,
  ApprovalWorkflowRecord,
  SubmitApprovalPayload
} from '../types';

const APPROVAL_REQUEST_STATUSES = {
  pending: 'pending',
  approved: 'approved',
  rejected: 'rejected',
  returned: 'returned',
  cancelled: 'cancelled'
} as const;

const APPROVAL_ACTIONS = {
  submit: 'submit',
  approve: 'approve',
  reject: 'reject',
  return: 'return',
  cancel: 'cancel'
} as const;

const QUOTATION_STATUS_CATEGORY = 'crm_quotation_status';

function mapWorkflowRecord(row: typeof crmApprovalWorkflows.$inferSelect): ApprovalWorkflowRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    code: row.code,
    name: row.name,
    entityType: row.entityType,
    isActive: row.isActive,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function mapStepRecord(row: typeof crmApprovalSteps.$inferSelect): ApprovalStepRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    workflowId: row.workflowId,
    stepNumber: row.stepNumber,
    roleCode: row.roleCode,
    roleName: row.roleName,
    isRequired: row.isRequired,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function mapRequestRecord(row: typeof crmApprovalRequests.$inferSelect): ApprovalRequestRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    workflowId: row.workflowId,
    entityType: row.entityType,
    entityId: row.entityId,
    currentStep: row.currentStep,
    status: row.status,
    requestedBy: row.requestedBy,
    requestedAt: row.requestedAt.toISOString(),
    completedAt: row.completedAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function mapActionRecord(row: typeof crmApprovalActions.$inferSelect): ApprovalActionRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    approvalRequestId: row.approvalRequestId,
    stepNumber: row.stepNumber,
    action: row.action,
    remark: row.remark,
    actedBy: row.actedBy,
    actedAt: row.actedAt.toISOString(),
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function parseSort(sort?: string) {
  if (!sort) {
    return desc(crmApprovalRequests.updatedAt);
  }

  try {
    const parsed = JSON.parse(sort) as Array<{ id: string; desc?: boolean }>;
    const [rule] = parsed;

    if (!rule) {
      return desc(crmApprovalRequests.updatedAt);
    }

    const sortMap = {
      status: crmApprovalRequests.status,
      currentStep: crmApprovalRequests.currentStep,
      requestedAt: crmApprovalRequests.requestedAt,
      updatedAt: crmApprovalRequests.updatedAt
    } as const;

    const column = sortMap[rule.id as keyof typeof sortMap];
    if (!column) {
      return desc(crmApprovalRequests.updatedAt);
    }

    return rule.desc ? desc(column) : asc(column);
  } catch {
    return desc(crmApprovalRequests.updatedAt);
  }
}

async function resolveQuotationStatusIdByCode(organizationId: string, code: string) {
  const options = await getActiveOptionsByCategory(QUOTATION_STATUS_CATEGORY, { organizationId });
  return options.find((option) => option.code === code)?.id ?? null;
}

async function resolveQuotationStatusCodeById(organizationId: string, id: string) {
  const options = await getActiveOptionsByCategory(QUOTATION_STATUS_CATEGORY, { organizationId });
  return options.find((option) => option.id === id)?.code ?? null;
}

async function assertWorkflowByCode(organizationId: string, code: string, entityType?: string) {
  const [workflow] = await db
    .select()
    .from(crmApprovalWorkflows)
    .where(
      and(
        eq(crmApprovalWorkflows.organizationId, organizationId),
        eq(crmApprovalWorkflows.code, code),
        eq(crmApprovalWorkflows.isActive, true),
        isNull(crmApprovalWorkflows.deletedAt),
        ...(entityType ? [eq(crmApprovalWorkflows.entityType, entityType)] : [])
      )
    )
    .limit(1);

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

  return workflow;
}

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

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

  return workflow;
}

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

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

  return step;
}

async function assertApprovalRequest(id: string, organizationId: string) {
  const [request] = await db
    .select()
    .from(crmApprovalRequests)
    .where(
      and(
        eq(crmApprovalRequests.id, id),
        eq(crmApprovalRequests.organizationId, organizationId),
        isNull(crmApprovalRequests.deletedAt)
      )
    )
    .limit(1);

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

  return request;
}

async function listWorkflowSteps(
  workflowId: string,
  organizationId: string
): Promise<ApprovalStepRecord[]> {
  const rows = await db
    .select()
    .from(crmApprovalSteps)
    .where(
      and(
        eq(crmApprovalSteps.workflowId, workflowId),
        eq(crmApprovalSteps.organizationId, organizationId),
        isNull(crmApprovalSteps.deletedAt)
      )
    )
    .orderBy(asc(crmApprovalSteps.stepNumber));

  return rows.map(mapStepRecord);
}

export async function listApprovalWorkflows(
  organizationId: string
): Promise<ApprovalWorkflowListItem[]> {
  const [workflows, steps] = await Promise.all([
    db
      .select()
      .from(crmApprovalWorkflows)
      .where(
        and(
          eq(crmApprovalWorkflows.organizationId, organizationId),
          isNull(crmApprovalWorkflows.deletedAt)
        )
      )
      .orderBy(asc(crmApprovalWorkflows.entityType), asc(crmApprovalWorkflows.code)),
    db
      .select()
      .from(crmApprovalSteps)
      .where(
        and(eq(crmApprovalSteps.organizationId, organizationId), isNull(crmApprovalSteps.deletedAt))
      )
  ]);

  return workflows.map((workflow) => ({
    ...mapWorkflowRecord(workflow),
    stepCount: steps.filter((step) => step.workflowId === workflow.id).length
  }));
}

export async function getApprovalWorkflow(
  id: string,
  organizationId: string
): Promise<ApprovalWorkflowDetail> {
  const workflow = await assertWorkflowById(id, organizationId);
  const steps = await listWorkflowSteps(id, organizationId);

  return {
    ...mapWorkflowRecord(workflow),
    steps
  };
}

export async function createApprovalWorkflow(
  organizationId: string,
  payload: ApprovalWorkflowMutationPayload
): Promise<ApprovalWorkflowRecord> {
  const [created] = await db
    .insert(crmApprovalWorkflows)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      code: payload.code.trim(),
      name: payload.name.trim(),
      entityType: payload.entityType.trim(),
      isActive: payload.isActive ?? true
    })
    .returning();

  return mapWorkflowRecord(created);
}

export async function updateApprovalWorkflow(
  id: string,
  organizationId: string,
  payload: Partial<ApprovalWorkflowMutationPayload>
): Promise<ApprovalWorkflowRecord> {
  const current = await assertWorkflowById(id, organizationId);
  const [updated] = await db
    .update(crmApprovalWorkflows)
    .set({
      code: payload.code?.trim() ?? current.code,
      name: payload.name?.trim() ?? current.name,
      entityType: payload.entityType?.trim() ?? current.entityType,
      isActive: payload.isActive ?? current.isActive,
      updatedAt: new Date()
    })
    .where(eq(crmApprovalWorkflows.id, id))
    .returning();

  return mapWorkflowRecord(updated);
}

export async function softDeleteApprovalWorkflow(id: string, organizationId: string) {
  const current = await assertWorkflowById(id, organizationId);
  const [updated] = await db
    .update(crmApprovalWorkflows)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmApprovalWorkflows.id, id))
    .returning();

  return {
    before: current,
    after: updated
  };
}

export async function createApprovalStep(
  workflowId: string,
  organizationId: string,
  payload: ApprovalStepMutationPayload
): Promise<ApprovalStepRecord> {
  await assertWorkflowById(workflowId, organizationId);
  const [created] = await db
    .insert(crmApprovalSteps)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      workflowId,
      stepNumber: payload.stepNumber,
      roleCode: payload.roleCode.trim(),
      roleName: payload.roleName.trim(),
      isRequired: payload.isRequired ?? true
    })
    .returning();

  return mapStepRecord(created);
}

export async function updateApprovalStep(
  id: string,
  organizationId: string,
  payload: Partial<ApprovalStepMutationPayload>
): Promise<ApprovalStepRecord> {
  const current = await assertWorkflowStep(id, organizationId);
  const [updated] = await db
    .update(crmApprovalSteps)
    .set({
      stepNumber: payload.stepNumber ?? current.stepNumber,
      roleCode: payload.roleCode?.trim() ?? current.roleCode,
      roleName: payload.roleName?.trim() ?? current.roleName,
      isRequired: payload.isRequired ?? current.isRequired,
      updatedAt: new Date()
    })
    .where(eq(crmApprovalSteps.id, id))
    .returning();

  return mapStepRecord(updated);
}

export async function deleteApprovalStep(id: string, organizationId: string) {
  const current = await assertWorkflowStep(id, organizationId);
  const [updated] = await db
    .update(crmApprovalSteps)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmApprovalSteps.id, id))
    .returning();

  return {
    before: current,
    after: updated
  };
}

export async function replaceApprovalWorkflowSteps(
  workflowId: string,
  organizationId: string,
  steps: ApprovalStepMutationPayload[]
): Promise<ApprovalStepRecord[]> {
  await assertWorkflowById(workflowId, organizationId);
  const existingSteps = await listWorkflowSteps(workflowId, organizationId);

  if (existingSteps.length) {
    await db
      .update(crmApprovalSteps)
      .set({
        deletedAt: new Date(),
        updatedAt: new Date()
      })
      .where(eq(crmApprovalSteps.workflowId, workflowId));
  }

  if (!steps.length) {
    return [];
  }

  const created = await db
    .insert(crmApprovalSteps)
    .values(
      steps.map((step, index) => ({
        id: crypto.randomUUID(),
        organizationId,
        workflowId,
        stepNumber: step.stepNumber ?? index + 1,
        roleCode: step.roleCode.trim(),
        roleName: step.roleName.trim(),
        isRequired: step.isRequired ?? true
      }))
    )
    .returning();

  return created.map(mapStepRecord);
}

async function assertActivePendingRequestForEntity(
  organizationId: string,
  entityType: string,
  entityId: string
) {
  const [request] = await db
    .select()
    .from(crmApprovalRequests)
    .where(
      and(
        eq(crmApprovalRequests.organizationId, organizationId),
        eq(crmApprovalRequests.entityType, entityType),
        eq(crmApprovalRequests.entityId, entityId),
        eq(crmApprovalRequests.status, APPROVAL_REQUEST_STATUSES.pending),
        isNull(crmApprovalRequests.deletedAt)
      )
    )
    .limit(1);

  return request ?? null;
}

async function assertQuotationForApprovalReadiness(entityId: string, organizationId: string) {
  const [quotation] = await db
    .select()
    .from(crmQuotations)
    .where(
      and(
        eq(crmQuotations.id, entityId),
        eq(crmQuotations.organizationId, organizationId),
        isNull(crmQuotations.deletedAt)
      )
    )
    .limit(1);

  if (!quotation) {
    throw new AuthError('Quotation not found', 404);
  }

  const statusCode = await resolveQuotationStatusCodeById(organizationId, quotation.status);
  if (!statusCode || !['draft', 'revised'].includes(statusCode)) {
    throw new AuthError('Only draft or revised quotations can be submitted for approval', 400);
  }

  const [itemCount] = await db
    .select({ value: count() })
    .from(crmQuotationItems)
    .where(
      and(
        eq(crmQuotationItems.organizationId, organizationId),
        eq(crmQuotationItems.quotationId, entityId),
        isNull(crmQuotationItems.deletedAt)
      )
    );

  if (!itemCount?.value) {
    throw new AuthError('Quotation must contain at least one item before approval', 400);
  }

  const [customer] = await db
    .select()
    .from(crmCustomers)
    .where(
      and(
        eq(crmCustomers.id, quotation.customerId),
        eq(crmCustomers.organizationId, organizationId),
        isNull(crmCustomers.deletedAt)
      )
    )
    .limit(1);

  if (!customer) {
    throw new AuthError('Quotation must be linked to an active customer', 400);
  }

  return quotation;
}

async function syncQuotationStatusFromApproval(
  quotationId: string,
  organizationId: string,
  userId: string,
  nextStatusCode: string
) {
  const statusId = await resolveQuotationStatusIdByCode(organizationId, nextStatusCode);

  if (!statusId) {
    throw new AuthError(`Quotation status ${nextStatusCode} is not configured`, 400);
  }

  const nextValues: Partial<typeof crmQuotations.$inferInsert> = {
    status: statusId,
    approvedAt: nextStatusCode === 'approved' ? new Date() : null,
    updatedAt: new Date(),
    updatedBy: userId
  };

  if (nextStatusCode !== 'approved') {
    nextValues.approvedArtifactId = null;
    nextValues.approvedPdfUrl = null;
    nextValues.approvedSnapshot = null;
    nextValues.approvedTemplateVersionId = null;
  }

  await db.update(crmQuotations).set(nextValues).where(eq(crmQuotations.id, quotationId));
}

async function syncEntityStatusFromApproval(
  organizationId: string,
  entityType: string,
  entityId: string,
  userId: string,
  approvalStatus: string
) {
  if (entityType !== 'quotation') {
    return;
  }

  if (approvalStatus === APPROVAL_REQUEST_STATUSES.pending) {
    await syncQuotationStatusFromApproval(entityId, organizationId, userId, 'pending_approval');
    return;
  }

  if (approvalStatus === APPROVAL_REQUEST_STATUSES.approved) {
    // TODO(task-h.1): add an async hook/job to auto-generate approved PDFs after final approval.
    await syncQuotationStatusFromApproval(entityId, organizationId, userId, 'approved');
    return;
  }

  if (approvalStatus === APPROVAL_REQUEST_STATUSES.rejected) {
    await syncQuotationStatusFromApproval(entityId, organizationId, userId, 'rejected');
    return;
  }

  if (approvalStatus === APPROVAL_REQUEST_STATUSES.returned) {
    await syncQuotationStatusFromApproval(entityId, organizationId, userId, 'draft');
  }
}

async function assertActorCanHandleStep(organizationId: string, userId: string, roleCode: string) {
  const [userRow] = await db.select().from(users).where(eq(users.id, userId)).limit(1);

  if (!userRow) {
    throw new AuthError('User not found', 404);
  }

  if (userRow.systemRole === 'super_admin') {
    return;
  }

  const [membership] = await db
    .select()
    .from(memberships)
    .where(and(eq(memberships.organizationId, organizationId), eq(memberships.userId, userId)))
    .limit(1);

  if (!membership) {
    throw new AuthError('Organization membership required', 403);
  }

  if (membership.role === 'admin' || membership.businessRole === roleCode) {
    return;
  }

  throw new AuthError('You are not allowed to act on this approval step', 403);
}

async function resolveEntitySummaries(
  organizationId: string,
  items: Array<{ entityType: string; entityId: string }>
) {
  const quotationIds = items
    .filter((item) => item.entityType === 'quotation')
    .map((item) => item.entityId);
  const quotations = quotationIds.length
    ? await db
        .select({
          id: crmQuotations.id,
          code: crmQuotations.code,
          projectName: crmQuotations.projectName
        })
        .from(crmQuotations)
        .where(
          and(
            eq(crmQuotations.organizationId, organizationId),
            inArray(crmQuotations.id, quotationIds),
            isNull(crmQuotations.deletedAt)
          )
        )
    : [];

  return new Map(
    quotations.map((quotation) => [
      `quotation:${quotation.id}`,
      {
        entityCode: quotation.code,
        entityTitle: quotation.projectName ?? quotation.code
      }
    ])
  );
}

function buildApprovalFilters(organizationId: string, filters: ApprovalFilters): SQL[] {
  return [
    eq(crmApprovalRequests.organizationId, organizationId),
    isNull(crmApprovalRequests.deletedAt),
    ...(filters.status ? [eq(crmApprovalRequests.status, filters.status)] : []),
    ...(filters.entityType ? [eq(crmApprovalRequests.entityType, filters.entityType)] : []),
    ...(filters.entityId ? [eq(crmApprovalRequests.entityId, filters.entityId)] : []),
    ...(filters.search
      ? [
          or(
            ilike(crmApprovalRequests.entityId, `%${filters.search}%`),
            ilike(crmApprovalRequests.status, `%${filters.search}%`)
          )!
        ]
      : [])
  ];
}

export async function listApprovalRequests(
  organizationId: string,
  filters: ApprovalFilters
): Promise<{ items: ApprovalListItem[]; totalItems: number }> {
  const page = filters.page ?? 1;
  const limit = filters.limit ?? 10;
  const whereFilters = buildApprovalFilters(organizationId, filters);
  const where = whereFilters.length === 1 ? whereFilters[0] : and(...whereFilters);
  const offset = (page - 1) * limit;

  const [totalResult] = await db.select({ value: count() }).from(crmApprovalRequests).where(where);
  const rows = await db
    .select()
    .from(crmApprovalRequests)
    .where(where)
    .orderBy(parseSort(filters.sort))
    .limit(limit)
    .offset(offset);

  const workflowIds = [...new Set(rows.map((row) => row.workflowId))];
  const requesterIds = [...new Set(rows.map((row) => row.requestedBy))];
  const [workflows, requesters, entitySummaries] = await Promise.all([
    workflowIds.length
      ? db.select().from(crmApprovalWorkflows).where(inArray(crmApprovalWorkflows.id, workflowIds))
      : [],
    requesterIds.length
      ? db
          .select({ id: users.id, name: users.name })
          .from(users)
          .where(inArray(users.id, requesterIds))
      : [],
    resolveEntitySummaries(
      organizationId,
      rows.map((row) => ({ entityType: row.entityType, entityId: row.entityId }))
    )
  ]);
  const workflowMap = new Map(workflows.map((row) => [row.id, row]));
  const requesterMap = new Map(requesters.map((row) => [row.id, row.name]));
  const stepMap = new Map<string, ApprovalStepRecord>();

  if (workflowIds.length) {
    const steps = await db
      .select()
      .from(crmApprovalSteps)
      .where(
        and(
          eq(crmApprovalSteps.organizationId, organizationId),
          inArray(crmApprovalSteps.workflowId, workflowIds),
          isNull(crmApprovalSteps.deletedAt)
        )
      );

    for (const step of steps) {
      stepMap.set(`${step.workflowId}:${step.stepNumber}`, mapStepRecord(step));
    }
  }

  return {
    items: rows.map((row) => {
      const workflow = workflowMap.get(row.workflowId);
      const currentStep = stepMap.get(`${row.workflowId}:${row.currentStep}`) ?? null;
      const entitySummary = entitySummaries.get(`${row.entityType}:${row.entityId}`) ?? null;

      return {
        ...mapRequestRecord(row),
        workflowCode: workflow?.code ?? '',
        workflowName: workflow?.name ?? '',
        currentStepRoleCode: currentStep?.roleCode ?? null,
        currentStepRoleName: currentStep?.roleName ?? null,
        requestedByName: requesterMap.get(row.requestedBy) ?? null,
        entityCode: entitySummary?.entityCode ?? null,
        entityTitle: entitySummary?.entityTitle ?? null
      };
    }),
    totalItems: totalResult?.value ?? 0
  };
}

export async function getApprovalTimeline(
  approvalRequestId: string,
  organizationId: string
): Promise<ApprovalTimelineItem[]> {
  await assertApprovalRequest(approvalRequestId, organizationId);
  const rows = await db
    .select()
    .from(crmApprovalActions)
    .where(
      and(
        eq(crmApprovalActions.approvalRequestId, approvalRequestId),
        eq(crmApprovalActions.organizationId, organizationId),
        isNull(crmApprovalActions.deletedAt)
      )
    )
    .orderBy(asc(crmApprovalActions.actedAt), asc(crmApprovalActions.createdAt));
  const actorIds = [...new Set(rows.map((row) => row.actedBy))];
  const actors = actorIds.length
    ? await db
        .select({ id: users.id, name: users.name })
        .from(users)
        .where(inArray(users.id, actorIds))
    : [];
  const actorMap = new Map(actors.map((row) => [row.id, row.name]));

  return rows.map((row) => ({
    ...mapActionRecord(row),
    actorName: actorMap.get(row.actedBy) ?? null
  }));
}

export async function getCurrentApprovalStep(
  approvalRequestId: string,
  organizationId: string
): Promise<ApprovalStepRecord | null> {
  const request = await assertApprovalRequest(approvalRequestId, organizationId);
  const [row] = await db
    .select()
    .from(crmApprovalSteps)
    .where(
      and(
        eq(crmApprovalSteps.workflowId, request.workflowId),
        eq(crmApprovalSteps.organizationId, organizationId),
        eq(crmApprovalSteps.stepNumber, request.currentStep),
        isNull(crmApprovalSteps.deletedAt)
      )
    )
    .limit(1);

  return row ? mapStepRecord(row) : null;
}

export async function getApprovalRequest(
  id: string,
  organizationId: string
): Promise<ApprovalDetailRecord> {
  const requestRow = await assertApprovalRequest(id, organizationId);
  const [workflowRow] = await db
    .select()
    .from(crmApprovalWorkflows)
    .where(eq(crmApprovalWorkflows.id, requestRow.workflowId))
    .limit(1);

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

  const [steps, timeline, entitySummaries] = await Promise.all([
    listWorkflowSteps(requestRow.workflowId, organizationId),
    getApprovalTimeline(id, organizationId),
    resolveEntitySummaries(organizationId, [
      { entityType: requestRow.entityType, entityId: requestRow.entityId }
    ])
  ]);
  const currentStep = steps.find((step) => step.stepNumber === requestRow.currentStep) ?? null;
  const entitySummary =
    entitySummaries.get(`${requestRow.entityType}:${requestRow.entityId}`) ?? null;

  return {
    request: mapRequestRecord(requestRow),
    workflow: mapWorkflowRecord(workflowRow),
    steps,
    currentStep,
    timeline,
    entityCode: entitySummary?.entityCode ?? null,
    entityTitle: entitySummary?.entityTitle ?? null
  };
}

export async function submitForApproval(
  organizationId: string,
  userId: string,
  payload: SubmitApprovalPayload
) {
  const workflow = await assertWorkflowByCode(
    organizationId,
    payload.workflowCode ?? `${payload.entityType}_standard_approval`,
    payload.entityType
  );
  const steps = await listWorkflowSteps(workflow.id, organizationId);

  if (!steps.length) {
    throw new AuthError('Approval workflow has no steps', 400);
  }

  const existingRequest = await assertActivePendingRequestForEntity(
    organizationId,
    payload.entityType,
    payload.entityId
  );

  if (existingRequest) {
    throw new AuthError('This document already has a pending approval request', 400);
  }

  if (payload.entityType === 'quotation') {
    await assertQuotationForApprovalReadiness(payload.entityId, organizationId);
  }

  const [createdRequest] = await db
    .insert(crmApprovalRequests)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      workflowId: workflow.id,
      entityType: payload.entityType,
      entityId: payload.entityId,
      currentStep: steps[0].stepNumber,
      status: APPROVAL_REQUEST_STATUSES.pending,
      requestedBy: userId
    })
    .returning();

  const [createdAction] = await db
    .insert(crmApprovalActions)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      approvalRequestId: createdRequest.id,
      stepNumber: 0,
      action: APPROVAL_ACTIONS.submit,
      remark: payload.remark?.trim() || null,
      actedBy: userId
    })
    .returning();

  await syncEntityStatusFromApproval(
    organizationId,
    payload.entityType,
    payload.entityId,
    userId,
    APPROVAL_REQUEST_STATUSES.pending
  );

  await Promise.all([
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_request',
      entityId: createdRequest.id,
      action: APPROVAL_ACTIONS.submit,
      afterData: createdRequest
    }),
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_action',
      entityId: createdAction.id,
      action: APPROVAL_ACTIONS.submit,
      afterData: createdAction
    })
  ]);

  return createdRequest;
}

export async function approveApproval(
  approvalRequestId: string,
  organizationId: string,
  userId: string,
  remark?: string
) {
  const request = await assertApprovalRequest(approvalRequestId, organizationId);

  if (request.status !== APPROVAL_REQUEST_STATUSES.pending) {
    throw new AuthError('Only pending approvals can be approved', 400);
  }

  const steps = await listWorkflowSteps(request.workflowId, organizationId);
  const currentStep = steps.find((step) => step.stepNumber === request.currentStep);

  if (!currentStep) {
    throw new AuthError('Current approval step not found', 404);
  }

  await assertActorCanHandleStep(organizationId, userId, currentStep.roleCode);
  const nextStep = steps.find((step) => step.stepNumber > currentStep.stepNumber) ?? null;

  const [createdAction] = await db
    .insert(crmApprovalActions)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      approvalRequestId,
      stepNumber: currentStep.stepNumber,
      action: APPROVAL_ACTIONS.approve,
      remark: remark?.trim() || null,
      actedBy: userId
    })
    .returning();

  const [updatedRequest] = await db
    .update(crmApprovalRequests)
    .set({
      currentStep: nextStep?.stepNumber ?? currentStep.stepNumber,
      status: nextStep ? APPROVAL_REQUEST_STATUSES.pending : APPROVAL_REQUEST_STATUSES.approved,
      completedAt: nextStep ? null : new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmApprovalRequests.id, approvalRequestId))
    .returning();

  if (!nextStep) {
    await syncEntityStatusFromApproval(
      organizationId,
      request.entityType,
      request.entityId,
      userId,
      APPROVAL_REQUEST_STATUSES.approved
    );
  }

  await Promise.all([
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_request',
      entityId: approvalRequestId,
      action: APPROVAL_ACTIONS.approve,
      beforeData: request,
      afterData: updatedRequest
    }),
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_action',
      entityId: createdAction.id,
      action: APPROVAL_ACTIONS.approve,
      afterData: createdAction
    })
  ]);

  return updatedRequest;
}

export async function rejectApproval(
  approvalRequestId: string,
  organizationId: string,
  userId: string,
  remark?: string
) {
  const request = await assertApprovalRequest(approvalRequestId, organizationId);

  if (request.status !== APPROVAL_REQUEST_STATUSES.pending) {
    throw new AuthError('Only pending approvals can be rejected', 400);
  }

  const currentStep = await getCurrentApprovalStep(approvalRequestId, organizationId);

  if (!currentStep) {
    throw new AuthError('Current approval step not found', 404);
  }

  await assertActorCanHandleStep(organizationId, userId, currentStep.roleCode);

  const [createdAction] = await db
    .insert(crmApprovalActions)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      approvalRequestId,
      stepNumber: currentStep.stepNumber,
      action: APPROVAL_ACTIONS.reject,
      remark: remark?.trim() || null,
      actedBy: userId
    })
    .returning();

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

  await syncEntityStatusFromApproval(
    organizationId,
    request.entityType,
    request.entityId,
    userId,
    APPROVAL_REQUEST_STATUSES.rejected
  );

  await Promise.all([
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_request',
      entityId: approvalRequestId,
      action: APPROVAL_ACTIONS.reject,
      beforeData: request,
      afterData: updatedRequest
    }),
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_action',
      entityId: createdAction.id,
      action: APPROVAL_ACTIONS.reject,
      afterData: createdAction
    })
  ]);

  return updatedRequest;
}

export async function returnApproval(
  approvalRequestId: string,
  organizationId: string,
  userId: string,
  remark?: string
) {
  const request = await assertApprovalRequest(approvalRequestId, organizationId);

  if (request.status !== APPROVAL_REQUEST_STATUSES.pending) {
    throw new AuthError('Only pending approvals can be returned', 400);
  }

  const currentStep = await getCurrentApprovalStep(approvalRequestId, organizationId);

  if (!currentStep) {
    throw new AuthError('Current approval step not found', 404);
  }

  await assertActorCanHandleStep(organizationId, userId, currentStep.roleCode);

  const [createdAction] = await db
    .insert(crmApprovalActions)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      approvalRequestId,
      stepNumber: currentStep.stepNumber,
      action: APPROVAL_ACTIONS.return,
      remark: remark?.trim() || null,
      actedBy: userId
    })
    .returning();

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

  await syncEntityStatusFromApproval(
    organizationId,
    request.entityType,
    request.entityId,
    userId,
    APPROVAL_REQUEST_STATUSES.returned
  );

  await Promise.all([
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_request',
      entityId: approvalRequestId,
      action: APPROVAL_ACTIONS.return,
      beforeData: request,
      afterData: updatedRequest
    }),
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_action',
      entityId: createdAction.id,
      action: APPROVAL_ACTIONS.return,
      afterData: createdAction
    })
  ]);

  return updatedRequest;
}

export async function cancelApproval(
  approvalRequestId: string,
  organizationId: string,
  userId: string
) {
  const request = await assertApprovalRequest(approvalRequestId, organizationId);

  if (request.status !== APPROVAL_REQUEST_STATUSES.pending) {
    throw new AuthError('Only pending approvals can be cancelled', 400);
  }

  if (request.requestedBy !== userId) {
    await assertActorCanHandleStep(organizationId, userId, 'sales_manager');
  }

  const [createdAction] = await db
    .insert(crmApprovalActions)
    .values({
      id: crypto.randomUUID(),
      organizationId,
      approvalRequestId,
      stepNumber: request.currentStep,
      action: APPROVAL_ACTIONS.cancel,
      remark: null,
      actedBy: userId
    })
    .returning();

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

  await syncEntityStatusFromApproval(
    organizationId,
    request.entityType,
    request.entityId,
    userId,
    APPROVAL_REQUEST_STATUSES.returned
  );

  await Promise.all([
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_request',
      entityId: approvalRequestId,
      action: APPROVAL_ACTIONS.cancel,
      beforeData: request,
      afterData: updatedRequest
    }),
    auditAction({
      organizationId,
      userId,
      entityType: 'crm_approval_action',
      entityId: createdAction.id,
      action: APPROVAL_ACTIONS.cancel,
      afterData: createdAction
    })
  ]);

  return updatedRequest;
}
