/*
 * 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 {html, render, TemplateResult} from 'lit-html'
import {defaultSections, idCodeWrapper, isSite, SettingsSection, updateInfo} from './settings-sections'

/**
 * @fileoverview This file contains the settings dialog. The config API lets plugins add sections to the settings dialog.
 */

import { WebsiteData, WebsiteSettings } from '../../types'
import { ClientEvent } from '../events'
import { SILEX_VERSION } from '../../constants'
import { Button, Editor } from 'grapesjs'

const sectionsSite: SettingsSection[] = [...defaultSections]
const sectionsPage: SettingsSection[] = [...defaultSections]

const el: HTMLDivElement = document.createElement('div')
let modal: any | undefined // the modal var should be of type ModalModule, not Modal, but it is not exported from grapesjs
let settingsState: {
  isOpen: boolean
  title: string
  page: unknown
  sectionId?: string
  sender?: Button
} | null = null
let customSettingsDialog: HTMLDivElement | null = null

export const cmdOpenSettings = 'open-settings'
export const cmdAddSection = 'settings:add-section'
export const cmdRemoveSection = 'settings:remove-section'
export const cmdRenderSection = 'settings:render-section'
export const cmdGetSettings = 'settings:get'
export const cmdSetSettings = 'settings:set'

export interface AddSectionOption {
  section: SettingsSection,
  siteOrPage: 'site' | 'page',
  position: 'first' | 'last' | number
}

let headEditor: ReturnType<Editor['CodeManager']['createViewer']> | null = null


function createCustomSettingsDialog(editor: Editor, opts: Record<string, unknown>, page: unknown, sectionId?: string): HTMLDivElement {
  const dialog = document.createElement('div')
  dialog.className = 'settings-dialog gjs-two-color'
  dialog.innerHTML = `
    <div class="settings-content gjs-mdl-dialog" role="dialog" aria-modal="true" aria-labelledby="settings-title">
      <div class="settings-header">
        <h3 id="settings-title">${page ? 'Page settings' : 'Site Settings'}</h3>
        <button type="button" class="settings-close" title="Close" aria-label="Close settings">×</button>
      </div>
      <div class="settings-body"></div>
    </div>
  `

  // Handle close button
  const closeButton = dialog.querySelector('.settings-close') as HTMLButtonElement
  closeButton.addEventListener('click', () => {
    closeCustomSettingsDialog(editor)
  })

  // Handle keyboard shortcuts
  dialog.addEventListener('keydown', (e: KeyboardEvent) => {
    // Close on Escape
    if (e.key === 'Escape') {
      e.preventDefault()
      closeCustomSettingsDialog(editor)
      return
    }

    // Save on Ctrl/Cmd + Enter
    if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
      e.preventDefault()
      const form = dialog.querySelector('form') as HTMLFormElement
      if (form) {
        form.requestSubmit()
      }
      return
    }

    // Handle Alt+C (Cancel) and Alt+A (Apply)
    if (e.altKey) {
      if (e.key.toLowerCase() === 'c') {
        e.preventDefault()
        closeCustomSettingsDialog(editor)
        return
      }
      if (e.key.toLowerCase() === 'a') {
        e.preventDefault()
        const form = dialog.querySelector('form') as HTMLFormElement
        if (form) {
          form.requestSubmit()
        }
        return
      }
    }
  })

  // Add content
  displaySettings(editor, opts, page, sectionId)
  const body = dialog.querySelector('.settings-body') as HTMLDivElement
  body.appendChild(el)

  // Initialize focus when dialog opens
  setTimeout(() => {
    const firstTab = dialog.querySelector('.silex-list--menu li[role="button"]') as HTMLElement
    if (firstTab) {
      firstTab.focus()
    }
  }, 100)

  // Handle form submission
  const form = el.querySelector('form') as HTMLFormElement
  form.onsubmit = (event: Event) => {
    event.preventDefault()
    editor.trigger(ClientEvent.SETTINGS_SAVE_START, page)
    saveSettings(editor, opts, page)
    editor.trigger(ClientEvent.SETTINGS_SAVE_END, page)
    closeCustomSettingsDialog(editor, false)
  }

  return dialog
}

