import type { MessagingAdapter } from '../adapter';
import type {
  Attachment,
  BulkAddResult,
  ChannelSummary,
  ChannelVisibility,
  Conversation,
  CreateChannelInput,
  Membership,
  Message,
  MessageEvent,
  Person,
  SendOpts,
  Unsubscribe,
} from '../types';

const ME = 'me';

export const MOCK_PEOPLE: Person[] = [
  { id: 'pp_sofia', name: 'Sofia Ramirez', kind: 'staff' },
  { id: 'pp_dan', name: 'Dan Whitaker', kind: 'staff' },
  { id: 'pp_priya', name: 'Priya Nair', kind: 'staff' },
  { id: 'cust_acme', name: 'Acme Roofing (Client)', kind: 'customer' },
  { id: 'cust_globex', name: 'Globex Homes (Client)', kind: 'customer' },
];

interface MockThread {
  threadId: string;
  membership: Membership;
  subject: string | null;
  participants: string[];
  messages: Message[];
  topic?: string | null;
  visibility?: ChannelVisibility;
}

const nameById = new Map(MOCK_PEOPLE.map((p) => [p.id, p.name]));

/**
 * In-memory adapter for demos and tests. State is PER INSTANCE — the CRM's version
 * used a module-level Map, which leaks between tests. Each `new MockAdapter()` is
 * fully isolated.
 */
export class MockAdapter implements MessagingAdapter {
  private seq = 100;
  private threads = new Map<string, MockThread>();
  private listeners = new Map<string, Set<(e: MessageEvent) => void>>();

  constructor(private readonly now: () => string = () => new Date().toISOString()) {
    const seededAt = this.now();
    this.threads.set('th_mock_1', {
      threadId: 'th_mock_1',
      membership: 'dm',
      subject: null,
      participants: [ME, 'pp_sofia'],
      messages: [
        {
          id: 'm1',
          actorId: 'pp_sofia',
          text: 'Can you review the Henderson estimate?',
          at: seededAt,
          reactions: [],
        },
      ],
    });
    this.threads.set('th_mock_2', {
      threadId: 'th_mock_2',
      membership: 'group',
      subject: 'Storm response — East side',
      participants: [ME, 'pp_dan', 'pp_priya'],
      messages: [
        { id: 'm2', actorId: 'pp_dan', text: 'Crew is rolling out at 7.', at: seededAt, reactions: [] },
      ],
    });
    // Channels: a joined public one, a joinable public one (I'm NOT in it → shows in browse only),
    // and a private one I'm a member of.
    this.threads.set('th_ch_general', {
      threadId: 'th_ch_general', membership: 'channel', subject: 'general', topic: 'Company-wide chatter',
      visibility: 'public', participants: [ME, 'pp_dan', 'pp_priya', 'pp_sofia'],
      messages: [{ id: 'c1', actorId: 'pp_priya', text: 'Welcome to #general 👋', at: seededAt, reactions: [] }],
    });
    this.threads.set('th_ch_random', {
      threadId: 'th_ch_random', membership: 'channel', subject: 'random', topic: 'Non-work banter',
      visibility: 'public', participants: ['pp_dan', 'pp_sofia'], messages: [],
    });
    this.threads.set('th_ch_deals', {
      threadId: 'th_ch_deals', membership: 'channel', subject: 'deals', topic: 'Big pipeline moves',
      visibility: 'private', participants: [ME, 'pp_sofia'], messages: [],
    });
  }

  currentActorId(): string {
    return ME;
  }

  async listConversations(): Promise<Conversation[]> {
    // Only threads I'm a member of — an un-joined public channel appears in browse, not here.
    return [...this.threads.values()]
      .filter((t) => t.participants.includes(ME))
      .map((t) => {
        const last = t.messages[t.messages.length - 1];
        const others = t.participants.filter((p) => p !== ME);
        return {
          threadId: t.threadId,
          title: t.subject || others.map((id) => nameById.get(id) ?? id).join(', ') || 'Conversation',
          subject: t.subject,
          membership: t.membership,
          participants: [...t.participants],
          unread: 0,
          ...(t.topic != null ? { topic: t.topic } : {}),
          ...(last ? { lastMessage: last.text, lastAt: last.at } : {}),
        };
      });
  }

  async browseChannels(): Promise<ChannelSummary[]> {
    // Public channels are discoverable; private ones only if I'm already a member.
    return [...this.threads.values()]
      .filter((t) => t.membership === 'channel' && (t.visibility === 'public' || t.participants.includes(ME)))
      .map((t) => ({
        threadId: t.threadId,
        name: t.subject ?? 'channel',
        topic: t.topic ?? null,
        visibility: t.visibility ?? 'public',
        memberCount: t.participants.length,
        joined: t.participants.includes(ME),
      }));
  }

  async createChannel(input: CreateChannelInput): Promise<{ threadId: string }> {
    const threadId = `th_ch_${this.seq++}`;
    this.threads.set(threadId, {
      threadId,
      membership: 'channel',
      subject: input.name,
      topic: input.topic ?? null,
      visibility: input.visibility,
      participants: [ME],
      messages: [],
    });
    return { threadId };
  }

