import {
  CLONE_ELEMENT,
  ElementHighlightMessageType,
  NAVIGATE_TO_SOURCE_CODE,
  REMOVE_ELEMENT,
  GENERATE_PAGE,
  CHANGE_GRAPHQL_ENDPOINT,
  CHANGE_THEME,
  TRANSLATE_ELEMENT,
  CLONE_AND_RENAME_ELEMENT
} from '../messaging/messageTypes'
import { findTypenameValue, getDataFromElement, getSourceFromElement } from '../devtools/source'
import { frontendActions, workbench } from '../index'
import { forceSaveLocaleElement } from './inlineEdit'
import {
  getColIndex,
  getFieldNameFromElement,
  isColumnTitle,
} from '../devtools/table'
import { TransferableError, Translations } from '../types'
import { HoverInspector } from './hoverInspector'
import {
  fetchHasuraMetadata,
  fetchHasuraQuery,
  filterIntrospection,
  getRoots,
  isByPkQuery,
  PageType,
} from '@iteria-app/graphql-lowcode'
import { normalizePath } from '../devtools/normalizePath'
import { fetchGraphqlIntrospectionSchema } from '@iteria-app/graphql-lowcode'
import debounce from 'lodash.debounce'
import { stripExtension } from '../util/strip'
import { FileWriteData } from '../workbench/Workbench'
import { guessCurrentWorkingDirectory } from '@iteria-app/generator/src/cwd'
import { isGridElement, isInsideTable, shouldDisplayElementHighlight } from './table'
import { CodeDir, CodeRW } from '@iteria-app/generator/src/io'
import {
  detailPageRegex,
  detailPageFullRegex,
  getEntityNameFromUrl,
} from '@iteria-app/component-templates/src/gql/entity/getEntityNameFromUrl'
import {
  createTable,
  trackTable,
  ColumnProps,
} from '@iteria-app/graphql-lowcode/src/generate/hasura/hasuraQuery'
import { createPermissions } from '@iteria-app/graphql-lowcode/src/generate/hasura/permissionUtils'
import {
  createColumn,
  handleErrors,
  resolveForeignKey,
} from '@iteria-app/graphql-lowcode/src/generate/hasura/addColumn'
import { showUserFeedback } from './userFeedback'
import * as Editable from '@livingdocs/editable.js'
import { setEndOfContenteditable } from '../helpers/editable'
import { getTheme } from './theme'

const ElementEditable = (() => {
  const editable = new Editable.Editable()
  let editingElement = undefined
  let buttonElement = undefined
  let prevContent = undefined

  const findClosestBtn = (el: any) => {
    const MAX_DEPTH = 5
    let depth = 0
    while (el.parentNode && depth < MAX_DEPTH) {
      el = el.parentNode
      if (el.onclick) {
        return el
      }
      depth++
    }
    return null
  }

  const save = () => {
    if (editingElement) {
      if (buttonElement) {
        buttonElement.setAttribute('style', 'pointer-events: auto')
        editingElement.removeAttribute('data-editable')
      }
      editable.remove(editingElement)
      editingElement.blur()
      editingElement.removeAttribute('style', 'display: block; outline: 0')
      const inspectedElement = getDataFromElement(editingElement)
      forceSaveLocaleElement(
        editingElement,
        inspectedElement,
        undefined,
        workbench
      )
      editingElement = undefined
      buttonElement = undefined
      prevContent = undefined
    }
  }

  return {
    editingElement: (el: any) => {
      if (!el) return
      editingElement = el
      buttonElement = findClosestBtn(editingElement)
      editingElement.focus()
      editable.add(editingElement)
      if (buttonElement) {
        buttonElement.setAttribute('style', 'pointer-events: none')
        setEndOfContenteditable(
          editingElement,
          editingElement,
          editingElement.innerHTML.length / 2
        ) // set carot cursor to middle of button content
      } else {
        setEndOfContenteditable(editingElement, editingElement) // set carot cursor to the end
        editingElement.setAttribute('style', 'display: block; outline: 0')
      }
      editable.add(editingElement)

      if (prevContent === undefined) {
        prevContent = el.innerHTML
      }

      const inspectedElement = getDataFromElement(editingElement)
      if (inspectedElement) {
        if (
          inspectedElement.displayName === 'Anonymous' ||
          inspectedElement.displayName.indexOf('FormattedMessage') !== -1 ||
          inspectedElement.displayName === 'Translate'
        ) {
          inspectedElement.displayName = 'FormattedMessage'
        }
      }
    },
    saveChanges: save,
    cancelChanges: () => {
      if (prevContent !== undefined) {
        editingElement.innerHTML = prevContent
      }
      save()
    },
  }
})()

