/**
 * ChatSync.ts
 *
 * チャットアプリの通信・状態管理モジュール
 *
 * 提供機能:
 *  1. 楽観的UI更新      — 送信ボタン押下の瞬間にローカル描画し、ACKで確定/失敗で差し替え
 *  2. 送信リトライキュー — ネットワーク切断時にキューイングし、指数バックオフで再送
 *  3. 順序保証と冪等性   — 復旧時にシーケンス番号順で再送、Idempotency-Keyで重複排除
 */

// ---------------------------------------------------------------------------
// 型定義
// ---------------------------------------------------------------------------

export type MessageStatus =
  | 'pending'   // 楽観描画中（未ACK）
  | 'queued'    // オフライン等でリトライキュー待ち
  | 'sent'      // サーバーACK済み
  | 'failed';   // リトライ上限超過

export interface ChatMessage {
  /** Idempotency-Key 兼 クライアント側一意ID（UUID v4 推奨） */
  readonly clientId: string;
  /** クライアントが発行する単調増加シーケンス番号（順序保証用） */
  readonly seq: number;
  readonly roomId: string;
  readonly authorId: string;
  readonly body: string;
  readonly createdAt: number; // epoch ms
  status: MessageStatus;
  /** ACK時にサーバーが採番した正式ID */
  serverId?: string;
  retryCount: number;
}

export interface ServerAck {
  clientId: string;
  serverId: string;
  seq: number;
  acceptedAt: number;
  /** true = サーバー側で既知のメッセージだった（重複送信を冪等に吸収） */
  duplicate: boolean;
}

/** 状態変化をUIへ通知する購読コールバック */
export type MessageListener = (messages: readonly ChatMessage[]) => void;

// ---------------------------------------------------------------------------
// Transport インターフェース（WebSocket / HTTP などを差し替え可能にする）
// ---------------------------------------------------------------------------

export interface Transport {
  /**
   * メッセージを1件送信し、ACKを返す。
   * ネットワークエラー時は例外をthrowすること。
   * 実装側は clientId を Idempotency-Key ヘッダ等に載せる想定。
   */
  send(message: ChatMessage): Promise<ServerAck>;
}

// ---------------------------------------------------------------------------
// RetryQueue — 指数バックオフ付き送信リトライキュー
// ---------------------------------------------------------------------------

export interface RetryPolicy {
  /** 初回リトライまでの待機 ms（デフォルト 1_000） */
  baseDelayMs: number;
  /** バックオフ係数（デフォルト 2） */
  factor: number;
  /** 待機時間の上限 ms（デフォルト 30_000） */
  maxDelayMs: number;
  /** 最大リトライ回数。超過で failed 化（デフォルト 8） */
  maxRetries: number;
  /** ジッター（0〜1）。待機時間にランダム幅を持たせ同時再送を防ぐ */
  jitterRatio: number;
}

export const DEFAULT_RETRY_POLICY: RetryPolicy = {
  baseDelayMs: 1_000,
  factor: 2,
  maxDelayMs: 30_000,
  maxRetries: 8,
  jitterRatio: 0.3,
};

/**
 * delay(n) = min(maxDelay, baseDelay * factor^n) ± jitter
 *   n=0: 1s → n=1: 2s → 4s → 8s → 16s → 30s(cap) → ...
 */
export function backoffDelay(retryCount: number, policy: RetryPolicy): number {
  const exp = Math.min(
    policy.maxDelayMs,
    policy.baseDelayMs * Math.pow(policy.factor, retryCount),
  );
  const jitter = exp * policy.jitterRatio * (Math.random() * 2 - 1);
  return Math.max(0, Math.round(exp + jitter));
}

interface QueueEntry {
  message: ChatMessage;
  /** この時刻以降に送信してよい（epoch ms） */
  notBefore: number;
}

class RetryQueue {
  private entries: QueueEntry[] = [];

  enqueue(message: ChatMessage, policy: RetryPolicy): void {
    // 同一 clientId の二重登録を防ぐ（冪等）
    if (this.entries.some((e) => e.message.clientId === message.clientId)) return;
    this.entries.push({
      message,
      notBefore: Date.now() + backoffDelay(message.retryCount, policy),
    });
  }

  /** 送信可能時刻に達したものを seq 昇順で取り出す（順序保証） */
  drainReady(now = Date.now()): ChatMessage[] {
    const ready = this.entries
      .filter((e) => e.notBefore <= now)
      .sort((a, b) => a.message.seq - b.message.seq);
    this.entries = this.entries.filter((e) => e.notBefore > now);
    return ready.map((e) => e.message);
  }

  remove(clientId: string): void {
    this.entries = this.entries.filter((e) => e.message.clientId !== clientId);
  }

  get size(): number {
    return this.entries.length;
  }
}

// ---------------------------------------------------------------------------
// MessageStore — 画面描画用の状態管理
// ---------------------------------------------------------------------------

class MessageStore {
  private byClientId = new Map<string, ChatMessage>();
  private listeners = new Set<MessageListener>();

  subscribe(listener: MessageListener): () => void {
    this.listeners.add(listener);
    listener(this.snapshot());
    return () => this.listeners.delete(listener);
  }

  /** 楽観的に即時挿入し、UIへ通知 */
  upsert(message: ChatMessage): void {
    this.byClientId.set(message.clientId, message);
    this.emit();
  }

