/* eslint-disable no-restricted-globals, no-restricted-syntax */

import {
  Environment,
  translateToNewGQLCollection,
  translateToNewRESTCollection,
} from "@hoppscotch/data"
import { StorageLike, watchDebounced } from "@vueuse/core"
import { Service } from "dioc"
import { assign, clone, isEmpty } from "lodash-es"
import { z } from "zod"

import { GQLTabService } from "~/services/tab/graphql"
import { RESTTabService } from "~/services/tab/rest"

import { useToast } from "~/composables/toast"
import { MQTTRequest$, setMQTTRequest } from "../../newstore/MQTTSession"
import { SSERequest$, setSSERequest } from "../../newstore/SSESession"
import { SIORequest$, setSIORequest } from "../../newstore/SocketIOSession"
import { WSRequest$, setWSRequest } from "../../newstore/WebSocketSession"
import {
  graphqlCollectionStore,
  restCollectionStore,
  setGraphqlCollections,
  setRESTCollections,
} from "../../newstore/collections"
import {
  addGlobalEnvVariable,
  environments$,
  globalEnv$,
  replaceEnvironments,
  selectedEnvironmentIndex$,
  setGlobalEnvVariables,
  setSelectedEnvironmentIndex,
} from "../../newstore/environments"
import {
  graphqlHistoryStore,
  restHistoryStore,
  setGraphqlHistoryEntries,
  setRESTHistoryEntries,
  translateToNewGQLHistory,
  translateToNewRESTHistory,
} from "../../newstore/history"
import { bulkApplyLocalState, localStateStore } from "../../newstore/localstate"
import {
  HoppAccentColor,
  HoppBgColor,
  applySetting,
  bulkApplySettings,
  getDefaultSettings,
  performSettingsDataMigrations,
  settingsStore,
} from "../../newstore/settings"
import {
  ENVIRONMENTS_SCHEMA,
  GLOBAL_ENV_SCHEMA,
  GQL_COLLECTION_SCHEMA,
  GQL_HISTORY_ENTRY_SCHEMA,
  GQL_TAB_STATE_SCHEMA,
  LOCAL_STATE_SCHEMA,
  MQTT_REQUEST_SCHEMA,
  NUXT_COLOR_MODE_SCHEMA,
  REST_COLLECTION_SCHEMA,
  REST_HISTORY_ENTRY_SCHEMA,
  REST_TAB_STATE_SCHEMA,
  SELECTED_ENV_INDEX_SCHEMA,
  SETTINGS_SCHEMA,
  SOCKET_IO_REQUEST_SCHEMA,
  SSE_REQUEST_SCHEMA,
  SECRET_ENVIRONMENT_VARIABLE_SCHEMA,
  THEME_COLOR_SCHEMA,
  VUEX_SCHEMA,
  WEBSOCKET_REQUEST_SCHEMA,
} from "./validation-schemas"
import { SecretEnvironmentService } from "../secret-environment.service"

/**
 * This service compiles persistence logic across the codebase
 */
export class PersistenceService extends Service {
  public static readonly ID = "PERSISTENCE_SERVICE"

  private readonly restTabService = this.bind(RESTTabService)
  private readonly gqlTabService = this.bind(GQLTabService)

  private readonly secretEnvironmentService = this.bind(
    SecretEnvironmentService
  )

  public hoppLocalConfigStorage: StorageLike = localStorage

  constructor() {
    super()
  }

  private showErrorToast(localStorageKey: string) {
    const toast = useToast()
    toast.error(
      `There's a mismatch with the expected schema for the value corresponding to ${localStorageKey} read from localStorage, keeping a backup in ${localStorageKey}-backup`
    )
  }

