import Editor, { Monaco, OnMount } from '@monaco-editor/react'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useRouter } from 'next/router'
import { MutableRefObject, useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
import { Admonition } from 'ui-patterns'
import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'

import type { IStandaloneCodeEditor } from './SQLEditor.types'
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { getEditorSelectionParts } from '@/components/ui/AIEditor/utils'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProfile } from '@/lib/profile'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
import { useTabsStateSnapshot } from '@/state/tabs'

export type MonacoEditorProps = {
  id: string
  snippetName: string
  className?: string
  editorRef: MutableRefObject<IStandaloneCodeEditor | null>
  monacoRef: MutableRefObject<Monaco | null>
  autoFocus?: boolean
  executeQuery: () => void
  executeExplainQuery: () => void
  prettifyQuery: () => void
  onHasSelection: (value: boolean) => void
  onMount?: (editor: IStandaloneCodeEditor) => void
  onPrompt?: (value: {
    selection: string
    beforeSelection: string
    afterSelection: string
    startLineNumber: number
    endLineNumber: number
  }) => void
  placeholder?: string
}

const MonacoEditor = ({
  id,
  snippetName,
  editorRef,
  monacoRef,
  autoFocus = true,
  placeholder = '',
  className,
  executeQuery,
  executeExplainQuery,
  prettifyQuery,
  onHasSelection,
  onPrompt,
  onMount,
}: MonacoEditorProps) => {
  const router = useRouter()
  const { profile } = useProfile()
  const { ref, content } = useParams()
  const { data: project } = useSelectedProjectQuery()

  const snapV2 = useSqlEditorV2StateSnapshot()
  const tabsSnap = useTabsStateSnapshot()
  const aiSnap = useAiAssistantStateSnapshot()
  const { openSidebar, toggleSidebar } = useSidebarManagerSnapshot()

  const [intellisenseEnabled] = useLocalStorageQuery(
    LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
    true
  )
  const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
  const isCommandMenuHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
  const setCommandMenuOpen = useSetCommandMenuOpen()

  // [Joshen] Lodash debounce doesn't seem to be working here, so opting to use useDebounce
  const [value, setValue] = useState('')
  const debouncedValue = useDebounce(value, 1000)

  const snippet = snapV2.snippets[id]
  const disableEdit =
    snippet?.snippet.visibility === 'project' && snippet?.snippet.owner_id !== profile?.id

  const executeQueryRef = useRef(executeQuery)
  executeQueryRef.current = executeQuery

  const executeExplainQueryRef = useRef(executeExplainQuery)
  executeExplainQueryRef.current = executeExplainQuery

  const prettifyQueryRef = useRef(prettifyQuery)
  prettifyQueryRef.current = prettifyQuery

  const aiHotkeyEnabledRef = useRef(isAIAssistantHotkeyEnabled)
  aiHotkeyEnabledRef.current = isAIAssistantHotkeyEnabled

  const commandMenuHotkeyEnabledRef = useRef(isCommandMenuHotkeyEnabled)
  commandMenuHotkeyEnabledRef.current = isCommandMenuHotkeyEnabled

  const setCommandMenuOpenRef = useRef(setCommandMenuOpen)
  setCommandMenuOpenRef.current = setCommandMenuOpen

  const handleEditorOnMount: OnMount = async (editor, monaco) => {
    editorRef.current = editor
    monacoRef.current = monaco

    const model = editorRef.current.getModel()
    if (model !== null) {
      monacoRef.current.editor.setModelMarkers(model, 'owner', [])
    }

    // Blur the editor on Escape so users can hop out to the rest of the UI.
    // The precondition defers to Monaco's own Escape consumers (suggest widget,
    // find widget, parameter hints, snippet/rename mode, inline suggestions) and
    // to selection/multi-cursor cancellation, so inline features keep working.
    editor.addCommand(
      monaco.KeyCode.Escape,
      () => {
        ;(document.activeElement as HTMLElement | null)?.blur()
      },
      [
        'editorTextFocus',
        '!editorHasSelection',
        '!editorHasMultipleSelections',
        '!suggestWidgetVisible',
        '!findWidgetVisible',
        '!parameterHintsVisible',
        '!renameInputVisible',
        '!inSnippetMode',
        '!accessibilityHelpWidgetVisible',
        '!inlineSuggestionVisible',
      ].join(' && ')
    )

    editor.addAction({
      id: 'run-query',
      label: 'Run Query',
      keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],
      contextMenuGroupId: 'operation',
      contextMenuOrder: 0,
      run: () => {
        executeQueryRef.current()
      },
    })

    editor.addAction({
      id: 'run-explain-query',
      label: 'Run EXPLAIN ANALYZE',
      keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter],
      contextMenuGroupId: 'operation',
      contextMenuOrder: 1,
      run: () => {
        executeExplainQueryRef.current()
      },
    })

    editor.addAction({
      id: 'save-query',
      label: 'Save Query',
      keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyS],
      contextMenuGroupId: 'operation',
      contextMenuOrder: 0,
      run: () => {
        if (snippet) snapV2.addNeedsSaving(snippet.snippet.id)
      },
    })

    editor.addAction({
      id: 'prettify-query',
      label: 'Prettify SQL',
      keybindings: [monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF],
      contextMenuGroupId: 'operation',
      contextMenuOrder: 2,
      run: () => {
        prettifyQueryRef.current()
      },
    })

    editor.addAction({
      id: 'explain-code',
      label: 'Explain Code',
      contextMenuGroupId: 'operation',
      contextMenuOrder: 1,
      run: () => {
        const selectedValue = (editorRef?.current as any)
          .getModel()
          .getValueInRange((editorRef?.current as any)?.getSelection())
        openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
        aiSnap.newChat({
          name: 'Explain code section',
          sqlSnippets: [selectedValue],
          initialInput: 'Can you explain this section to me in more detail?',
        })
      },
    })

    editor.addAction({
      id: 'toggle-ai-assistant',
      label: 'Toggle AI Assistant',
      keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyI],
      run: () => {
        if (aiHotkeyEnabledRef.current) {
          toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
        }
      },
    })

    if (onPrompt) {
      editor.addAction({
        id: 'generate-sql',
        label: 'Generate SQL',
        keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyK],
        run: () => {
          const selectionParts = getEditorSelectionParts(editor)
          if (selectionParts) onPrompt(selectionParts)
        },
      })
    }

    // Monaco claims Cmd+K as a chord prefix, which swallows the global command
    // menu shortcut while the editor is focused. Intercept it here and open the
    // command menu directly so it works the same inside and outside the editor.
    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
      if (commandMenuHotkeyEnabledRef.current) {
        setCommandMenuOpenRef.current(true)
      }
    })

    editor.onDidChangeCursorSelection(({ selection }) => {
      const noSelection =
        selection.startLineNumber === selection.endLineNumber &&
        selection.startColumn === selection.endColumn
      onHasSelection(!noSelection)
    })

    if (autoFocus) {
      if (editor.getValue().length === 1) editor.setPosition({ lineNumber: 1, column: 2 })
      editor.focus()
    }

    onMount?.(editor)
  }

  function handleEditorChange(value: string | undefined) {
    tabsSnap.makeActiveTabPermanent()
    if (id && value) {
      if (!snippet && ref && profile !== undefined && project !== undefined) {
        const snippet = createSqlSnippetSkeletonV2({
          idOverride: id,
          name: snippetName,
          sql: value,
          owner_id: profile?.id,
          project_id: project?.id,
        })
        snapV2.addSnippet({ projectRef: ref, snippet })
        router.push(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
      }
      setValue(value)
    }
  }

  useEffect(() => {
    if (debouncedValue.length > 0 && snippet) {
      const shouldInvalidate = snippet.snippet.isNotSavedInDatabaseYet
      snapV2.setSql({ id, sql: value, shouldInvalidate })
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [debouncedValue])

  // if an SQL query is passed by the content parameter, set the editor value to its content. This
  // is usually used for sending the user to SQL editor from other pages with SQL.
  useEffect(() => {
    if (content && content.length > 0) handleEditorChange(content)
  }, [])

  return (
    <>
      {disableEdit && (
        <Admonition
          type="default"
          className="rounded-none border-0 border-b"
          title="Read-only snippet"
          description="This snippet has been shared to the project and is only editable by the owner who created this snippet. You may duplicate this snippet into a personal copy by right clicking on the snippet and selecting “Duplicate query”."
        />
      )}
      <Editor
        className={cn(className, 'monaco-editor')}
        theme={'briven'}
        onMount={handleEditorOnMount}
        onChange={handleEditorChange}
        defaultLanguage="pgsql"
        defaultValue={snippet?.snippet.content?.unchecked_sql}
        path={id}
        options={{
          tabSize: 2,
          fontSize: 13,
          placeholder,
          lineDecorationsWidth: 0,
          readOnly: disableEdit,
          minimap: { enabled: false },
          wordWrap: 'on',
          padding: { top: 4 },
          // [Joshen] Commenting the following out as it causes the autocomplete suggestion popover
          // to be positioned wrongly somehow. I'm not sure if this affects anything though, but leaving
          // comment just in case anyone might be wondering. Relevant issues:
          // - https://github.com/microsoft/monaco-editor/issues/2229
          // - https://github.com/microsoft/monaco-editor/issues/2503
          // fixedOverflowWidgets: true,
          suggest: {
            showMethods: intellisenseEnabled,
            showFunctions: intellisenseEnabled,
            showConstructors: intellisenseEnabled,
            showDeprecated: intellisenseEnabled,
            showFields: intellisenseEnabled,
            showVariables: intellisenseEnabled,
            showClasses: intellisenseEnabled,
            showStructs: intellisenseEnabled,
            showInterfaces: intellisenseEnabled,
            showModules: intellisenseEnabled,
            showProperties: intellisenseEnabled,
            showEvents: intellisenseEnabled,
            showOperators: intellisenseEnabled,
            showUnits: intellisenseEnabled,
            showValues: intellisenseEnabled,
            showConstants: intellisenseEnabled,
            showEnums: intellisenseEnabled,
            showEnumMembers: intellisenseEnabled,
            showKeywords: intellisenseEnabled,
            showWords: intellisenseEnabled,
            showColors: intellisenseEnabled,
            showFiles: intellisenseEnabled,
            showReferences: intellisenseEnabled,
            showFolders: intellisenseEnabled,
            showTypeParameters: intellisenseEnabled,
            showIssues: intellisenseEnabled,
            showUsers: intellisenseEnabled,
            showSnippets: intellisenseEnabled,
          },
        }}
      />
    </>
  )
}

export default MonacoEditor