  async joinChannel(threadId: string): Promise<void> {
    const t = this.threads.get(threadId);
    if (t && !t.participants.includes(ME)) t.participants = [...t.participants, ME];
  }

  async leaveChannel(threadId: string): Promise<void> {
    const t = this.threads.get(threadId);
    if (t) t.participants = t.participants.filter((p) => p !== ME);
  }

  async listMembers(threadId: string): Promise<Person[]> {
    const t = this.threads.get(threadId);
    if (!t) return [];
    return t.participants.map((id) => {
      if (id === ME) return { id: ME, name: 'You', kind: 'staff' };
      return MOCK_PEOPLE.find((p) => p.id === id) ?? { id, name: id, kind: 'staff' };
    });
  }

  async directory(): Promise<Person[]> {
    return MOCK_PEOPLE.map((p) => ({ ...p }));
  }

  async addMember(threadId: string, userId: string): Promise<void> {
    const t = this.threads.get(threadId);
    if (t && !t.participants.includes(userId)) t.participants = [...t.participants, userId];
  }

  /** Bulk add, mirroring the live door: already-members come back as `skipped`, never re-added. */
  async addMembers(threadId: string, userIds: string[]): Promise<BulkAddResult> {
    const t = this.threads.get(threadId);
    if (!t) return { added: [], skipped: [], failed: [...userIds] };
    const added: string[] = [];
    const skipped: string[] = [];
    for (const id of [...new Set(userIds)]) {
      if (t.participants.includes(id)) skipped.push(id);
      else {
        t.participants = [...t.participants, id];
        added.push(id);
      }
    }
    return { added, skipped, failed: [] };
  }

  async removeMember(threadId: string, userId: string): Promise<void> {
    const t = this.threads.get(threadId);
    if (t) t.participants = t.participants.filter((p) => p !== userId);
  }

  async renameConversation(threadId: string, subject: string): Promise<void> {
    const t = this.threads.get(threadId);
    if (t) t.subject = subject;
  }

  async openThread(p: { participantIds: string[]; membership?: Membership; subject?: string }): Promise<{ threadId: string }> {
    // A DM to someone you already have reuses the existing 1:1 thread (dedupe, like the live door).
    if ((p.membership ?? (p.participantIds.length === 1 ? 'dm' : 'group')) === 'dm' && p.participantIds.length === 1) {
      const target = p.participantIds[0];
      for (const [id, t] of this.threads) {
        if (t.membership === 'dm' && t.participants.length === 2 && t.participants.includes(ME) && t.participants.includes(target)) {
          return { threadId: id };
        }
      }
    }
    const membership = p.membership ?? (p.participantIds.length === 1 ? 'dm' : 'group');
    const threadId = `th_mock_${this.seq++}`;
    this.threads.set(threadId, {
      threadId,
      membership,
      subject: p.subject ?? null,
      participants: [ME, ...p.participantIds],
      messages: [],
    });
    return { threadId };
  }

  async history(threadId: string): Promise<Message[]> {
    return [...(this.threads.get(threadId)?.messages ?? [])];
  }

  async send(threadId: string, content: string, opts?: SendOpts): Promise<Message> {
    const t = this.threads.get(threadId);
    if (!t) throw new Error(`Unknown thread: ${threadId}`);
    const message: Message = {
      id: `m_${this.seq++}`,
      actorId: ME,
      text: content,
      at: this.now(),
      reactions: [],
      ...(opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : {}),
      ...(opts?.attachment ? { attachment: opts.attachment } : {}),
    };
    t.messages = [...t.messages, message];
    this.emit(threadId, { kind: 'message', message });
    return message;
  }

  async react(threadId: string, messageId: string, emoji: string): Promise<void> {
    const t = this.threads.get(threadId);
    if (!t) return;
    let next: Message | undefined;
    t.messages = t.messages.map((m) => {
      if (m.id !== messageId) return m;
      const existing = (m.reactions ?? []).find((r) => r.emoji === emoji);
      const reactions = existing
        ? (m.reactions ?? []).filter((r) => r.emoji !== emoji)
        : [...(m.reactions ?? []), { emoji, count: 1, mine: true }];
      next = { ...m, reactions };
      return next;
    });
    if (next) this.emit(threadId, { kind: 'reaction', messageId, reactions: next.reactions ?? [] });
  }

  async upload(file: File): Promise<Attachment> {
    return { url: `mock://uploads/${file.name}`, mime: file.type, name: file.name };
  }

  subscribe(threadId: string, cb: (e: MessageEvent) => void): Unsubscribe {
    if (!this.listeners.has(threadId)) this.listeners.set(threadId, new Set());
    this.listeners.get(threadId)!.add(cb);
    return () => {
      this.listeners.get(threadId)?.delete(cb);
    };
  }

  sendTyping(): void {
    // No-op: nobody is typing back in a mock.
  }

  async markRead(): Promise<void> {
    // No-op: the mock has no second party to report a read.
  }

  isConnected(): boolean {
    return true;
  }

  private emit(threadId: string, e: MessageEvent): void {
    this.listeners.get(threadId)?.forEach((cb) => cb(e));
  }
}
