import { get, writable } from 'svelte/store';
import { sendMessage } from './api';
import { timelines } from './store';
import type { Mention } from './mentions';
import type { ReplyReference, TimelineItem } from './types';

export interface PendingMessage {
  id: string;
  body: string;
  replyTo: ReplyReference | null;
  status: 'sending' | 'failed';
  error: string | null;
  createdAt: number;
}

export const pendingByRoom = writable<Record<string, PendingMessage[]>>({});

interface PendingRecord extends PendingMessage {
  roomId: string;
  mentions: Mention[];
  knownIds: Set<string>;
}

const records = new Map<string, PendingRecord>();
const queues = new Map<string, Promise<void>>();
let nextSeq = 1;

function nextId(): string {
  return `pending-${nextSeq++}`;
}

function ownIdentities(items: TimelineItem[]): Set<string> {
  const out = new Set<string>();
  for (const item of items) {
    if (item.kind !== 'event' || !item.event.own) continue;
    const id = item.event.transaction_id ?? item.event.event_id;
    if (id) out.add(id);
  }
  return out;
}

function toPublic(record: PendingRecord): PendingMessage {
  return { id: record.id, body: record.body, replyTo: record.replyTo, status: record.status, error: record.error, createdAt: record.createdAt };
}

function roomList(roomId: string): PendingMessage[] {
  return get(pendingByRoom)[roomId] ?? [];
}

function setRoom(roomId: string, list: PendingMessage[]): void {
  pendingByRoom.update((all) => ({ ...all, [roomId]: list }));
}

function addRecord(record: PendingRecord): void {
  records.set(record.id, record);
  setRoom(record.roomId, [...roomList(record.roomId), toPublic(record)]);
}

function updateRecord(id: string, patch: Partial<Pick<PendingRecord, 'status' | 'error'>>): void {
  const record = records.get(id);
  if (!record) return;
  Object.assign(record, patch);
  setRoom(record.roomId, roomList(record.roomId).map((p) => (p.id === id ? toPublic(record) : p)));
}

function removeRecord(id: string): void {
  const record = records.get(id);
  if (!record) return;
  records.delete(id);
  setRoom(record.roomId, roomList(record.roomId).filter((p) => p.id !== id));
}

async function attemptSend(record: PendingRecord): Promise<void> {
  if (!records.has(record.id)) return;
  updateRecord(record.id, { status: 'sending', error: null });
  try {
    await sendMessage(record.roomId, record.body, record.replyTo?.event_id ?? null, record.mentions);
  } catch (e) {
    updateRecord(record.id, { status: 'failed', error: e instanceof Error ? e.message : 'Message could not be sent' });
  }
}

function enqueue(record: PendingRecord): void {
  const previous = queues.get(record.roomId) ?? Promise.resolve();
  const next = previous.then(() => attemptSend(record));
  queues.set(record.roomId, next.catch(() => undefined));
}

export function queueSend(roomId: string, body: string, replyTo: ReplyReference | null = null, mentions: Mention[] = []): void {
  const record: PendingRecord = {
    id: nextId(),
    roomId,
    body,
    replyTo,
    mentions,
    status: 'sending',
    error: null,
    createdAt: Date.now(),
    knownIds: ownIdentities(get(timelines)[roomId] ?? [])
  };
  addRecord(record);
  enqueue(record);
}

export function retryPending(id: string): void {
  const record = records.get(id);
  if (!record) return;
  record.knownIds = ownIdentities(get(timelines)[record.roomId] ?? []);
  enqueue(record);
}

export function discardPending(id: string): void {
  removeRecord(id);
}

export function resetPending(): void {
  records.clear();
  queues.clear();
  pendingByRoom.set({});
}

function reconcileRoom(roomId: string, items: TimelineItem[]): void {
  const list = roomList(roomId);
  if (list.length === 0) return;
  const ownItems = items.filter((item) => item.kind === 'event' && item.event.own) as Extract<
    TimelineItem,
    { kind: 'event' }
  >[];
  const claimed = new Set<string>();
  for (const pending of list) {
    const record = records.get(pending.id);
    if (!record) continue;
    const match = ownItems.find((item) => {
      if (claimed.has(item.event.item_id)) return false;
      const id = item.event.transaction_id ?? item.event.event_id;
      if (id && record.knownIds.has(id)) return false;
      return item.event.content.kind === 'Message' && item.event.content.message.body === record.body;
    });
    if (match) {
      claimed.add(match.event.item_id);
      removeRecord(record.id);
    }
  }
}

timelines.subscribe((all) => {
  for (const roomId of Object.keys(get(pendingByRoom))) {
    reconcileRoom(roomId, all[roomId] ?? []);
  }
});
