/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import createStore from "../common/store";
import { db } from "../common/db";
import { store as noteStore } from "./note-store";
import { store as notebookStore } from "./notebook-store";
import { store as trashStore } from "./trash-store";
import { store as tagStore } from "./tag-store";
import { store as attachmentStore } from "./attachment-store";
import { store as monographStore } from "./monograph-store";
import { store as reminderStore } from "./reminder-store";
import { store as settingStore } from "./setting-store";
import BaseStore from "./index";
import { showToast } from "../utils/toast";
import { Notice } from "../common/notices";
import { EVENTS, SYNC_CHECK_IDS, SyncOptions } from "@notesnook/core";
import { logger } from "../utils/logger";
import Config from "../utils/config";
import {
  onNetworkStatusChanged,
  onPageVisibilityChanged
} from "../utils/page-visibility";
import { NetworkCheck } from "../utils/network-check";
import { Color, Notebook, Tag } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useEditorStore } from "./editor-store";
import { useEditorManager } from "../components/editor/manager";
import { exitFullscreen } from "../utils/fullscreen";
import { NavigationTabItem } from "../components/navigation-menu";

type SyncState =
  | "synced"
  | "syncing"
  | "conflicts"
  | "failed"
  | "completed"
  | "offline"
  | "disabled";
type SyncStatus = {
  key: SyncState;
  progress: number | null;
  type?: "download" | "upload" | "sync";
};
const networkCheck = new NetworkCheck();
let syncStatusTimeout = 0;
let syncTimeout = 0;
let pendingSync: SyncOptions | undefined = undefined;

class AppStore extends BaseStore<AppStore> {
  isFocusMode = false;
  isListPaneVisible = true;
  isNavPaneCollapsed = false;
  isVaultCreated = false;
  vaultLockAfter = 1000 * 60 * 30;
  isAutoSyncEnabled = Config.get("autoSyncEnabled", true);
  isSyncEnabled = Config.get("syncEnabled", true);
  isRealtimeSyncEnabled = Config.get("isRealtimeSyncEnabled", true);
  syncStatus: SyncStatus = {
    key: navigator.onLine ? "disabled" : "offline",
    progress: null,
    type: undefined
  };
  colors: Color[] = [];
  hiddenColors: string[] = Config.get("sidebarHiddenItems:colors", []);
  hiddenRoutes: string[] = Config.get("sidebarHiddenItems:routes", []);
  notices: Notice[] = [];
  shortcuts: (Notebook | Tag)[] = [];
  lastSynced = 0;
  navigationTab: NavigationTabItem["id"] = settingStore.get().defaultSidebarTab;

  init = () => {
    this.refresh();
    this.set({
      hiddenColors: db.settings.getSideBarHiddenItems("colors"),
      hiddenRoutes: db.settings.getSideBarHiddenItems("routes"),
      vaultLockAfter: db.settings.getVaultLockAfter()
    });
    this.get().sync({ type: "full" });

    db.eventManager.subscribe(EVENTS.appRefreshRequested, () => this.refresh());
    db.eventManager.subscribe(EVENTS.syncCompleted, () => this.refresh());

    db.eventManager.subscribe(EVENTS.syncProgress, ({ type, current }) => {
      this.set((state) => {
        state.syncStatus = {
          key: "syncing",
          progress: current,
          type
        };
      });
    });

    db.eventManager.subscribe(EVENTS.syncCheckStatus, async (type) => {
      const { isAutoSyncEnabled, isSyncEnabled } = this.get();
      switch (type) {
        case SYNC_CHECK_IDS.sync:
          return { type, result: isSyncEnabled };
        case SYNC_CHECK_IDS.autoSync:
          return { type, result: isAutoSyncEnabled };
        default:
          return { type, result: true };
      }
    });

    db.eventManager.subscribe(
      EVENTS.databaseSyncRequested,
      async (full, force) => {
        if (!this.get().isAutoSyncEnabled) return;
        await this.get().sync({ type: full ? "full" : "send", force });
      }
    );

    db.eventManager.subscribe(EVENTS.syncAborted, () => {
      this.updateSyncStatus("failed");
    });

    onNetworkStatusChanged(async (status) => {
      if (status === "offline") {
        await this.abortSync("offline");
      } else {
        // a slight delay to make sure sockets are open and can be connected
        // to. Otherwise, this fails miserably.
        await networkCheck.waitForInternet();
        await this.get().sync({ type: "full" });
        await db.connectSSE({ force: true }).catch(logger.error);
      }
    });

    onPageVisibilityChanged(async (_, documentHidden) => {
      if (documentHidden) return;

      logger.info("Page visibility changed. Reconnecting SSE...");
      await this.get().sync({ type: "full" });
      await db.connectSSE({ force: false }).catch(logger.error);
    });
  };

