import { Q } from '@nozbe/watermelondb';
import { database } from 'src/watermelondb';
import Chat from 'src/watermelondb/models/Chat';
import BlockedUser from 'src/watermelondb/models/BlockedUser';
import { chatApi } from '@api/chat';
import NetInfo from '@react-native-community/netinfo';
import { testConnectionSpeed } from 'src/database/speedService';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);

export type DirtyAction = {
  type: 'pin' | 'archive' | 'mute' | 'block' | 'unblock' | 'delete' | 'leave_group';
  value?: number | string | null;
  ts: number;
};

export function addDirtyAction(
  record: { isDirty: boolean; dirtyActions: string | null },
  action: Omit<DirtyAction, 'ts'>
) {
  const list: DirtyAction[] = record.dirtyActions ? JSON.parse(record.dirtyActions) : [];
  list.push({ ...action, ts: Date.now() });

  record.isDirty = true;
  record.dirtyActions = JSON.stringify(list);
}

async function retryWithBackoff<T>(fn: () => Promise<T>, tries = 3, delay = 300): Promise<T> {
  let error: any = null;

  for (let i = 0; i < tries; i++) {
    try {
      return await fn();
    } catch (e) {
      error = e;
      await new Promise((r) => setTimeout(r, delay));
      delay *= 2;
    }
  }

  throw error;
}

export function compactDirtyActions(actions: DirtyAction[]): DirtyAction[] {
  if (!actions?.length) return [];

  actions = [...actions].sort((a, b) => a.ts - b.ts);

  const result: DirtyAction[] = [];

  for (const a of actions) {
    const last = result[result.length - 1];

    if (a.type === 'delete' || a.type === 'leave_group') {
      return [a];
    }

    if (
      (last?.type === 'block' && a.type === 'unblock') ||
      (last?.type === 'unblock' && a.type === 'block')
    ) {
      result.pop();
      continue;
    }

    if (['pin', 'archive', 'mute'].includes(a.type) && last?.type === a.type) {
      last.value = a.value;
      last.ts = a.ts;
      continue;
    }

    result.push(a);
  }

  return result;
}

async function collectDirtyQueue() {
  const chatCollection = database.get<Chat>('chats');
  const blockedCollection = database.get<BlockedUser>('blocked_users');

  const dirtyChats = await chatCollection.query(Q.where('is_dirty', true)).fetch();

  const dirtyBlocked = await blockedCollection.query(Q.where('is_dirty', true)).fetch();

  const queue: Array<{
    source: 'chat' | 'blocked';
    recordId: string;
    chatUid: number | null;
    groupToken: string | null;
    action: DirtyAction;
  }> = [];

  for (const c of dirtyChats) {
    const raw = (c as any)._raw;
    const list: DirtyAction[] = raw.dirty_actions ? JSON.parse(raw.dirty_actions) : [];

    const compacted = compactDirtyActions(list);

    for (const a of compacted) {
      queue.push({
        source: 'chat',
        recordId: c.id,
        chatUid: c.chatUid ?? null,
        groupToken: c.groupChatToken ?? null,
        action: a
      });
    }
  }

  for (const b of dirtyBlocked) {
    const raw = (b as any)._raw;
    const list: DirtyAction[] = raw.dirty_actions ? JSON.parse(raw.dirty_actions) : [];

    const compacted = compactDirtyActions(list);

    for (const a of compacted) {
      queue.push({
        source: 'blocked',
        recordId: b.id,
        chatUid: b.userId ?? null,
        groupToken: null,
        action: a
      });
    }
  }

  queue.sort((a, b) => a.action.ts - b.action.ts);

  return queue;
}

async function performAction(
  token: string,
  item: {
    source: 'chat' | 'blocked';
    recordId: string;
    chatUid: number | null;
    groupToken: string | null;
    action: DirtyAction;
  }
): Promise<{ success: boolean; shouldDeleteLocal?: boolean }> {
  const { chatUid, groupToken, action } = item;

  return retryWithBackoff(async () => {
    switch (action.type) {
      case 'pin':
        if (groupToken)
          await chatApi.setPinForGroup({
            token,
            group_token: groupToken,
            value: action.value as 0 | 1
          });
        else if (chatUid)
          await chatApi.setPin({
            token,
            conversation_with_user: chatUid,
            value: action.value as 0 | 1
          });
        return { success: true };

      case 'archive':
        if (groupToken)
          await chatApi.setArchiveForGroup({
            token,
            group_token: groupToken,
            value: action.value as 0 | 1
          });
        else if (chatUid)
          await chatApi.setArchive({
            token,
            conversation_with_user: chatUid,
            value: action.value as 0 | 1
          });
        return { success: true };

      case 'mute':
        if (groupToken)
          await chatApi.setMuteForGroup({
            token,
            group_token: groupToken,
            value: action.value as 0 | 1
          });
        else if (chatUid)
          await chatApi.setMute({
            token,
            conversation_with_user: chatUid,
            value: action.value as 0 | 1
          });
        return { success: true };

      case 'block':
        if (chatUid)
          await chatApi.setBlock({
            token,
            conversation_with_user: chatUid,
            value: 1
          });
        return { success: true, shouldDeleteLocal: true };

      case 'unblock':
        if (chatUid)
          await chatApi.setBlock({
            token,
            conversation_with_user: chatUid,
            value: 0
          });
        return { success: true, shouldDeleteLocal: true };

      case 'delete':
        if (groupToken) await chatApi.removeGroupFromList(token, groupToken);
        else if (chatUid)
          await chatApi.deleteChat({
            token,
            conversation_with_user: chatUid
          });
        return { success: true, shouldDeleteLocal: true };

      case 'leave_group':
        if (groupToken) await chatApi.leaveGroup(token, groupToken);
        return { success: true, shouldDeleteLocal: true };

      default:
        console.warn('Unknown action type', action);
        return { success: true };
    }
  });
}

