import { Router, Request, Response, NextFunction } from "express";
import { ApiError } from "../shared/errors";
import { getDeviceScopeError, requireDeviceToken } from "../shared/middleware/auth";
import { successResponse } from "../shared/middleware/errorMiddleware";
import { getDeviceById, patchDevice } from "../shared/store/deviceStore";
import { createDeviceHeartbeat } from "../shared/store/heartbeatStore";
import { acknowledgeDeviceCommand } from "../shared/store/deviceCommandStore";
import { getActiveBindingByDevice } from "../shared/store/bindingStore";
import { getTerminalById } from "../shared/store/locationStore";
import { readIdempotency, writeIdempotency } from "../shared/idempotency/idempotencyStore";
import { env } from "../config/env";
import { supportsDynamicQrFlow } from "../shared/services/deviceCapabilityResolver";
import { createDynamicQrTransaction } from "../shared/services/dynamicQrOrchestrator";
import { createMqttMessage } from "../shared/store/mqttMessageStore";
import { publishDynamicQrResponse } from "../shared/services/mqttPublisher";
import {
  createDeviceConfigAck,
  getOrCreateDeviceConfig,
  toDeviceConfigAckPayload,
  toDeviceConfigPayload
} from "../shared/store/deviceConfigStore";

const router = Router();

type HeartbeatInput = {
  device_id?: string;
  timestamp?: string;
  firmware_version?: string;
  network_strength?: unknown;
  battery_level?: unknown;
  state?: string;
};

type CommandAckInput = {
  device_id?: string;
  command_id?: string;
  status?: "delivered" | "failed" | "timeout";
  reason?: string;
  result_payload?: Record<string, unknown>;
};

type DynamicQrInput = {
  device_id?: string;
  terminal_id?: string;
  amount?: unknown;
  currency?: string;
  request_id?: string;
  expires_in_seconds?: unknown;
};

type MqttDynamicQrInput = DynamicQrInput & {
  message_type?: string;
  created_at?: string;
};

type ConfigAckInput = {
  device_id?: string;
  config_version?: unknown;
  status?: "applied" | "failed";
  reason?: string;
  result_payload?: Record<string, unknown>;
};

function normalizeNumberOrNull(value: unknown): number | null {
  if (typeof value === "string") {
    const parsed = Number(value);
    if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
      return parsed;
    }
    return null;
  }

  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }
  return null;
}

function normalizePositiveAmount(value: unknown): number | null {
  const normalized = normalizeNumberOrNull(value);
  if (normalized === null || normalized <= 0) {
    return null;
  }
  return normalized;
}

function normalizeTtl(value: unknown): number | undefined {
  if (value === undefined || value === null || value === "") {
    return undefined;
  }

  const normalized = normalizeNumberOrNull(value);
  if (normalized === null || normalized <= 0) {
    return undefined;
  }

  return normalized;
}

function normalizePositiveInteger(value: unknown): number | null {
  const normalized = normalizeNumberOrNull(value);
  if (normalized === null || normalized <= 0 || !Number.isInteger(normalized)) {
    return null;
  }
  return normalized;
}

function normalizeSignalStrength(value: unknown): number | null {
  const normalized = normalizeNumberOrNull(value);
  if (normalized === null) {
    return null;
  }

  if (normalized < 0 || normalized > 100) {
    throw new Error("NETWORK_STRENGTH_OUT_OF_RANGE");
  }

  return normalized;
}

function normalizeBatteryLevel(value: unknown): number | null {
  const normalized = normalizeNumberOrNull(value);
  if (normalized === null) {
    return null;
  }

  if (normalized < 0 || normalized > 100) {
    throw new Error("BATTERY_LEVEL_OUT_OF_RANGE");
  }

  return normalized;
}

router.post("/heartbeat", requireDeviceToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as HeartbeatInput;
  if (!payload || !payload.device_id) {
    return next(new ApiError("BAD_REQUEST", "device_id is required", 400));
  }
  const scopeError = getDeviceScopeError(req, payload.device_id);
  if (scopeError) {
    return next(scopeError);
  }

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

  const eventTs = payload.timestamp ? new Date(payload.timestamp) : new Date();
  if (Number.isNaN(eventTs.getTime())) {
    return next(new ApiError("BAD_REQUEST", "timestamp must be valid ISO datetime", 400));
  }

  let payloadNetworkStrength: number | null;
  let payloadBattery: number | null;
  try {
    payloadNetworkStrength = normalizeSignalStrength(payload.network_strength);
    payloadBattery = normalizeBatteryLevel(payload.battery_level);
  } catch (error) {
    if (error instanceof Error && error.message === "NETWORK_STRENGTH_OUT_OF_RANGE") {
      return next(new ApiError("BAD_REQUEST", "network_strength must be between 0 and 100", 400));
    }
    if (error instanceof Error && error.message === "BATTERY_LEVEL_OUT_OF_RANGE") {
      return next(new ApiError("BAD_REQUEST", "battery_level must be between 0 and 100", 400));
    }
    return next(error as Error);
  }

  const heartbeat = await createDeviceHeartbeat({
    device_id: payload.device_id,
    timestamp: eventTs.toISOString(),
    firmware_version: payload.firmware_version,
    network_strength: payloadNetworkStrength,
    battery_level: payloadBattery,
    state: payload.state,
    payload_json: {
      network_strength_raw: payload.network_strength,
      battery_level_raw: payload.battery_level,
      state: payload.state,
      firmware_version: payload.firmware_version,
      timestamp: payload.timestamp,
      request_id: (req as unknown as { requestId: string }).requestId
    }
  });

  await patchDevice(payload.device_id, {
    last_seen_at: heartbeat.timestamp,
    firmware_version: payload.firmware_version || device.firmware_version
  });

  res.json(
    successResponse(req, {
      heartbeat_id: heartbeat.id,
      device_id: heartbeat.device_id,
      request_id: req.requestId,
      server_time: heartbeat.received_at
    })
  );
});