export const updateWysiwygErrors = (error: TransferableError) => {
  const el = document.getElementById('iteria-wysiwyg')
  // @ts-ignore
  if (el) el.bundlerError = error
}

export const addElementHighlight = async (window: Window) => {
  const el = document.createElement('iteria-wysiwyg')

  const hoverInspector = new HoverInspector(workbench)

  const hoverCallback = async (e: any) => {
    const {
      detail: { inspect, hoverElement },
    } = e
    const tooltipText = await hoverInspector.getComponentName(hoverElement)
    const display = shouldDisplayElementHighlight(hoverElement)

    //@ts-ignore
    el.isInsideTable =
      isInsideTable(hoverElement) || hoverElement.nodeName === 'INPUT'

    //@ts-ignore
    el.isGridElement = isGridElement(hoverElement)

    const formattedMessageElement = getFormattedMessageElement(hoverElement)

    inspect({
      display,
      tooltipText:
        tooltipText === 'Translate' ? 'FormattedMessage' : tooltipText,
      disabled: false,
      icon: 'settings',
      icons: [],
      extraPayload: {
        formattedMessageElement: formattedMessageElement,
      },
    })
  }
  const debouncedHoverCallback = debounce(hoverCallback, 50)

  el.addEventListener('hover', debouncedHoverCallback)

  el.addEventListener('delete', (e: any) => {
    const editingElement = e?.detail?.editingElement

    if (isColumnTitle(editingElement) && isDataGridCell(editingElement)) {
      handleRemoveTableColumn(window, editingElement, REMOVE_ELEMENT)
    } else if (isFormInput(editingElement)) {
      handleRemoveFormColumn(window, editingElement, REMOVE_ELEMENT)
    } else {
      handleIconClick(window, editingElement, REMOVE_ELEMENT)
    }
  })

  el.addEventListener('clone', async (e: any) => {
    const detail = e.detail

    if (detail.type === 'CLONE_ELEMENT') {
      handleIconClick(window, detail.payload, CLONE_ELEMENT)
    } else if (detail.type === 'CLONE_COLUMN') {
      const payloadArr: any[] = Array.isArray(detail.payload)
        ? detail.payload
        : [detail.payload]

      payloadArr.forEach((field) => cloneColumn(field))
    } else if (detail.type === 'CLONE_AND_RENAME_ELEMENT') {
      let source = getSourceFromElement(detail.payload.el)
      if (!window.__REACT_DEVTOOLS_GLOBAL_HOOK__)
        throw new Error(
          '__REACT_DEVTOOLS_GLOBAL_HOOK__ not available on window object'
        )
      if (!source) {
        return console.error('Source not found', el)
      }

      const fields = detail.payload.fieldToBeAdded.split('.')
      const fieldToBeAdded = fields[fields.length - 1]
      //@ts-ignore
      source.fieldToBeAdded = fieldToBeAdded
      frontendActions.elementHighlightClick(CLONE_AND_RENAME_ELEMENT, source)
      handleIconClick(window, detail.payload, CLONE_AND_RENAME_ELEMENT)
    }
  })

  el.addEventListener('showsource', (e: any) => {
    const editingElement = e?.detail?.editingElement
    console.log('showsource event', e, 'editingElement:', editingElement)

    handleIconClick(window, editingElement, NAVIGATE_TO_SOURCE_CODE)
  })

  el.addEventListener('translate', (e: any) => {
    const editingElement = e?.detail?.editingElement
    console.log('translate event', e, 'editingElement:', editingElement)
    handleIconClick(window, editingElement, TRANSLATE_ELEMENT)
    ElementEditable.editingElement(editingElement)
  })

  el.addEventListener('confirmaction', (e: any) => {
    if (e.detail.actionType === 'translate') {
      ElementEditable.saveChanges()
    }
  })

  el.addEventListener('cancelaction', (e: any) => {
    if (e.detail.actionType === 'translate') {
      ElementEditable.cancelChanges()
    }
  })
  el.addEventListener('editing', async (e: any) => {
    // let editingElement = e?.detail?.editingElement
    // if (isFormattedMessage(editingElement)) {
    //   ElementEditable.editingElement(editingElement)
    /* 
        -causes 'cannot read property of undefined' error from imported function
        from 'react-lowcode', consider moving fixing and moving this function into monorepo
        if needed or remove that functionality
        -remove react-lowcode from package.json, we are not using it anymore and this is the last
        import from that package
      */
    // TODO need rework after lowcode and wysiwyg rework
    // } else if (isFormInput(editingElement)) {
    // FIXME
    // const dialog = e?.detail?.dialogWidgetFields
    // const hide = e?.detail?.hide
    // const source = getSourceFromElement(editingElement)
    // const props = await getFormWidgetProperties(workbench, source)
    // const file = await workbench.readFile(source.fileName)
    // if (!file) throw new Error('File not found in Workbench')
    // console.log('PP', props, source)
    // await dialog({
    //   fields: props.properties,
    //   fieldsChanged: async () => {
    //     console.log('neco')
    //     const data = await setFormWidgetProperties(workbench, source, props)
    //     console.log('data', data)
    //     if (data) {
    //       if (typeof hide === 'function') hide()
    //       frontendActions.fileWrite(source.fileName, data)
    //     }
    //   },
    // })
    // }
  })

  el.addEventListener('modechange', async (e: any) => { })

  el.addEventListener('generatePage', async (e: any) => {
    handleGeneratePageClick(window, GENERATE_PAGE, e)
  })

  el.addEventListener('listAllTranslations', async (e: any) => {
    handleShowAllTranslations(e)
  })

  el.addEventListener('changeGraphqlEndpoint', async (e: any) => {
    handleChangeGraphqlEndpoint(CHANGE_GRAPHQL_ENDPOINT, e)
  })

  el.addEventListener('changeTheme', async (e: CustomEvent) => {
    handleChangeTheme(CHANGE_THEME, e)
  })

  el.addEventListener('createTable', async (e: any) => {
    handleCreateTable(el, e)
  })

  el.addEventListener('addNewColumn', async (e: any) => {
    handleAddColumn(el, e)
  })

  el.addEventListener('loadIntrospection', async () => {
    const introspection = await loadIntrospection()
    if (introspection) {
      workbench.setIntrospection(introspection)
      //@ts-ignore
      el.introspection = introspection
    }
  })

  el.addEventListener('filterIntrospection', async () => {
    const entityName = findTypenameValue() ?? getEntityNameFromUrl()
    const filteredIntrospection = filterIntrospection(
      workbench.introspection,
      entityName
    )
    //@ts-ignore
    el.introspection = filteredIntrospection
  })

  document.body.appendChild(el)

  el.id = 'iteria-wysiwyg'

  // Set introspection when creating element
  const introspection = await loadIntrospection()
  //@ts-ignore
  el.introspection = introspection
  workbench.setIntrospection(introspection)
  // @ts-ignore
  el.win = window
}