async function pushLocalChanges(token: string) {
  const queue = await collectDirtyQueue();

  const CHUNK_SIZE = 3;
  for (let i = 0; i < queue.length; i += CHUNK_SIZE) {
    const chunk = queue.slice(i, i + CHUNK_SIZE);

    await Promise.all(
      chunk.map(async (item) => {
        try {
          const res = await performAction(token, item);

          if (res.shouldDeleteLocal) {
            const col =
              item.source === 'chat'
                ? database.get<Chat>('chats')
                : database.get<BlockedUser>('blocked_users');

            const rec = await col.find(item.recordId);
            await database.write(() => rec.destroyPermanently());
          } else {
            const col: any =
              item.source === 'chat'
                ? database.get<Chat>('chats')
                : database.get<BlockedUser>('blocked_users');
            const rec = await col.find(item.recordId);
            await database.write(() =>
              rec.update((r: Chat | BlockedUser) => {
                r.dirtyActions = null;
                r.isDirty = false;
              })
            );
          }
        } catch (e) {
          console.warn('pushLocalChanges: failed', e);
        }
      })
    );
  }
}

async function fetchList(token: string, archive: 0 | 1) {
  return retryWithBackoff(async () => {
    const { data } = await chatApi.getChatsList(token, archive);
    return data?.conversations ?? [];
  });
}