function closeCustomSettingsDialog(editor: Editor, fromCommand = false) {
  if (customSettingsDialog) {
    customSettingsDialog.remove()
    customSettingsDialog = null
  }
  settingsState?.sender?.set && settingsState.sender.set('active', 0)

  // Only call stopCommand if not already being called from the command's stop method
  if (!fromCommand) {
    editor.stopCommand(cmdOpenSettings)
  }

  settingsState = null
  editor.trigger(ClientEvent.SETTINGS_CLOSE)
}

function reopenSettingsModal(editor: Editor, opts: Record<string, unknown>) {
  if (!settingsState) {
    return
  }

  modal = editor.Modal.open({
    title: settingsState.title,
    content: '',
    attributes: { class: 'settings-dialog' },
  })
    .onceClose(() => {
      settingsState?.sender?.set && settingsState.sender.set('active', 0)
      editor.stopCommand(cmdOpenSettings)
      settingsState = null
    })

  displaySettings(editor, opts, settingsState.page, settingsState.sectionId)
  modal.setContent(el)

  const form = el.querySelector('form') as HTMLFormElement
  form.onsubmit = (event: Event) => {
    event.preventDefault()
    editor.trigger(ClientEvent.SETTINGS_SAVE_START, settingsState?.page)
    saveSettings(editor, opts, settingsState?.page)
    editor.trigger(ClientEvent.SETTINGS_SAVE_END, settingsState?.page)
    editor.stopCommand(cmdOpenSettings)
  }
}

export const settingsDialog = (
  editor: Editor,
  opts: Record<string, unknown>
): void => {
  // No need to override Modal system - we use custom dialog

  editor.Commands.add(cmdOpenSettings, {
    run: (
      _: Editor,
      sender: Button,
      {page, sectionId}: {page?: unknown, sectionId?: string}
    ) => {
      const title = page ? 'Page settings' : 'Site Settings'

      // Store settings state
      settingsState = {
        isOpen: true,
        title,
        page,
        sectionId,
        sender
      }

      // Create and show custom dialog instead of using GrapesJS modal
      customSettingsDialog = createCustomSettingsDialog(editor, opts, page, sectionId)
      document.body.appendChild(customSettingsDialog)

      // Let the first focusable element get focus naturally

      editor.trigger(ClientEvent.SETTINGS_OPEN, page)
      return customSettingsDialog
    },
    stop: (): void => {
      closeCustomSettingsDialog(editor, true)
    },
  })
  editor.Commands.add(cmdAddSection, (_editor: Editor, _sender: Button, options: AddSectionOption): void => {
    addSection(options.section, options.siteOrPage, options.position)
  })
  editor.Commands.add(cmdRenderSection, (_editor: Editor, _sender: Button, options: AddSectionOption): void => {
    if (!settingsState) return
    displaySettings(editor, opts, settingsState.page, settingsState.sectionId)
  })
  editor.Commands.add(cmdRemoveSection, (_editor: Editor, _sender: Button, options: AddSectionOption): void => {
    removeSection(options.section.id, options.siteOrPage)
  })
  editor.Commands.add(cmdGetSettings, (_editor: Editor, _sender: Button, options: any = {}) => {
    const { page } = options
    if (page) {
      const p = typeof page === 'string'
        ? editor.Pages.getAll().find(pp => pp.getName() === page || pp.id === page)
        : editor.Pages.getSelected()
      if (!p) throw new Error(`Page not found: "${page}". Use pages:list to see all pages.`)
      return p.get('settings') || {}
    }
    return editor.getModel().get('settings') || {}
  })
  editor.Commands.add(cmdSetSettings, (_editor: Editor, _sender: Button, options: Record<string, unknown> = {}) => {
    const { page, ...settings } = options
    if (!Object.keys(settings).length) throw new Error('Required: at least one setting key. Valid keys: title, description, favicon, lang, head, og:title, og:description, og:image')
    if (page) {
      const p = typeof page === 'string'
        ? editor.Pages.getAll().find(pp => pp.getName() === page || pp.id === page)
        : editor.Pages.getSelected()
      if (!p) throw new Error(`Page not found: "${page}". Use pages:list to see all pages.`)
      const current = (p.get('settings') || {}) as Record<string, unknown>
      p.set('settings', { ...current, ...settings })
    } else {
      const current = (editor.getModel().get('settings') || {}) as Record<string, unknown>
      editor.getModel().set('settings', { ...current, ...settings })
    }
    editor.getModel().set('changesCount', editor.getDirtyCount() + 1)
    updateDom(editor)
  })
  editor.on('storage:start:store', (data: WebsiteData): void => {
    data.settings = editor.getModel().get('settings')
    /* @ts-ignore FIXME: this should not be there? or is it used on the server side in the sotrage providers? */
    data.name = editor.getModel().get('name')
  })
  editor.on('storage:end:load', (data: WebsiteData): void => {
    const model = editor.getModel()
    model.set('settings', data.settings || {})
    model.set('name', editor.getModel().get('name'))
  })
  editor.on('canvas:frame:load', (): void => {
    updateDom(editor)
    updateInfo()
  })
  editor.on('page', (_e: unknown): void => {
    updateDom(editor)
  })
  headEditor = editor.CodeManager.createViewer({
    readOnly: false,
    codeName: 'htmlmixed',
    lineNumbers: true,
    lineWrapping: true,
    autoFormat: false,
  })

  // Register AI capabilities
  editor.on('ai-capabilities:ready', (addCapability) => {
    addCapability({
      id: cmdGetSettings,
      command: cmdGetSettings,
      description: 'Get site or page settings',
      readOnly: true,
      inputSchema: {
        type: 'object',
        properties: {
          page: { type: 'string' },
        },
      },
      tags: ['settings'],
    })
    addCapability({
      id: cmdSetSettings,
      command: cmdSetSettings,
      description: 'Set site or page settings',
      inputSchema: {
        type: 'object',
        properties: {
          page: { type: 'string' },
          title: { type: 'string' },
          description: { type: 'string' },
          favicon: { type: 'string' },
          lang: { type: 'string' },
          head: { type: 'string' },
          'og:title': { type: 'string' },
          'og:description': { type: 'string' },
          'og:image': { type: 'string' },
        },
      },
      tags: ['settings'],
    })
  })
}