const loadIntrospection = async () => {
  try {
    const schema = await fetchGraphqlIntrospectionSchema(
      workbench.graphQLEndpoint
    )
    window.postMessage({
      type: 'SHOW_USER_FEEDBACK',
      payload: { type: 'LOAD_INTROSPECTION_SUCCESS' },
    })
    return schema
  } catch (err) {
    window.postMessage({
      type: 'SHOW_USER_FEEDBACK',
      payload: { type: 'LOAD_INTROSPECTION_ERROR', payload: err },
    })
    console.error(err)
    return null
  }
}

const getPageTypeFromUrl = () => {
  const url = window.location.href
  const urlParts = url.split('/').filter(Boolean)

  // url could be in format /entityName/:id, where id is a number or uuid
  if (
    detailPageRegex.test(urlParts[urlParts.length - 1]) ||
    detailPageFullRegex.test(urlParts[urlParts.length - 1])
  ) {
    return PageType.DETAIL
  } else return PageType.LIST
}

const cloneColumn = async (selectedFields: string) => {
  const entityName = findTypenameValue() ?? getEntityNameFromUrl()

  let source: any = {}
  source.columnToAdd = selectedFields
  source.clonedColumnIndex = 1
  source.entityName = entityName
  // TODO Check if we are in detail
  source.page = getPageTypeFromUrl()
  frontendActions.elementHighlightClick(CLONE_ELEMENT, source)
}