  private checkAndMigrateOldSettings() {
    if (window.localStorage.getItem("selectedEnvIndex")) {
      const index = window.localStorage.getItem("selectedEnvIndex")
      if (index) {
        if (index === "-1") {
          window.localStorage.setItem(
            "selectedEnvIndex",
            JSON.stringify({
              type: "NO_ENV_SELECTED",
            })
          )
        } else if (Number(index) >= 0) {
          window.localStorage.setItem(
            "selectedEnvIndex",
            JSON.stringify({
              type: "MY_ENV",
              index: parseInt(index),
            })
          )
        }
      }
    }

    const vuexKey = "vuex"
    let vuexData = JSON.parse(window.localStorage.getItem(vuexKey) || "{}")

    if (isEmpty(vuexData)) return

    // Validate data read from localStorage
    const result = VUEX_SCHEMA.safeParse(vuexData)
    if (result.success) {
      vuexData = result.data
    } else {
      this.showErrorToast(vuexKey)
      window.localStorage.setItem(`${vuexKey}-backup`, JSON.stringify(vuexData))
    }

    const { postwoman } = vuexData

    if (!isEmpty(postwoman?.settings)) {
      const settingsData = assign(
        clone(getDefaultSettings()),
        postwoman.settings
      )

      window.localStorage.setItem("settings", JSON.stringify(settingsData))

      delete postwoman.settings
      window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
    }

    if (postwoman?.collections) {
      window.localStorage.setItem(
        "collections",
        JSON.stringify(postwoman.collections)
      )

      delete postwoman.collections
      window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
    }

    if (postwoman?.collectionsGraphql) {
      window.localStorage.setItem(
        "collectionsGraphql",
        JSON.stringify(postwoman.collectionsGraphql)
      )

      delete postwoman.collectionsGraphql
      window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
    }

    if (postwoman?.environments) {
      window.localStorage.setItem(
        "environments",
        JSON.stringify(postwoman.environments)
      )

      delete postwoman.environments
      window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
    }

    const themeColorKey = "THEME_COLOR"
    let themeColorValue = window.localStorage.getItem(themeColorKey)

    if (themeColorValue) {
      // Validate data read from localStorage
      const result = THEME_COLOR_SCHEMA.safeParse(themeColorValue)

      if (result.success) {
        themeColorValue = result.data
      } else {
        this.showErrorToast(themeColorKey)
        window.localStorage.setItem(`${themeColorKey}-backup`, themeColorValue)
      }

      applySetting(themeColorKey, themeColorValue as HoppAccentColor)
      window.localStorage.removeItem(themeColorKey)
    }

    const nuxtColorModeKey = "nuxt-color-mode"
    let nuxtColorModeValue = window.localStorage.getItem(nuxtColorModeKey)

    if (nuxtColorModeValue) {
      // Validate data read from localStorage
      const result = NUXT_COLOR_MODE_SCHEMA.safeParse(nuxtColorModeValue)

      if (result.success) {
        nuxtColorModeValue = result.data
      } else {
        this.showErrorToast(nuxtColorModeKey)
        window.localStorage.setItem(
          `${nuxtColorModeKey}-backup`,
          nuxtColorModeValue
        )
      }

      applySetting("BG_COLOR", nuxtColorModeValue as HoppBgColor)
      window.localStorage.removeItem(nuxtColorModeKey)
    }
  }

  public setupLocalStatePersistence() {
    const localStateKey = "localState"
    let localStateData = JSON.parse(
      window.localStorage.getItem(localStateKey) ?? "{}"
    )

    // Validate data read from localStorage
    const result = LOCAL_STATE_SCHEMA.safeParse(localStateData)

    if (result.success) {
      localStateData = result.data
    } else {
      this.showErrorToast(localStateKey)
      window.localStorage.setItem(
        `${localStateKey}-backup`,
        JSON.stringify(localStateData)
      )
    }

    if (localStateData) bulkApplyLocalState(localStateData)

    localStateStore.subject$.subscribe((state) => {
      window.localStorage.setItem(localStateKey, JSON.stringify(state))
    })
  }

