import { env } from "../../config/env";
import mqtt, { type IClientOptions, type MqttClient } from "mqtt";

type PaymentSuccessPayload = {
  message_type: "payment_success";
  device_id: string;
  event_id: string;
  transaction_id: string;
  merchant_id: string;
  merchant_name: string;
  amount: number;
  currency: string;
  paid_at?: string;
  partner_reference?: string;
  audio_text: string;
  display_text: string;
};

type PaymentSuccessProtocol = "platform_v1" | "qf100";

type Qf100PaymentSuccessPayload = {
  header: {
    category: 1;
  };
  data: {
    "pay-amount": number;
  };
};

type Qf100DynamicQrDisplayPayload = {
  header: {
    category: 4;
  };
  data: {
    "qr-url": string;
    amount: number;
    "expire-seconds": number;
  };
};

type Qf100DeviceCommand = "reboot" | "poweroff";

type Qf100DeviceCommandPayload = {
  header: {
    category: 5;
  };
  data: {
    command: Qf100DeviceCommand;
  };
};

type DynamicQrResponsePayload = {
  message_type: "dynamic_qr_response";
  correlation_id: string;
  transaction_id: string;
  status: "success";
  qr_payload: string;
  expires_at: string;
};

type ConfigPushPayload = {
  message_type: "config_push";
  config_version: number;
  settings: Record<string, unknown>;
};

export type MqttPublishResult<TPayload = PaymentSuccessPayload> = {
  ok: boolean;
  topic: string;
  qos: 1;
  retained: false;
  publishedAt: string;
  reason?: string;
  payload: TPayload;
};

const forcedFailAll = String(env.MQTT_PUBLISH_FORCE_FAIL_ALL).toLowerCase() === "true";
const forcedFailDevices = new Set(
  String(env.MQTT_PUBLISH_FORCE_FAIL_DEVICE_IDS)
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean)
);
const publishMode = String(env.MQTT_PUBLISH_MODE || "simulator").toLowerCase();
let brokerClientPromise: Promise<MqttClient> | null = null;
let brokerClientRef: MqttClient | null = null;
let lastConnectedAt: string | null = null;
let lastDisconnectedAt: string | null = null;
let lastError: { message: string; at: string } | null = null;
let publishAttemptCount = 0;
let publishSuccessCount = 0;
let publishFailureCount = 0;

function shouldForceFail(deviceId: string): boolean {
  return forcedFailAll || forcedFailDevices.has(deviceId);
}

function getBrokerClient(): Promise<MqttClient> {
  if (brokerClientPromise) {
    return brokerClientPromise;
  }

  brokerClientPromise = new Promise((resolve, reject) => {
    if (!env.MQTT_BROKER_URL) {
      reject(new Error("MQTT_BROKER_URL_MISSING"));
      return;
    }

    const options: IClientOptions = {
      clientId: env.MQTT_CLIENT_ID,
      username: env.MQTT_USERNAME || undefined,
      password: env.MQTT_PASSWORD || undefined,
      connectTimeout: env.MQTT_CONNECT_TIMEOUT_MS,
      reconnectPeriod: 5000,
      clean: true
    };
    const client = mqtt.connect(env.MQTT_BROKER_URL, options);
    const timeout = setTimeout(() => {
      client.end(true);
      brokerClientPromise = null;
      reject(new Error("MQTT_BROKER_CONNECT_TIMEOUT"));
    }, env.MQTT_CONNECT_TIMEOUT_MS + 1000);

    const failBeforeConnect = (error: Error) => {
      clearTimeout(timeout);
      brokerClientPromise = null;
      lastError = { message: error.message, at: new Date().toISOString() };
      client.end(true);
      reject(error);
    };

    client.once("connect", () => {
      clearTimeout(timeout);
      client.removeListener("error", failBeforeConnect);
      brokerClientRef = client;
      lastConnectedAt = new Date().toISOString();
      resolve(client);
    });

    client.once("error", failBeforeConnect);
    client.on("error", (error) => {
      lastError = { message: error.message, at: new Date().toISOString() };
    });
    client.on("close", () => {
      if (!client.connected) {
        brokerClientPromise = null;
        brokerClientRef = null;
        lastDisconnectedAt = new Date().toISOString();
      }
    });
  });

  return brokerClientPromise;
}

async function publishToBroker<TPayload>(
  topic: string,
  payload: TPayload
): Promise<void> {
  const client = await getBrokerClient();
  await new Promise<void>((resolve, reject) => {
    client.publish(
      topic,
      JSON.stringify(payload),
      {
        qos: 1,
        retain: false
      },
      (error?: Error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      }
    );
  });
}

export function buildPaymentSuccessPayload(
  input: {
    transaction_id: string;
    merchant_id: string;
    merchant_name: string;
    device_id: string;
    amount: number;
    currency: string;
    paid_at?: string;
    partner_reference?: string;
    event_id: string;
  }
): PaymentSuccessPayload {
  const displayAmount = `${input.amount.toLocaleString("id-ID")}`;

  return {
    message_type: "payment_success",
    device_id: input.device_id,
    event_id: input.event_id,
    transaction_id: input.transaction_id,
    merchant_id: input.merchant_id,
    merchant_name: input.merchant_name,
    amount: input.amount,
    currency: input.currency,
    paid_at: input.paid_at,
    partner_reference: input.partner_reference,
    audio_text: `Pembayaran diterima ${input.currency} ${displayAmount}`,
    display_text: `Pembayaran diterima Rp${displayAmount}`
  };
}