  setHiddenColors = (ids: string[]) => {
    Config.set("sidebarHiddenItems:colors", ids);
    db.settings.setSideBarHiddenItems("colors", ids);
    this.set({ hiddenColors: ids });
  };

  setHiddenRoutes = (ids: string[]) => {
    Config.set("sidebarHiddenItems:routes", ids);
    db.settings.setSideBarHiddenItems("routes", ids);
    this.set({ hiddenRoutes: ids });
  };

  toggleListPane = (booleanState?: boolean) => {
    this.set(
      (state) =>
        (state.isListPaneVisible = booleanState ?? !state.isListPaneVisible)
    );
  };

  refresh = async () => {
    logger.measure("refreshing app");

    await this.updateLastSynced();
    // await resetNotices();
    await noteStore.refresh();
    await notebookStore.refresh();
    await reminderStore.refresh();
    await trashStore.refresh();
    await tagStore.refresh();
    await attachmentStore.refresh();
    await monographStore.refresh();
    await settingStore.refresh();

    await this.refreshNavItems();

    this.set({ isVaultCreated: await db.vault.exists() });

    logger.measure("refreshing app");
  };

  refreshNavItems = async () => {
    const shortcuts = await db.shortcuts.resolved();
    const colors = await db.colors.all.items();
    this.set((state) => {
      state.shortcuts = shortcuts;
      state.colors = colors;
    });
  };

  toggleFocusMode = () => {
    this.set((state) => (state.isFocusMode = !state.isFocusMode));
    if (document.fullscreenElement) exitFullscreen();
    const session = useEditorStore.getState().getActiveSession();
    const editor = session && useEditorManager.getState().getEditor(session.id);
    if (editor) editor.editor?.focus();
  };

  toggleAutoSync = () => {
    Config.set("autoSyncEnabled", !this.get().isAutoSyncEnabled);
    this.set((state) => (state.isAutoSyncEnabled = !state.isAutoSyncEnabled));
  };

  toggleSync = () => {
    const { isSyncEnabled } = this.get();
    Config.set("syncEnabled", !isSyncEnabled);
    this.set((state) => (state.isSyncEnabled = !state.isSyncEnabled));

    if (isSyncEnabled) {
      this.abortSync("disabled");
    }
  };

  toggleRealtimeSync = () => {
    const { isRealtimeSyncEnabled } = this.get();
    Config.set("isRealtimeSyncEnabled", !isRealtimeSyncEnabled);
    this.set(
      (state) => (state.isRealtimeSyncEnabled = !state.isRealtimeSyncEnabled)
    );
  };

  setIsVaultCreated = (toggleState: boolean) => {
    this.set((state) => (state.isVaultCreated = toggleState));
  };

  setVaultLockAfter = async (ms: number) => {
    await db.settings.setVaultLockAfter(ms);
    this.set((state) => (state.vaultLockAfter = ms));

    if (db.vault.unlocked) {
      await db.vault.lock();
      showToast("success", strings.vaultLocked());
    }
  };