async function pullUpdates(token: string) {
  const [active, archived] = await Promise.all([fetchList(token, 0), fetchList(token, 1)]);

  const blockedRes = await retryWithBackoff(async () => {
    const { data } = await chatApi.getBlocked(token);
    return data ?? {};
  });
  const serverBlockedList: any[] = blockedRes.blocked ?? [];
  const serverChats = [...active, ...archived];

  const chatCollection = database.get<Chat>('chats');
  const blockedCollection = database.get<BlockedUser>('blocked_users');

  const localChats = await chatCollection.query().fetch();
  const localBlocked = await blockedCollection.query().fetch();

  const keyForServer = (s: any) =>
    s.group_chat_token ? `g:${s.group_chat_token}` : `u:${s.uid ?? s.chat_uid}`;

  const localKeyFor = (c: Chat) => (c.groupChatToken ? `g:${c.groupChatToken}` : `u:${c.chatUid}`);

  const localMap = new Map<string, Chat>();
  const localDuplicates = new Map<string, Chat[]>();
  for (const c of localChats) {
    const k = localKeyFor(c);

    if (!localMap.has(k)) {
      localMap.set(k, c);
    } else {
      if (!localDuplicates.has(k)) {
        localDuplicates.set(k, [localMap.get(k)!]);
      }
      localDuplicates.get(k)!.push(c);
    }
  }

  await database.write(async () => {
    for (const [, list] of localDuplicates) {
      list.sort((a, b) => {
        if (a.isDirty && !b.isDirty) return -1;
        if (!a.isDirty && b.isDirty) return 1;
        return b.updated - a.updated;
      });

      const [, ...duplicates] = list;
      for (const d of duplicates) {
        await d.destroyPermanently();
      }
    }
  });

  const serverMap = new Map<string, any>();
  for (const s of serverChats) {
    const k = keyForServer(s);
    serverMap.set(k, s);
  }

  const serverBlockedSet = new Set(serverBlockedList.map((id: any) => String(id)));

  const toCreate: any[] = [];
  const toUpdate: Array<{ local: Chat; server: any }> = [];
  const toDeleteLocalKeys: string[] = [];

  for (const [key, serverChat] of serverMap.entries()) {
    const local = localMap.get(key);
    if (!local) {
      toCreate.push({ key, serverChat });
    } else {
      toUpdate.push({ local, server: serverChat });
    }
  }

  for (const [localKey, local] of localMap.entries()) {
    if (!serverMap.has(localKey)) {
      toDeleteLocalKeys.push(localKey);
    }
  }

  const blockedToDelete: BlockedUser[] = localBlocked.filter(
    (b) => !serverBlockedSet.has(String(b.userId))
  );

  const createdLocalRecordKeysToCacheAvatar: string[] = [];

  await database.write(async () => {
    const batch: any[] = [];

    for (const item of toCreate) {
      const s = item.serverChat;
      if (localMap.has(item.key)) {
        continue;
      }

      batch.push(
        chatCollection.prepareCreate((r: any) => {
          r.chatUid = s.uid ?? s.chat_uid ?? null;
          r.groupChatToken = s.group_chat_token ?? null;

          r.name = s.name ?? '';
          r.avatar = s.avatar ?? null;

          r.short = s.short ?? '';
          r.sentBy = s.sent_by ?? 0;
          r.updated =
            typeof s.updated === 'number'
              ? s.updated
              : s.updated
                ? dayjs.utc(s.updated, 'YYYY-MM-DD HH:mm:ss', true).valueOf()
                : Date.now();

          r.status = s.status ?? 0;
          r.unreadCount = s.unread_count ?? 0;
          r.lastMessageId = s.last_message_id ?? 0;
          r.pin = s.pin ?? 0;
          r.pinOrder = s.pin_order ?? 0;
          r.archive = s.archive ?? 0;
          r.archiveOrder = s.archive_order ?? 0;
          r.attachmentName = s.attachement_name ?? s.attachment_name ?? '';
          r.encrypted = s.encrypted ?? 0;
          r.muted = s.muted ?? 0;
          r.userType = s.user_type ?? null;

          r.canSendMessages = s.can_send_message ?? 1;
          r.isAdmin = s.is_admin ?? 0;
          r.announcement = s.announcement ?? 0;

          r.removed = false;
          r.isDirty = false;
          r.dirtyActions = null;
        })
      );

      createdLocalRecordKeysToCacheAvatar.push(item.key);
    }

    for (const { local, server } of toUpdate) {
      // if ((local as any).isDirty) {
      //   // to do
      //   const serverUpdated =
      //     typeof server.updated === 'number'
      //       ? server.updated
      //       : server.updated
      //         ? new Date(server.updated).getTime()
      //         : null;
      //   if (serverUpdated && serverUpdated > (local as any).updated) {
      //     await local.update((r: any) => {
      //       r.unreadCount = server.unread_count ?? r.unreadCount;
      //       r.lastMessageId = server.last_message_id ?? r.lastMessageId;
      //       r.updated = serverUpdated;
      //       r.removed = false;
      //     });
      //   }
      //   continue;
      // }

      await local.update((r: any) => {
        r.name = server.name ?? r.name;
        r.avatar = server.avatar ?? r.avatar;
        r.short = server.short ?? r.short;
        r.sentBy = server.sent_by ?? r.sentBy;
        r.updated =
          typeof server.updated === 'number'
            ? server.updated
            : server.updated
              ? dayjs.utc(server.updated, 'YYYY-MM-DD HH:mm:ss', true).valueOf()
              : r.updated;
        r.status = server.status ?? r.status;
        r.unreadCount = server.unread_count ?? r.unreadCount;
        r.lastMessageId = server.last_message_id ?? r.lastMessageId;
        r.pin = server.pin ?? r.pin;
        r.pinOrder = server.pin_order ?? r.pinOrder;
        r.archive = server.archive ?? r.archive;
        r.archiveOrder = server.archive_order ?? r.archiveOrder;
        r.attachmentName = server.attachement_name ?? server.attachment_name ?? r.attachmentName;
        r.encrypted = server.encrypted ?? r.encrypted;
        r.muted = server.muted ?? r.muted;
        r.userType = server.user_type ?? r.userType;

        r.canSendMessages =
          server.can_send_message ?? r.canSendMessages ?? 1;
        r.isAdmin = server.is_admin ?? r.isAdmin;
        r.announcement = server.announcement ?? r.announcement;
        r.removed = false;
      });
    }

    for (const localKey of toDeleteLocalKeys) {
      const local = localMap.get(localKey)!;
      await local.destroyPermanently();
    }

    for (const b of blockedToDelete) {
      await b.destroyPermanently();
    }

    if (batch.length) {
      await database.batch(...batch);
    }
  });

  await database.write(async () => {
    const localBlockedMap = new Map<string, BlockedUser>();
    const localBlockedNow = await blockedCollection.query().fetch();
    for (const b of localBlockedNow) localBlockedMap.set(String(b.userId), b);

    for (const serverId of serverBlockedList) {
      const sId = String(serverId.id);
      if (!localBlockedMap.has(sId)) {
        await blockedCollection.create((r: any) => {
          r.userId = Number(sId);
          r.firstName = serverId.first_name;
          r.lastName = serverId.last_name;
          r.avatar = serverId.avatar ?? null;
          r.removed = false;
          r.isDirty = false;
          r.dirtyActions = null;
        });
      } else {
        const rec = localBlockedMap.get(sId)!;
        await rec.update((r: any) => {
          r.removed = false;
          r.isDirty = false;
          r.dirtyActions = null;
        });
      }
    }
  });
}

export async function syncChatsIncremental(token: string) {
  const net = await NetInfo.fetch();
  if (!net.isConnected) return;

  try {
    const speed = await testConnectionSpeed();
    if ((speed?.downloadSpeed && speed.downloadSpeed < 0.2) || (speed?.ping && speed.ping > 1500)) {
      console.warn('Internet too slow for sync');
      return;
    }
  } catch {}

  await pushLocalChanges(token);
  console.log('push ended');
  await pullUpdates(token);
  console.log('pull ended');
}
