import { logger } from "../observability/logger.js";
import { getProducer } from "../kafka/kafka.client.js";
import { resolveTopic } from "../kafka/topic-map.js";
import {
  findPending,
  incrementRetry,
  markFailed,
  markPublished,
} from "./outbox.repository.js";
import type { OutboxEvent } from "./outbox.schema.js";

/**
 * OutboxPublisher —— 轮询 pending 记录并投递到 Kafka（多 topic 路由）。
 *
 * 参照 core-edu OutboxPublisher 模式，支持 TOPIC_MAP 多 topic 路由：
 * - notification.sent → edu.notification.sent
 * - notification.read → edu.notification.read
 * - notification.recalled → edu.notification.recalled
 * - notification.failed → edu.notification.failed
 *
 * 仲裁依据：at-least-once 投递 + 指数退避重试 + 幂等（消费端 event_id 去重）。
 */

const POLL_INTERVAL_MS = 5000;
const BATCH_SIZE = 100;
const MAX_RETRY = 5;
const RETRY_BACKOFF_MS = 2000;

class OutboxPublisher {
  private intervalId: ReturnType<typeof setInterval> | null = null;
  private isPolling = false;

  async start(): Promise<void> {
    if (this.intervalId) return;
    logger.info(
      { pollIntervalMs: POLL_INTERVAL_MS, batchSize: BATCH_SIZE },
      "OutboxPublisher started",
    );
    this.intervalId = setInterval(() => {
      void this.poll();
    }, POLL_INTERVAL_MS);
  }

  async stop(): Promise<void> {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
    logger.info("OutboxPublisher stopped");
  }

  private async poll(): Promise<void> {
    if (this.isPolling) return;
    this.isPolling = true;
    try {
      const messages = await findPending(BATCH_SIZE);
      for (const message of messages) {
        await this.dispatch(message);
      }
    } catch (error) {
      logger.error({ error }, "Outbox poll failed");
    } finally {
      this.isPolling = false;
    }
  }

  private async dispatch(message: OutboxEvent): Promise<void> {
    const topic = resolveTopic(message.eventType);
    try {
      const producer = getProducer();
      await producer.send({
        topic,
        messages: [
          {
            key: message.aggregateId,
            value:
              typeof message.payload === "string"
                ? message.payload
                : JSON.stringify(message.payload),
            headers: this.buildHeaders(message),
          },
        ],
      });
      await markPublished(message.eventId);
      logger.info(
        { eventId: message.eventId, eventType: message.eventType, topic },
        "Outbox message published",
      );
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      logger.error(
        {
          eventId: message.eventId,
          eventType: message.eventType,
          error: errorMessage,
        },
        "Outbox publish failed",
      );
      if (message.retryCount + 1 >= MAX_RETRY) {
        await markFailed(message.eventId, errorMessage);
      } else {
        const backoffMs = RETRY_BACKOFF_MS * 2 ** message.retryCount;
        await incrementRetry(message.eventId, errorMessage, backoffMs);
      }
    }
  }

  private buildHeaders(message: OutboxEvent): Record<string, string> {
    const headers: Record<string, string> = {
      eventId: message.eventId,
      eventType: message.eventType,
      aggregateType: message.aggregateType,
      aggregateId: message.aggregateId,
    };
    if (message.metadata) {
      for (const [key, value] of Object.entries(message.metadata)) {
        headers[key] = value;
      }
    }
    return headers;
  }
}

export const outboxPublisher = new OutboxPublisher();
