import { createHmac } from "node:crypto";

const BASE = process.env.BASE_URL || `http://127.0.0.1:${process.env.PORT || "3000"}`;
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "admin-dev-token";
const DEVICE_TOKEN = process.env.DEVICE_TOKEN || "device-dev-token";
const SECRET = process.env.INTEGRATION_WEBHOOK_SECRET || "dev-callback-secret";
const STATIC_SN = process.env.QF100_STATIC_SN || `QF100-STATIC-${Date.now()}`;
const DYNAMIC_SN = process.env.QF100_DYNAMIC_SN || `QF100-DYNAMIC-${Date.now()}`;

function short(data) {
  const text = typeof data === "string" ? data : JSON.stringify(data || {});
  return text.length > 220 ? `${text.slice(0, 220)}...` : text;
}

async function req(path, options = {}) {
  const response = await fetch(`${BASE}${path}`, {
    method: options.method || "GET",
    headers: {
      "Content-Type": "application/json",
      ...(options.headers || {})
    },
    body: Object.prototype.hasOwnProperty.call(options, "body") ? JSON.stringify(options.body) : undefined
  });

  const text = await response.text();
  let body = null;
  try {
    body = text ? JSON.parse(text) : null;
  } catch {
    body = text;
  }

  if (!response.ok) {
    throw new Error(`${options._label || path} failed: ${response.status} ${short(body)}`);
  }

  console.log(`${options._label || `${options.method || "GET"} ${path}`} => ${response.status} ${short(body)}`);
  return body?.data !== undefined ? body.data : body;
}

function reqAdmin(path, options = {}) {
  return req(path, {
    ...options,
    headers: {
      ...(options.headers || {}),
      Authorization: `Bearer ${ADMIN_TOKEN}`
    }
  });
}

function reqDevice(path, options = {}) {
  return req(path, {
    ...options,
    headers: {
      ...(options.headers || {}),
      Authorization: `Bearer ${DEVICE_TOKEN}`
    }
  });
}