  private setupSettingsPersistence() {
    const settingsKey = "settings"
    let settingsData = JSON.parse(
      window.localStorage.getItem(settingsKey) ?? "null"
    )

    if (!settingsData) {
      settingsData = getDefaultSettings()
    }

    // Validate data read from localStorage
    const result = SETTINGS_SCHEMA.safeParse(settingsData)
    if (result.success) {
      settingsData = result.data
    } else {
      this.showErrorToast(settingsKey)
      window.localStorage.setItem(
        `${settingsKey}-backup`,
        JSON.stringify(settingsData)
      )
    }

    const updatedSettings = settingsData
      ? performSettingsDataMigrations(settingsData)
      : settingsData

    if (updatedSettings) {
      bulkApplySettings(updatedSettings)
    }

    settingsStore.subject$.subscribe((settings) => {
      window.localStorage.setItem(settingsKey, JSON.stringify(settings))
    })
  }

  private setupHistoryPersistence() {
    const restHistoryKey = "history"
    let restHistoryData = JSON.parse(
      window.localStorage.getItem(restHistoryKey) || "[]"
    )

    const graphqlHistoryKey = "graphqlHistory"
    let graphqlHistoryData = JSON.parse(
      window.localStorage.getItem(graphqlHistoryKey) || "[]"
    )

    // Validate data read from localStorage
    const restHistorySchemaParsedresult = z
      .array(REST_HISTORY_ENTRY_SCHEMA)
      .safeParse(restHistoryData)

    if (restHistorySchemaParsedresult.success) {
      restHistoryData = restHistorySchemaParsedresult.data
    } else {
      this.showErrorToast(restHistoryKey)
      window.localStorage.setItem(
        `${restHistoryKey}-backup`,
        JSON.stringify(restHistoryData)
      )
    }

    const gqlHistorySchemaParsedresult = z
      .array(GQL_HISTORY_ENTRY_SCHEMA)
      .safeParse(graphqlHistoryData)

    if (gqlHistorySchemaParsedresult.success) {
      graphqlHistoryData = gqlHistorySchemaParsedresult.data
    } else {
      this.showErrorToast(graphqlHistoryKey)
      window.localStorage.setItem(
        `${graphqlHistoryKey}-backup`,
        JSON.stringify(graphqlHistoryData)
      )
    }

    const translatedRestHistoryData = restHistoryData.map(
      translateToNewRESTHistory
    )
    const translatedGraphqlHistoryData = graphqlHistoryData.map(
      translateToNewGQLHistory
    )

    setRESTHistoryEntries(translatedRestHistoryData)
    setGraphqlHistoryEntries(translatedGraphqlHistoryData)

    restHistoryStore.subject$.subscribe(({ state }) => {
      window.localStorage.setItem(restHistoryKey, JSON.stringify(state))
    })

    graphqlHistoryStore.subject$.subscribe(({ state }) => {
      window.localStorage.setItem(graphqlHistoryKey, JSON.stringify(state))
    })
  }

  private setupCollectionsPersistence() {
    const restCollectionsKey = "collections"
    let restCollectionsData = JSON.parse(
      window.localStorage.getItem(restCollectionsKey) || "[]"
    )

    const graphqlCollectionsKey = "collectionsGraphql"
    let graphqlCollectionsData = JSON.parse(
      window.localStorage.getItem(graphqlCollectionsKey) || "[]"
    )

    // Validate data read from localStorage
    const restCollectionsSchemaParsedresult = z
      .array(REST_COLLECTION_SCHEMA)
      .safeParse(restCollectionsData)

    if (restCollectionsSchemaParsedresult.success) {
      restCollectionsData = restCollectionsSchemaParsedresult.data
    } else {
      this.showErrorToast(restCollectionsKey)
      window.localStorage.setItem(
        `${restCollectionsKey}-backup`,
        JSON.stringify(restCollectionsData)
      )
    }

    const gqlCollectionsSchemaParsedresult = z
      .array(GQL_COLLECTION_SCHEMA)
      .safeParse(graphqlCollectionsData)

    if (gqlCollectionsSchemaParsedresult.success) {
      graphqlCollectionsData = gqlCollectionsSchemaParsedresult.data
    } else {
      this.showErrorToast(graphqlCollectionsKey)
      window.localStorage.setItem(
        `${graphqlCollectionsKey}-backup`,
        JSON.stringify(graphqlCollectionsData)
      )
    }

    const translatedRestCollectionsData = restCollectionsData.map(
      translateToNewRESTCollection
    )
    const translatedGraphqlCollectionsData = graphqlCollectionsData.map(
      translateToNewGQLCollection
    )

    setRESTCollections(translatedRestCollectionsData)
    setGraphqlCollections(translatedGraphqlCollectionsData)

    restCollectionStore.subject$.subscribe(({ state }) => {
      window.localStorage.setItem(restCollectionsKey, JSON.stringify(state))
    })

    graphqlCollectionStore.subject$.subscribe(({ state }) => {
      window.localStorage.setItem(graphqlCollectionsKey, JSON.stringify(state))
    })
  }

