import { Router, Request, Response, NextFunction } from "express";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import { ApiError } from "../shared/errors";
import { requireAdminPermission, requireAdminToken, type RequestWithAdminAuth } from "../shared/middleware/auth";
import { successResponse } from "../shared/middleware/errorMiddleware";
import { env } from "../config/env";
import { createSessionToken } from "../shared/services/sessionToken";
import { getUserByEmail, verifyPassword } from "../shared/store/userStore";
import { idempotency } from "../shared/middleware/idempotency";
import { createMerchant, getMerchantById, listMerchants, patchMerchant, toMerchantPayload, PayoutMode } from "../shared/store/merchantStore";
import {
  createOutlet,
  createTerminal,
  getOutletById,
  getTerminalById,
  listOutlets,
  listTerminals,
  patchOutlet,
  patchTerminal,
  toOutletPayload,
  toTerminalPayload
} from "../shared/store/locationStore";
import { bindDevice, getActiveBindingByDevice, getActiveBindingByTerminal, toBindingPayload, unbindDevice } from "../shared/store/bindingStore";
import {
  createDevice,
  getDeviceById,
  getDeviceBySerialNumber,
  listDevices,
  patchDevice,
  rotateDeviceMqttCredential,
  DeviceEntity,
  toDevicePayload
} from "../shared/store/deviceStore";
import {
  deriveDeviceHealthSummary,
  getHeartbeatCountForDeviceLastHours,
  getLatestHeartbeatByDeviceId,
  listHeartbeats,
  createDeviceHeartbeat
} from "../shared/store/heartbeatStore";
import {
  acknowledgeDeviceCommand,
  createDeviceCommand,
  getDeviceCommandById,
  listDeviceCommands,
  toDeviceCommandPayload,
  toDeviceCommandPayloadBrief
} from "../shared/store/deviceCommandStore";
import {
  createTransaction,
  getTransactionById,
  listTransactions,
  toTransactionEventPayload,
  toTransactionPayload,
  getTransactionEvents
} from "../shared/store/transactionStore";
import {
  getNotificationByTransactionId,
  listNotifications,
  listNotificationsByDevice,
  toNotificationPayload
} from "../shared/store/notificationStore";
import { retryNotificationByTransactionId } from "../shared/orchestrators/notificationOrchestrator";
import { createAuditLog, listAuditLogs, toAuditLogPayload } from "../shared/store/auditLogStore";
import { listLedgerEntries, toLedgerEntryPayload } from "../shared/store/ledgerStore";
import {
  createSettlementBatches,
  createSettlementBatchEvent,
  getSettlementFinanceSummary,
  getSettlementReconciliationReport,
  getSettlementBatchReportRows,
  getSettlementBatchById,
  listSettlementAdjustments,
  listSettlementBatchAdjustments,
  listSettlementBatchEntries,
  listSettlementBatchEvents,
  listSettlementBatches,
  markSettlementBatchPaid,
  recordSettlementBatchAdjustment,
  reprocessSettlementBatch,
  settlementAdjustmentReportToCsv,
  settlementBatchReportToBankGenericCsv,
  settlementBatchReportToCsv,
  SettlementBatchStatus,
  transitionSettlementBatchAdjustmentApproval,
  transitionSettlementBatchStatus,
  toSettlementBatchEventPayload,
  toSettlementBatchAdjustmentPayload,
  toSettlementBatchEntryPayload,
  toSettlementBatchPayload,
  updateSettlementBatchReference
} from "../shared/store/settlementStore";
import { resolveDeviceCapabilitySummary } from "../shared/services/deviceCapabilityResolver";
import {
  getOrCreateDeviceConfig,
  listDeviceConfigAcks,
  toDeviceConfigAckPayload,
  toDeviceConfigPayload,
  upsertDeviceConfig
} from "../shared/store/deviceConfigStore";
import { listMqttMessages, toMqttMessagePayload, createMqttMessage } from "../shared/store/mqttMessageStore";
import { getMqttPublisherStatus, publishConfigPush, publishQf100DeviceCommand, publishQf100DynamicQrDisplay } from "../shared/services/mqttPublisher";
import { getMqttSubscriberStatus } from "../shared/services/mqttSubscriber";
import { getDatabaseHealth, getServiceHealth } from "../shared/services/health";
import { logger } from "../shared/services/logger";
import {
  countExportJobsByStatus,
  getExportJobById,
  listExportJobs,
  toExportJobPayload
} from "../shared/store/exportJobStore";
import { buildDeviceConfigStatus } from "../shared/services/deviceConfigStatus";
import { expireDueDynamicQrTransactions } from "../shared/services/dynamicQrExpiry";
import { getDynamicQrExpirySchedulerStatus } from "../shared/services/dynamicQrExpiryScheduler";
import { createSettlementAdjustmentExportJob, getExportJobWorkerStatus } from "../shared/services/exportJobWorker";
import {
  createSoundboxModel,
  createSoundboxVendor,
  deleteSoundboxModel,
  deleteSoundboxVendor,
  getSoundboxModelById,
  getSoundboxVendorById,
  listSoundboxModels,
  listSoundboxVendors,
  patchSoundboxModel,
  patchSoundboxVendor,
  toSoundboxModelPayload,
  toSoundboxVendorPayload
} from "../shared/store/soundboxCatalogStore";

const router = Router();

type LoginInput = { username?: string; password?: string };

type MerchantCreateInput = {
  legal_name?: string;
  brand_name?: string;
  settlement_account_reference?: string;
  settlement_account_type?: string;
  payout_mode?: PayoutMode;
  fee_profile_id?: string;
  status?: "active" | "inactive";
  onboarding_status?: "pending" | "approved" | "rejected";
};

type OutletCreateInput = {
  name?: string;
  address?: string;
  outlet_code?: string;
  status?: "active" | "inactive";
};

type TerminalCreateInput = {
  terminal_code?: string;
  qr_mode?: "static" | "dynamic_mqtt" | "dynamic_api";
  partner_reference?: string;
  status?: "active" | "inactive";
};

type SoundboxVendorInput = {
  vendor_code?: string;
  name?: string;
  country?: string;
  support_contact?: string;
  support_pic_name?: string;
  support_email?: string;
  support_phone?: string;
  support_notes?: string;
  status?: "active" | "inactive";
};

type SoundboxModelInput = {
  vendor_id?: string;
  model_code?: string;
  name?: string;
  communication_mode?: "static" | "mqtt" | "api";
  screen_flag?: boolean;
  qr_mode?: "static" | "dynamic_mqtt" | "dynamic_api";
  mqtt_payload_profile?: string;
  thumbnail_url?: string;
  capability_template_json?: Record<string, unknown>;
  status?: "active" | "inactive";
};

type DeviceCreateInput = {
  device_code?: string;
  serial_number?: string;
  vendor?: string;
  model?: string;
  communication_mode?: "static" | "mqtt" | "api";
  capability_profile_json?: Record<string, unknown>;
  auth_method?: string;
  status?: "active" | "inactive";
  firmware_version?: string;
  last_seen_at?: string;
};

type DeviceCommandInput = {
  command?: string;
  payload?: Record<string, unknown>;
};

type DeviceConfigInput = {
  settings?: Record<string, unknown>;
  config_version?: number;
};

type DeviceConfigRetryInput = {
  force?: boolean;
};

type BindingInput = {
  merchant_id?: string;
  outlet_id?: string;
  terminal_id?: string;
};

type MerchantRejectInput = {
  reason?: string;
};

type SeedInput = {
  include_demo_heartbeat?: boolean;
  include_demo_transactions?: boolean;
};

type TransactionCreateInput = {
  partner_reference?: string;
  merchant_id?: string;
  outlet_id?: string;
  terminal_id?: string;
  device_id?: string;
  amount?: number;
  currency?: string;
  qr_mode?: "static" | "dynamic";
  initiation_mode?: "manual" | "dynamic_api" | "dynamic_mqtt" | "static";
  status?: "initiated" | "awaiting_payment";
  expired_at?: string;
};

type SettlementBatchCreateInput = {
  merchant_id?: string;
  cutoff_at?: string;
  limit_per_merchant?: number;
};

type SettlementBatchPaidInput = {
  paid_at?: string;
  paid_reference?: string;
  paid_note?: string;
};

type SettlementBatchReferenceInput = {
  paid_reference?: string;
  paid_note?: string;
};

type SettlementBatchResolutionInput = {
  reason?: string;
  note?: string;
};

type SettlementBatchAdjustmentInput = {
  adjustment_type?: "credit" | "debit";
  amount?: number;
  reason?: string;
  note?: string;
};

type SettlementAdjustmentApprovalInput = {
  note?: string;
};

type SettlementAdjustmentExportJobInput = {
  merchant_id?: string;
  adjustment_type?: "credit" | "debit";
  approval_status?: "pending" | "approved" | "rejected";
  from?: string;
  to?: string;
  limit?: number;
};

function parseIdempotentReplay(req: Request) {
  return (req.body as { __idempotentReplay?: boolean; __idempotentResponse?: unknown }).__idempotentReplay;
}

function getReplayResponse(req: Request) {
  return (req.body as { __idempotentResponse?: unknown }).__idempotentResponse;
}

function isIsoDate(value: string | undefined): value is string {
  if (!value) {
    return false;
  }
  return Number.isFinite(Date.parse(value));
}

function isTxInDateRange(tx: { created_at: string }, from?: string, to?: string): boolean {
  const createdAt = Date.parse(tx.created_at);
  if (Number.isNaN(createdAt)) {
    return false;
  }

  if (from && createdAt < Date.parse(from)) {
    return false;
  }

  if (to && createdAt > Date.parse(to)) {
    return false;
  }

  return true;
}

function parseDeviceStatusFilter(value: string | undefined) {
  if (value === "online" || value === "offline" || value === "degraded" || value === "stale") {
    return value;
  }
  return undefined;
}

function parseCommunicationModeFilter(value: string | undefined) {
  if (value === "static" || value === "mqtt" || value === "api") {
    return value;
  }
  return undefined;
}

function parseDeviceCommunicationMode(value: string | undefined): "static" | "mqtt" | "api" | undefined {
  if (value === "static" || value === "mqtt" || value === "api") {
    return value;
  }
  return undefined;
}

function parseDeviceStatusValue(value: string | undefined): "active" | "inactive" | undefined {
  if (value === "active" || value === "inactive") {
    return value;
  }
  return undefined;
}

function parsePayoutMode(value: string | undefined): "merchant_direct" | "manual" | undefined {
  if (value === "merchant_direct" || value === "manual") {
    return value;
  }
  return undefined;
}

function parseSettlementBatchStatus(value: string | undefined): SettlementBatchStatus | undefined {
  if (value === "created" || value === "paid" || value === "failed" || value === "cancelled") {
    return value;
  }
  return undefined;
}

function parseSettlementExportFormat(value: string | undefined): "standard" | "bank_generic" | undefined {
  if (!value || value === "standard" || value === "general") {
    return "standard";
  }
  if (value === "bank_generic") {
    return "bank_generic";
  }
  return undefined;
}

function parseSettlementAdjustmentType(value: string | undefined): "credit" | "debit" | undefined {
  if (value === "credit" || value === "debit") {
    return value;
  }
  return undefined;
}

function parseSettlementAdjustmentApprovalStatus(value: string | undefined): "pending" | "approved" | "rejected" | undefined {
  if (value === "pending" || value === "approved" || value === "rejected") {
    return value;
  }
  return undefined;
}

function parseOutletStatusFilter(value: string | undefined): "active" | "inactive" | undefined {
  if (value === "active" || value === "inactive") {
    return value;
  }
  return undefined;
}

function parseTerminalStatusFilter(value: string | undefined): "active" | "inactive" | undefined {
  if (value === "active" || value === "inactive") {
    return value;
  }
  return undefined;
}

function parseTerminalModeFilter(value: string | undefined): "static" | "dynamic_mqtt" | "dynamic_api" | undefined {
  if (value === "static" || value === "dynamic_mqtt" || value === "dynamic_api") {
    return value;
  }
  return undefined;
}

function parseTransactionStatusFilter(
  value: string | undefined
): "initiated" | "awaiting_payment" | "paid" | "failed" | "expired" | "reversed" | undefined {
  if (
    value === "initiated" ||
    value === "awaiting_payment" ||
    value === "paid" ||
    value === "failed" ||
    value === "expired" ||
    value === "reversed"
  ) {
    return value;
  }
  return undefined;
}

function parseCommandStatusFilter(value: string | undefined) {
  if (value === "accepted" || value === "delivered" || value === "failed" || value === "timeout") {
    return value;
  }
  return undefined;
}

function buildBindingSummary(
  binding: Awaited<ReturnType<typeof getActiveBindingByDevice>> | null
) {
  if (!binding) {
    return null;
  }

  return {
    id: binding.id,
    merchant_id: binding.merchant_id,
    outlet_id: binding.outlet_id,
    terminal_id: binding.terminal_id
  };
}

async function buildDeviceAdminPayload(device: DeviceEntity) {
  const latestHeartbeat = await getLatestHeartbeatByDeviceId(device.id);
  const healthSummary = deriveDeviceHealthSummary({
    last_seen_at: device.last_seen_at,
    network_strength: latestHeartbeat?.network_strength ?? null,
    battery_level: latestHeartbeat?.battery_level ?? null
  });
  return {
    ...toDevicePayload(device),
    capability_summary: resolveDeviceCapabilitySummary(device),
    derived_status: healthSummary.status,
    health_summary: healthSummary,
    heartbeat_count_24h: await getHeartbeatCountForDeviceLastHours(device.id),
    binding_summary: buildBindingSummary(await getActiveBindingByDevice(device.id)),
    latest_heartbeat: latestHeartbeat
      ? {
          id: latestHeartbeat.id,
          timestamp: latestHeartbeat.timestamp,
          received_at: latestHeartbeat.received_at,
          state: latestHeartbeat.state,
          network_strength: latestHeartbeat.network_strength,
          battery_level: latestHeartbeat.battery_level,
          firmware_version: latestHeartbeat.firmware_version
        }
      : null
  };
}