router.post("/commands/ack", requireDeviceToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as CommandAckInput;
  if (!payload || !payload.command_id || !payload.device_id || !payload.status) {
    return next(new ApiError("BAD_REQUEST", "command_id, device_id, status are required", 400));
  }
  const scopeError = getDeviceScopeError(req, payload.device_id);
  if (scopeError) {
    return next(scopeError);
  }

  if (!["delivered", "failed", "timeout"].includes(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be delivered, failed, or timeout", 400));
  }

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

  const updated = await acknowledgeDeviceCommand({
    device_id: device.id,
    command_id: payload.command_id,
    status: payload.status,
    reason: payload.reason,
    result_payload: payload.result_payload
  });
  if (!updated) {
    return next(new ApiError("NOT_FOUND", "command not found", 404));
  }

  res.json(
    successResponse(req, {
      command_id: updated.id,
      device_id: updated.device_id,
      status: updated.status,
      acknowledged_at: updated.acknowledged_at
    })
  );
});

router.post("/transactions/dynamic-qr", requireDeviceToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as DynamicQrInput;
  if (!payload || !payload.device_id || !payload.terminal_id || !payload.request_id) {
    return next(new ApiError("BAD_REQUEST", "device_id, terminal_id, request_id are required", 400));
  }
  const scopeError = getDeviceScopeError(req, payload.device_id);
  if (scopeError) {
    return next(scopeError);
  }

  const amount = normalizePositiveAmount(payload.amount);
  if (amount === null) {
    return next(new ApiError("INVALID_AMOUNT", "amount must be a positive number", 400));
  }

  const currency = payload.currency && payload.currency.trim() ? payload.currency.trim().toUpperCase() : "IDR";
  if (currency !== "IDR") {
    return next(new ApiError("BAD_REQUEST", "currency must be IDR for QRIS dynamic MVP", 400));
  }

  const idempotencyKey = req.header("Idempotency-Key") || payload.request_id;
  const cached = readIdempotency("device.dynamic_qr.create", idempotencyKey);
  if (cached) {
    return res.json(successResponse(req, (cached as { data: unknown }).data ?? cached));
  }

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

  if (device.status !== "active") {
    return next(new ApiError("DEVICE_CAPABILITY_NOT_SUPPORTED", "inactive device cannot create dynamic QR", 400));
  }

  if (!supportsDynamicQrFlow(device, "api_direct")) {
    return next(new ApiError("DEVICE_CAPABILITY_NOT_SUPPORTED", "device does not support API-direct dynamic QR", 400));
  }

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

  if (terminal.qr_mode !== "dynamic_api") {
    return next(new ApiError("DEVICE_CAPABILITY_NOT_SUPPORTED", "terminal is not configured for API dynamic QR", 400));
  }

  const binding = await getActiveBindingByDevice(device.id);
  if (!binding || binding.terminal_id !== terminal.id) {
    return next(new ApiError("DEVICE_NOT_BOUND", "device is not actively bound to requested terminal", 400));
  }

  const created = await createDynamicQrTransaction({
    request_id: payload.request_id,
    device_id: device.id,
    merchant_id: binding.merchant_id,
    outlet_id: binding.outlet_id,
    terminal_id: binding.terminal_id,
    amount,
    currency,
    expires_in_seconds: normalizeTtl(payload.expires_in_seconds)
  });

  const responseData = {
    ...created,
    request_id: payload.request_id
  };

  writeIdempotency(
    "device.dynamic_qr.create",
    idempotencyKey,
    { data: responseData },
    env.IDEMPOTENCY_TTL_MS
  );

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

router.post("/mqtt/uplink/dynamic-qr/request", requireDeviceToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as MqttDynamicQrInput;
  if (!payload || !payload.device_id || !payload.terminal_id || !payload.request_id) {
    return next(new ApiError("BAD_REQUEST", "device_id, terminal_id, request_id are required", 400));
  }
  const scopeError = getDeviceScopeError(req, payload.device_id);
  if (scopeError) {
    return next(scopeError);
  }

  if (payload.message_type && payload.message_type !== "dynamic_qr_request") {
    return next(new ApiError("BAD_REQUEST", "message_type must be dynamic_qr_request", 400));
  }

  const amount = normalizePositiveAmount(payload.amount);
  if (amount === null) {
    return next(new ApiError("INVALID_AMOUNT", "amount must be a positive number", 400));
  }

  const currency = payload.currency && payload.currency.trim() ? payload.currency.trim().toUpperCase() : "IDR";
  if (currency !== "IDR") {
    return next(new ApiError("BAD_REQUEST", "currency must be IDR for QRIS dynamic MVP", 400));
  }

  const cached = readIdempotency("device.dynamic_qr.mqtt", payload.request_id);
  if (cached) {
    return res.json(successResponse(req, (cached as { data: unknown }).data ?? cached));
  }

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

  if (device.status !== "active" || !supportsDynamicQrFlow(device, "mqtt")) {
    return next(new ApiError("DEVICE_CAPABILITY_NOT_SUPPORTED", "device does not support MQTT dynamic QR", 400));
  }

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

  if (terminal.qr_mode !== "dynamic_mqtt") {
    return next(new ApiError("DEVICE_CAPABILITY_NOT_SUPPORTED", "terminal is not configured for MQTT dynamic QR", 400));
  }

  const binding = await getActiveBindingByDevice(device.id);
  if (!binding || binding.terminal_id !== terminal.id) {
    return next(new ApiError("DEVICE_NOT_BOUND", "device is not actively bound to requested terminal", 400));
  }

  await createMqttMessage({
    direction: "uplink",
    device_id: device.id,
    topic: `devices/${device.id}/uplink/dynamic-qr/request`,
    message_type: "dynamic_qr_request",
    correlation_id: payload.request_id,
    payload_json: {
      ...payload,
      amount,
      currency
    }
  });

  const created = await createDynamicQrTransaction({
    request_id: payload.request_id,
    device_id: device.id,
    merchant_id: binding.merchant_id,
    outlet_id: binding.outlet_id,
    terminal_id: binding.terminal_id,
    amount,
    currency,
    expires_in_seconds: normalizeTtl(payload.expires_in_seconds),
    initiation_mode: "dynamic_mqtt"
  });

  const mqttPayload = {
    message_type: "dynamic_qr_response" as const,
    correlation_id: payload.request_id,
    transaction_id: created.transaction_id,
    status: "success" as const,
    qr_payload: created.qr_payload,
    expires_at: created.expires_at
  };
  const publishResult = await publishDynamicQrResponse(device.id, mqttPayload);
  const outbox = await createMqttMessage({
    direction: "downlink",
    device_id: device.id,
    topic: publishResult.topic,
    message_type: "dynamic_qr_response",
    correlation_id: payload.request_id,
    payload_json: mqttPayload,
    publish_status: publishResult.ok ? "sent" : "failed",
    reason: publishResult.reason
  });

  const responseData = {
    correlation_id: payload.request_id,
    transaction_id: created.transaction_id,
    status: "success",
    qr_payload: created.qr_payload,
    expires_at: created.expires_at,
    downlink_message_id: outbox.id,
    publish_status: outbox.publish_status,
    partner_reference: created.partner_reference
  };

  writeIdempotency("device.dynamic_qr.mqtt", payload.request_id, { data: responseData }, env.IDEMPOTENCY_TTL_MS);
  res.status(201).json(successResponse(req, responseData));
});

