import type { Message, MessageReactionSummary } from "@slackwsh/contracts";
import { parseMentions } from "@slackwsh/core";
import { clampReadSeq, type PersistedMention } from "../invariants";
import { Clock, CommitCrash, EmitCrash, HttpFault, Transport, type FaultConfig, type Rng } from "./sim";

export const SOCKET_TTL_MS = 60_000;
export const SWEEP_INTERVAL_MS = 30_000;

export interface SimUser {
  id: number;
  username: string;
}

const EMPTY_BLOCKS = { v: 1 as const, doc: {} };

export class SimServer {
  readonly workspaceId = 1;
  readonly channelId = 1;
  lastSeq = 0;
  private nextMessageId = 1;
  readonly messages = new Map<number, Message>();
  readonly byClientMsg = new Map<string, number>();
  readonly mentions: PersistedMention[] = [];
  readonly members = new Map<number, SimUser>();
  readonly lastReadSeq = new Map<number, number>();
  readonly lastReadReplySeq = new Map<string, number>(); // `${userId}:${rootId}`
  readonly mentionCount = new Map<number, number>();
  readonly reactions = new Map<string, { messageId: number; userId: number; emoji: string }>();
  readonly threadFollowers = new Map<number, Set<number>>(); // rootId -> userIds

  /** presence:{userId} socket set + liveness tokens. */
  private readonly sockets = new Map<string, { userId: number; expiresAt: number }>();
  private readonly userSockets = new Map<number, Set<string>>();
  readonly displayedPresence = new Map<number, "active" | "offline">();

  constructor(
    users: readonly SimUser[],
    private readonly clock: Clock,
    private readonly transport: Transport,
    private readonly rng: Rng,
    private readonly faults: FaultConfig,
  ) {
    for (const user of users) {
      this.members.set(user.id, user);
      this.lastReadSeq.set(user.id, 0);
      this.mentionCount.set(user.id, 0);
      this.displayedPresence.set(user.id, "offline");
    }
  }

  memberIds(): number[] {
    return [...this.members.keys()];
  }

  allMessages(): Message[] {
    return [...this.messages.values()].sort((a, b) => a.seq - b.seq);
  }

  connectSocket(userId: number, socketId: string): { becameOnline: boolean } {
    const set = this.userSockets.get(userId) ?? new Set<string>();
    set.add(socketId);
    this.userSockets.set(userId, set);
    this.sockets.set(socketId, { userId, expiresAt: this.clock.nowMs + SOCKET_TTL_MS });
    const becameOnline = this.displayedPresence.get(userId) !== "active";
    this.publishPresence(userId, "active");
    return { becameOnline };
  }

  heartbeat(socketId: string): void {
    const token = this.sockets.get(socketId);
    if (!token) return;
    token.expiresAt = this.clock.nowMs + SOCKET_TTL_MS;
  }

  disconnectSocket(userId: number, socketId: string): { becameOffline: boolean } {
    this.sockets.delete(socketId);
    const set = this.userSockets.get(userId);
    set?.delete(socketId);
    const remaining = set?.size ?? 0;
    if (remaining === 0) {
      this.publishPresence(userId, "offline");
      return { becameOffline: true };
    }
    return { becameOffline: false };
  }

  /** Ungraceful death: socket vanishes without a disconnect handler. */
  dropSocketUngraceful(socketId: string): void {
    this.sockets.delete(socketId);
    // Intentionally leave userSockets stale — the sweeper reconciles, §4.4.
  }

  sweep(): void {
    for (const [userId, set] of this.userSockets) {
      const stale: string[] = [];
      for (const socketId of set) {
        const token = this.sockets.get(socketId);
        if (!token || token.expiresAt <= this.clock.nowMs) stale.push(socketId);
      }
      for (const socketId of stale) {
        set.delete(socketId);
        this.sockets.delete(socketId);
      }
      if (set.size === 0 && this.displayedPresence.get(userId) !== "offline") {
        this.publishPresence(userId, "offline");
      }
    }
  }

  liveSocketCount(userId: number): number {
    const set = this.userSockets.get(userId);
    if (!set) return 0;
    let n = 0;
    for (const socketId of set) {
      const token = this.sockets.get(socketId);
      if (token && token.expiresAt > this.clock.nowMs) n++;
    }
    return n;
  }

  private publishPresence(userId: number, status: "active" | "offline"): void {
    const previous = this.displayedPresence.get(userId);
    this.displayedPresence.set(userId, status);
    if (previous === status) return;
    this.transport.broadcast(this.memberIds(), { type: "presence:changed", userId, status });
  }

  send(input: {
    userId: number;
    clientMsgId: string;
    text: string;
    parentId?: number | null;
    isBroadcast?: boolean;
  }): Message {
    if (this.rng.chance(this.faults.crashBeforeCommitRate)) throw new CommitCrash();

    const existingId = this.byClientMsg.get(input.clientMsgId);
    if (existingId != null) {
      const existing = this.messages.get(existingId)!;
      if (this.rng.chance(this.faults.crashAfterCommitRate)) throw new EmitCrash();
      this.transport.broadcast(this.memberIds(), { type: "message:created", messageId: existing.id });
      return existing;
    }

    this.lastSeq += 1;
    const id = this.nextMessageId++;
    const message: Message = {
      id,
      workspaceId: this.workspaceId,
      channelId: this.channelId,
      seq: this.lastSeq,
      clientMsgId: input.clientMsgId,
      authorId: input.userId,
      type: "text",
      text: input.text,
      blocks: EMPTY_BLOCKS,
      revision: 0,
      parentId: input.parentId ?? null,
      isBroadcast: input.isBroadcast ?? false,
      threadReplyCount: 0,
      threadLastReplyAt: null,
      editedAt: null,
      deletedAt: null,
      createdAt: this.clock.iso(),
      reactions: [],
    };
    this.messages.set(id, message);
    this.byClientMsg.set(input.clientMsgId, id);
    this.persistMentions(message);
    if (message.parentId != null) {
      const root = this.messages.get(Number(message.parentId));
      if (root && root.parentId == null) {
        root.threadReplyCount += 1;
        root.threadLastReplyAt = message.createdAt;
        root.revision += 1;
        const followers = this.threadFollowers.get(root.id) ?? new Set<number>();
        followers.add(input.userId);
        followers.add(Number(root.authorId));
        this.threadFollowers.set(root.id, followers);
        this.lastReadReplySeq.set(`${input.userId}:${root.id}`, clampReadSeq(this.lastReadReplySeq.get(`${input.userId}:${root.id}`) ?? 0, message.seq, false));
      }
    }

    if (this.rng.chance(this.faults.crashAfterCommitRate)) throw new EmitCrash();
    this.transport.broadcast(this.memberIds(), { type: "message:created", messageId: id });
    return message;
  }