async function deriveDeviceStatusesForDashboard() {
  const devices = await listDevices();

  return Promise.all(
    devices.map(async (device) => {
      const latestHeartbeat = await getLatestHeartbeatByDeviceId(device.id);
      const healthSummary = deriveDeviceHealthSummary({
        last_seen_at: device.last_seen_at,
        network_strength: latestHeartbeat?.network_strength ?? null,
        battery_level: latestHeartbeat?.battery_level ?? null
      });
      return {
        device,
        status: healthSummary.status,
        healthSummary
      };
    })
  );
}

function buildDashboardRange() {
  const start = new Date();
  start.setUTCHours(0, 0, 0, 0);
  const end = new Date(start);
  end.setUTCDate(start.getUTCDate() + 1);
  return { start, end };
}

function toStartEndDateFilter(from?: string, to?: string) {
  if (from && Number.isNaN(Date.parse(from))) {
    return null;
  }
  if (to && Number.isNaN(Date.parse(to))) {
    return null;
  }

  return {
    fromTs: from ? Date.parse(from) : null,
    toTs: to ? Date.parse(to) : null
  };
}

function normalizeMerchantMode(payloadMode: MerchantCreateInput["payout_mode"]): NonNullable<MerchantCreateInput["payout_mode"]> {
  return payloadMode || "merchant_direct";
}

async function auditAdminAction(
  req: Request,
  payload: {
    action: string;
    entity_type: string;
    entity_id: string;
    before_json?: unknown;
    after_json?: unknown;
  }
) {
  await createAuditLog({
    actor_type: "admin",
    actor_id: "admin",
    action: payload.action,
    entity_type: payload.entity_type,
    entity_id: payload.entity_id,
    before_json: payload.before_json,
    after_json: payload.after_json,
    source_ip: req.ip,
    request_id: req.requestId,
    trace_id: req.traceId
  });
}

async function publishDeviceConfigPush(deviceId: string, config: Awaited<ReturnType<typeof getOrCreateDeviceConfig>>) {
  const mqttPayload = {
    message_type: "config_push" as const,
    config_version: config.config_version,
    settings: config.settings_json
  };
  const publishResult = await publishConfigPush(deviceId, mqttPayload);
  return createMqttMessage({
    direction: "downlink",
    device_id: deviceId,
    topic: publishResult.topic,
    message_type: "config_push",
    correlation_id: `config:${config.config_version}`,
    payload_json: mqttPayload,
    publish_status: publishResult.ok ? "sent" : "failed",
    reason: publishResult.reason
  });
}

function validatePayoutConfig(payload: MerchantCreateInput) {
  const mode = normalizeMerchantMode(payload.payout_mode);
  if (mode === "merchant_direct") {
    if (!payload.settlement_account_reference || !payload.settlement_account_type) {
      throw new ApiError(
        "BAD_REQUEST",
        "settlement_account_reference and settlement_account_type required when payout_mode=merchant_direct",
        400
      );
    }
  }
}

async function ensureMerchant(req: Request, next: NextFunction) {
  const merchantId = req.params.merchantId;
  const merchant = await getMerchantById(merchantId);
  if (!merchant) {
    return next(new ApiError("NOT_FOUND", "merchant not found", 404));
  }
  return merchant;
}

router.post("/login", async (req: Request, res: Response, next: NextFunction) => {
  const { username, password } = req.body as LoginInput;
  const loginId = (username || "").trim();
  const isDevLogin = env.ADMIN_DEV_LOGIN_ENABLED !== "false" && loginId === "admin" && password === "admin";
  const user = !isDevLogin && loginId ? await getUserByEmail(loginId) : null;
  const validUser = user && user.status === "active" && verifyPassword(user.password_hash, password || "");

  if (!isDevLogin && !validUser) {
    await createAuditLog({
      actor_type: "admin",
      actor_id: loginId || "unknown",
      action: "admin.login.failed",
      entity_type: "admin_session",
      entity_id: loginId || "unknown",
      after_json: {
        reason: user && user.status !== "active" ? "inactive_user" : "invalid_credentials",
        auth_mode: "session"
      },
      source_ip: req.ip,
      request_id: req.requestId,
      trace_id: req.traceId
    });
    return next(new ApiError("UNAUTHORIZED", "Invalid credentials", 401));
  }

  const authenticatedUser = isDevLogin ? null : user;
  if (!isDevLogin && !authenticatedUser) {
    return next(new ApiError("UNAUTHORIZED", "Invalid credentials", 401));
  }
  const sessionUser = authenticatedUser!;

  const token = isDevLogin
    ? env.ADMIN_TOKEN
    : createSessionToken(
        {
          typ: "admin",
          sub: sessionUser.id,
          name: sessionUser.name,
          email: sessionUser.email,
          role_id: sessionUser.role_id,
          role_name: sessionUser.role_name,
          permissions: sessionUser.permissions_json
        },
        env.ADMIN_SESSION_SECRET,
        env.ADMIN_SESSION_TTL_SECONDS
      );
  await createAuditLog({
    actor_type: "admin",
    actor_id: isDevLogin ? "legacy_admin" : sessionUser.id,
    action: "admin.login.success",
    entity_type: "admin_session",
    entity_id: isDevLogin ? "legacy_admin" : sessionUser.id,
    after_json: {
      auth_mode: isDevLogin ? "legacy_token" : "session",
      role_name: isDevLogin ? "admin" : sessionUser.role_name
    },
    source_ip: req.ip,
    request_id: req.requestId,
    trace_id: req.traceId
  });
  res.json(
    successResponse(req, {
      token,
      auth_mode: isDevLogin ? "legacy_token" : "session",
      user: isDevLogin
        ? {
            id: "legacy_admin",
            name: "Dev Admin",
            role_name: "admin"
          }
        : {
            id: sessionUser.id,
            name: sessionUser.name,
            email: sessionUser.email,
            role_name: sessionUser.role_name,
            permissions: sessionUser.permissions_json
          }
    })
  );
});

router.get("/me", requireAdminToken, async (req: RequestWithAdminAuth, res: Response) => {
  res.json(
    successResponse(req, {
      user_id: req.adminAuth?.user_id,
      role_name: req.adminAuth?.role_name,
      auth_mode: req.adminAuth?.mode,
      permissions: req.adminAuth?.permissions
    })
  );
});

router.use(async (req: Request, res: Response, next: NextFunction) => {
  if (req.path === "/login") {
    return next();
  }

  return requireAdminToken(req, res, next);
});

router.get("/health", requireAdminToken, async (_req: Request, res: Response) => {
  res.json(
    successResponse(_req, {
      ok: true,
      now: new Date().toISOString()
    })
  );
});

router.get("/health/deep", requireAdminToken, async (req: Request, res: Response) => {
  const health = await getServiceHealth();
  res.status(health.status === "healthy" ? 200 : 503).json(
    successResponse(req, {
      ...health,
      time: new Date().toISOString()
    })
  );
});

router.post(
  "/sample-idempotent",
  requireAdminToken,
  idempotency({ scope: "admin.sample", required: false }),
  async (req: Request, res: Response) => {
    if (parseIdempotentReplay(req)) {
      return res.status(200).json(getReplayResponse(req));
    }

    const id = randomUUID();
    res.json(successResponse(req, { id, generated_at: new Date().toISOString() }));
  }
);

router.post(
  "/merchants",
  requireAdminToken,
  idempotency({ scope: "merchant.create", required: false }),
  async (req: Request, res: Response, next: NextFunction) => {
    if (parseIdempotentReplay(req)) {
      return res.status(200).json(getReplayResponse(req));
    }

    const payload = req.body as MerchantCreateInput;
    if (!payload?.legal_name) {
      return next(new ApiError("BAD_REQUEST", "legal_name is required", 400));
    }

    const normalizedPayoutMode = parsePayoutMode(payload.payout_mode);
    if (payload.payout_mode && !normalizedPayoutMode) {
      return next(new ApiError("BAD_REQUEST", "payout_mode must be merchant_direct or manual", 400));
    }

  try {
    validatePayoutConfig(payload);
  } catch (err) {
    return next(err as Error);
  }

  const created = await createMerchant({
    legal_name: payload.legal_name,
    brand_name: payload.brand_name,
    settlement_account_reference: payload.settlement_account_reference,
    settlement_account_type: payload.settlement_account_type,
    payout_mode: normalizeMerchantMode(payload.payout_mode),
    fee_profile_id: payload.fee_profile_id,
    status: payload.status,
    onboarding_status: payload.onboarding_status
  });

    await auditAdminAction(req, {
      action: "merchant.create",
      entity_type: "merchant",
      entity_id: created.id,
      after_json: toMerchantPayload(created)
    });

    res.status(201).json(successResponse(req, toMerchantPayload(created)));
  }
);

router.get("/merchants", requireAdminToken, async (_req: Request, res: Response) => {
  res.json(successResponse(_req, (await listMerchants()).map(toMerchantPayload)));
});

router.get("/merchants/:merchantId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const merchant = await getMerchantById(req.params.merchantId);
  if (!merchant) {
    return next(new ApiError("NOT_FOUND", "merchant not found", 404));
  }

  res.json(successResponse(req, toMerchantPayload(merchant)));
});

router.patch("/merchants/:merchantId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as MerchantCreateInput;
  if (!payload || Object.keys(payload).length === 0) {
    return next(new ApiError("BAD_REQUEST", "patch payload required", 400));
  }

  const existing = await getMerchantById(req.params.merchantId);
  if (!existing) {
    return next(new ApiError("NOT_FOUND", "merchant not found", 404));
  }

  const normalized: MerchantCreateInput = {
    ...payload,
    payout_mode: payload.payout_mode ? payload.payout_mode : existing.payout_mode
  };

  if (normalized.payout_mode && !parsePayoutMode(normalized.payout_mode)) {
    return next(new ApiError("BAD_REQUEST", "payout_mode must be merchant_direct or manual", 400));
  }

  if (normalized.payout_mode === "merchant_direct") {
    normalized.settlement_account_reference =
      normalized.settlement_account_reference || existing.settlement_account_reference;
    normalized.settlement_account_type =
      normalized.settlement_account_type || existing.settlement_account_type;
  }

  try {
    validatePayoutConfig(normalized);
  } catch (err) {
    return next(err as Error);
  }

  const updated = await patchMerchant(req.params.merchantId, normalized);
  await auditAdminAction(req, {
    action: "merchant.update",
    entity_type: "merchant",
    entity_id: updated.id,
    before_json: toMerchantPayload(existing),
    after_json: toMerchantPayload(updated)
  });
  res.json(successResponse(req, toMerchantPayload(updated)));
});

router.post("/merchants/:merchantId/approve", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const existing = await getMerchantById(req.params.merchantId);
  if (!existing) {
    return next(new ApiError("NOT_FOUND", "merchant not found", 404));
  }

  if (existing.onboarding_status === "approved") {
    return res.json(successResponse(req, toMerchantPayload(existing)));
  }

  const updated = await patchMerchant(req.params.merchantId, {
    onboarding_status: "approved"
  });

  await auditAdminAction(req, {
    action: "merchant.approve",
    entity_type: "merchant",
    entity_id: updated.id,
    before_json: toMerchantPayload(existing),
    after_json: toMerchantPayload(updated)
  });

  res.json(successResponse(req, toMerchantPayload(updated)));
});

router.post(
  "/merchants/:merchantId/reject",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const payload = req.body as MerchantRejectInput;
    if (!payload?.reason || payload.reason.trim() === "") {
      return next(new ApiError("BAD_REQUEST", "reason is required", 400));
    }

    const existing = await getMerchantById(req.params.merchantId);
    if (!existing) {
      return next(new ApiError("NOT_FOUND", "merchant not found", 404));
    }

    const updated = await patchMerchant(req.params.merchantId, {
      onboarding_status: "rejected",
      status: "inactive"
    });

    await auditAdminAction(req, {
      action: "merchant.reject",
      entity_type: "merchant",
      entity_id: updated.id,
      before_json: toMerchantPayload(existing),
      after_json: {
        ...toMerchantPayload(updated),
        rejection_reason: payload.reason
      }
    });

    res.json(
      successResponse(req, {
        ...toMerchantPayload(updated),
        rejection_reason: payload.reason
      })
    );
  }
);

