import { derived, get, readable, 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';
import type { ThreadListEntry, ThreadUnread, TimelineItem } from './types';

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[]>>({});

const NO_TIMELINE: TimelineItem[] = [];
const NO_PINS: string[] = [];
const NO_ENTRIES: ThreadListEntry[] = [];
const NO_UNREADS: Record<string, ThreadUnread> = {};

interface RoomThreadsCache {
  timeline: TimelineItem[];
  pins: string[];
  entries: ThreadListEntry[];
  unreads: Record<string, ThreadUnread>;
  list: ThreadEntry[];
}

function createThreadsByRoom() {
  let cache = new Map<string, RoomThreadsCache>();
  let last: Record<string, ThreadEntry[]> = {};
  const computed = derived([timelines, pinnedThreads, threadLists, rooms], ([$timelines, $pinned, $lists, $rooms]) => {
    const unreads = new Map($rooms.map((room) => [room.room_id, room.thread_unreads]));
    const next = new Map<string, RoomThreadsCache>();
    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 timeline = $timelines[roomId] ?? NO_TIMELINE;
      const pins = $pinned[roomId] ?? NO_PINS;
      const entries = $lists[roomId]?.entries ?? NO_ENTRIES;
      const roomUnreads = unreads.get(roomId) ?? NO_UNREADS;
      const hit = cache.get(roomId);
      const reuse = hit && hit.timeline === timeline && hit.pins === pins && hit.entries === entries && hit.unreads === roomUnreads;
      const entry = reuse ? hit : { timeline, pins, entries, unreads: roomUnreads, list: roomThreads(roomId, timeline, pins, entries, roomUnreads) };
      next.set(roomId, entry);
      if (entry.list.length) out[roomId] = entry.list;
    }
    const keys = Object.keys(out);
    const unchanged = keys.length === Object.keys(last).length && keys.every((roomId) => out[roomId] === last[roomId]);
    cache = next;
    if (unchanged) return last;
    last = out;
    return out;
  });
  return readable(last, (set) => {
    let sent: Record<string, ThreadEntry[]> | null = null;
    return computed.subscribe((value) => {
      if (value === sent) return;
      sent = value;
      set(value);
    });
  });
}

export const threadsByRoom = createThreadsByRoom();

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({});
});