export function makePaymentSuccessTopic(deviceId: string) {
  return `devices/${deviceId}/downlink/payment/success`;
}

export function makeQf100DownlinkTopic(serialNumber: string) {
  return `soundbox/${serialNumber}/down`;
}

export function makeDynamicQrResponseTopic(deviceId: string) {
  return `devices/${deviceId}/downlink/dynamic-qr/response`;
}

export function makeConfigPushTopic(deviceId: string) {
  return `devices/${deviceId}/downlink/config/push`;
}

async function publishMqttPayload<TPayload>(
  deviceId: string,
  topic: string,
  payload: TPayload
): Promise<MqttPublishResult<TPayload>> {
  const publishedAt = new Date().toISOString();
  publishAttemptCount += 1;

  if (shouldForceFail(deviceId)) {
    publishFailureCount += 1;
    return {
      ok: false,
      topic,
      qos: 1,
      retained: false,
      publishedAt,
      reason: "MQTT_PUBLISH_SIMULATED_FAILURE",
      payload
    };
  }

  if (publishMode === "broker") {
    try {
      await publishToBroker(topic, payload);
    } catch (error) {
      const message = error instanceof Error ? error.message : "unknown";
      publishFailureCount += 1;
      return {
        ok: false,
        topic,
        qos: 1,
        retained: false,
        publishedAt,
        reason: `MQTT_BROKER_PUBLISH_FAILED:${message}`,
        payload
      };
    }
  }

  publishSuccessCount += 1;
  return {
    ok: true,
    topic,
    qos: 1,
    retained: false,
    publishedAt,
    payload
  };
}

export async function publishPaymentSuccess(payload: PaymentSuccessPayload): Promise<MqttPublishResult> {
  return publishMqttPayload(payload.device_id, makePaymentSuccessTopic(payload.device_id), payload);
}

export async function publishPaymentSuccessForProtocol(
  payload: PaymentSuccessPayload,
  protocol: PaymentSuccessProtocol,
  options: {
    serialNumber?: string;
  } = {}
): Promise<MqttPublishResult<PaymentSuccessPayload | Qf100PaymentSuccessPayload>> {
  if (protocol !== "qf100") {
    return publishMqttPayload(payload.device_id, makePaymentSuccessTopic(payload.device_id), payload);
  }

  const qf100Payload: Qf100PaymentSuccessPayload = {
    header: {
      category: 1
    },
    data: {
      "pay-amount": payload.amount
    }
  };

  return publishMqttPayload(
    payload.device_id,
    makeQf100DownlinkTopic(options.serialNumber || payload.device_id),
    qf100Payload
  );
}

export async function publishDynamicQrResponse(deviceId: string, payload: DynamicQrResponsePayload) {
  return publishMqttPayload(deviceId, makeDynamicQrResponseTopic(deviceId), payload);
}

export async function publishQf100DynamicQrDisplay(
  deviceId: string,
  serialNumber: string,
  payload: {
    qrUrl: string;
    amount: number;
    expireSeconds?: number;
  }
): Promise<MqttPublishResult<Qf100DynamicQrDisplayPayload>> {
  const qrPayload: Qf100DynamicQrDisplayPayload = {
    header: {
      category: 4
    },
    data: {
      "qr-url": payload.qrUrl,
      amount: payload.amount,
      "expire-seconds": payload.expireSeconds && payload.expireSeconds > 0 ? payload.expireSeconds : 60
    }
  };

  return publishMqttPayload(deviceId, makeQf100DownlinkTopic(serialNumber), qrPayload);
}

export async function publishQf100DeviceCommand(
  deviceId: string,
  serialNumber: string,
  command: Qf100DeviceCommand
): Promise<MqttPublishResult<Qf100DeviceCommandPayload>> {
  const commandPayload: Qf100DeviceCommandPayload = {
    header: {
      category: 5
    },
    data: {
      command
    }
  };

  return publishMqttPayload(deviceId, makeQf100DownlinkTopic(serialNumber), commandPayload);
}

export async function publishQf100RebootCommand(
  deviceId: string,
  serialNumber: string
): Promise<MqttPublishResult<Qf100DeviceCommandPayload>> {
  return publishQf100DeviceCommand(deviceId, serialNumber, "reboot");
}

export async function publishConfigPush(deviceId: string, payload: ConfigPushPayload) {
  return publishMqttPayload(deviceId, makeConfigPushTopic(deviceId), payload);
}

export function getMqttPublisherStatus() {
  const mode = publishMode === "broker" ? "broker" : "simulator";
  const brokerUrl = env.MQTT_BROKER_URL
    ? env.MQTT_BROKER_URL.replace(/(mqtts?:\/\/)([^:@/]+):([^@/]+)@/i, "$1***:***@")
    : "";

  return {
    mode,
    broker_url: mode === "broker" ? brokerUrl : null,
    client_id: mode === "broker" ? env.MQTT_CLIENT_ID : null,
    username: mode === "broker" ? env.MQTT_USERNAME || null : null,
    connected: mode === "broker" ? Boolean(brokerClientRef?.connected) : true,
    last_connected_at: lastConnectedAt,
    last_disconnected_at: lastDisconnectedAt,
    last_error: lastError,
    publish_attempt_count: publishAttemptCount,
    publish_success_count: publishSuccessCount,
    publish_failure_count: publishFailureCount,
    forced_fail_all: forcedFailAll,
    forced_fail_device_count: forcedFailDevices.size
  };
}