router.post(
  "/seed",
  requireAdminToken,
  idempotency({ scope: "seed.demo", required: false }),
  async (req: Request, res: Response, next: NextFunction) => {
    if (parseIdempotentReplay(req)) {
      return res.status(200).json(getReplayResponse(req));
    }

    const payload = (req.body as SeedInput | undefined) || {};
    const includeHeartbeat = payload.include_demo_heartbeat !== false;
    const includeTransactions = payload.include_demo_transactions !== false;

    if ( (await listMerchants()).length > 0 ||  (await listDevices()).length > 0 ||  (await listOutlets()).length > 0 ||  (await listTerminals()).length > 0) {
      return next(new ApiError("BAD_REQUEST", "seed requires empty demo environment", 400));
    }

    const merchantA = await createMerchant({
      legal_name: "Seed Merchant A",
      brand_name: "Seed A",
      settlement_account_reference: "seed-bank:111111111",
      settlement_account_type: "merchant_bank_account",
      payout_mode: "merchant_direct",
      status: "active",
      onboarding_status: "approved"
    });
    const merchantB = await createMerchant({
      legal_name: "Seed Merchant B",
      brand_name: "Seed B",
      settlement_account_reference: "seed-bank:222222222",
      settlement_account_type: "merchant_bank_account",
      payout_mode: "manual",
      status: "active",
      onboarding_status: "pending"
    });

    const outletA = await createOutlet({
      merchant_id: merchantA.id,
      name: "Outlet Seed A",
      address: "Jl. Contoh Nomor 1"
    });
    const outletB = await createOutlet({
      merchant_id: merchantB.id,
      name: "Outlet Seed B",
      address: "Jl. Contoh Nomor 2"
    });

    const terminalA = await createTerminal({
      outlet_id: outletA.id,
      terminal_code: "TERM_SEED_A",
      qr_mode: "static"
    });
    const terminalB = await createTerminal({
      outlet_id: outletB.id,
      terminal_code: "TERM_SEED_B",
      qr_mode: "static"
    });

    const deviceA = await createDevice({
      device_code: "DEV_SEED_A",
      vendor: "seed-maker",
      model: "v1",
      communication_mode: "static",
      capability_profile_json: {
        dynamic_qr: false,
        flows: ["static_payment_notification"]
      },
      status: "active"
    });
    const deviceB = await createDevice({
      device_code: "DEV_SEED_B",
      vendor: "seed-maker",
      model: "v1",
      communication_mode: "mqtt",
      capability_profile_json: {
        dynamic_qr: {
          mqtt: true,
          api_direct: false
        },
        flows: ["dynamic_qr:mqtt", "static_payment_notification"]
      },
      status: "active"
    });
    const deviceC = await createDevice({
      device_code: "DEV_SEED_C",
      vendor: "seed-maker",
      model: "v1",
      communication_mode: "api",
      capability_profile_json: {
        dynamic_qr: {
          api_direct: true,
          mqtt: false
        },
        flows: ["dynamic_qr:api_direct", "static_payment_notification"]
      },
      status: "active"
    });

    await bindDevice({
      device_id: deviceA.id,
      merchant_id: merchantA.id,
      outlet_id: outletA.id,
      terminal_id: terminalA.id
    });
    await bindDevice({
      device_id: deviceB.id,
      merchant_id: merchantB.id,
      outlet_id: outletB.id,
      terminal_id: terminalB.id
    });

    if (includeHeartbeat) {
      await createDeviceHeartbeat({
        device_id: deviceA.id,
        timestamp: new Date().toISOString(),
        firmware_version: "1.0.0",
        network_strength: 92,
        battery_level: 89,
        state: "idle"
      });
      await createDeviceHeartbeat({
        device_id: deviceB.id,
        timestamp: new Date().toISOString(),
        firmware_version: "1.0.0",
        network_strength: 83,
        battery_level: 76,
        state: "idle"
      });
    }

    const transactions = includeTransactions
      ? [
          await createTransaction({
            merchant_id: merchantA.id,
            outlet_id: outletA.id,
            terminal_id: terminalA.id,
            device_id: deviceA.id,
            partner_reference: "seed-pr-001",
            amount: 25000,
            currency: "IDR",
            qr_mode: "static",
            initiation_mode: "static",
            status: "initiated"
          }),
          await createTransaction({
            merchant_id: merchantB.id,
            outlet_id: outletB.id,
            terminal_id: terminalB.id,
            device_id: deviceB.id,
            partner_reference: "seed-pr-002",
            amount: 50000,
            currency: "IDR",
            qr_mode: "static",
            initiation_mode: "static",
            status: "awaiting_payment"
          })
        ]
      : [];

    const seeded = {
      merchants: [toMerchantPayload(merchantA), toMerchantPayload(merchantB)],
      outlets: [outletA, outletB],
      terminals: [terminalA, terminalB],
      devices: [deviceA, deviceB, deviceC],
      transactions: transactions.map((tx) => toTransactionPayload(tx)),
      include_demo_heartbeat: includeHeartbeat,
      include_demo_transactions: includeTransactions
    };

    res.status(201).json(successResponse(req, seeded));
  }
);

router.get("/seed/status", requireAdminToken, async (_req: Request, res: Response) => {
  res.json(
    successResponse(_req, {
      merchants:  (await listMerchants()).length,
      outlets:  (await listOutlets()).length,
      terminals:  (await listTerminals()).length,
      devices:  (await listDevices()).length,
      transactions: (await listTransactions()).length,
      heartbeats: (await listHeartbeats()).length,
      notifications: (await listNotifications()).length,
      seed_eligible:  (await listMerchants()).length === 0 &&  (await listDevices()).length === 0 &&  (await listOutlets()).length === 0 &&  (await listTerminals()).length === 0
    })
  );
});

router.post(
  "/merchants/:merchantId/outlets",
  requireAdminToken,
  idempotency({ scope: "outlet.create", required: false }),
  async (req: Request, res: Response, next: NextFunction) => {
    if (parseIdempotentReplay(req)) {
      return res.status(200).json(getReplayResponse(req));
    }

    const merchant = await ensureMerchant(req, next);
    if (!merchant) {
      return;
    }

    const payload = req.body as OutletCreateInput;
    if (!payload?.name) {
      return next(new ApiError("BAD_REQUEST", "name is required", 400));
    }

    if (payload.status) {
      if (!parseOutletStatusFilter(payload.status)) {
        return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
      }
    }

    const outlet = await createOutlet({
      merchant_id: merchant.id,
      name: payload.name,
      address: payload.address,
      outlet_code: payload.outlet_code,
      status: payload.status
    });

    await auditAdminAction(req, {
      action: "outlet.create",
      entity_type: "outlet",
      entity_id: outlet.id,
      after_json: toOutletPayload(outlet)
    });

    res.status(201).json(successResponse(req, outlet));
  }
);

router.get("/outlets", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const statusRaw = (req.query.status as string | undefined)?.trim();
  const status = parseOutletStatusFilter(statusRaw);
  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }
  const q = (req.query.q as string | undefined)?.trim();
  res.json(
    successResponse(
      req,
      (await listOutlets({
        merchant_id: merchantId,
        status,
        q: q || undefined
      })).map(toOutletPayload)
    )
  );
});

router.get("/outlets/:outletId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const outlet = await getOutletById(req.params.outletId);
  if (!outlet) {
    return next(new ApiError("NOT_FOUND", "outlet not found", 404));
  }

  res.json(successResponse(req, toOutletPayload(outlet)));
});

router.patch("/outlets/:outletId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as OutletCreateInput;
  if (!payload || Object.keys(payload).length === 0) {
    return next(new ApiError("BAD_REQUEST", "patch payload required", 400));
  }
  if (payload.status && !parseOutletStatusFilter(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }

  try {
    const existing = await getOutletById(req.params.outletId);
    const updated = await patchOutlet(req.params.outletId, payload);
    await auditAdminAction(req, {
      action: "outlet.update",
      entity_type: "outlet",
      entity_id: updated.id,
      before_json: existing ? toOutletPayload(existing) : null,
      after_json: toOutletPayload(updated)
    });
    res.json(successResponse(req, toOutletPayload(updated)));
  } catch (err) {
    if (err instanceof Error && err.message === "OUTLET_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "outlet not found", 404));
    }
    return next(err as Error);
  }
});

router.post(
  "/outlets/:outletId/terminals",
  requireAdminToken,
  idempotency({ scope: "terminal.create", required: false }),
  async (req: Request, res: Response, next: NextFunction) => {
    if (parseIdempotentReplay(req)) {
      return res.status(201).json(getReplayResponse(req));
    }

    const outlet = await getOutletById(req.params.outletId);
    if (!outlet) {
      return next(new ApiError("NOT_FOUND", "outlet not found", 404));
    }

    const payload = req.body as TerminalCreateInput;
    if (!payload || typeof payload !== "object") {
      return next(new ApiError("BAD_REQUEST", "terminal payload required", 400));
    }

    if (payload.qr_mode && !parseTerminalModeFilter(payload.qr_mode)) {
      return next(new ApiError("BAD_REQUEST", "qr_mode must be static|dynamic_mqtt|dynamic_api", 400));
    }
    if (payload.status && !parseTerminalStatusFilter(payload.status)) {
      return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
    }

    const terminal = await createTerminal({
      outlet_id: outlet.id,
      terminal_code: payload.terminal_code,
      qr_mode: payload.qr_mode,
      partner_reference: payload.partner_reference,
      status: payload.status
    });

    await auditAdminAction(req, {
      action: "terminal.create",
      entity_type: "terminal",
      entity_id: terminal.id,
      after_json: toTerminalPayload(terminal)
    });

    res.status(201).json(successResponse(req, toTerminalPayload(terminal)));
  }
);

router.get("/terminals", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const outletId = (req.query.outlet_id as string | undefined)?.trim();
  const statusRaw = (req.query.status as string | undefined)?.trim();
  const status = parseTerminalStatusFilter(statusRaw);
  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }
  const q = (req.query.q as string | undefined)?.trim();
  res.json(
    successResponse(
      req,
      (await listTerminals({
        outlet_id: outletId,
        status,
        q: q || undefined
      })).map(toTerminalPayload)
    )
  );
});

router.get("/terminals/:terminalId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const terminal = await getTerminalById(req.params.terminalId);
  if (!terminal) {
    return next(new ApiError("NOT_FOUND", "terminal not found", 404));
  }

  res.json(successResponse(req, toTerminalPayload(terminal)));
});

router.patch("/terminals/:terminalId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as TerminalCreateInput;
  if (!payload || Object.keys(payload).length === 0) {
    return next(new ApiError("BAD_REQUEST", "patch payload required", 400));
  }
  if (payload.qr_mode && !parseTerminalModeFilter(payload.qr_mode)) {
    return next(new ApiError("BAD_REQUEST", "qr_mode must be static|dynamic_mqtt|dynamic_api", 400));
  }
  if (payload.status && !parseTerminalStatusFilter(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }

  try {
    const existing = await getTerminalById(req.params.terminalId);
    const updated = await patchTerminal(req.params.terminalId, payload);
    await auditAdminAction(req, {
      action: "terminal.update",
      entity_type: "terminal",
      entity_id: updated.id,
      before_json: existing ? toTerminalPayload(existing) : null,
      after_json: toTerminalPayload(updated)
    });
    res.json(successResponse(req, toTerminalPayload(updated)));
  } catch (err) {
    if (err instanceof Error && err.message === "TERMINAL_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "terminal not found", 404));
    }
    return next(err as Error);
  }
});

router.get("/soundbox-vendors", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const statusRaw = (req.query.status as string | undefined)?.trim();
  const status = parseDeviceStatusValue(statusRaw);
  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }
  res.json(successResponse(req, (await listSoundboxVendors({ status })).map(toSoundboxVendorPayload)));
});

router.post("/soundbox-vendors", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as SoundboxVendorInput;
  const vendorCode = payload?.vendor_code?.trim();
  const name = payload?.name?.trim();
  if (!vendorCode || !name) {
    return next(new ApiError("BAD_REQUEST", "vendor_code and name are required", 400));
  }
  if (payload.status && !parseDeviceStatusValue(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }

  try {
    const created = await createSoundboxVendor({
      vendor_code: vendorCode,
      name,
      country: payload.country?.trim() || undefined,
      support_contact: payload.support_contact?.trim() || undefined,
      support_pic_name: payload.support_pic_name?.trim() || undefined,
      support_email: payload.support_email?.trim() || undefined,
      support_phone: payload.support_phone?.trim() || undefined,
      support_notes: payload.support_notes?.trim() || undefined,
      status: payload.status
    });
    await auditAdminAction(req, {
      action: "soundbox_vendor.create",
      entity_type: "soundbox_vendor",
      entity_id: created.id,
      after_json: toSoundboxVendorPayload(created)
    });
    res.status(201).json(successResponse(req, toSoundboxVendorPayload(created)));
  } catch (err) {
    return next(err as Error);
  }
});

router.patch("/soundbox-vendors/:vendorId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as SoundboxVendorInput;
  if (!payload || Object.keys(payload).length === 0) {
    return next(new ApiError("BAD_REQUEST", "patch payload required", 400));
  }
  if (payload.status && !parseDeviceStatusValue(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }

  try {
    const existing = await getSoundboxVendorById(req.params.vendorId);
    const updated = await patchSoundboxVendor(req.params.vendorId, {
      ...(payload.vendor_code !== undefined ? { vendor_code: payload.vendor_code.trim() } : {}),
      ...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
      ...(payload.country !== undefined ? { country: payload.country.trim() } : {}),
      ...(payload.support_contact !== undefined ? { support_contact: payload.support_contact.trim() } : {}),
      ...(payload.support_pic_name !== undefined ? { support_pic_name: payload.support_pic_name.trim() } : {}),
      ...(payload.support_email !== undefined ? { support_email: payload.support_email.trim() } : {}),
      ...(payload.support_phone !== undefined ? { support_phone: payload.support_phone.trim() } : {}),
      ...(payload.support_notes !== undefined ? { support_notes: payload.support_notes.trim() } : {}),
      ...(payload.status !== undefined ? { status: payload.status } : {})
    });
    await auditAdminAction(req, {
      action: "soundbox_vendor.update",
      entity_type: "soundbox_vendor",
      entity_id: updated.id,
      before_json: existing ? toSoundboxVendorPayload(existing) : null,
      after_json: toSoundboxVendorPayload(updated)
    });
    res.json(successResponse(req, toSoundboxVendorPayload(updated)));
  } catch (err) {
    if (err instanceof Error && err.message === "SOUNDBOX_VENDOR_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "soundbox vendor not found", 404));
    }
    return next(err as Error);
  }
});