function assert(condition, message) {
  if (!condition) {
    throw new Error(message);
  }
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function createBundle({ ts, suffix, serialNumber, terminalMode, capability }) {
  const merchant = await reqAdmin("/admin/merchants", {
    method: "POST",
    body: {
      legal_name: `QF100 Smoke Merchant ${suffix} ${ts}`,
      brand_name: `QF100-${suffix}-${ts}`,
      settlement_account_reference: `bank:qf100:${suffix}:${ts}`,
      settlement_account_type: "merchant_bank_account",
      payout_mode: "merchant_direct"
    },
    _label: `POST /admin/merchants ${suffix}`
  });

  const outlet = await reqAdmin(`/admin/merchants/${merchant.id}/outlets`, {
    method: "POST",
    body: { name: `QF100 Smoke Outlet ${suffix} ${ts}` },
    _label: `POST /admin/merchants/:id/outlets ${suffix}`
  });

  const terminal = await reqAdmin(`/admin/outlets/${outlet.id}/terminals`, {
    method: "POST",
    body: { terminal_code: `QF100-${suffix}-TERM-${ts}`, qr_mode: terminalMode },
    _label: `POST /admin/outlets/:id/terminals ${suffix}`
  });

  const device = await reqAdmin("/admin/devices", {
    method: "POST",
    body: {
      device_code: `QF100-${suffix}-DEV-${ts}`,
      serial_number: serialNumber,
      vendor: "QF",
      model: "QF100",
      communication_mode: "mqtt",
      status: "active",
      capability_profile_json: {
        mqtt_payload_profile: "qf100",
        ...(capability || {})
      }
    },
    _label: `POST /admin/devices ${suffix}`
  });

  await reqAdmin(`/admin/devices/${device.id}/bind`, {
    method: "POST",
    body: {
      merchant_id: merchant.id,
      outlet_id: outlet.id,
      terminal_id: terminal.id
    },
    _label: `POST /admin/devices/:id/bind ${suffix}`
  });

  return { merchant, outlet, terminal, device };
}

async function pullQf100Config(serialNumber, label) {
  const query = new URLSearchParams({
    "dev-model": "QF100",
    "item-number": "00",
    "dev-sn": serialNumber,
    "fw-version": "1.0.0",
    "fw-build": "1",
    "app-config-version": "1",
    imei: `${label}-imei`,
    imsi: `${label}-imsi`,
    iccid: `${label}-iccid`
  });
  const config = await req(`/speaker/dev-config?${query.toString()}`, {
    _label: `GET /speaker/dev-config ${label}`
  });

  assert(config["error-code"] === 0, `${label} config error-code must be 0`);
  assert(config.mqtt?.["broker-ip"], `${label} config must include mqtt.broker-ip`);
  assert(config.mqtt?.["broker-port"], `${label} config must include mqtt.broker-port`);
  assert(config.mqtt?.["subscribe-topic"] === `soundbox/${serialNumber}/down`, `${label} must subscribe soundbox SN topic`);
  return config;
}

async function waitForQf100PaymentMessage(deviceId, serialNumber) {
  for (let i = 0; i < 20; i += 1) {
    const data = await reqAdmin(`/admin/devices/${deviceId}/mqtt-messages?direction=downlink&message_type=payment_success&limit=10`, {
      _label: `GET /admin/devices/:id/mqtt-messages attempt ${i + 1}`
    });
    const found = data.messages?.find((message) => message.topic === `soundbox/${serialNumber}/down`);
    if (found) {
      return found;
    }
    await sleep(250);
  }
  throw new Error("QF100 payment downlink message not found");
}

async function triggerStaticPayment({ bundle, ts }) {
  const partnerReference = `QF100-PR-${ts}`;
  await reqAdmin("/admin/transactions", {
    method: "POST",
    body: {
      partner_reference: partnerReference,
      merchant_id: bundle.merchant.id,
      outlet_id: bundle.outlet.id,
      terminal_id: bundle.terminal.id,
      device_id: bundle.device.id,
      amount: 15000,
      currency: "IDR",
      qr_mode: "static",
      initiation_mode: "static",
      status: "initiated"
    },
    _label: "POST /admin/transactions static"
  });

  const callback = {
    partner_reference: partnerReference,
    partner_txn_id: `QF100-PTX-${ts}`,
    amount: 15000,
    currency: "IDR",
    payment_status: "paid",
    status: "paid",
    paid_at: new Date().toISOString()
  };
  const signature = createHmac("sha256", SECRET).update(JSON.stringify(callback)).digest("hex");
  await req("/integrations/qris/callback", {
    method: "POST",
    headers: { "X-Partner-Signature": signature },
    body: { ...callback, signature },
    _label: "POST /integrations/qris/callback static paid"
  });

  const message = await waitForQf100PaymentMessage(bundle.device.id, bundle.device.serial_number);
  assert(message.payload_json?.header?.category === 1, "QF100 payment payload header.category must be 1");
  assert(message.payload_json?.data?.["pay-amount"] === 15000, "QF100 payment payload pay-amount must match");
  return message;
}

async function triggerDynamicMqttQr({ bundle, ts }) {
  const requestId = `QF100-DYN-${ts}`;
  const response = await reqDevice("/device/mqtt/uplink/dynamic-qr/request", {
    method: "POST",
    body: {
      message_type: "dynamic_qr_request",
      request_id: requestId,
      device_id: bundle.device.id,
      terminal_id: bundle.terminal.id,
      amount: 27500,
      currency: "IDR",
      created_at: new Date().toISOString()
    },
    _label: "POST /device/mqtt/uplink/dynamic-qr/request"
  });

  assert(response.status === "success", "dynamic MQTT QR response must be success");
  assert(response.qr_payload, "dynamic MQTT QR response must include qr_payload");
  return response;
}

async function triggerDynamicQrDisplay({ bundle }) {
  const result = await reqAdmin(`/admin/devices/${bundle.device.id}/commands`, {
    method: "POST",
    body: {
      command: "dynamic_qr.display",
      payload: {
        "qr-url": "https://pay.example/qr/qf100-smoke",
        amount: 25000,
        "expire-seconds": 60
      }
    },
    _label: "POST /admin/devices/:id/commands dynamic QR display"
  });

  assert(result.status === "delivered", "dynamic QR display command must be delivered");
  assert(result.result_payload?.topic === `soundbox/${bundle.device.serial_number}/down`, "dynamic QR display topic must use serial number");
  assert(result.result_payload?.payload?.header?.category === 4, "dynamic QR display category must be 4");
  return result;
}

async function triggerRebootCommand({ bundle }) {
  const result = await reqAdmin(`/admin/devices/${bundle.device.id}/commands`, {
    method: "POST",
    body: {
      command: "device.reboot",
      payload: {
        requested_from: "qf100_smoke"
      }
    },
    _label: "POST /admin/devices/:id/commands reboot"
  });

  assert(result.status === "delivered", "reboot command must be delivered");
  assert(result.result_payload?.topic === `soundbox/${bundle.device.serial_number}/down`, "reboot topic must use serial number");
  assert(result.result_payload?.payload?.header?.category === 5, "reboot category must be 5");
  assert(result.result_payload?.payload?.data?.command === "reboot", "reboot command payload must be reboot");
  return result;
}

async function triggerPoweroffCommand({ bundle }) {
  const result = await reqAdmin(`/admin/devices/${bundle.device.id}/commands`, {
    method: "POST",
    body: {
      command: "device.poweroff",
      payload: {
        requested_from: "qf100_smoke"
      }
    },
    _label: "POST /admin/devices/:id/commands poweroff"
  });

  assert(result.status === "delivered", "poweroff command must be delivered");
  assert(result.result_payload?.topic === `soundbox/${bundle.device.serial_number}/down`, "poweroff topic must use serial number");
  assert(result.result_payload?.payload?.header?.category === 5, "poweroff category must be 5");
  assert(result.result_payload?.payload?.data?.command === "poweroff", "poweroff command payload must be poweroff");
  return result;
}

async function main() {
  await req("/health", { _label: "GET /health" });
  const ts = Date.now();

  const staticBundle = await createBundle({
    ts,
    suffix: "STATIC",
    serialNumber: STATIC_SN,
    terminalMode: "static",
    capability: {}
  });
  const dynamicBundle = await createBundle({
    ts,
    suffix: "DYNAMIC",
    serialNumber: DYNAMIC_SN,
    terminalMode: "dynamic_mqtt",
    capability: {
      dynamic_qr: {
        mqtt: true
      }
    }
  });

  const staticConfig = await pullQf100Config(STATIC_SN, "static");
  const dynamicConfig = await pullQf100Config(DYNAMIC_SN, "dynamic");
  assert(staticConfig.mqtt["client-id"] === `soundbox-${STATIC_SN}`, "static client-id must match serial number");
  assert(dynamicConfig.mqtt["client-id"] === `soundbox-${DYNAMIC_SN}`, "dynamic client-id must match serial number");

  const staticPaymentMessage = await triggerStaticPayment({ bundle: staticBundle, ts });
  const dynamicQr = await triggerDynamicMqttQr({ bundle: dynamicBundle, ts });
  const dynamicQrDisplay = await triggerDynamicQrDisplay({ bundle: dynamicBundle });
  const rebootCommand = await triggerRebootCommand({ bundle: staticBundle });
  const poweroffCommand = await triggerPoweroffCommand({ bundle: staticBundle });

  console.log("\nQF100 adapter smoke passed");
  console.log(`static_sn=${STATIC_SN}`);
  console.log(`static_device_id=${staticBundle.device.id}`);
  console.log(`static_payment_topic=${staticPaymentMessage.topic}`);
  console.log(`dynamic_sn=${DYNAMIC_SN}`);
  console.log(`dynamic_device_id=${dynamicBundle.device.id}`);
  console.log(`dynamic_transaction_id=${dynamicQr.transaction_id}`);
  console.log(`dynamic_qr_display_command=${dynamicQrDisplay.id || dynamicQrDisplay.command_id}`);
  console.log(`reboot_command=${rebootCommand.id || rebootCommand.command_id}`);
  console.log(`poweroff_command=${poweroffCommand.id || poweroffCommand.command_id}`);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