  private setupEnvironmentsPersistence() {
    const environmentsKey = "environments"
    let environmentsData: Environment[] = JSON.parse(
      window.localStorage.getItem(environmentsKey) || "[]"
    )

    // Validate data read from localStorage
    const result = ENVIRONMENTS_SCHEMA.safeParse(environmentsData)
    if (result.success) {
      environmentsData = result.data
    } else {
      this.showErrorToast(environmentsKey)
      window.localStorage.setItem(
        `${environmentsKey}-backup`,
        JSON.stringify(environmentsData)
      )
    }

    // Check if a global env is defined and if so move that to globals
    const globalIndex = environmentsData.findIndex(
      (x) => x.name.toLowerCase() === "globals"
    )

    if (globalIndex !== -1) {
      const globalEnv = environmentsData[globalIndex]
      globalEnv.variables.forEach((variable) => addGlobalEnvVariable(variable))

      // Remove global from environments
      environmentsData.splice(globalIndex, 1)

      // Just sync the changes manually
      window.localStorage.setItem(
        environmentsKey,
        JSON.stringify(environmentsData)
      )
    }

    replaceEnvironments(environmentsData)

    environments$.subscribe((envs) => {
      window.localStorage.setItem(environmentsKey, JSON.stringify(envs))
    })
  }

  private setupSecretEnvironmentsPersistence() {
    const secretEnvironmentsKey = "secretEnvironments"
    const secretEnvironmentsData = window.localStorage.getItem(
      secretEnvironmentsKey
    )

    try {
      if (secretEnvironmentsData) {
        let parsedSecretEnvironmentsData = JSON.parse(secretEnvironmentsData)

        // Validate data read from localStorage
        const result = SECRET_ENVIRONMENT_VARIABLE_SCHEMA.safeParse(
          parsedSecretEnvironmentsData
        )

        if (result.success) {
          parsedSecretEnvironmentsData = result.data
        } else {
          this.showErrorToast(secretEnvironmentsKey)
          window.localStorage.setItem(
            `${secretEnvironmentsKey}-backup`,
            JSON.stringify(parsedSecretEnvironmentsData)
          )
        }

        this.secretEnvironmentService.loadSecretEnvironmentsFromPersistedState(
          parsedSecretEnvironmentsData
        )
      }
    } catch (e) {
      console.error(
        `Failed parsing persisted secret environment, state:`,
        secretEnvironmentsData
      )
    }

    watchDebounced(
      this.secretEnvironmentService.persistableSecretEnvironments,
      (newSecretEnvironment) => {
        window.localStorage.setItem(
          secretEnvironmentsKey,
          JSON.stringify(newSecretEnvironment)
        )
      },
      {
        debounce: 500,
      }
    )
  }

