/*
 * 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 { DataSourceId, DATA_SOURCE_DATA_LOAD_START, DATA_SOURCE_DATA_LOAD_END, DATA_SOURCE_DATA_LOAD_CANCEL } from '../types'
import { getPreviewData as getPreviewDataFromManager, setPreviewData as setPreviewDataInManager } from './dataSourceManager'
import { getAllDataSources } from './dataSourceRegistry'
import { getPageQuery } from './queryBuilder'
import { Page, Editor } from 'grapesjs'
import { NOTIFICATION_GROUP } from '../utils'

/**
 * Preview data loader state
 */
interface PreviewDataLoaderState {
  editor: Editor
  currentUpdatePid: number
  lastQueries: Record<DataSourceId, string>
}

// Global loader instance
let globalLoader: PreviewDataLoaderState | null = null

/**
 * Initialize the preview data loader
 */
export function initializePreviewDataLoader(editor: Editor): void {
  globalLoader = {
    editor,
    currentUpdatePid: 0,
    lastQueries: {},
  }
}

/**
 * Get the global loader (throws if not initialized)
 */
function getLoader(): PreviewDataLoaderState {
  if (!globalLoader) {
    throw new Error('PreviewDataLoader not initialized. Call initializePreviewDataLoader first.')
  }
  return globalLoader
}

/**
 * Compare two query objects to see if they are equal
 */
function areQueriesEqual(queries1: Record<DataSourceId, string>, queries2: Record<DataSourceId, string>): boolean {
  const keys1 = Object.keys(queries1).sort()
  const keys2 = Object.keys(queries2).sort()

  // Check if they have the same number of keys
  if (keys1.length !== keys2.length) {
    return false
  }

  // Check if all keys are the same
  if (!keys1.every(key => keys2.includes(key))) {
    return false
  }

  // Check if all values are the same
  return keys1.every(key => queries1[key] === queries2[key])
}

/**
 * Load preview data for the current page
 * @param forceRefresh - If true, bypass query comparison and force refresh
 */
export async function loadPreviewData(forceRefresh: boolean = false): Promise<void> {
  const loader = getLoader()
  loader.editor.trigger(DATA_SOURCE_DATA_LOAD_START)

  const page = loader.editor.Pages.getSelected()
  if (!page) return

  // Get current queries
  const currentQueries = getPageQuery(page, loader.editor)

  // Compare with last queries to see if we need to refresh
  const queriesChanged = !areQueriesEqual(loader.lastQueries, currentQueries)

  if (!forceRefresh && !queriesChanged) {
    // Queries haven't changed, no need to refresh data sources
    // But still trigger load end to maintain expected event flow
    loader.editor.trigger(DATA_SOURCE_DATA_LOAD_END, getPreviewDataFromManager())
    return
  }

  // Update last queries
  loader.lastQueries = { ...currentQueries }

  loader.currentUpdatePid++
  const data = await fetchPagePreviewData(page)

  if (data !== 'interrupted') {
    loader.editor.trigger(DATA_SOURCE_DATA_LOAD_END, data)
  } else {
    console.warn(`Preview data update process for PID ${loader.currentUpdatePid} was interrupted.`)
    loader.editor.trigger(DATA_SOURCE_DATA_LOAD_CANCEL, data)
  }
}

/**
 * Fetch preview data for a specific page
 * @param page - The page object for which preview data needs to be fetched
 * @return The preview data returned by all data sources, or 'interrupted' if cancelled
 */
export async function fetchPagePreviewData(page: Page): Promise<Record<DataSourceId, unknown> | 'interrupted'> {
  const loader = getLoader()
  const myPid = loader.currentUpdatePid
  const queries = getPageQuery(page, loader.editor)

  // Reset preview data
  setPreviewDataInManager({})

  try {
    const results = await Promise.all(
      Object.entries(queries)
        .map(async ([dataSourceId, query]) => {
          if (myPid !== loader.currentUpdatePid) return

          const ds = getAllDataSources().find(ds => ds.id === dataSourceId)
          if (!ds) {
            console.error(`Data source ${dataSourceId} not found`)
            return null
          }

          if (!ds.isConnected()) {
            console.warn(`Data source ${dataSourceId} is not connected.`)
            return null
          }

          try {
            const value = await ds.fetchValues(query)
            const currentData = getPreviewDataFromManager()
            setPreviewDataInManager({ ...currentData, [dataSourceId]: value })
            return { dataSourceId, value }
          } catch (err) {
            console.error(`Error fetching preview data for data source ${dataSourceId}:`, err)
            loader.editor.runCommand('notifications:add', {
              type: 'error',
              group: NOTIFICATION_GROUP,
              message: `Error fetching preview data for data source ${dataSourceId}: ${err}`,
            })
            return null
          }
        })
    )

    if (myPid !== loader.currentUpdatePid) return 'interrupted'

    return results
      .filter(result => result !== null)
      .reduce((acc, result) => {
        const { dataSourceId, value } = result!
        acc[dataSourceId] = value
        return acc
      }, {} as Record<DataSourceId, unknown>)
  } catch (err) {
    console.error('Error while fetching preview data:', err)
    loader.editor.runCommand('notifications:add', {
      type: 'error',
      group: NOTIFICATION_GROUP,
      message: `Error while fetching preview data: ${err}`,
    })
    return {}
  }
}

/**
 * Get current preview data
 */
export function getPreviewData(): Record<DataSourceId, unknown> {
  return getPreviewDataFromManager()
}

/**
 * Clear preview data
 */
export function clearPreviewData(): void {
  setPreviewDataInManager({})
}