  setNotices = (...notices: Notice[]) => {
    this.set((state) => {
      for (const notice of notices) {
        const oldIndex = state.notices.findIndex((a) => a.type === notice.type);
        if (oldIndex > -1) state.notices[oldIndex] = notice;
        else state.notices.push(notice);
      }
    });
  };

  dismissNotices = (...notices: Notice[]) => {
    this.set((state) => {
      for (const notice of notices) {
        state.notices.splice(state.notices.indexOf(notice), 1);
      }
    });
  };

  addToShortcuts = async (item: { type: "tag" | "notebook"; id: string }) => {
    if (db.shortcuts.exists(item.id)) {
      await db.shortcuts.remove(item.id);
      this.refreshNavItems();
      showToast("success", strings.shortcutRemoved());
    } else {
      await db.shortcuts.add({
        itemType: item.type,
        itemId: item.id
      });
      this.refreshNavItems();
      showToast("success", strings.shortcutCreated());
    }

    // refresh the respective list
    switch (item.type) {
      case "notebook": {
        notebookStore.refresh();
        break;
      }
      case "tag": {
        tagStore.refresh();
        break;
      }
      default:
        return;
    }
  };

  updateLastSynced = async () => {
    const lastSynced = await db.lastSynced();
    this.set((state) => (state.lastSynced = lastSynced));
  };

  sync = async (options: SyncOptions = { type: "full", force: false }) => {
    if (this.isSyncing() || !this.get().isSyncEnabled) {
      logger.info("Ignoring duplicate sync", {
        options,
        syncing: this.isSyncing(),
        syncDisabled: !this.get().isSyncEnabled,
        offline: !navigator.onLine
      });
      if (this.isSyncing()) pendingSync = options;
      return;
    }

    clearTimeout(syncTimeout);
    syncTimeout = setTimeout(async () => {
      clearTimeout(syncStatusTimeout);
      this.updateSyncStatus("syncing");
      this.updateLastSynced();

      try {
        options.offlineMode = settingStore.get().isFullOfflineMode;
        const result = await db.sync(options);

        if (!result) return this.updateSyncStatus("failed");
        this.updateSyncStatus("completed", true);

        await this.updateLastSynced();

        if (pendingSync) {
          logger.info("Running pending sync", pendingSync);
          const syncOptions = { ...pendingSync };
          pendingSync = undefined;
          await this.get().sync(syncOptions);
        }
      } catch (err) {
        if (!(err instanceof Error)) {
          console.error(err);
          return;
        }

        logger.error(err);
        if (err.cause === "MERGE_CONFLICT") {
          await this.refresh();
          this.updateSyncStatus("conflicts");
        } else {
          this.updateSyncStatus("failed");
        }

        if (
          err?.message?.indexOf("Failed to fetch") > -1 ||
          err?.message?.indexOf("Could not connect to the Sync server.") > -1 ||
          !navigator.onLine ||
          !(await networkCheck.waitForInternet())
        )
          return;

        showToast("error", err.message);
      }
    }, 300) as unknown as number;
  };

  abortSync = async (status: SyncState = "offline") => {
    if (this.isSyncing()) this.updateSyncStatus("failed");
    else this.updateSyncStatus(status);

    await db.syncer.stop();
  };

  updateSyncStatus = (key: SyncState, reset = false) => {
    logger.info(`Sync status updated: ${key}`);
    this.set((state) => {
      state.syncStatus = { key, progress: null, type: undefined };
    });

    if (reset) {
      syncStatusTimeout = setTimeout(() => {
        if (this.get().syncStatus.key === key)
          this.updateSyncStatus("synced", false);
      }, 3000) as unknown as number;
    }
  };

  isSyncing = () => {
    const status = this.get().syncStatus.key;
    return status === "syncing";
  };

  setNavigationTab = (tab: NavigationTabItem["id"]) => {
    this.set((state) => (state.navigationTab = tab));
  };
}

const [useStore, store] = createStore<AppStore>(
  (set, get) => new AppStore(set, get)
);
export { useStore, store };