  private setupSelectedEnvPersistence() {
    const selectedEnvIndexKey = "selectedEnvIndex"
    let selectedEnvIndexValue = JSON.parse(
      window.localStorage.getItem(selectedEnvIndexKey) ?? "null"
    )

    // Validate data read from localStorage
    const result = SELECTED_ENV_INDEX_SCHEMA.safeParse(selectedEnvIndexValue)
    if (result.success) {
      selectedEnvIndexValue = result.data
    } else {
      this.showErrorToast(selectedEnvIndexKey)
      window.localStorage.setItem(
        `${selectedEnvIndexKey}-backup`,
        JSON.stringify(selectedEnvIndexValue)
      )
    }

    // If there is a selected env index, set it to the store else set it to null
    if (selectedEnvIndexValue) {
      setSelectedEnvironmentIndex(selectedEnvIndexValue)
    } else {
      setSelectedEnvironmentIndex({
        type: "NO_ENV_SELECTED",
      })
    }

    selectedEnvironmentIndex$.subscribe((envIndex) => {
      window.localStorage.setItem(selectedEnvIndexKey, JSON.stringify(envIndex))
    })
  }

  private setupWebsocketPersistence() {
    const wsRequestKey = "WebsocketRequest"
    let wsRequestData = JSON.parse(
      window.localStorage.getItem(wsRequestKey) || "null"
    )

    // Validate data read from localStorage
    const result = WEBSOCKET_REQUEST_SCHEMA.safeParse(wsRequestData)
    if (result.success) {
      wsRequestData = result.data
    } else {
      this.showErrorToast(wsRequestKey)
      window.localStorage.setItem(
        `${wsRequestKey}-backup`,
        JSON.stringify(wsRequestData)
      )
    }

    setWSRequest(wsRequestData)

    WSRequest$.subscribe((req) => {
      window.localStorage.setItem(wsRequestKey, JSON.stringify(req))
    })
  }

  private setupSocketIOPersistence() {
    const sioRequestKey = "SocketIORequest"
    let sioRequestData = JSON.parse(
      window.localStorage.getItem(sioRequestKey) || "null"
    )

    // Validate data read from localStorage
    const result = SOCKET_IO_REQUEST_SCHEMA.safeParse(sioRequestData)
    if (result.success) {
      sioRequestData = result.data
    } else {
      this.showErrorToast(sioRequestKey)
      window.localStorage.setItem(
        `${sioRequestKey}-backup`,
        JSON.stringify(sioRequestData)
      )
    }

    setSIORequest(sioRequestData)

    SIORequest$.subscribe((req) => {
      window.localStorage.setItem(sioRequestKey, JSON.stringify(req))
    })
  }

  private setupSSEPersistence() {
    const sseRequestKey = "SSERequest"
    let sseRequestData = JSON.parse(
      window.localStorage.getItem(sseRequestKey) || "null"
    )

    // Validate data read from localStorage
    const result = SSE_REQUEST_SCHEMA.safeParse(sseRequestData)
    if (result.success) {
      sseRequestData = result.data
    } else {
      this.showErrorToast(sseRequestKey)
      window.localStorage.setItem(
        `${sseRequestKey}-backup`,
        JSON.stringify(sseRequestData)
      )
    }

    setSSERequest(sseRequestData)

    SSERequest$.subscribe((req) => {
      window.localStorage.setItem(sseRequestKey, JSON.stringify(req))
    })
  }

  private setupMQTTPersistence() {
    const mqttRequestKey = "MQTTRequest"
    let mqttRequestData = JSON.parse(
      window.localStorage.getItem(mqttRequestKey) || "null"
    )

    // Validate data read from localStorage
    const result = MQTT_REQUEST_SCHEMA.safeParse(mqttRequestData)
    if (result.success) {
      mqttRequestData = result.data
    } else {
      this.showErrorToast(mqttRequestKey)
      window.localStorage.setItem(
        `${mqttRequestKey}-backup`,
        JSON.stringify(mqttRequestData)
      )
    }

    setMQTTRequest(mqttRequestData)

    MQTTRequest$.subscribe((req) => {
      window.localStorage.setItem(mqttRequestKey, JSON.stringify(req))
    })
  }