router.get("/config", requireDeviceToken, async (req: Request, res: Response, next: NextFunction) => {
  const deviceId = (req.query.device_id as string | undefined) || (req.body as { device_id?: string } | undefined)?.device_id;
  if (!deviceId) {
    return next(new ApiError("BAD_REQUEST", "device_id is required", 400));
  }
  const scopeError = getDeviceScopeError(req, deviceId);
  if (scopeError) {
    return next(scopeError);
  }

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

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

router.post("/config/ack", requireDeviceToken, async (req: Request, res: Response, next: NextFunction) => {
  const payload = req.body as ConfigAckInput;
  if (!payload || !payload.device_id || !payload.status) {
    return next(new ApiError("BAD_REQUEST", "device_id, status are required", 400));
  }
  const scopeError = getDeviceScopeError(req, payload.device_id);
  if (scopeError) {
    return next(scopeError);
  }

  if (!["applied", "failed"].includes(payload.status)) {
    return next(new ApiError("BAD_REQUEST", "status must be applied or failed", 400));
  }

  const configVersion = normalizePositiveInteger(payload.config_version);
  if (configVersion === null) {
    return next(new ApiError("BAD_REQUEST", "config_version must be a positive integer", 400));
  }

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

  const ack = await createDeviceConfigAck({
    device_id: device.id,
    config_version: configVersion,
    status: payload.status,
    reason: payload.reason,
    payload_json: payload.result_payload || {}
  });
  await createMqttMessage({
    direction: "uplink",
    device_id: device.id,
    topic: `devices/${device.id}/uplink/config/ack`,
    message_type: "config_ack",
    correlation_id: `config:${configVersion}`,
    payload_json: {
      message_type: "config_ack",
      device_id: device.id,
      config_version: configVersion,
      status: payload.status,
      reason: payload.reason,
      result_payload: payload.result_payload || {}
    }
  });

  res.json(successResponse(req, toDeviceConfigAckPayload(ack)));
});

export default router;