router.delete("/soundbox-vendors/:vendorId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  try {
    const deleted = await deleteSoundboxVendor(req.params.vendorId);
    await auditAdminAction(req, {
      action: "soundbox_vendor.delete",
      entity_type: "soundbox_vendor",
      entity_id: deleted.id,
      before_json: toSoundboxVendorPayload(deleted)
    });
    res.json(successResponse(req, toSoundboxVendorPayload(deleted)));
  } catch (err) {
    if (err instanceof Error && err.message === "SOUNDBOX_VENDOR_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "soundbox vendor not found", 404));
    }
    if (err instanceof Error && err.message === "SOUNDBOX_VENDOR_HAS_MODELS") {
      return next(new ApiError("CONFLICT", "delete models for this vendor first", 409));
    }
    return next(err as Error);
  }
});

router.get("/soundbox-models", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const statusRaw = (req.query.status as string | undefined)?.trim();
  const status = parseDeviceStatusValue(statusRaw);
  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }
  const vendorId = (req.query.vendor_id as string | undefined)?.trim();
  const vendorCode = (req.query.vendor_code as string | undefined)?.trim();
  res.json(
    successResponse(
      req,
      (await listSoundboxModels({ vendor_id: vendorId, vendor_code: vendorCode, status })).map(toSoundboxModelPayload)
    )
  );
});

router.post("/soundbox-models", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as SoundboxModelInput;
  const vendorId = payload?.vendor_id?.trim();
  const name = payload?.name?.trim();
  if (!vendorId || !name) {
    return next(new ApiError("BAD_REQUEST", "vendor_id and name are required", 400));
  }
  if (payload.communication_mode && !parseDeviceCommunicationMode(payload.communication_mode)) {
    return next(new ApiError("BAD_REQUEST", "communication_mode must be static|mqtt|api", 400));
  }
  if (payload.qr_mode && !parseTerminalModeFilter(payload.qr_mode)) {
    return next(new ApiError("BAD_REQUEST", "qr_mode must be static|dynamic_mqtt|dynamic_api", 400));
  }
  if (payload.status && !parseDeviceStatusValue(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }
  const vendor = await getSoundboxVendorById(vendorId);
  if (!vendor) {
    return next(new ApiError("BAD_REQUEST", "vendor_id is invalid", 400));
  }

  try {
    const created = await createSoundboxModel({
      vendor_id: vendorId,
      model_code: payload.model_code?.trim() || undefined,
      name,
      communication_mode: payload.communication_mode,
      screen_flag: Boolean(payload.screen_flag),
      qr_mode: payload.qr_mode,
      mqtt_payload_profile: payload.mqtt_payload_profile?.trim() || undefined,
      thumbnail_url: payload.thumbnail_url?.trim() || undefined,
      capability_template_json: payload.capability_template_json,
      status: payload.status
    });
    await auditAdminAction(req, {
      action: "soundbox_model.create",
      entity_type: "soundbox_model",
      entity_id: created.id,
      after_json: toSoundboxModelPayload(created)
    });
    res.status(201).json(successResponse(req, toSoundboxModelPayload(created)));
  } catch (err) {
    return next(err as Error);
  }
});

router.patch("/soundbox-models/:modelId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as SoundboxModelInput;
  if (!payload || Object.keys(payload).length === 0) {
    return next(new ApiError("BAD_REQUEST", "patch payload required", 400));
  }
  if (payload.communication_mode && !parseDeviceCommunicationMode(payload.communication_mode)) {
    return next(new ApiError("BAD_REQUEST", "communication_mode must be static|mqtt|api", 400));
  }
  if (payload.qr_mode && !parseTerminalModeFilter(payload.qr_mode)) {
    return next(new ApiError("BAD_REQUEST", "qr_mode must be static|dynamic_mqtt|dynamic_api", 400));
  }
  if (payload.status && !parseDeviceStatusValue(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }
  if (payload.vendor_id) {
    const vendor = await getSoundboxVendorById(payload.vendor_id);
    if (!vendor) {
      return next(new ApiError("BAD_REQUEST", "vendor_id is invalid", 400));
    }
  }

  try {
    const existing = await getSoundboxModelById(req.params.modelId);
    const updated = await patchSoundboxModel(req.params.modelId, {
      ...(payload.vendor_id !== undefined ? { vendor_id: payload.vendor_id.trim() } : {}),
      ...(payload.model_code !== undefined ? { model_code: payload.model_code.trim() } : {}),
      ...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
      ...(payload.communication_mode !== undefined ? { communication_mode: payload.communication_mode } : {}),
      ...(payload.screen_flag !== undefined ? { screen_flag: Boolean(payload.screen_flag) } : {}),
      ...(payload.qr_mode !== undefined ? { qr_mode: payload.qr_mode } : {}),
      ...(payload.mqtt_payload_profile !== undefined ? { mqtt_payload_profile: payload.mqtt_payload_profile.trim() } : {}),
      ...(payload.thumbnail_url !== undefined ? { thumbnail_url: payload.thumbnail_url.trim() } : {}),
      ...(payload.capability_template_json !== undefined ? { capability_template_json: payload.capability_template_json } : {}),
      ...(payload.status !== undefined ? { status: payload.status } : {})
    });
    await auditAdminAction(req, {
      action: "soundbox_model.update",
      entity_type: "soundbox_model",
      entity_id: updated.id,
      before_json: existing ? toSoundboxModelPayload(existing) : null,
      after_json: toSoundboxModelPayload(updated)
    });
    res.json(successResponse(req, toSoundboxModelPayload(updated)));
  } catch (err) {
    if (err instanceof Error && err.message === "SOUNDBOX_MODEL_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "soundbox model not found", 404));
    }
    return next(err as Error);
  }
});

router.delete("/soundbox-models/:modelId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  try {
    const deleted = await deleteSoundboxModel(req.params.modelId);
    await auditAdminAction(req, {
      action: "soundbox_model.delete",
      entity_type: "soundbox_model",
      entity_id: deleted.id,
      before_json: toSoundboxModelPayload(deleted)
    });
    res.json(successResponse(req, toSoundboxModelPayload(deleted)));
  } catch (err) {
    if (err instanceof Error && err.message === "SOUNDBOX_MODEL_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "soundbox model not found", 404));
    }
    return next(err as Error);
  }
});

router.post("/devices", requireAdminToken, idempotency({ scope: "device.create", required: false }), async (req: Request, res: Response, next: NextFunction) => {
  if (parseIdempotentReplay(req)) {
    return res.status(201).json(getReplayResponse(req));
  }

  const payload = req.body as DeviceCreateInput;
  if (!payload) {
    return next(new ApiError("BAD_REQUEST", "device payload required", 400));
  }

  if (payload.communication_mode && !parseDeviceCommunicationMode(payload.communication_mode)) {
    return next(new ApiError("BAD_REQUEST", "communication_mode must be static|mqtt|api", 400));
  }

  if (payload.status && !parseDeviceStatusValue(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }

  const serialNumber = payload.serial_number?.trim();
  if (payload.serial_number !== undefined) {
    if (!serialNumber) {
      return next(new ApiError("BAD_REQUEST", "serial_number must not be empty", 400));
    }
    payload.serial_number = serialNumber;
  }
  if (payload.vendor !== undefined) {
    payload.vendor = payload.vendor.trim() || undefined;
  }
  if (payload.model !== undefined) {
    payload.model = payload.model.trim() || undefined;
  }
  if (payload.firmware_version !== undefined) {
    payload.firmware_version = payload.firmware_version.trim() || undefined;
  }
  if (serialNumber) {
    const existingSerialDevice = await getDeviceBySerialNumber(serialNumber);
    if (existingSerialDevice) {
      return next(new ApiError("CONFLICT", "serial_number already belongs to another device", 409));
    }
  }

  const created = await createDevice(payload);
  await auditAdminAction(req, {
    action: "device.create",
    entity_type: "device",
    entity_id: created.id,
    after_json: toDevicePayload(created)
  });
  res.status(201).json(successResponse(req, toDevicePayload(created)));
});

router.get("/devices", requireAdminToken, async (req: Request, res: Response) => {
  const status = parseDeviceStatusFilter(req.query.status as string | undefined);
  const vendor = (req.query.vendor as string | undefined)?.trim();
  const communicationMode = parseCommunicationModeFilter(req.query.communication_mode as string | undefined);
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const q = (req.query.q as string | undefined)?.trim();

  const rawDevices = await listDevices();
  const evaluated = await Promise.all(
    rawDevices.map(async (device) => {
      const latestHeartbeat = await getLatestHeartbeatByDeviceId(device.id);
      const binding = merchantId ? await getActiveBindingByDevice(device.id) : null;
      const healthSummary = deriveDeviceHealthSummary({
        last_seen_at: device.last_seen_at,
        network_strength: latestHeartbeat?.network_strength ?? null,
        battery_level: latestHeartbeat?.battery_level ?? null
      });
      return {
        device,
        latestHeartbeat,
        binding,
        derivedStatus: healthSummary.status
      };
    })
  );

  const data = evaluated
    .filter((entry) => {
      const { device, derivedStatus, binding } = entry;

      if (status && derivedStatus !== status) {
        return false;
      }

      if (vendor && device.vendor !== vendor) {
        return false;
      }

      if (communicationMode && device.communication_mode !== communicationMode) {
        return false;
      }

      if (merchantId && (!binding || binding.merchant_id !== merchantId)) {
        return false;
      }

      if (q) {
        const text = q.toLowerCase();
        const codeMatch = device.device_code.toLowerCase().includes(text);
        const serialMatch = device.serial_number?.toLowerCase().includes(text);
        if (!codeMatch && !serialMatch) {
          return false;
        }
      }

      return true;
    })
    .map((entry) => entry.device);

  const payloads = await Promise.all(
    data
      .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
      .map((device) => buildDeviceAdminPayload(device))
  );
  res.json(successResponse(req, payloads));
});

router.get("/devices/:deviceId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const activeBinding = await getActiveBindingByDevice(device.id);
  const latestHeartbeat = await getLatestHeartbeatByDeviceId(device.id);
  const heartbeatCount24h = await getHeartbeatCountForDeviceLastHours(device.id);
  const healthSummary = deriveDeviceHealthSummary({
    last_seen_at: device.last_seen_at,
    network_strength: latestHeartbeat?.network_strength ?? null,
    battery_level: latestHeartbeat?.battery_level ?? null
  });
  const notifications = (await listNotificationsByDevice(device.id))
    .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
    .slice(0, 10)
    .map(toNotificationPayload);

  res.json(
    successResponse(req, {
      ...toDevicePayload(device),
      capability_summary: resolveDeviceCapabilitySummary(device),
      derived_status: healthSummary.status,
      health_summary: healthSummary,
      active_binding: activeBinding ? toBindingPayload(activeBinding) : null,
      latest_heartbeat: latestHeartbeat
        ? {
            id: latestHeartbeat.id,
            timestamp: latestHeartbeat.timestamp,
            received_at: latestHeartbeat.received_at,
            state: latestHeartbeat.state,
            network_strength: latestHeartbeat.network_strength,
            battery_level: latestHeartbeat.battery_level,
            firmware_version: latestHeartbeat.firmware_version
          }
        : null,
      heartbeat_count_24h: heartbeatCount24h,
      notifications_latest: notifications
    })
  );
});

router.get("/devices/:deviceId/heartbeats", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const from = req.query.from as string | undefined;
  const to = req.query.to as string | undefined;
  const state = req.query.state as string | undefined;

  if (from && Number.isNaN(Date.parse(from))) {
    return next(new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400));
  }

  if (to && Number.isNaN(Date.parse(to))) {
    return next(new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400));
  }

  res.json(
    successResponse(req, {
      device_id: device.id,
      heartbeats: await listHeartbeats({
        device_id: device.id,
        from,
        to,
        state
      })
    })
  );
});

router.patch("/devices/:deviceId", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as DeviceCreateInput;
  if (!payload || Object.keys(payload).length === 0) {
    return next(new ApiError("BAD_REQUEST", "patch payload required", 400));
  }
  if (payload.communication_mode && !parseDeviceCommunicationMode(payload.communication_mode)) {
    return next(new ApiError("BAD_REQUEST", "communication_mode must be static|mqtt|api", 400));
  }
  if (payload.status && !parseDeviceStatusValue(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be active|inactive", 400));
  }

  const serialNumber = payload.serial_number?.trim();
  if (payload.serial_number !== undefined) {
    if (!serialNumber) {
      return next(new ApiError("BAD_REQUEST", "serial_number must not be empty", 400));
    }
    payload.serial_number = serialNumber;
  }
  if (payload.vendor !== undefined) {
    payload.vendor = payload.vendor.trim() || undefined;
  }
  if (payload.model !== undefined) {
    payload.model = payload.model.trim() || undefined;
  }
  if (payload.firmware_version !== undefined) {
    payload.firmware_version = payload.firmware_version.trim() || undefined;
  }
  if (serialNumber) {
    const serialDevice = await getDeviceBySerialNumber(serialNumber);
    if (serialDevice && serialDevice.id !== req.params.deviceId) {
      return next(new ApiError("CONFLICT", "serial_number already belongs to another device", 409));
    }
  }

  try {
    const existing = await getDeviceById(req.params.deviceId);
    const updated = await patchDevice(req.params.deviceId, payload);
    await auditAdminAction(req, {
      action: "device.update",
      entity_type: "device",
      entity_id: updated.id,
      before_json: existing ? toDevicePayload(existing) : null,
      after_json: toDevicePayload(updated)
    });
    res.json(successResponse(req, toDevicePayload(updated)));
  } catch (err) {
    if (err instanceof Error && err.message === "DEVICE_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "device not found", 404));
    }
    return next(err as Error);
  }
});