  private setupGlobalEnvsPersistence() {
    const globalEnvKey = "globalEnv"
    let globalEnvData: Environment["variables"] = JSON.parse(
      window.localStorage.getItem(globalEnvKey) || "[]"
    )

    // Validate data read from localStorage
    const result = GLOBAL_ENV_SCHEMA.safeParse(globalEnvData)
    if (result.success) {
      globalEnvData = result.data
    } else {
      this.showErrorToast(globalEnvKey)
      window.localStorage.setItem(
        `${globalEnvKey}-backup`,
        JSON.stringify(globalEnvData)
      )
    }

    setGlobalEnvVariables(globalEnvData)

    globalEnv$.subscribe((vars) => {
      window.localStorage.setItem(globalEnvKey, JSON.stringify(vars))
    })
  }

  private setupGQLTabsPersistence() {
    const gqlTabStateKey = "gqlTabState"
    const gqlTabStateData = window.localStorage.getItem(gqlTabStateKey)

    try {
      if (gqlTabStateData) {
        let parsedGqlTabStateData = JSON.parse(gqlTabStateData)

        // Validate data read from localStorage
        const result = GQL_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)

        if (result.success) {
          parsedGqlTabStateData = result.data
        } else {
          this.showErrorToast(gqlTabStateKey)
          window.localStorage.setItem(
            `${gqlTabStateKey}-backup`,
            JSON.stringify(parsedGqlTabStateData)
          )
        }

        this.gqlTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
      }
    } catch (e) {
      console.error(
        `Failed parsing persisted tab state, state:`,
        gqlTabStateData
      )
    }

    watchDebounced(
      this.gqlTabService.persistableTabState,
      (newGqlTabStateData) => {
        window.localStorage.setItem(
          gqlTabStateKey,
          JSON.stringify(newGqlTabStateData)
        )
      },
      { debounce: 500, deep: true }
    )
  }

  private setupRESTTabsPersistence() {
    const restTabStateKey = "restTabState"
    const restTabStateData = window.localStorage.getItem(restTabStateKey)

    try {
      if (restTabStateData) {
        let parsedGqlTabStateData = JSON.parse(restTabStateData)

        // Validate data read from localStorage
        const result = REST_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)

        if (result.success) {
          parsedGqlTabStateData = result.data
        } else {
          this.showErrorToast(restTabStateKey)
          window.localStorage.setItem(
            `${restTabStateKey}-backup`,
            JSON.stringify(parsedGqlTabStateData)
          )
        }

        this.restTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
      }
    } catch (e) {
      console.error(
        `Failed parsing persisted tab state, state:`,
        restTabStateData
      )
    }

    watchDebounced(
      this.restTabService.persistableTabState,
      (newRestTabStateData) => {
        window.localStorage.setItem(
          restTabStateKey,
          JSON.stringify(newRestTabStateData)
        )
      },
      { debounce: 500, deep: true }
    )
  }

  public setupLocalPersistence() {
    this.checkAndMigrateOldSettings()

    this.setupLocalStatePersistence()
    this.setupSettingsPersistence()
    this.setupRESTTabsPersistence()

    this.setupGQLTabsPersistence()

    this.setupHistoryPersistence()
    this.setupCollectionsPersistence()
    this.setupGlobalEnvsPersistence()
    this.setupEnvironmentsPersistence()
    this.setupSelectedEnvPersistence()
    this.setupWebsocketPersistence()
    this.setupSocketIOPersistence()
    this.setupSSEPersistence()
    this.setupMQTTPersistence()

    this.setupSecretEnvironmentsPersistence()
  }

  /**
   * Gets a value from localStorage
   *
   * NOTE: Use localStorage to only store non-reactive simple data
   * For more complex data, use stores and connect it to `PersistenceService`
   */
  public getLocalConfig(name: string) {
    return window.localStorage.getItem(name)
  }

  /**
   * Sets a value in localStorage
   *
   * NOTE: Use localStorage to only store non-reactive simple data
   * For more complex data, use stores and connect it to `PersistenceService`
   */
  public setLocalConfig(key: string, value: string) {
    window.localStorage.setItem(key, value)
  }

  /**
   * Clear config value in localStorage
   */
  public removeLocalConfig(key: string) {
    window.localStorage.removeItem(key)
  }
}
