/*
 * Silex website builder, free/libre no-code tool for makers.
 * Copyright (c) 2023 lexoyo and Silex Labs foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import { ConnectorId, WebsiteId, WebsiteData, ConnectorUser, ConnectorType, ApiError } from '../../types'
import { websiteLoad, websiteSave } from '../api'
import { cmdLogin, eventLoggedIn, eventLoggedOut, getCurrentUser, updateUser } from './LoginDialog'
import { addTempDataToAssetUrl, addTempDataToStyles, removeTempDataFromAssetUrl, removeTempDataFromStyles } from '../assetUrl'
import { PublicationStatus, PublishableEditor } from './PublicationManager'
import { ClientEvent } from '../events'
import { Page, PageProperties, ProjectData } from 'grapesjs'
import { html, render, TemplateResult } from 'lit-html'
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'

export const cmdPauseAutoSave = 'pause-auto-save'

const loader = document.querySelector('.silex-loader') as HTMLDivElement

function renderLoader(text: string, current: number, total: number) {
  if (loader) {
    render(getLoaderHtml(text, current, total), loader)
  }
}

if (loader) {
  loader.innerHTML = ''
  renderLoader('Loading', 0, 0)
  setTimeout(() => loader.classList.add('silex-loader--active'))
}

function getLoaderHtml(text: string, current: number, total: number): TemplateResult {
  return html`
    <div class="silex-loader__text">
      <div>${ unsafeHTML(text) }</div>
      <progress
        class="silex-loader__progress"
        max="${total}" value="${current}"></progress>
    </div>
  `
}

// Wait for the next frame to avoid blocking the main thread
function nextFrame(): Promise<void> {
  const fn = window.requestIdleCallback ?? window.requestAnimationFrame
  return new Promise(resolve => fn(() => resolve()))
}

// Mechanism to keep only the last save during publication
let lastPendingSaving: WebsiteData = null

// Mechanism to avoid concurrent saving
// Note: there is already such a mechanism in grapesjs but it fails when the save is delayed by the publication
let isSaving = false

export const storagePlugin = (editor: PublishableEditor) => {
  // Add useful commands
  editor.Commands.add(cmdPauseAutoSave, {
    run: () => {
      if(!editor.StorageManager.isAutosave()) console.warn('Autosave is not enabled')
      editor.StorageManager.setAutosave(false)
    },
    stop: () => {
      if(editor.StorageManager.isAutosave()) console.warn('Autosave is already enabled')
      editor.editor.set('changesCount', 0)
      editor.UndoManager.clear()
      editor.StorageManager.setAutosave(true)
    },
  })
  // Add the storage connector
  editor.Storage.add('connector', {
    async load(options: { id: WebsiteId, connectorId: ConnectorId, mode: '' | 'progressive' }): Promise<ProjectData> {
      try {
        renderLoader('Loading user data', 0, 3)
        await nextFrame()
        const user: ConnectorUser = await getCurrentUser(editor) ?? await updateUser(editor, ConnectorType.STORAGE, options.connectorId)
        if (!user) throw new ApiError('Not logged in', 401)
        renderLoader('Loading website', 1, 3)
        await nextFrame()
        const data = await websiteLoad({ websiteId: options.id, connectorId: user.storage.connectorId }) as WebsiteData
        editor.runCommand(cmdPauseAutoSave)
        if (data.assets) data.assets = addTempDataToAssetUrl(data.assets, options.id, user.storage.connectorId)
        if (data.styles) data.styles = addTempDataToStyles(data.styles, options.id, user.storage.connectorId)
        if (options.mode == 'progressive') {
          if (!data.pages) {
            // This happens when the website was just created
            // Let grapesjs create the pages in the frontend
          } else {
            const { pages, pagesFolder, ...rest } = data
            // Load any additional project data, e.g. symbols, but not the ones we progressive load
            renderLoader('Loading styles, assets and symbols', 0, pages.length + 1)
            await nextFrame()
            // Add to the project, everything but pages
            editor.loadProjectData(rest)
            editor.getModel().set('pagesFolder', pagesFolder)
            // Add the pages to the project
            await progressiveLoadPages(editor, pages)
            await nextFrame()
            // Trigger symbol update to recount instances now that pages are loaded
            editor.trigger('symbol')
            await nextFrame()
            // Select the first page
            const firstPage = editor.Pages.getAll()[0]
            if (firstPage) editor.Pages.select(firstPage)
          }
        }
        // Always return the full data in the end
        renderLoader('Starting', 1, 1)
        await nextFrame()
        editor.once('canvas:frame:load', () => {
          setTimeout(() => { // This is needed in chrome, otherwise a save is triggered
            editor.stopCommand(cmdPauseAutoSave)
          }, 500)
        })
        return data
      } catch (err) {
        editor.UndoManager.clear()
        if (err.httpStatusCode === 401) {
          editor.once(eventLoggedIn, async () => {
            // This should work but well it doesn't... window.location.reload()
            // Here comes the workaround:
            // eslint-disable-next-line no-self-assign
            window.location.href = window.location.href
            return null
            // try {
            //   await this.load(options)
            // } catch (err) {
            //   console.error('connectorPlugin load error', err)
            //   // Breaks grapesjs: throw err
            //   editor.trigger('storage:error:load', err)
            //   return null
            // }
          })
          editor.Commands.run(cmdLogin)
        }
        console.error('connectorPlugin load error', err)
        // Breaks grapesjs: throw err
        editor.trigger('storage:error:load', err)
      }
    },

    async store(data: WebsiteData, options: { id: WebsiteId, connectorId: ConnectorId }) {
      // Be sure that it is immutable
      const myData = { ...data }
      return await this.addToQueue(myData, options)
    },

    async addToQueue(data: WebsiteData, options: { id: WebsiteId, connectorId: ConnectorId }) {
      // Handle concurrent saving
      if (lastPendingSaving && lastPendingSaving !== data) {
        // Cancel previous saving
      }
      lastPendingSaving = data
      // Go ahaed and save
      return this.doStore(lastPendingSaving, options)
    },

    async doStore(data: WebsiteData, options: { id: WebsiteId, connectorId: ConnectorId }) {
      if (editor.PublicationManager?.status === PublicationStatus.STATUS_PENDING) {
        // Publication is pending, save is delayed
        return new Promise((resolve) => {
          editor.once(ClientEvent.PUBLISH_END, () => {
            resolve(this.doStore(data, options))
          })
        })
      }
      try {
        if (isSaving) {
          // Concurrent saving, save is delayed
          editor.once('storage:end:store', () => {
            this.doStore(data, options)
          })
        } else {
          if (lastPendingSaving === data) {
            lastPendingSaving = null
            isSaving = true
            const user = await getCurrentUser(editor)
            if (user) {
              data.assets = removeTempDataFromAssetUrl(data.assets)
              data.styles = removeTempDataFromStyles(data.styles)
              data.pagesFolder = editor.getModel().get('pagesFolder')
              await websiteSave({ websiteId: options.id, connectorId: user.storage.connectorId, data })
              isSaving = false
            } else {
              // This should never happen,
              // because the user canot save when they never logged in before
            }
          } else {
            // Canceled saving
          }
        }
      } catch (err) {
        console.error('connectorPlugin store error', err)
        isSaving = false
        if (err.httpStatusCode === 401) {
          editor.once(eventLoggedIn, () => {
            isSaving = false
            return editor.Storage.store(data, options)
          })
        }
        // Breaks grapesjs: throw err
        editor.trigger('storage:error:load', err)
      }
    }
  })
  // Handle errors
  editor.on('storage:error:load', (err: Error) => {
    handleError(err)
  })
  editor.on('storage:error:store', (err: Error) => {
    handleError(err)
  })
  editor.on('asset:upload:end', (err: Error) => {
    editor.store()
  })
  editor.on('asset:upload:error', (err: Error) => {
    handleError(err)
  })
  function handleError(_err: Error | string) {
    const err: Error = typeof _err === 'string' ? JSON.parse(_err) : _err
    console.error('Error with loading or saving website or uploading asset', err, err instanceof ApiError)
    if (err instanceof ApiError) {
      console.error('ApiError', err.httpStatusCode, err.message)
      switch (err.httpStatusCode) {
      case 404:
        return editor.Modal.open({
          title: 'Website not found',
          content: `This website could not be found.<br><hr>${err.message}`,
        })
      case 403:
        return editor.Modal.open({
          title: 'Access denied',
          content: `You are not allowed to access this website.<br><hr>${err.message}`,
        })
      case 401:
        editor.trigger(eventLoggedOut)
        return
      default:
        return editor.Modal.open({
          title: 'Error',
          content: `An error occured.<br>${err.message}`,
        })
      }
    } else {
      return editor.Modal.open({
        title: 'Error',
        content: `An error occured.<br>${err.message}`,
      })
    }
  }
}

async function progressiveLoadPages(editor: PublishableEditor, pages: Page[]) {
  editor.Pages.getAll().forEach(page => editor.Pages.remove(page))
  let i = 0
  for (const page of pages) {
    renderLoader(`Loading page <strong>${++i}</strong> / ${pages.length + 1}`, i, pages.length + 1)
    await nextFrame()
    const newPage = editor.Pages.add({
      ...page,
    } as PageProperties)
  }
}