router.post("/devices/:deviceId/credentials/rotate", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  try {
    const existing = await getDeviceById(req.params.deviceId);
    const rotated = await rotateDeviceMqttCredential(req.params.deviceId);
    await auditAdminAction(req, {
      action: "device.credentials.rotate",
      entity_type: "device",
      entity_id: rotated.device.id,
      before_json: existing ? toDevicePayload(existing) : null,
      after_json: toDevicePayload(rotated.device)
    });

    res.json(
      successResponse(req, {
        device: toDevicePayload(rotated.device),
        credential: {
          mqtt_username: rotated.username,
          mqtt_password: rotated.password,
          status: rotated.device.credential_status,
          rotated_at: rotated.device.credential_rotated_at,
          one_time_secret: true
        }
      })
    );
  } catch (err) {
    if (err instanceof Error && err.message === "DEVICE_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "device not found", 404));
    }
    return next(err as Error);
  }
});

router.post("/devices/:deviceId/bind", requireAdminToken, idempotency({ scope: "device.bind", required: false }), async (req: Request, res: Response, next: NextFunction) => {
  if (parseIdempotentReplay(req)) {
    return res.status(200).json(getReplayResponse(req));
  }

  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const payload = req.body as BindingInput;
  if (!payload?.merchant_id || !payload.outlet_id || !payload.terminal_id) {
    return next(new ApiError("BAD_REQUEST", "merchant_id, outlet_id, terminal_id required", 400));
  }

  const merchant = await getMerchantById(payload.merchant_id);
  const outlet = await getOutletById(payload.outlet_id);
  const terminal = await getTerminalById(payload.terminal_id);
  if (!merchant || !outlet || !terminal) {
    return next(new ApiError("BAD_REQUEST", "merchant/outlet/terminal reference invalid", 400));
  }

  if (outlet.merchant_id !== merchant.id) {
    return next(new ApiError("BAD_REQUEST", "outlet does not belong to merchant", 400));
  }

  if (terminal.outlet_id !== outlet.id) {
    return next(new ApiError("BAD_REQUEST", "terminal does not belong to outlet", 400));
  }

  const binding = await bindDevice({
    device_id: device.id,
    merchant_id: merchant.id,
    outlet_id: outlet.id,
    terminal_id: terminal.id
  });

  await auditAdminAction(req, {
    action: "device.bind",
    entity_type: "device_binding",
    entity_id: binding.id,
    after_json: toBindingPayload(binding)
  });

  res.json(successResponse(req, toBindingPayload(binding)));
});

router.post("/devices/:deviceId/unbind", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const binding = await unbindDevice(device.id);
  if (!binding) {
    return next(new ApiError("BAD_REQUEST", "device has no active binding", 400));
  }

  await auditAdminAction(req, {
    action: "device.unbind",
    entity_type: "device_binding",
    entity_id: binding.id,
    before_json: toBindingPayload(binding),
    after_json: toBindingPayload(binding)
  });

  res.json(successResponse(req, toBindingPayload(binding)));
});

router.post(
  "/devices/:deviceId/commands",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const device = await getDeviceById(req.params.deviceId);
    if (!device) {
      return next(new ApiError("NOT_FOUND", "device not found", 404));
    }

    const payload = req.body as DeviceCommandInput;
    if (!payload || typeof payload.command !== "string" || payload.command.trim() === "") {
      return next(new ApiError("BAD_REQUEST", "command is required", 400));
    }

    const commandName = payload.command.trim();
    const command = await createDeviceCommand({
      device_id: device.id,
      command: commandName,
      payload: payload.payload || {}
    });

    if (commandName === "dynamic_qr.test" || commandName === "dynamic_qr.display") {
      if (!device.serial_number) {
        const failed = await acknowledgeDeviceCommand({
          device_id: device.id,
          command_id: command.id,
          status: "failed",
          reason: "DEVICE_SERIAL_NUMBER_REQUIRED",
          result_payload: { command: commandName }
        });
        return res.status(201).json(successResponse(req, toDeviceCommandPayload(failed || command)));
      }

      const commandPayload = payload.payload || {};
      const commandData = commandPayload.data && typeof commandPayload.data === "object"
        ? (commandPayload.data as Record<string, unknown>)
        : commandPayload;
      const amount = Number(commandData.amount || 1000);
      const expireSeconds = Number(commandData["expire-seconds"] || commandData.expire_seconds || commandData.expires_in_seconds || 60);
      const qrUrl = String(
        commandData["qr-url"] ||
          commandData.qr_url ||
          commandData.qr_payload ||
          commandData.qrPayload ||
          `https://sms.bizone.id/ui/device-ui-qr-payment-display?device_id=${encodeURIComponent(device.id)}&amount=${encodeURIComponent(String(amount))}`
      );

      if (!qrUrl || !Number.isFinite(amount) || amount <= 0) {
        const failed = await acknowledgeDeviceCommand({
          device_id: device.id,
          command_id: command.id,
          status: "failed",
          reason: "DYNAMIC_QR_PAYLOAD_INVALID",
          result_payload: { command: commandName, qr_url: qrUrl, amount }
        });
        return res.status(201).json(successResponse(req, toDeviceCommandPayload(failed || command)));
      }

      const publishResult = await publishQf100DynamicQrDisplay(device.id, device.serial_number, {
        qrUrl,
        amount,
        expireSeconds
      });
      await createMqttMessage({
        direction: "downlink",
        device_id: device.id,
        topic: publishResult.topic,
        message_type: "dynamic_qr_display",
        correlation_id: command.id,
        payload_json: publishResult.payload,
        publish_status: publishResult.ok ? "sent" : "failed",
        reason: publishResult.reason
      });

      const acknowledged = await acknowledgeDeviceCommand({
        device_id: device.id,
        command_id: command.id,
        status: publishResult.ok ? "delivered" : "failed",
        reason: publishResult.reason,
        result_payload: {
          topic: publishResult.topic,
          payload: publishResult.payload,
          published_at: publishResult.publishedAt
        }
      });
      return res.status(201).json(successResponse(req, toDeviceCommandPayload(acknowledged || command)));
    }

    if (commandName === "device.reboot" || commandName === "device.poweroff") {
      if (!device.serial_number) {
        const failed = await acknowledgeDeviceCommand({
          device_id: device.id,
          command_id: command.id,
          status: "failed",
          reason: "DEVICE_SERIAL_NUMBER_REQUIRED",
          result_payload: { command: commandName }
        });
        return res.status(201).json(successResponse(req, toDeviceCommandPayload(failed || command)));
      }

      const deviceCommand = commandName === "device.poweroff" ? "poweroff" : "reboot";
      const publishResult = await publishQf100DeviceCommand(device.id, device.serial_number, deviceCommand);
      await createMqttMessage({
        direction: "downlink",
        device_id: device.id,
        topic: publishResult.topic,
        message_type: `${deviceCommand}_command`,
        correlation_id: command.id,
        payload_json: publishResult.payload,
        publish_status: publishResult.ok ? "sent" : "failed",
        reason: publishResult.reason
      });

      const acknowledged = await acknowledgeDeviceCommand({
        device_id: device.id,
        command_id: command.id,
        status: publishResult.ok ? "delivered" : "failed",
        reason: publishResult.reason,
        result_payload: {
          topic: publishResult.topic,
          payload: publishResult.payload,
          published_at: publishResult.publishedAt
        }
      });
      return res.status(201).json(successResponse(req, toDeviceCommandPayload(acknowledged || command)));
    }

    res.status(201).json(successResponse(req, toDeviceCommandPayload(command)));
  }
);

router.get(
  "/devices/:deviceId/commands",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const device = await getDeviceById(req.params.deviceId);
    if (!device) {
      return next(new ApiError("NOT_FOUND", "device not found", 404));
    }

    const statusFilter = parseCommandStatusFilter(req.query.status as string | undefined);
    const limitRaw = req.query.limit as string | undefined;
    const limit = limitRaw ? Number(limitRaw) : 50;

    if (!Number.isFinite(limit) || limit <= 0) {
      return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
    }

    const commands = (await listDeviceCommands(device.id))
      .filter((command) => !statusFilter || command.status === statusFilter)
      .slice(0, Math.min(limit, 200))
      .map(toDeviceCommandPayloadBrief);

    res.json(successResponse(req, { device_id: device.id, commands }));
  }
);

router.get(
  "/devices/:deviceId/commands/:commandId",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const device = await getDeviceById(req.params.deviceId);
    if (!device) {
      return next(new ApiError("NOT_FOUND", "device not found", 404));
    }

    const command = await getDeviceCommandById(device.id, req.params.commandId);
    if (!command) {
      return next(new ApiError("NOT_FOUND", "command not found", 404));
    }

    res.json(successResponse(req, toDeviceCommandPayload(command)));
  }
);

router.get("/devices/:deviceId/notifications", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 50;
  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const notifications = (await listNotificationsByDevice(device.id))
    .sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))
    .slice(0, Math.min(limit, 200))
    .map(toNotificationPayload);

  res.json(successResponse(req, { device_id: device.id, notifications }));
});

router.get("/devices/:deviceId/config", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const config = await getOrCreateDeviceConfig(device.id);
  const acks = (await listDeviceConfigAcks(device.id, 10)).map(toDeviceConfigAckPayload);
  const status = await buildDeviceConfigStatus(config);
  res.json(successResponse(req, { ...toDeviceConfigPayload(config), latest_acks: acks, config_status: status }));
});

router.get("/devices/:deviceId/config/status", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const config = await getOrCreateDeviceConfig(device.id);
  res.json(successResponse(req, await buildDeviceConfigStatus(config)));
});

router.patch("/devices/:deviceId/config", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const payload = req.body as DeviceConfigInput;
  if (!payload || !payload.settings || typeof payload.settings !== "object") {
    return next(new ApiError("BAD_REQUEST", "settings object is required", 400));
  }

  const config = await upsertDeviceConfig({
    device_id: device.id,
    settings_json: payload.settings,
    config_version: payload.config_version
  });
  const outbox = await publishDeviceConfigPush(device.id, config);

  await auditAdminAction(req, {
    action: "device.config_push",
    entity_type: "device",
    entity_id: device.id,
    after_json: {
      config,
      downlink_message_id: outbox.id
    }
  });

  res.json(
    successResponse(req, {
      config: toDeviceConfigPayload(config),
      downlink_message: toMqttMessagePayload(outbox)
    })
  );
});

router.post("/devices/:deviceId/config/retry-push", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const payload = (req.body || {}) as DeviceConfigRetryInput;
  const config = await getOrCreateDeviceConfig(device.id);
  const beforeStatus = await buildDeviceConfigStatus(config);
  if (beforeStatus.drift_status === "applied" && !payload.force) {
    return next(new ApiError("CONFIG_ALREADY_APPLIED", "config already applied; set force=true to push again", 409));
  }

  const outbox = await publishDeviceConfigPush(device.id, config);
  const afterStatus = await buildDeviceConfigStatus(config);

  await auditAdminAction(req, {
    action: "device.config_retry_push",
    entity_type: "device",
    entity_id: device.id,
    before_json: beforeStatus,
    after_json: {
      ...afterStatus,
      downlink_message_id: outbox.id,
      force: payload.force === true
    }
  });

  res.json(
    successResponse(req, {
      config: toDeviceConfigPayload(config),
      config_status: afterStatus,
      downlink_message: toMqttMessagePayload(outbox)
    })
  );
});

router.get("/devices/:deviceId/mqtt-messages", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const device = await getDeviceById(req.params.deviceId);
  if (!device) {
    return next(new ApiError("NOT_FOUND", "device not found", 404));
  }

  const directionRaw = (req.query.direction as string | undefined)?.trim();
  const direction = directionRaw === "uplink" || directionRaw === "downlink" ? directionRaw : undefined;
  if (directionRaw && !direction) {
    return next(new ApiError("BAD_REQUEST", "direction must be uplink or downlink", 400));
  }

  const messageType = (req.query.message_type as string | undefined)?.trim();
  const correlationId = (req.query.correlation_id as string | undefined)?.trim();
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 100;
  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const messages = await listMqttMessages({
    device_id: device.id,
    direction,
    message_type: messageType || undefined,
    correlation_id: correlationId || undefined,
    limit
  });

  res.json(successResponse(req, { device_id: device.id, messages: messages.map(toMqttMessagePayload) }));
});

