import { derived, get, writable } from 'svelte/store';
import { closeThreadList, getAccountData, openThreadList, paginateThreadList, runCommand, setAccountData } from './api';
import { pushBanner, rooms, sessionUser, threadLists, timelines } from './store';
import { roomThreads } from './thread-index';
import type { ThreadEntry } from './thread-index';

const PINNED_TYPE = 'dev.malice.pinned_threads';

export interface OpenThread {
  room_id: string;
  root_event_id: string;
  focus_event_id?: string;
}

export const openThread = writable<OpenThread | null>(null);
export const pinnedThreads = writable<Record<string, string[]>>({});

export const threadsByRoom = derived([timelines, pinnedThreads, threadLists, rooms], ([$timelines, $pinned, $lists, $rooms]) => {
  const unreads = new Map($rooms.map((room) => [room.room_id, room.thread_unreads]));
  const out: Record<string, ThreadEntry[]> = {};
  const ids = [...Object.keys($timelines), ...Object.keys($pinned), ...Object.keys($lists), ...unreads.keys()];
  for (const roomId of new Set(ids)) {
    const list = roomThreads(roomId, $timelines[roomId] ?? [], $pinned[roomId] ?? [], $lists[roomId]?.entries ?? [], unreads.get(roomId) ?? {});
    if (list.length) out[roomId] = list;
  }
  return out;
});

const THREAD_PAGE = 20;
const openLists = new Set<string>();

/** Keeps a server thread list open for exactly the rooms in `wanted`, loading a first page for each new one. */
export function syncThreadLists(wanted: string[]): void {
  const want = new Set(wanted);
  for (const roomId of [...openLists]) {
    if (want.has(roomId)) continue;
    openLists.delete(roomId);
    void closeThreadList(roomId).catch(() => undefined);
  }
  for (const roomId of want) {
    if (openLists.has(roomId)) continue;
    openLists.add(roomId);
    void openThreadList(roomId)
      .then(() => paginateThreadList(roomId, THREAD_PAGE))
      .catch(() => openLists.delete(roomId));
  }
}

export async function loadOlderThreads(roomId: string): Promise<void> {
  try {
    await paginateThreadList(roomId, THREAD_PAGE);
  } catch (e) {
    pushBanner(e instanceof Error ? e.message : 'Could not load older threads');
  }
}

function parsePinned(raw: unknown): Record<string, string[]> {
  if (typeof raw !== 'object' || raw === null) return {};
  const rooms = (raw as Record<string, unknown>)['rooms'];
  if (typeof rooms !== 'object' || rooms === null) return {};
  const out: Record<string, string[]> = {};
  for (const [roomId, list] of Object.entries(rooms)) {
    if (Array.isArray(list)) out[roomId] = list.filter((id): id is string => typeof id === 'string');
  }
  return out;
}

export async function loadPinnedThreads(): Promise<void> {
  try {
    pinnedThreads.set(parsePinned(await getAccountData(PINNED_TYPE)));
  } catch {
    pinnedThreads.set({});
  }
}

export async function togglePinnedThread(roomId: string, rootEventId: string): Promise<void> {
  const before = get(pinnedThreads);
  const list = before[roomId] ?? [];
  const nextList = list.includes(rootEventId) ? list.filter((id) => id !== rootEventId) : [...list, rootEventId];
  const next = { ...before, [roomId]: nextList };
  if (nextList.length === 0) delete next[roomId];
  pinnedThreads.set(next);
  try {
    await setAccountData(PINNED_TYPE, { rooms: next });
  } catch (e) {
    pinnedThreads.set(before);
    pushBanner(e instanceof Error ? e.message : 'Pinning the thread failed');
  }
}

let opened: OpenThread | null = null;

/** The room must already be open, since macltok builds thread timelines inside it. */
export async function showThread(target: OpenThread | null): Promise<void> {
  const previous = opened;
  opened = target;
  openThread.set(target);
  if (previous && target && previous.room_id === target.room_id && previous.root_event_id === target.root_event_id) return;
  if (previous) runCommand({ op: 'close_thread', room_id: previous.room_id, root_event_id: previous.root_event_id }).catch(() => undefined);
  if (!target) return;
  try {
    await runCommand({ op: 'open_thread', room_id: target.room_id, root_event_id: target.root_event_id });
  } catch (e) {
    if (opened === target) {
      opened = null;
      openThread.set(null);
    }
    pushBanner(e instanceof Error ? e.message : 'Opening the thread failed');
  }
}

let account: string | null | undefined;
sessionUser.subscribe((user) => {
  if (user === account) return;
  account = user;
  openLists.clear();
  opened = null;
  openThread.set(null);
  pinnedThreads.set({});
});