export const getAllTranslations = async (
  path: string,
  io: CodeRW & CodeDir
) => {
  if (path) {
    const allTranslations: Translations = {
      langs: {},
    }

    const directory = await io.readDirectory(path)
    if (directory) {
      const langs = Object.values(directory)
      for (let index in langs) {
        const item = langs[index] as any
        const fileName = item.fileName
        const lang = stripExtension(fileName)
        const translation = await io.readFile(item.filePath)
        if (translation) {
          allTranslations['langs'][lang] = JSON.parse(translation)
          allTranslations[fileName] = item.filePath
        }
      }
      return allTranslations
    }
  }
  return { langs: {} }
}

export const handleShowAllTranslations = async (event: any) => {
  const dialog = event?.detail?.dialogTranslationSheet
  const hide = event?.detail?.hide

  const LANGS_PATH = guessCurrentWorkingDirectory() + '/src/compiled-lang'
  const translation: Translations = await getAllTranslations(
    LANGS_PATH,
    workbench
  )

  await dialog({
    translations: translation.langs,
    ok: async (e: any) => {
      let arr: FileWriteData[] = []
      for (let item in e) {
        arr = [
          ...arr,
          ...[
            {
              path: translation[item + '.json'],
              data: JSON.stringify(e[item], undefined, 2),
            },
          ],
        ]
      }
      frontendActions.multipleFileWrite(arr)
      if (typeof hide === 'function') hide()
    },
  })
}

export const handleCloneTableColumn = async (
  type: ElementHighlightMessageType,
  event: CustomEvent
) => {
  const dialog = event.detail.dialogInsertField
  const hide = event.detail.hide

  const entityName = getEntityNameFromUrl()
  const filteredIntrospection = filterIntrospection(
    workbench.introspection,
    entityName
  )

  let source: any = {}

  await dialog({
    intro: filteredIntrospection,
    fieldSelected: async (e: any) => {
      source.columnToAdd = e.selectedFields
      source.clonedColumnIndex = 1
      source.entityName = entityName
      source.page = PageType.LIST
      frontendActions.elementHighlightClick(type, source)
      hide()
    },
  })
}

export const handleCloneFormInput = async (
  window: Window,
  el: HTMLElement,
  type: ElementHighlightMessageType,
  event: CustomEvent
) => {
  if (!window.__REACT_DEVTOOLS_GLOBAL_HOOK__)
    throw new Error(
      '__REACT_DEVTOOLS_GLOBAL_HOOK__ not available on window object'
    )
  const source = getSourceFromElement(el) as any
  const filePath = normalizePath(source.fileName)

  const dialog = event.detail.dialogInsertField
  const hide = event.detail.hide

  const schema = await fetchGraphqlIntrospectionSchema(
    workbench.graphQLEndpoint
  )

  const entityName = getEntityNameFromUrl()
  const filteredSchema = filterIntrospection(schema, entityName)

  await dialog({
    intro: filteredSchema,
    fieldSelected: async (e: any) => {
      source.columnToAdd = e.selectedFields
      source.entityName = entityName
      source.filePath = filePath
      source.intro = schema
      source.page = PageType.DETAIL
      frontendActions.elementHighlightClick(type, source)
      if (typeof hide === 'function') hide()
    },
  })
}