router.post(
  "/transactions",
  requireAdminToken,
  idempotency({ scope: "transaction.create", required: false }),
  async (req: Request, res: Response, next: NextFunction) => {
    if (parseIdempotentReplay(req)) {
      return res.status(201).json(getReplayResponse(req));
    }

    const payload = req.body as TransactionCreateInput;
    if (
      !payload ||
      !payload.partner_reference ||
      !payload.merchant_id ||
      !payload.outlet_id ||
      !payload.terminal_id
    ) {
      return next(new ApiError("BAD_REQUEST", "partner_reference, merchant_id, outlet_id, terminal_id required", 400));
    }

    const merchant = await getMerchantById(payload.merchant_id);
    if (!merchant) {
      return next(new ApiError("BAD_REQUEST", "merchant not found", 400));
    }

    const outlet = await getOutletById(payload.outlet_id);
    if (!outlet) {
      return next(new ApiError("BAD_REQUEST", "outlet not found", 400));
    }

    if (outlet.merchant_id !== merchant.id) {
      return next(new ApiError("BAD_REQUEST", "outlet does not belong to merchant", 400));
    }

    const terminal = await getTerminalById(payload.terminal_id);
    if (!terminal) {
      return next(new ApiError("BAD_REQUEST", "terminal not found", 400));
    }

    if (terminal.outlet_id !== outlet.id) {
      return next(new ApiError("BAD_REQUEST", "terminal does not belong to outlet", 400));
    }

    if (payload.device_id && !await getDeviceById(payload.device_id)) {
      return next(new ApiError("BAD_REQUEST", "device not found", 400));
    }

    const amount = Number(payload.amount);
    if (!Number.isFinite(amount) || amount <= 0) {
      return next(new ApiError("BAD_REQUEST", "amount must be a positive number", 400));
    }

    if (payload.status && !parseTransactionStatusFilter(payload.status)) {
      return next(new ApiError("BAD_REQUEST", "invalid status", 400));
    }

    if (payload.expired_at && Number.isNaN(Date.parse(payload.expired_at))) {
      return next(new ApiError("BAD_REQUEST", "expired_at must be valid ISO datetime", 400));
    }

    const created = await createTransaction({
      merchant_id: merchant.id,
      outlet_id: outlet.id,
      terminal_id: terminal.id,
      device_id: payload.device_id,
      partner_reference: payload.partner_reference,
      amount,
      currency: payload.currency,
      qr_mode: payload.qr_mode || "static",
      initiation_mode: payload.initiation_mode || "static",
      status: payload.status || "initiated",
      expired_at: payload.expired_at
    });

    await auditAdminAction(req, {
      action: "transaction.create",
      entity_type: "transaction",
      entity_id: created.id,
      after_json: toTransactionPayload(created)
    });

    res.status(201).json(successResponse(req, toTransactionPayload(created)));
  }
);

router.get("/transactions", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const statusRaw = (req.query.status as string | undefined)?.trim();
  const status = parseTransactionStatusFilter(statusRaw);
  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "invalid status", 400));
  }

  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const from = req.query.from as string | undefined;
  const to = req.query.to as string | undefined;
  const partnerReference = req.query.partner_reference as string | undefined;
  const q = (req.query.q as string | undefined)?.trim();

  if (from && !isIsoDate(from)) {
    return next(new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400));
  }

  if (to && !isIsoDate(to)) {
    return next(new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400));
  }

  const normalizedPartnerRef = partnerReference?.trim();
  const normalizedQ = q || "";

  res.json(
    successResponse(
      req,
      (await listTransactions({
        status,
        merchant_id: merchantId
      }))
        .filter((tx) => isTxInDateRange(tx, from, to))
        .filter((tx) => !normalizedPartnerRef || tx.partner_reference === normalizedPartnerRef)
        .filter((tx) => {
          if (!normalizedQ) {
            return true;
          }

          const lower = normalizedQ.toLowerCase();
          return (
            tx.partner_reference.toLowerCase().includes(lower) ||
            tx.transaction_code.toLowerCase().includes(lower)
          );
        })
        .map(toTransactionPayload)
    )
  );
});

router.post("/transactions/expire-due", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const limitRaw = (req.body as { limit?: unknown } | undefined)?.limit ?? req.query.limit;
  const limit = limitRaw === undefined || limitRaw === "" ? 100 : Number(limitRaw);
  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const result = await expireDueDynamicQrTransactions({
    limit,
    source: "admin",
    request_id: req.requestId
  });

  await auditAdminAction(req, {
    action: "transaction.expire_due_dynamic_qr",
    entity_type: "transaction_batch",
    entity_id: `dynamic_qr_expiry_${result.swept_at}`,
    after_json: {
      scanned: result.scanned,
      expired_count: result.expired_count,
      skipped_count: result.skipped_count,
      expired_ids: result.expired.map((tx) => tx.id),
      skipped: result.skipped
    }
  });

  res.json(successResponse(req, result));
});

router.get("/transactions/expiry-scheduler", requireAdminToken, async (req: Request, res: Response) => {
  res.json(successResponse(req, getDynamicQrExpirySchedulerStatus()));
});

router.get(
  "/transactions/:transactionId",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const tx = await getTransactionById(req.params.transactionId);
    if (!tx) {
      return next(new ApiError("NOT_FOUND", "transaction not found", 404));
    }

    const events = (await getTransactionEvents(tx.id)).map(toTransactionEventPayload);
    const ledger_entries = (await listLedgerEntries({ transaction_id: tx.id })).map(toLedgerEntryPayload);
    const bindingByTerminal = tx.terminal_id ? await getActiveBindingByTerminal(tx.terminal_id) : null;
    const heartbeatDeviceId = tx.device_id || bindingByTerminal?.device_id;
    const heartbeatHistory = heartbeatDeviceId
      ? (await listHeartbeats({ device_id: heartbeatDeviceId })).map((heartbeat) => ({
          id: heartbeat.id,
          device_id: heartbeat.device_id,
          timestamp: heartbeat.timestamp,
          state: heartbeat.state,
          network_strength: heartbeat.network_strength,
          battery_level: heartbeat.battery_level,
          firmware_version: heartbeat.firmware_version,
          received_at: heartbeat.received_at
        }))
      : [];

    res.json(
      successResponse(req, {
        transaction: toTransactionPayload(tx),
        events,
        ledger_entries,
        heartbeat_device_id: heartbeatDeviceId,
        heartbeat_history: heartbeatHistory
      })
    );
  }
);

router.get(
  "/transactions/:transactionId/events",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const tx = await getTransactionById(req.params.transactionId);
    if (!tx) {
      return next(new ApiError("NOT_FOUND", "transaction not found", 404));
    }

    const events = (await getTransactionEvents(tx.id))
      .sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))
      .map(toTransactionEventPayload);

    res.json(successResponse(req, { transaction_id: tx.id, events }));
  }
);

router.get(
  "/transactions/:transactionId/heartbeats",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const tx = await getTransactionById(req.params.transactionId);
    if (!tx) {
      return next(new ApiError("NOT_FOUND", "transaction not found", 404));
    }

    const from = req.query.from as string | undefined;
    const to = req.query.to as string | undefined;
    const state = req.query.state as string | undefined;
    const limitRaw = req.query.limit as string | undefined;
    const limit = limitRaw ? Number(limitRaw) : 100;

    if (from && Number.isNaN(Date.parse(from))) {
      return next(new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400));
    }

    if (to && Number.isNaN(Date.parse(to))) {
      return next(new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400));
    }

    if (!Number.isFinite(limit) || limit <= 0) {
      return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
    }

    const bindingByTerminal = tx.terminal_id ? await getActiveBindingByTerminal(tx.terminal_id) : null;
    const heartbeatDeviceId = tx.device_id || bindingByTerminal?.device_id;
    if (!heartbeatDeviceId) {
      return res.json(
        successResponse(req, {
          transaction_id: tx.id,
          heartbeat_device_id: null,
          heartbeats: []
        })
      );
    }

    const heartbeats = (await listHeartbeats({
      device_id: heartbeatDeviceId,
      from,
      to,
      state
    }))
      .slice(0, Math.min(limit, 500))
      .map((heartbeat) => ({
        id: heartbeat.id,
        device_id: heartbeat.device_id,
        timestamp: heartbeat.timestamp,
        received_at: heartbeat.received_at,
        state: heartbeat.state,
        network_strength: heartbeat.network_strength,
        battery_level: heartbeat.battery_level,
        firmware_version: heartbeat.firmware_version
      }));

    res.json(
      successResponse(req, {
        transaction_id: tx.id,
        heartbeat_device_id: heartbeatDeviceId,
        heartbeats
      })
    );
  }
);

router.post(
  "/transactions/:transactionId/retry-notification",
  requireAdminToken,
  async (req: Request, res: Response, next: NextFunction) => {
    const transactionId = req.params.transactionId;
    try {
      const tx = await getTransactionById(transactionId);
      if (!tx) {
        return next(new ApiError("NOT_FOUND", "transaction not found", 404));
      }

      if (tx.status !== "paid") {
        return next(
          new ApiError("BAD_REQUEST", "notification retry only allowed for paid transactions", 400)
        );
      }

      const before = await getNotificationByTransactionId(transactionId);
      if (!before) {
        return next(
          new ApiError("NOT_FOUND", "notification not found for this transaction", 404)
        );
      }

      if (before.delivery_status === "acknowledged") {
        return res.status(200).json(
          successResponse(req, {
            transaction_id: tx.id,
            notification_id: before.id,
            delivery_status: before.delivery_status,
            next_retry_at: before.next_retry_at || null
          })
        );
      }

      const updated = await retryNotificationByTransactionId(transactionId);
      if (!updated) {
        return next(new ApiError("NOTIFICATION_PUBLISH_FAILED", "notification retry could not be executed", 500));
      }

      res.json(
        successResponse(req, {
          transaction_id: tx.id,
          notification_id: updated.id,
          delivery_status: updated.delivery_status,
          next_retry_at: updated.next_retry_at || null
        })
      );
    } catch (error) {
      if (error instanceof Error && error.message === "NOTIFICATION_PUBLISH_CONDITION") {
        return next(new ApiError("BAD_REQUEST", "notification retry only allowed for paid transactions", 400));
      }

      return next(error as Error);
    }
  }
);

router.get("/dashboard/summary", requireAdminToken, async (req: Request, res: Response) => {
  let dashboard = {
    transactions_today: 0,
    success_rate_today: 0,
    active_devices: 0,
    devices_online: 0,
    devices_degraded: 0,
    pending_notifications: 0,
    devices_stale: 0,
    devices_offline: 0,
    settlement_pending_amount: 0,
    settlement_paid_amount: 0,
    settlement_adjustment_amount: 0,
    settlement_adjusted_paid_amount: 0,
    settlement_platform_fee_amount: 0,
    settlement_created_batches: 0,
    settlement_paid_batches: 0,
    settlement_total_batches: 0
  };

  try {
    const { start, end } = buildDashboardRange();
    const startTs = start.getTime();
    const endTs = end.getTime();

    const todayTransactions = (await listTransactions()).filter((tx) => {
      const createdTs = Date.parse(tx.created_at);
      return createdTs >= startTs && createdTs < endTs;
    });
    const paidToday = todayTransactions.filter((tx) => tx.status === "paid").length;
    const transactionsToday = todayTransactions.length;
    const successRateToday = transactionsToday > 0 ? (paidToday / transactionsToday) * 100 : 0;

    const statuses = await deriveDeviceStatusesForDashboard();
    const devicesOnline = statuses.filter((row) => row.status === "online").length;
    const devicesDegraded = statuses.filter((row) => row.status === "degraded").length;
    const activeDevices = statuses.filter((row) => row.status !== "offline").length;
    const devicesStale = statuses.filter((row) => row.status === "stale").length;
    const devicesOffline = statuses.filter((row) => row.status === "offline").length;

    const pendingNotifications = (await listNotifications()).filter((notification) => {
      return notification.delivery_status === "queued" || notification.delivery_status === "retrying";
    }).length;

    const settlementFinanceSummary = await getSettlementFinanceSummary();

    dashboard = {
      transactions_today: transactionsToday,
      success_rate_today: Number(successRateToday.toFixed(2)),
      active_devices: activeDevices,
      devices_online: devicesOnline,
      devices_degraded: devicesDegraded,
      pending_notifications: pendingNotifications,
      devices_stale: devicesStale,
      devices_offline: devicesOffline,
      settlement_pending_amount: settlementFinanceSummary.pending_amount,
      settlement_paid_amount: settlementFinanceSummary.paid_amount,
      settlement_adjustment_amount: settlementFinanceSummary.adjustment_amount,
      settlement_adjusted_paid_amount: settlementFinanceSummary.adjusted_paid_amount,
      settlement_platform_fee_amount: settlementFinanceSummary.platform_fee_amount,
      settlement_created_batches: settlementFinanceSummary.created_batches,
      settlement_paid_batches: settlementFinanceSummary.paid_batches,
      settlement_total_batches: settlementFinanceSummary.total_batches
    };
  } catch (error) {
    logger.warn("dashboard_summary_fallback", {
      request_id: req.requestId,
      trace_id: req.traceId,
      error
    });
  }

  res.json(successResponse(req, dashboard));
});

router.get("/mqtt/status", requireAdminToken, async (req: Request, res: Response) => {
  const limitRaw = req.query.limit;
  const limit = limitRaw === undefined || limitRaw === "" ? 10 : Number(limitRaw);
  const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 500) : 10;
  const direction = req.query.direction === "uplink" || req.query.direction === "downlink"
    ? req.query.direction
    : undefined;
  const messageType = typeof req.query.message_type === "string" && req.query.message_type.trim()
    ? req.query.message_type.trim()
    : undefined;
  const deviceId = typeof req.query.device_id === "string" && req.query.device_id.trim()
    ? req.query.device_id.trim()
    : undefined;

  res.json(
    successResponse(req, {
      publisher: getMqttPublisherStatus(),
      subscriber: getMqttSubscriberStatus(),
      last_messages: (await listMqttMessages({
        limit: safeLimit,
        direction,
        message_type: messageType,
        device_id: deviceId
      })).map(toMqttMessagePayload)
    })
  );
});

router.get("/observability/summary", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  try {
    const [database, notifications, reconciliation, exportJobs] = await Promise.all([
      getDatabaseHealth(),
      listNotifications(),
      getSettlementReconciliationReport({ limit: 100, mismatches_only: true }),
      countExportJobsByStatus()
    ]);
    const failedNotifications = notifications.filter((notification) => notification.delivery_status === "failed");
    const pendingNotifications = notifications.filter((notification) =>
      notification.delivery_status === "queued" || notification.delivery_status === "retrying"
    );
    const mqttPublisher = getMqttPublisherStatus();
    const mqttSubscriber = getMqttSubscriberStatus();

    res.json(
      successResponse(req, {
        generated_at: new Date().toISOString(),
        database,
        mqtt: {
          publisher: mqttPublisher,
          subscriber: mqttSubscriber
        },
        notifications: {
          failed_count: failedNotifications.length,
          pending_count: pendingNotifications.length
        },
        settlement_reconciliation: {
          mismatch_batches: reconciliation.mismatch_batches,
          issue_count: reconciliation.issue_count,
          sampled_batches: reconciliation.rows.length
        },
        export_jobs: {
          worker: getExportJobWorkerStatus(),
          status_counts: exportJobs
        }
      })
    );
  } catch (error) {
    return next(error as Error);
  }
});