  patch(clientId: string, partial: Partial<ChatMessage>): void {
    const current = this.byClientId.get(clientId);
    if (!current) return;
    this.byClientId.set(clientId, { ...current, ...partial });
    this.emit();
  }

  /** 表示は seq 昇順（＝送信順）で返す */
  snapshot(): readonly ChatMessage[] {
    return [...this.byClientId.values()].sort((a, b) => a.seq - b.seq);
  }

  private emit(): void {
    const snap = this.snapshot();
    for (const l of this.listeners) l(snap);
  }
}

// ---------------------------------------------------------------------------
// ChatSync — 司令塔。UIはこのクラスだけを触る
// ---------------------------------------------------------------------------

export interface ChatSyncOptions {
  transport: Transport;
  roomId: string;
  authorId: string;
  retryPolicy?: Partial<RetryPolicy>;
  /** キュー処理のポーリング間隔 ms（デフォルト 250） */
  pumpIntervalMs?: number;
}

export class ChatSync {
  private readonly transport: Transport;
  private readonly roomId: string;
  private readonly authorId: string;
  private readonly policy: RetryPolicy;
  private readonly pumpIntervalMs: number;

  private readonly store = new MessageStore();
  private readonly queue = new RetryQueue();
  private pumpTimer: ReturnType<typeof setInterval> | null = null;

  /** 送信パイプライン。直列化して順序保証する */
  private pipeline: Promise<void> = Promise.resolve();
  private seqCounter = 0;

  /** サーバーから受信済みの clientId（受信側の重複排除） */
  private readonly seenClientIds = new Set<string>();

  private online = true;

  constructor(options: ChatSyncOptions) {
    this.transport = options.transport;
    this.roomId = options.roomId;
    this.authorId = options.authorId;
    this.policy = { ...DEFAULT_RETRY_POLICY, ...options.retryPolicy };
    this.pumpIntervalMs = options.pumpIntervalMs ?? 250;
  }

  // -- UI 向け API ---------------------------------------------------------

  /** UIの描画購読。返り値の関数で解除 */
  onMessages(listener: MessageListener): () => void {
    return this.store.subscribe(listener);
  }

  start(): void {
    this.pumpTimer ??= setInterval(() => this.pump(), this.pumpIntervalMs);
  }

  stop(): void {
    if (this.pumpTimer) clearInterval(this.pumpTimer);
    this.pumpTimer = null;
  }

  /** オンライン/オフライン遷移を通知（navigator.onLine 等と接続） */
  setOnline(online: boolean): void {
    const recovered = online && !this.online;
    this.online = online;
    if (recovered) this.flushQueue(); // 復旧時に溜まった分を順序通り再送
  }

  // -- 送信（楽観的UI更新） -------------------------------------------------

  /**
   * 送信ボタン押下時に呼ぶ。
   * ネットワークを待たずに即座にストアへ pending として描画する。
   */
  sendMessage(body: string): ChatMessage {
    const message: ChatMessage = {
      clientId: crypto.randomUUID(),
      seq: this.seqCounter++,
      roomId: this.roomId,
      authorId: this.authorId,
      body,
      createdAt: Date.now(),
      status: 'pending',
      retryCount: 0,
    };

    // 1) 楽観描画（同期。ACKを待たない）
    this.store.upsert(message);

    // 2) 実送信はパイプラインへ直列投入
    this.pipeline = this.pipeline.then(() => this.attemptSend(message));
    return message;
  }

  // -- 受信（重複排除） ------------------------------------------------------

  /**
   * サーバーpush（WebSocket受信等）を反映する。
   * 同一 clientId / serverId を二度適用しない（冪等）。
   */
  receiveFromServer(message: ChatMessage): boolean {
    if (this.seenClientIds.has(message.clientId)) return false; // 重複を捨てる
    this.seenClientIds.add(message.clientId);
    this.store.upsert({ ...message, status: 'sent' });
    return true;
  }

  // -- 内部: 送信・リトライ ---------------------------------------------------

  private async attemptSend(message: ChatMessage): Promise<void> {
    if (!this.online) {
      this.enqueue(message);
      return;
    }
    try {
      const ack = await this.transport.send(message);
      this.queue.remove(message.clientId);
      // ACKで確定。duplicate=true はサーバーが冪等吸収した再送分
      this.store.patch(message.clientId, {
        status: 'sent',
        serverId: ack.serverId,
      });
    } catch {
      this.enqueue(message);
    }
  }

  private enqueue(message: ChatMessage): void {
    if (message.retryCount >= this.policy.maxRetries) {
      this.queue.remove(message.clientId);
      this.store.patch(message.clientId, { status: 'failed' });
      return;
    }
    this.queue.enqueue(message, this.policy);
    this.store.patch(message.clientId, { status: 'queued' });
  }

  /** バックオフ時刻に達したキューを取り出して再送 */
  private pump(): void {
    if (!this.online) return;
    for (const message of this.queue.drainReady()) {
      message.retryCount += 1;
      this.pipeline = this.pipeline.then(() => this.attemptSend(message));
    }
  }

  /** 復旧時: バックオフを待たず即時ドレインし、seq順に再送 */
  private flushQueue(): void {
    for (const message of this.queue.drainReady(Date.now() + Number.MAX_SAFE_INTEGER)) {
      this.pipeline = this.pipeline.then(() => this.attemptSend(message));
    }
  }
}