export const handleRemoveTableColumn = async (
  window: Window,
  el: HTMLElement,
  type: ElementHighlightMessageType
) => {
  if (!window.__REACT_DEVTOOLS_GLOBAL_HOOK__)
    throw new Error(
      '__REACT_DEVTOOLS_GLOBAL_HOOK__ not available on window object'
    )

  const entityName = getEntityNameFromUrl()

  const source = getSourceOfTable(el, window) as any
  if (!source) return console.error('Source not found', el)

  source.colIndex = getColIndex(el)
  source.schema = workbench.introspection
  source.page = PageType.LIST
  source.entityName = entityName
  source.columnToDelete = getFieldNameFromElement(el)

  if (!source) return console.error('Source not found', el)
  frontendActions.elementHighlightClick(type, source)
}

export const handleRemoveFormColumn = async (
  contentWindow: Window,
  el: HTMLElement,
  type: ElementHighlightMessageType
) => {
  const schema = await fetchGraphqlIntrospectionSchema(
    workbench.graphQLEndpoint
  )

  const entityName = getEntityNameFromUrl()

  const source = getSourceFromElement(el) as any
  if (!source) return console.error('Source not found', el)

  source.schema = schema
  source.page = PageType.DETAIL
  source.entityName = entityName
  source.columnToDelete = el.getAttribute('name')

  if (!source) return console.error('Source not found', el)
  frontendActions.elementHighlightClick(type, source)
}

export const handleIconClick = (
  window: Window,
  el: HTMLElement,
  type: ElementHighlightMessageType
) => {
  const source = getSourceFromElement(el)
  if (!window.__REACT_DEVTOOLS_GLOBAL_HOOK__)
    throw new Error(
      '__REACT_DEVTOOLS_GLOBAL_HOOK__ not available on window object'
    )
  if (!source) {
    window.postMessage({
      type: 'SHOW_USER_FEEDBACK',
      payload: { type: `${type}_ERROR` },
    })
    return console.error('Source not found', el)
  }

  frontendActions.elementHighlightClick(type, source)
}

export const handleGeneratePageClick = async (
  window: Window,
  type: ElementHighlightMessageType,
  event: CustomEvent
) => {
  //TODO change graphql-lowcode getRoots to work with copy of introspection, not with the initial introspection
  const introspection = JSON.parse(JSON.stringify(workbench.introspection))

  const dialog = event?.detail?.dialogChooseEntityFields
  const hide = event?.detail?.hide

  //filters out _by_pk queries
  const [queryRoot] = getRoots(introspection)
  queryRoot.fields = queryRoot.fields.filter((field) => !isByPkQuery(field))

  introspection.types.map((type) => {
    if (type.name === 'query_root') {
      type.fields = queryRoot.fields
    }
  })

  await dialog({
    intro: introspection,
    fieldSelected: async (e: any) => {
      const source: any = {}
      source.fieldToGenerate = e.selectedFields
      source.typeOfField = e.selectedFields.type
      source.intro = introspection
      frontendActions.elementHighlightClick(type, source)
      hide()
    },
  })
}

export const handleChangeGraphqlEndpoint = async (
  type: ElementHighlightMessageType,
  event: CustomEvent
) => {
  const graphqlEndpointURL = workbench.graphQLEndpoint
  const dialog = event?.detail?.dialogChangeGraphqlEndpoint

  await dialog({
    currentEndpoint: graphqlEndpointURL,
    endpointChanged: async (e: any) => {
      const source: any = {}
      source.newEndpointURL = e
      frontendActions.elementHighlightClick(type, source)
    },
  })
}