router.get("/notifications/failed", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const deviceId = req.query.device_id as string | undefined;
  const from = req.query.from as string | undefined;
  const to = req.query.to as string | undefined;

  const range = toStartEndDateFilter(from, to);
  if ((from || to) && range === null) {
    return next(new ApiError("BAD_REQUEST", "from/to must be valid ISO datetime", 400));
  }

  const filtered = (await listNotifications())
    .filter((notification) => notification.delivery_status === "failed")
    .filter((notification) => !deviceId || notification.device_id === deviceId)
    .filter((notification) => {
      if (!range) {
        return true;
      }

      const createdTs = Date.parse(notification.created_at);
      if (range.fromTs && createdTs < range.fromTs) {
        return false;
      }

      if (range.toTs && createdTs > range.toTs) {
        return false;
      }

      return true;
    })
    .sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))
    .map((notification) => ({
      notification_id: notification.id,
      transaction_id: notification.transaction_id,
      device_id: notification.device_id,
      delivery_status: notification.delivery_status,
      retry_count: notification.retry_count,
      reason: notification.reason,
      created_at: notification.created_at
    }));

  res.json(successResponse(req, filtered));
});

router.get("/audit-logs", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const entityType = (req.query.entity_type as string | undefined)?.trim();
  const entityId = (req.query.entity_id as string | undefined)?.trim();
  const action = (req.query.action as string | undefined)?.trim();
  const actionContains = (req.query.action_contains as string | undefined)?.trim();
  const from = req.query.from as string | undefined;
  const to = req.query.to as string | undefined;
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 100;

  if (from && Number.isNaN(Date.parse(from))) {
    return next(new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400));
  }

  if (to && Number.isNaN(Date.parse(to))) {
    return next(new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400));
  }

  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const logs = await listAuditLogs({
    entity_type: entityType || undefined,
    entity_id: entityId || undefined,
    action: action || undefined,
    action_contains: actionContains || undefined,
    from,
    to,
    limit
  });

  res.json(successResponse(req, logs.map(toAuditLogPayload)));
});

router.post("/settlement-batches", requireAdminToken, requireAdminPermission("settlement:write"), async (req: Request, res: Response, next: NextFunction) => {
  const payload = (req.body || {}) as SettlementBatchCreateInput;
  const merchantId = payload.merchant_id?.trim();
  const cutoffAt = payload.cutoff_at?.trim();
  const limitPerMerchant = payload.limit_per_merchant ? Number(payload.limit_per_merchant) : undefined;

  if (merchantId && !(await getMerchantById(merchantId))) {
    return next(new ApiError("NOT_FOUND", "merchant not found", 404));
  }

  if (cutoffAt && Number.isNaN(Date.parse(cutoffAt))) {
    return next(new ApiError("BAD_REQUEST", "cutoff_at must be valid ISO datetime", 400));
  }

  if (limitPerMerchant !== undefined && (!Number.isFinite(limitPerMerchant) || limitPerMerchant <= 0)) {
    return next(new ApiError("BAD_REQUEST", "limit_per_merchant must be a positive number", 400));
  }

  const batches = await createSettlementBatches({
    merchant_id: merchantId || undefined,
    cutoff_at: cutoffAt || undefined,
    limit_per_merchant: limitPerMerchant
  });

  for (const batch of batches) {
    await createSettlementBatchEvent({
      batch_id: batch.id,
      merchant_id: batch.merchant_id,
      event_type: "created",
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        batch_code: batch.batch_code,
        cutoff_at: batch.cutoff_at,
        entry_count: batch.entry_count,
        net_payable_amount: batch.net_payable_amount
      }
    });
    await auditAdminAction(req, {
      action: "settlement.batch.create",
      entity_type: "settlement_batch",
      entity_id: batch.id,
      after_json: toSettlementBatchPayload(batch)
    });
  }

  res.status(201).json(
    successResponse(req, {
      created_count: batches.length,
      batches: batches.map(toSettlementBatchPayload)
    })
  );
});

router.get("/settlement-batches", requireAdminToken, requireAdminPermission("settlement:read"), async (req: Request, res: Response, next: NextFunction) => {
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const statusRaw = req.query.status as string | undefined;
  const status = parseSettlementBatchStatus(statusRaw);
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 100;

  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "status must be created|paid|failed|cancelled", 400));
  }

  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const batches = await listSettlementBatches({
    merchant_id: merchantId || undefined,
    status,
    limit
  });

  res.json(successResponse(req, batches.map(toSettlementBatchPayload)));
});

router.get("/reconciliation/settlement-batches", requireAdminToken, requireAdminPermission("reconciliation:read"), async (req: Request, res: Response, next: NextFunction) => {
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const statusRaw = req.query.status as string | undefined;
  const status = parseSettlementBatchStatus(statusRaw);
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 100;
  const mismatchesOnlyRaw = req.query.mismatches_only as string | undefined;
  const mismatchesOnly = mismatchesOnlyRaw === "true" || mismatchesOnlyRaw === "1";

  if (statusRaw && !status) {
    return next(new ApiError("BAD_REQUEST", "status must be created|paid|failed|cancelled", 400));
  }

  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const report = await getSettlementReconciliationReport({
    merchant_id: merchantId || undefined,
    status,
    limit,
    mismatches_only: mismatchesOnly
  });

  res.json(successResponse(req, report));
});

router.get("/settlement-adjustments", requireAdminToken, requireAdminPermission("settlement:read"), async (req: Request, res: Response, next: NextFunction) => {
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const adjustmentTypeRaw = req.query.adjustment_type as string | undefined;
  const adjustmentType = parseSettlementAdjustmentType(adjustmentTypeRaw);
  const approvalStatusRaw = req.query.approval_status as string | undefined;
  const approvalStatus = parseSettlementAdjustmentApprovalStatus(approvalStatusRaw);
  const from = (req.query.from as string | undefined)?.trim();
  const to = (req.query.to as string | undefined)?.trim();
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 100;

  if (adjustmentTypeRaw && !adjustmentType) {
    return next(new ApiError("BAD_REQUEST", "adjustment_type must be credit|debit", 400));
  }
  if (approvalStatusRaw && !approvalStatus) {
    return next(new ApiError("BAD_REQUEST", "approval_status must be pending|approved|rejected", 400));
  }
  if (from && !isIsoDate(from)) {
    return next(new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400));
  }
  if (to && !isIsoDate(to)) {
    return next(new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400));
  }
  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const report = await listSettlementAdjustments({
    merchant_id: merchantId || undefined,
    adjustment_type: adjustmentType,
    approval_status: approvalStatus,
    from: from || undefined,
    to: to || undefined,
    limit
  });

  res.json(successResponse(req, report));
});

router.get("/settlement-adjustments/export.csv", requireAdminToken, requireAdminPermission("settlement:export"), async (req: Request, res: Response, next: NextFunction) => {
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const adjustmentTypeRaw = req.query.adjustment_type as string | undefined;
  const adjustmentType = parseSettlementAdjustmentType(adjustmentTypeRaw);
  const approvalStatusRaw = req.query.approval_status as string | undefined;
  const approvalStatus = parseSettlementAdjustmentApprovalStatus(approvalStatusRaw);
  const from = (req.query.from as string | undefined)?.trim();
  const to = (req.query.to as string | undefined)?.trim();
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 500;

  if (adjustmentTypeRaw && !adjustmentType) {
    return next(new ApiError("BAD_REQUEST", "adjustment_type must be credit|debit", 400));
  }
  if (approvalStatusRaw && !approvalStatus) {
    return next(new ApiError("BAD_REQUEST", "approval_status must be pending|approved|rejected", 400));
  }
  if (from && !isIsoDate(from)) {
    return next(new ApiError("BAD_REQUEST", "from must be valid ISO datetime", 400));
  }
  if (to && !isIsoDate(to)) {
    return next(new ApiError("BAD_REQUEST", "to must be valid ISO datetime", 400));
  }
  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const report = await listSettlementAdjustments({
    merchant_id: merchantId || undefined,
    adjustment_type: adjustmentType,
    approval_status: approvalStatus,
    from: from || undefined,
    to: to || undefined,
    limit
  });
  const csv = settlementAdjustmentReportToCsv(report);
  res.setHeader("Content-Type", "text/csv; charset=utf-8");
  res.setHeader("Content-Disposition", 'attachment; filename="settlement-adjustment-report.csv"');
  res.send(csv);
});

router.post(
  "/exports/settlement-adjustments",
  requireAdminToken,
  requireAdminPermission("settlement:export"),
  async (req: RequestWithAdminAuth, res: Response, next: NextFunction) => {
    try {
      const job = await createSettlementAdjustmentExportJob({
        requested_by: req.adminAuth?.user_id || "admin",
        request: (req.body || {}) as SettlementAdjustmentExportJobInput
      });
      await auditAdminAction(req, {
        action: "settlement.adjustment.export_job_create",
        entity_type: "export_job",
        entity_id: job.id,
        after_json: toExportJobPayload(job)
      });
      res.status(202).json(successResponse(req, toExportJobPayload(job)));
    } catch (error) {
      return next(error as Error);
    }
  }
);

router.get("/exports", requireAdminToken, requireAdminPermission("settlement:export"), async (req: Request, res: Response) => {
  const statusRaw = req.query.status as string | undefined;
  const status =
    statusRaw === "pending" || statusRaw === "running" || statusRaw === "completed" || statusRaw === "failed"
      ? statusRaw
      : undefined;
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 20;
  const jobs = await listExportJobs({
    job_type: (req.query.job_type as string | undefined) || undefined,
    status,
    limit: Number.isFinite(limit) ? limit : 20
  });
  res.json(successResponse(req, jobs.map(toExportJobPayload)));
});

router.get("/exports/:jobId", requireAdminToken, requireAdminPermission("settlement:export"), async (req: Request, res: Response, next: NextFunction) => {
  const job = await getExportJobById(req.params.jobId);
  if (!job) {
    return next(new ApiError("NOT_FOUND", "export job not found", 404));
  }
  res.json(successResponse(req, toExportJobPayload(job)));
});

router.get("/exports/:jobId/download", requireAdminToken, requireAdminPermission("settlement:export"), async (req: Request, res: Response, next: NextFunction) => {
  const job = await getExportJobById(req.params.jobId);
  if (!job) {
    return next(new ApiError("NOT_FOUND", "export job not found", 404));
  }
  if (job.status !== "completed" || (!job.result_body && !job.result_storage_path)) {
    return next(new ApiError("CONFLICT", "export job is not completed", 409));
  }
  const resultBody = job.result_storage_path ? await fs.readFile(job.result_storage_path, "utf8") : job.result_body;
  res.setHeader("Content-Type", job.result_content_type || "text/csv; charset=utf-8");
  res.setHeader("Content-Disposition", `attachment; filename="${job.result_filename || `${job.id}.csv`}"`);
  res.send(resultBody);
});

async function resolveSettlementAdjustmentApproval(
  req: RequestWithAdminAuth,
  res: Response,
  next: NextFunction,
  approvalStatus: "approved" | "rejected"
) {
  const payload = (req.body || {}) as SettlementAdjustmentApprovalInput;
  const note = payload.note?.trim();
  if (note && note.length > 500) {
    return next(new ApiError("BAD_REQUEST", "note must be 500 characters or less", 400));
  }

  try {
    const result = await transitionSettlementBatchAdjustmentApproval(req.params.adjustmentId, {
      approval_status: approvalStatus,
      actor_id: req.adminAuth?.user_id || "admin",
      note: note || undefined
    });
    await createSettlementBatchEvent({
      batch_id: result.batch.id,
      merchant_id: result.batch.merchant_id,
      event_type: approvalStatus === "approved" ? "adjustment_approved" : "adjustment_rejected",
      actor_type: "admin",
      actor_id: req.adminAuth?.user_id || "admin",
      payload_json: {
        adjustment: toSettlementBatchAdjustmentPayload(result.adjustment),
        total_adjustment_amount: result.batch.metadata_json.total_adjustment_amount || 0,
        note: note || null
      }
    });
    await auditAdminAction(req, {
      action: `settlement.adjustment.${approvalStatus}`,
      entity_type: "settlement_batch_adjustment",
      entity_id: result.adjustment.id,
      after_json: toSettlementBatchAdjustmentPayload(result.adjustment)
    });
    return res.json(
      successResponse(req, {
        adjustment: toSettlementBatchAdjustmentPayload(result.adjustment),
        batch: toSettlementBatchPayload(result.batch)
      })
    );
  } catch (error) {
    if (error instanceof Error && error.message === "SETTLEMENT_ADJUSTMENT_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "settlement adjustment not found", 404));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_ADJUSTMENT_NOT_PENDING") {
      return next(new ApiError("CONFLICT", "settlement adjustment is not pending approval", 409));
    }
    return next(error as Error);
  }
}

router.post(
  "/settlement-adjustments/:adjustmentId/approve",
  requireAdminToken,
  requireAdminPermission("settlement:adjust"),
  async (req: RequestWithAdminAuth, res: Response, next: NextFunction) =>
    resolveSettlementAdjustmentApproval(req, res, next, "approved")
);