function showSection(item: SettingsSection): void {
  const aside = el.querySelector('aside.silex-bar') as HTMLElement
  const ul = aside.querySelector('ul.silex-list--menu') as HTMLUListElement
  const li = ul.querySelector('li#settings-sidebar-' + item.id) as HTMLLIElement
  currentSection = item
  if(!li) {
    console.warn('Cannot find section', item.id, 'in the side bar, fallback to the first section')
    if(!defaultSections[0] || defaultSections[0].id === item.id) {
      console.error('Cannot find the default section in the side bar')
      return
    }
    showSection(defaultSections[0])
    return
  }
  Array.from(ul.querySelectorAll('.active')).forEach(el => el.classList.remove('active'))
  li.classList.add('active')

  const main = el.querySelector('main#settings__main') as HTMLElement
  const mainItem = main.querySelector(`#settings-${item.id}`)
  if(!mainItem) {
    console.warn('Cannot find section', item.id, 'in the settings dialog, fallback to the first section')
    if(!defaultSections[0] || defaultSections[0].id === item.id) {
      console.error('Cannot find the default section in the settings dialog')
      return
    }
    showSection(defaultSections[0])
    return
  }
  Array.from(main.querySelectorAll('.silex-hideable')).forEach(el => el.classList.add('silex-hidden'))
  mainItem.classList.remove('silex-hidden')
  requestAnimationFrame(() => headEditor?.refresh())
}

export function addSection(
  section: SettingsSection,
  siteOrPage: 'site' | 'page',
  position: 'first' | 'last' | number
): void {
  const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
  if (position === 'first') {
    sections.unshift(section)
  } else if (position === 'last') {
    sections.push(section)
  } else if (typeof position === 'number') {
    sections.splice(position, 0, section)
  } else {
    throw new Error('Invalid position for settings section')
  }
}