export const handleChangeTheme = async (
  type: ElementHighlightMessageType,
  event: CustomEvent
) => {
  const THEME_NAME = 'colors'
  const dialog = event?.detail?.dialogThemeEditor
  const currentWorkingDirectory = guessCurrentWorkingDirectory()
  if(currentWorkingDirectory) {
    const THEME_DIR = currentWorkingDirectory + '/src/theme'
    const theme = await getTheme(THEME_DIR, THEME_NAME, workbench, frontendActions)

    if(theme) {
      await dialog({
        theme: theme[THEME_NAME],
        save: async (e: any) => {
          const source: any = {}
          source.theme = e
          frontendActions.fileWrite(theme.file, JSON.stringify(e, undefined, 2))
          frontendActions.elementHighlightClick(type, source)
        },
      })
    }
  }
}

export const handleCreateTable = async (
  el: HTMLElement,
  event: CustomEvent
) => {
  const dialog = event?.detail?.dialogCreateTable

  await dialog({
    tableCreated: async (tableInfo: {
      schema: string
      name: string
      migration: boolean
    }) => {
      showUserFeedback({ type: 'CREATE_TABLE_INFO' })
      fetchHasuraQuery(
        createTable(tableInfo.name),
        'create_table_' + tableInfo.name,
        tableInfo.migration
      )
        .then(handleErrors)
        .then(() => {
          fetchHasuraMetadata(
            trackTable(tableInfo.name),
            'track_table_' + tableInfo.name,
            tableInfo.migration
          )
            .then(handleErrors)
            .then(() => {
              createPermissions(tableInfo.name, el, tableInfo.migration)
            })
            .catch((res) => console.log(res))
        })
        .catch(() => showUserFeedback({ type: 'CREATE_TABLE_ERROR' }))
    },
  })
}

export const handleAddColumn = async (el: HTMLElement, event: CustomEvent) => {
  const dialog = event?.detail?.dialogAddNewColum

  await dialog({
    columnAdded: async (e: ColumnProps) => {
      addNewColumn(el, e)
    },
  })
}

const getSourceOfTable = (el: HTMLElement, window: Window) => {
  let currElement = el

  while (currElement.parentElement) {
    if (currElement.classList.contains('MuiDataGrid-root')) {
      return getSourceFromElement(currElement)
    }
    currElement = currElement.parentElement
  }
}

const isDataGridCell = (el: HTMLElement) =>
  el.classList.contains('MuiDataGrid-columnHeaderTitleContainer') ||
  isColumnTitle(el)

const isFormInput = (el: HTMLElement) =>
  el.classList.contains('MuiTextField-root') ||
  el.classList.contains('MuiInput-input') ||
  el.classList.contains('MuiInputBase-input')

const findFormattedMessageElementUp = (el: any) => {
  let foundedFormattedMessageElement = undefined
  if (el.tagName === 'SPAN' && el.dataset.messageId) {
    foundedFormattedMessageElement = el
  } else {
    const existParentFormattedMessage = (el: any) => {
      let depth = 5
      while (el.parentNode && depth--) {
        el = el.parentNode
        if (el.tagName === 'SPAN' && el.dataset.messageId) {
          foundedFormattedMessageElement = el
          break
        }
      }
    }
    existParentFormattedMessage(el)
  }
  return foundedFormattedMessageElement
}

const getFormattedMessageElement = (element: Element): Element => {
  let foundedFormattedMessageElement = undefined
  const findFormattedMessageElement = (el: any, depth: number): any => {
    if (el.tagName === 'SPAN' && el.dataset.messageId) {
      foundedFormattedMessageElement = el
      return el
    }
    if (depth === 0) {
      return el
    }
    el.childNodes.forEach((node) => {
      return findFormattedMessageElement(node, depth - 1)
    })
  }
  findFormattedMessageElement(element, 5)
  return (
    foundedFormattedMessageElement || findFormattedMessageElementUp(element)
  )
}

const addNewColumn = async (el: HTMLElement, columnProps: ColumnProps) => {
  showUserFeedback({ type: 'ADD_COLUMN_INFO' })
  let promise = createColumn(columnProps)

  resolveForeignKey(columnProps, promise, el)
}

export async function loadNewIntrospection(el: HTMLElement) {
  const introspection = await loadIntrospection()
  if (introspection) {
    workbench.setIntrospection(introspection)
    //@ts-ignore
    el.introspection = introspection
  }
}