  private persistMentions(message: Message): void {
    const parsed = parseMentions(message.text);
    const memberIds = this.memberIds();
    const recipients = new Set<number>();
    for (const m of parsed) {
      if (m.kind === "user") {
        const target = [...this.members.values()].find((u) => u.username === m.handle);
        if (!target) continue;
        this.mentions.push({ messageId: message.id, targetType: "user", targetId: target.id });
        if (target.id !== Number(message.authorId)) recipients.add(target.id);
      } else {
        this.mentions.push({ messageId: message.id, targetType: m.kind, targetId: null });
        for (const id of memberIds) {
          if (id !== Number(message.authorId)) recipients.add(id);
        }
      }
    }
    for (const id of recipients) {
      this.mentionCount.set(id, (this.mentionCount.get(id) ?? 0) + 1);
    }
  }

  edit(userId: number, messageId: number, text: string): Message {
    const existing = this.messages.get(messageId);
    if (!existing || existing.deletedAt) throw new Error("missing");
    const next: Message = {
      ...existing,
      text,
      revision: existing.revision + 1,
      editedAt: this.clock.iso(),
    };
    this.messages.set(messageId, next);
    this.transport.broadcast(this.memberIds(), { type: "message:edited", messageId });
    return next;
  }

  delete(messageId: number): void {
    const existing = this.messages.get(messageId);
    if (!existing || existing.deletedAt) return;
    this.messages.set(messageId, {
      ...existing,
      text: "",
      revision: existing.revision + 1,
      deletedAt: this.clock.iso(),
    });
    this.transport.broadcast(this.memberIds(), { type: "message:deleted", messageId, seq: existing.seq });
  }

  react(userId: number, messageId: number, emoji: string): void {
    const key = `${messageId}:${userId}:${emoji}`;
    if (this.reactions.has(key)) this.reactions.delete(key);
    else this.reactions.set(key, { messageId, userId, emoji });
    this.syncReactionSummaries(messageId);
    this.transport.broadcast(this.memberIds(), { type: "reaction:changed", messageId });
  }

  private syncReactionSummaries(messageId: number): void {
    const message = this.messages.get(messageId);
    if (!message) return;
    const counts = new Map<string, number>();
    for (const row of this.reactions.values()) {
      if (row.messageId !== messageId) continue;
      counts.set(row.emoji, (counts.get(row.emoji) ?? 0) + 1);
    }
    const reactions: MessageReactionSummary[] = [...counts.entries()].map(([emoji, count]) => ({
      emoji,
      count,
      reactedByMe: false,
    }));
    this.messages.set(messageId, { ...message, reactions });
  }

  ackRead(userId: number, seq: number): void {
    this.lastReadSeq.set(userId, clampReadSeq(this.lastReadSeq.get(userId) ?? 0, seq, false));
    this.mentionCount.set(userId, 0);
  }

  markUnread(userId: number, seq: number): void {
    this.lastReadSeq.set(userId, clampReadSeq(this.lastReadSeq.get(userId) ?? 0, Math.max(0, seq - 1), true));
  }

  ackThreadRead(userId: number, rootId: number, seq: number): void {
    const key = `${userId}:${rootId}`;
    this.lastReadReplySeq.set(key, clampReadSeq(this.lastReadReplySeq.get(key) ?? 0, seq, false));
    const followers = this.threadFollowers.get(rootId) ?? new Set<number>();
    followers.add(userId);
    this.threadFollowers.set(rootId, followers);
  }

  scrollback(opts: { afterSeq?: number; beforeSeq?: number; limit: number }): Message[] {
    if (this.rng.chance(this.faults.httpFailRate)) throw new HttpFault();
    const channelView = this.allMessages().filter((m) => m.parentId == null || m.isBroadcast);
    let rows = channelView;
    if (opts.afterSeq !== undefined) rows = rows.filter((m) => m.seq > opts.afterSeq!);
    if (opts.beforeSeq !== undefined) rows = rows.filter((m) => m.seq < opts.beforeSeq!);
    const backward = opts.beforeSeq !== undefined || opts.afterSeq === undefined;
    if (backward) {
      rows = rows.slice().sort((a, b) => b.seq - a.seq).slice(0, opts.limit).reverse();
    } else {
      rows = rows.slice(0, opts.limit);
    }
    return rows.map((m) => this.wire(m));
  }

  wire(message: Message, viewerId?: number): Message {
    const reactions = (message.reactions ?? []).map((r) => ({
      ...r,
      reactedByMe: viewerId != null && [...this.reactions.values()].some((row) => row.messageId === message.id && row.userId === viewerId && row.emoji === r.emoji),
    }));
    return { ...message, reactions };
  }
}