router.post(
  "/settlement-adjustments/:adjustmentId/reject",
  requireAdminToken,
  requireAdminPermission("settlement:adjust"),
  async (req: RequestWithAdminAuth, res: Response, next: NextFunction) =>
    resolveSettlementAdjustmentApproval(req, res, next, "rejected")
);

router.get("/settlement-batches/:batchId", requireAdminToken, requireAdminPermission("settlement:read"), async (req: Request, res: Response, next: NextFunction) => {
  const batch = await getSettlementBatchById(req.params.batchId);
  if (!batch) {
    return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
  }

  const entries = await listSettlementBatchEntries(batch.id);
  const adjustments = await listSettlementBatchAdjustments(batch.id);
  const events = await listSettlementBatchEvents(batch.id);
  res.json(
    successResponse(req, {
      batch: toSettlementBatchPayload(batch),
      entries: entries.map(toSettlementBatchEntryPayload),
      adjustments: adjustments.map(toSettlementBatchAdjustmentPayload),
      events: events.map(toSettlementBatchEventPayload)
    })
  );
});

router.get("/settlement-batches/:batchId/export.csv", requireAdminToken, requireAdminPermission("settlement:export"), async (req: Request, res: Response, next: NextFunction) => {
  const batch = await getSettlementBatchById(req.params.batchId);
  if (!batch) {
    return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
  }
  const formatRaw = req.query.format as string | undefined;
  const format = parseSettlementExportFormat(formatRaw);
  if (!format) {
    return next(new ApiError("BAD_REQUEST", "format must be standard|bank_generic", 400));
  }

  const rows = await getSettlementBatchReportRows(batch.id);
  const csv = format === "bank_generic" ? settlementBatchReportToBankGenericCsv(rows) : settlementBatchReportToCsv(rows);
  const filename =
    format === "bank_generic"
      ? `${batch.batch_code}-bank-generic-payout.csv`
      : `${batch.batch_code}-payout-report.csv`;
  await createSettlementBatchEvent({
    batch_id: batch.id,
    merchant_id: batch.merchant_id,
    event_type: "csv_exported",
    actor_type: "admin",
    actor_id: "admin",
    payload_json: {
      format,
      report_rows: rows.length,
      filename
    }
  });
  res.setHeader("Content-Type", "text/csv; charset=utf-8");
  res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
  res.send(csv);
});

router.post("/settlement-batches/:batchId/mark-paid", requireAdminToken, requireAdminPermission("settlement:pay"), async (req: Request, res: Response, next: NextFunction) => {
  const payload = (req.body || {}) as SettlementBatchPaidInput;
  const paidReference = payload.paid_reference?.trim();
  const paidNote = payload.paid_note?.trim();
  if (payload.paid_at && Number.isNaN(Date.parse(payload.paid_at))) {
    return next(new ApiError("BAD_REQUEST", "paid_at must be valid ISO datetime", 400));
  }
  if (paidReference && paidReference.length > 120) {
    return next(new ApiError("BAD_REQUEST", "paid_reference must be 120 characters or less", 400));
  }
  if (paidNote && paidNote.length > 500) {
    return next(new ApiError("BAD_REQUEST", "paid_note must be 500 characters or less", 400));
  }

  try {
    const before = await getSettlementBatchById(req.params.batchId);
    const updated = await markSettlementBatchPaid(req.params.batchId, {
      paid_at: payload.paid_at,
      paid_reference: paidReference || undefined,
      paid_note: paidNote || undefined
    });
    await createSettlementBatchEvent({
      batch_id: updated.id,
      merchant_id: updated.merchant_id,
      event_type: "marked_paid",
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        paid_at: updated.paid_at,
        paid_reference: paidReference || null,
        paid_note: paidNote || null
      }
    });
    await auditAdminAction(req, {
      action: "settlement.batch.mark_paid",
      entity_type: "settlement_batch",
      entity_id: updated.id,
      before_json: before ? toSettlementBatchPayload(before) : null,
      after_json: toSettlementBatchPayload(updated)
    });
    res.json(successResponse(req, toSettlementBatchPayload(updated)));
  } catch (error) {
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_PAYABLE") {
      return next(new ApiError("CONFLICT", "settlement batch is not in created status", 409));
    }
    return next(error as Error);
  }
});

router.patch("/settlement-batches/:batchId/reference", requireAdminToken, requireAdminPermission("settlement:pay"), async (req: Request, res: Response, next: NextFunction) => {
  const payload = (req.body || {}) as SettlementBatchReferenceInput;
  const paidReference = payload.paid_reference?.trim();
  const paidNote = payload.paid_note?.trim();

  if (!paidReference) {
    return next(new ApiError("BAD_REQUEST", "paid_reference is required", 400));
  }
  if (paidReference.length > 120) {
    return next(new ApiError("BAD_REQUEST", "paid_reference must be 120 characters or less", 400));
  }
  if (paidNote && paidNote.length > 500) {
    return next(new ApiError("BAD_REQUEST", "paid_note must be 500 characters or less", 400));
  }

  try {
    const before = await getSettlementBatchById(req.params.batchId);
    const previousMetadata = before?.metadata_json || {};
    const updated = await updateSettlementBatchReference(req.params.batchId, {
      paid_reference: paidReference,
      paid_note: paidNote || undefined
    });
    await createSettlementBatchEvent({
      batch_id: updated.id,
      merchant_id: updated.merchant_id,
      event_type: "reference_updated",
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        previous_paid_reference: previousMetadata.paid_reference || null,
        paid_reference: paidReference,
        previous_paid_note: previousMetadata.paid_note || null,
        paid_note: paidNote || null
      }
    });
    await auditAdminAction(req, {
      action: "settlement.batch.reference_update",
      entity_type: "settlement_batch",
      entity_id: updated.id,
      before_json: before ? toSettlementBatchPayload(before) : null,
      after_json: toSettlementBatchPayload(updated)
    });
    res.json(successResponse(req, toSettlementBatchPayload(updated)));
  } catch (error) {
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_REFERENCE_NOT_EDITABLE") {
      return next(new ApiError("CONFLICT", "settlement reference can only be updated after batch is paid", 409));
    }
    return next(error as Error);
  }
});

router.post("/settlement-batches/:batchId/adjustments", requireAdminToken, requireAdminPermission("settlement:adjust"), async (req: Request, res: Response, next: NextFunction) => {
  const payload = (req.body || {}) as SettlementBatchAdjustmentInput;
  const adjustmentType = payload.adjustment_type;
  const amount = Number(payload.amount);
  const reason = payload.reason?.trim();
  const note = payload.note?.trim();

  if (adjustmentType !== "credit" && adjustmentType !== "debit") {
    return next(new ApiError("BAD_REQUEST", "adjustment_type must be credit|debit", 400));
  }
  if (!Number.isFinite(amount) || amount <= 0) {
    return next(new ApiError("BAD_REQUEST", "amount must be a positive number", 400));
  }
  if (!reason) {
    return next(new ApiError("BAD_REQUEST", "reason is required", 400));
  }
  if (reason.length > 300) {
    return next(new ApiError("BAD_REQUEST", "reason must be 300 characters or less", 400));
  }
  if (note && note.length > 500) {
    return next(new ApiError("BAD_REQUEST", "note must be 500 characters or less", 400));
  }

  try {
    const before = await getSettlementBatchById(req.params.batchId);
    const updated = await recordSettlementBatchAdjustment(req.params.batchId, {
      adjustment_type: adjustmentType,
      amount,
      reason,
      note: note || undefined,
      actor_id: "admin"
    });
    const adjustments = Array.isArray(updated.metadata_json.adjustments) ? updated.metadata_json.adjustments : [];
    const latestAdjustment = adjustments[adjustments.length - 1] || null;
    await createSettlementBatchEvent({
      batch_id: updated.id,
      merchant_id: updated.merchant_id,
      event_type: "adjustment_recorded",
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        adjustment: latestAdjustment,
        total_adjustment_amount: updated.metadata_json.total_adjustment_amount || 0
      }
    });
    await auditAdminAction(req, {
      action: "settlement.batch.adjustment_record",
      entity_type: "settlement_batch",
      entity_id: updated.id,
      before_json: before ? toSettlementBatchPayload(before) : null,
      after_json: toSettlementBatchPayload(updated)
    });
    res.status(201).json(successResponse(req, toSettlementBatchPayload(updated)));
  } catch (error) {
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_ADJUSTMENT_NOT_EDITABLE") {
      return next(new ApiError("CONFLICT", "archived reprocessed settlement batch cannot be adjusted", 409));
    }
    return next(error as Error);
  }
});

async function resolveSettlementBatch(
  req: Request,
  res: Response,
  next: NextFunction,
  status: Extract<SettlementBatchStatus, "failed" | "cancelled">
) {
  const payload = (req.body || {}) as SettlementBatchResolutionInput;
  const reason = payload.reason?.trim();
  const note = payload.note?.trim();

  if (!reason) {
    return next(new ApiError("BAD_REQUEST", "reason is required", 400));
  }
  if (reason.length > 300) {
    return next(new ApiError("BAD_REQUEST", "reason must be 300 characters or less", 400));
  }
  if (note && note.length > 500) {
    return next(new ApiError("BAD_REQUEST", "note must be 500 characters or less", 400));
  }

  try {
    const before = await getSettlementBatchById(req.params.batchId);
    const updated = await transitionSettlementBatchStatus(req.params.batchId, {
      status,
      reason,
      note: note || undefined
    });
    await createSettlementBatchEvent({
      batch_id: updated.id,
      merchant_id: updated.merchant_id,
      event_type: status,
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        reason,
        note: note || null
      }
    });
    await auditAdminAction(req, {
      action: `settlement.batch.${status}`,
      entity_type: "settlement_batch",
      entity_id: updated.id,
      before_json: before ? toSettlementBatchPayload(before) : null,
      after_json: toSettlementBatchPayload(updated)
    });
    return res.json(successResponse(req, toSettlementBatchPayload(updated)));
  } catch (error) {
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_TRANSITIONABLE") {
      return next(new ApiError("CONFLICT", "settlement batch is not in created status", 409));
    }
    return next(error as Error);
  }
}

router.post("/settlement-batches/:batchId/mark-failed", requireAdminToken, requireAdminPermission("settlement:write"), async (req: Request, res: Response, next: NextFunction) => {
  return resolveSettlementBatch(req, res, next, "failed");
});

router.post("/settlement-batches/:batchId/cancel", requireAdminToken, requireAdminPermission("settlement:write"), async (req: Request, res: Response, next: NextFunction) => {
  return resolveSettlementBatch(req, res, next, "cancelled");
});

router.post("/settlement-batches/:batchId/reprocess", requireAdminToken, requireAdminPermission("settlement:write"), async (req: Request, res: Response, next: NextFunction) => {
  try {
    const before = await getSettlementBatchById(req.params.batchId);
    const result = await reprocessSettlementBatch(req.params.batchId);
    await createSettlementBatchEvent({
      batch_id: result.source_batch.id,
      merchant_id: result.source_batch.merchant_id,
      event_type: "reprocessed",
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        reprocessed_to_batch_id: result.new_batch.id,
        reprocessed_to_batch_code: result.new_batch.batch_code,
        moved_entry_count: result.moved_entry_count
      }
    });
    await createSettlementBatchEvent({
      batch_id: result.new_batch.id,
      merchant_id: result.new_batch.merchant_id,
      event_type: "created",
      actor_type: "admin",
      actor_id: "admin",
      payload_json: {
        batch_code: result.new_batch.batch_code,
        reprocessed_from_batch_id: result.source_batch.id,
        reprocessed_from_batch_code: result.source_batch.batch_code,
        entry_count: result.new_batch.entry_count,
        net_payable_amount: result.new_batch.net_payable_amount
      }
    });
    await auditAdminAction(req, {
      action: "settlement.batch.reprocess",
      entity_type: "settlement_batch",
      entity_id: result.source_batch.id,
      before_json: before ? toSettlementBatchPayload(before) : null,
      after_json: {
        source_batch: toSettlementBatchPayload(result.source_batch),
        new_batch: toSettlementBatchPayload(result.new_batch),
        moved_entry_count: result.moved_entry_count
      }
    });
    return res.status(201).json(
      successResponse(req, {
        source_batch: toSettlementBatchPayload(result.source_batch),
        new_batch: toSettlementBatchPayload(result.new_batch),
        moved_entry_count: result.moved_entry_count
      })
    );
  } catch (error) {
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_FOUND") {
      return next(new ApiError("NOT_FOUND", "settlement batch not found", 404));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_NOT_REPROCESSABLE") {
      return next(new ApiError("CONFLICT", "settlement batch must be failed or cancelled", 409));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_ALREADY_REPROCESSED") {
      return next(new ApiError("CONFLICT", "settlement batch was already reprocessed", 409));
    }
    if (error instanceof Error && error.message === "SETTLEMENT_BATCH_EMPTY") {
      return next(new ApiError("CONFLICT", "settlement batch has no entries to reprocess", 409));
    }
    return next(error as Error);
  }
});

router.get("/ledger-entries", requireAdminToken, async (req: Request, res: Response, next: NextFunction) => {
  const transactionId = (req.query.transaction_id as string | undefined)?.trim();
  const merchantId = (req.query.merchant_id as string | undefined)?.trim();
  const limitRaw = req.query.limit as string | undefined;
  const limit = limitRaw ? Number(limitRaw) : 100;

  if (!Number.isFinite(limit) || limit <= 0) {
    return next(new ApiError("BAD_REQUEST", "limit must be a positive number", 400));
  }

  const entries = await listLedgerEntries({
    transaction_id: transactionId || undefined,
    merchant_id: merchantId || undefined,
    limit
  });

  res.json(successResponse(req, entries.map(toLedgerEntryPayload)));
});

export default router;