export function removeSection(
  id: string,
  siteOrPage: 'site' | 'page'
): void {
  const sections = siteOrPage === 'site' ? sectionsSite : sectionsPage
  const index = sections.findIndex(section => section.id === id)
  if(index === -1) throw new Error(`Cannot find section with id ${id}`)
  sections.splice(index, 1)
}


let currentSection: SettingsSection | undefined
function displaySettings(
  editor: Editor,
  config: Record<string, unknown>,
  model: any = editor.getModel(),
  sectionId?: string
): void {
  const settings: WebsiteSettings = model.get('settings') || {} as WebsiteSettings
  model.set('settings', settings)
  const menuItemsCurrent: SettingsSection[] = isSite(model) ? sectionsSite : sectionsPage
  const targetSection = !!sectionId && menuItemsCurrent.find(section => section.id === sectionId)
  currentSection = targetSection || currentSection || menuItemsCurrent[0]
  const sections: TemplateResult[] = menuItemsCurrent.map(item => {
    try {
      return item.render(settings, model)
    } catch (e) {
      console.error('Error rendering settings section', item.id, e)
      return html`<div class="silex-error">Error rendering settings section ${item.id}</div>`
    }
  })
  render(html`
    <form class="silex-form">
      <section class="silex-sidebar-dialog">
        <aside class="silex-bar">
          <ul class="silex-list silex-list--menu" aria-label="Settings sections">
            ${menuItemsCurrent.map((item, index) => html`
              <li
                id="settings-sidebar-${item.id}"
                class=${item.id === currentSection!.id ? 'active' : ''}
                role="button"
                tabindex="0"
                @click=${(e: Event) => {
    e.preventDefault()
    showSection(item)
    editor.trigger(ClientEvent.SETTINGS_SECTION_CHANGE, item.id)
  }}
                @keydown=${(e: KeyboardEvent) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      showSection(item)
      editor.trigger(ClientEvent.SETTINGS_SECTION_CHANGE, item.id)
    }
  }}
              >
                ${item.label}
              </li>
            `)}
          </ul>
        </aside>
        <main id="settings__main">
            ${ sections }
        </main>
      </section>
      <footer role="contentinfo">
        <p class="silex-version">Silex ${SILEX_VERSION}</p>
        <input class="silex-button" type="button" value="Cancel" @click=${() => editor.stopCommand(cmdOpenSettings)} accesskey="c">
        <input class="silex-button" type="submit" value="Apply" accesskey="a">
      </footer>
    </form>
  `, el)
  showSection(currentSection!)
  el.querySelector(`#${idCodeWrapper}`)?.appendChild(headEditor!.getElement())
  headEditor!.setContent(settings.head || '')
  requestAnimationFrame(() => headEditor?.refresh())
}

function saveSettings(
  editor: Editor,
  config: Record<string, unknown>,
  model: any = editor.getModel()
): void {
  const form = el.querySelector('form') as HTMLFormElement
  const formData = new FormData(form)
  const data = Array.from(formData)
    .reduce((aggregate: {[key: string]: any}, [key, value]) => {
      if(value !== '') {
        aggregate[key] = value
      }
      return aggregate
    }, {})
  const { name, ...settings } = data
  model.set({
    settings: {
      ...data,
      head: headEditor!.getContent(),
    },
    name,
  })
  editor.getModel().set('changesCount', editor.getDirtyCount() + 1)
  updateDom(editor)
}
function getHeadContainer(doc: Document, className: string): HTMLElement {
  const container = doc.head.querySelector(`.${className}`) as HTMLElement | null
  if(container) {
    return container
  }
  const newContainer = doc.createElement('div')
  newContainer.classList.add(className)
  doc.head.appendChild(newContainer)
  return newContainer
}
function updateDom(editor: Editor): void {
  const doc = editor.Canvas.getDocument()
  const settings = editor.getModel().get('settings')
  const pageSettings = editor.Pages.getSelected().get('settings') as WebsiteSettings
  if(doc && settings) {
    setTimeout(() => {
      getHeadContainer(doc, 'site-head')
        .innerHTML = settings.head || ''
      getHeadContainer(doc, 'page-head')
        .innerHTML = pageSettings?.head || ''
    })
  }
}
