'use client'

import Editor, { useMonaco } from '@monaco-editor/react'
import {
  formatCurl,
  formatHttp,
  HttpRequest,
  ParsingError,
  processSql,
  RenderError,
  renderHttp,
  renderBrivenJs,
  Statement,
  BrivenJsQuery,
  UnimplementedError,
  UnsupportedError,
} from '@supabase/sql-to-rest'
import { ChevronUp, GitPullRequest } from 'lucide-react'
import type { editor } from 'monaco-editor'
import { useTheme } from 'next-themes'
import {
  PropsWithChildren,
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useState,
} from 'react'
import Markdown from 'react-markdown'
import { format } from 'sql-formatter'
import { Alert, cn, Collapsible, CollapsibleContent, CollapsibleTrigger, Tabs } from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'

import { assumptions } from './assumptions'
import { BaseUrlDialog } from './base-url-dialog'
import { faqs } from './faqs'
import { transformRenderer } from './syntax-highlighter/transform-renderer'
import { ResultBundle } from './util'

export interface SqlToRestProps {
  defaultValue?: string
  defaultBaseUrl?: string
}

export default function SqlToRest({
  defaultValue,
  defaultBaseUrl = 'http://localhost:54321/rest/v1',
}: SqlToRestProps) {
  const monaco = useMonaco()
  const { resolvedTheme } = useTheme()
  const isDark = resolvedTheme?.includes('dark') ?? true

  useLayoutEffect(() => {
    if (monaco) {
      const lightMode = getTheme(false)
      const darkMode = getTheme(true)
      monaco.editor.defineTheme('briven-light', lightMode)
      monaco.editor.defineTheme('briven-dark', darkMode)
    }
  }, [monaco])

  const [sql, setSql] = useState(defaultValue ?? '')
  const [statement, setStatement] = useState<Statement>()
  const [currentLanguage, setCurrentLanguage] = useState('curl')

  const [httpRequest, setHttpRequest] = useState<HttpRequest>()
  const [jsQuery, setJsQuery] = useState<BrivenJsQuery>()

  const [parsingError, setParsingError] = useState<ParsingError>()
  const [unimplementedError, setUnimplementedError] = useState<UnimplementedError>()
  const [unsupportedError, setUnsupportedError] = useState<UnsupportedError>()
  const [httpRenderError, setHttpRenderError] = useState<RenderError>()
  const [brivenJsRenderError, setBrivenJsRenderError] = useState<RenderError>()

  const [isBaseUrlDialogOpen, setIsBaseUrlDialogOpen] = useState(false)
  const [baseUrl, setBaseUrl] = useState(defaultBaseUrl)

  const baseUrlObject = useMemo(() => {
    try {
      return new URL(baseUrl)
    } catch (err) {
      return undefined
    }
  }, [baseUrl])

  const codeBlockRenderer = useMemo(
    () =>
      transformRenderer({
        search: (text) => !!baseUrlObject && text.includes(baseUrlObject.host),
        wrapper: ({ children }) => (
          <span
            className="cursor-pointer border-b border-dotted border-neutral-500"
            onClick={() => setIsBaseUrlDialogOpen(true)}
          >
            {children}
          </span>
        ),
      }),
    [baseUrlObject]
  )

  const rawHttp = useMemo(() => {
    if (!httpRequest) {
      return
    }
    return formatHttp(baseUrl, httpRequest)
  }, [httpRequest, baseUrl])

  const curlCommand = useMemo(() => {
    if (!httpRequest) {
      return
    }
    return formatCurl(baseUrl, httpRequest)
  }, [httpRequest, baseUrl])

  const jsCommand = useMemo(() => {
    if (!jsQuery) {
      return
    }
    const { code } = jsQuery
    return code
  }, [jsQuery])

  const relevantFaqs = useMemo(() => {
    if (!statement) {
      return []
    }

    switch (currentLanguage) {
      case 'http':
      case 'curl': {
        if (!httpRequest) {
          return []
        }

        const result: ResultBundle = {
          type: 'http',
          language: currentLanguage,
          statement,
          ...httpRequest,
        }

        return faqs.filter((faq) => faq.condition(result))
      }
      case 'js': {
        if (!jsQuery) {
          return []
        }

        const result: ResultBundle = {
          type: 'briven-js',
          language: currentLanguage,
          statement,
          ...jsQuery,
        }

        return faqs.filter((faq) => faq.condition(result))
      }
      default:
        return []
    }
  }, [currentLanguage, statement, httpRequest, jsQuery])

  const relevantAssumptions = useMemo(() => {
    if (!statement) {
      return []
    }

    switch (currentLanguage) {
      case 'http':
      case 'curl': {
        if (!httpRequest) {
          return []
        }

        const result: ResultBundle = {
          type: 'http',
          language: currentLanguage,
          statement,
          ...httpRequest,
        }

        return assumptions.filter((a) => a.condition(result)).flatMap((a) => a.assumptions(result))
      }
      case 'js': {
        if (!jsQuery) {
          return []
        }

        const result: ResultBundle = {
          type: 'briven-js',
          language: currentLanguage,
          statement,
          ...jsQuery,
        }

        return assumptions.filter((a) => a.condition(result)).flatMap((a) => a.assumptions(result))
      }
      default:
        return []
    }
  }, [currentLanguage, statement, httpRequest, jsQuery])

  const process = useCallback(async (sql: string) => {
    setSql(sql)

    try {
      const statement = await processSql(sql)
      const httpRequest = await renderHttp(statement)
      const jsQuery = await renderBrivenJs(statement)

      setParsingError(undefined)
      setUnimplementedError(undefined)
      setUnsupportedError(undefined)
      setHttpRenderError(undefined)
      setBrivenJsRenderError(undefined)

      setStatement(statement)
      setHttpRequest(httpRequest)
      setJsQuery(jsQuery)
    } catch (error) {
      setParsingError(undefined)
      setUnimplementedError(undefined)
      setUnsupportedError(undefined)
      setHttpRenderError(undefined)
      setBrivenJsRenderError(undefined)

      if (error instanceof ParsingError) {
        setParsingError(error)
      } else if (error instanceof UnimplementedError) {
        setUnimplementedError(error)
      } else if (error instanceof UnsupportedError) {
        setUnsupportedError(error)
      } else if (error instanceof RenderError) {
        if (error.renderer === 'http') {
          setHttpRenderError(error)
        } else if (error.renderer === 'briven-js') {
          setBrivenJsRenderError(error)
        } else {
          console.error(error)
        }
      } else {
        console.error(error)
      }
    }
  }, [])

  // Process initial value only
  useEffect(() => {
    process(sql)
  }, [process])

  return (
    <div className="grid grid-cols-1 2xl:grid-cols-2 gap-4 mt-4">
      <div className="flex flex-col gap-4">
        <div className="font-medium">Enter SQL to translate</div>
        <div
          className={cn('h-96 py-4 border rounded-md', isDark ? 'bg-[#1f1f1f]' : 'bg-[#f0f0f0]')}
        >
          <Editor
            language="pgsql"
            theme={isDark ? 'briven-dark' : 'briven-light'}
            value={sql}
            options={{
              tabSize: 2,
              minimap: {
                enabled: false,
              },
              fontSize: 13,
            }}
            onMount={async (editor, monaco) => {
              // Register pgsql formatter
              monaco.languages.registerDocumentFormattingEditProvider('pgsql', {
                async provideDocumentFormattingEdits(model) {
                  const currentCode = editor.getValue()
                  const formattedCode = format(currentCode, {
                    language: 'postgresql',
                    keywordCase: 'lower',
                  })
                  return [
                    {
                      range: model.getFullModelRange(),
                      text: formattedCode,
                    },
                  ]
                },
              })

              // Format on cmd+s
              editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, async () => {
                await editor.getAction('editor.action.formatDocument')?.run()
              })

              // Run format on the initial value
              await editor.getAction('editor.action.formatDocument')?.run()
            }}
            onChange={async (sql) => {
              if (!sql) {
                return
              }
              await process(sql)
            }}
          />
        </div>

        {parsingError && (
          <Alert className="text-red-900">
            {parsingError.message}. {parsingError.hint}
          </Alert>
        )}
        {unsupportedError && (
          <Alert className="text-red-900">
            <div>
              {unsupportedError.message}. {unsupportedError.hint}
            </div>
            <div className="prose text-sm mt-2">
              PostgREST doesn't support this query. If you're sure the syntax is correct and are
              unable to modify it, wrap it in a database function and call it using the{' '}
              <a href="https://postgrest.org/en/v12/references/api/stored_procedures.html#stored-procedures">
                RPC
              </a>{' '}
              endpoint.
            </div>
          </Alert>
        )}
        {unimplementedError && (
          <Alert className="text-orange-1000">
            {unimplementedError.message}.
            <div className="mt-2 text-white flex gap-1 leading-6">
              <GitPullRequest className="inline-block" width={16} />
              <a
                href="https://github.com/supabase-community/sql-to-rest/issues/new/choose"
                target="_blank"
                rel="noopener noreferrer"
              >
                Create a PR
              </a>
            </div>
          </Alert>
        )}
      </div>
      <BaseUrlDialog
        defaultValue={baseUrl}
        onChange={(value) => setBaseUrl(value)}
        open={isBaseUrlDialogOpen}
        onOpenChange={(open) => setIsBaseUrlDialogOpen(open)}
      />

      <div
        className={cn(
          'flex flex-col gap-4',
          parsingError || unsupportedError || unimplementedError
            ? 'opacity-25 pointer-events-none'
            : ''
        )}
      >
        <div className="font-medium">Choose language to translate to</div>
        <Tabs activeId={currentLanguage} onChange={(id: string) => setCurrentLanguage(id)}>
          <Tabs.Panel id="curl" label="cURL" className="flex flex-col gap-4">
            {httpRenderError && <Alert className="text-red-900">{httpRenderError.message}</Alert>}
            <CodeBlock
              language="curl"
              hideLineNumbers
              className={cn(
                'self-stretch  overflow-y-hidden',
                httpRenderError || !baseUrlObject ? 'opacity-25 pointer-events-none' : ''
              )}
              renderer={codeBlockRenderer}
            >
              {curlCommand}
            </CodeBlock>
          </Tabs.Panel>
          <Tabs.Panel id="http" label="HTTP" className="flex flex-col gap-4">
            {httpRenderError && <Alert className="text-red-900">{httpRenderError.message}</Alert>}
            <CodeBlock
              language="http"
              hideLineNumbers
              className={cn(
                'self-stretch overflow-y-hidden',
                httpRenderError ? 'opacity-25 pointer-events-none' : ''
              )}
              renderer={codeBlockRenderer}
            >
              {rawHttp}
            </CodeBlock>
          </Tabs.Panel>
          <Tabs.Panel id="js" label="JavaScript" className="flex flex-col gap-4">
            {brivenJsRenderError && (
              <Alert className="text-red-900">{brivenJsRenderError.message}</Alert>
            )}
            <CodeBlock
              language="js"
              hideLineNumbers
              className={cn(
                'self-stretch overflow-y-hidden',
                brivenJsRenderError ? 'opacity-25 pointer-events-none' : ''
              )}
              renderer={codeBlockRenderer}
            >
              {jsCommand}
            </CodeBlock>
          </Tabs.Panel>
        </Tabs>
        <div
          className={cn(
            'flex flex-col gap-4',
            ((currentLanguage === 'http' || currentLanguage === 'curl') && httpRenderError) ||
              (currentLanguage === 'js' && brivenJsRenderError)
              ? 'opacity-25 pointer-events-none'
              : ''
          )}
        >
          {relevantAssumptions.length > 0 && (
            <div>
              <h3 className="my-1 text-base text-inherit">Assumptions</h3>
              <ol className="my-0 text-foreground">
                {relevantAssumptions.map((assumption) => (
                  <li className="text-sm">
                    <Markdown>{assumption}</Markdown>
                  </li>
                ))}
              </ol>
            </div>
          )}

          {relevantFaqs.length > 0 && (
            <>
              <h3 className="my-1 text-base text-inherit">FAQs</h3>
              {relevantFaqs.map((faq) => (
                <Collapsible
                  key={faq.id}
                  className="flex flex-col items-stretch justify-start bg-surface-100 rounded-sm border border-default px-4"
                >
                  <CollapsibleTrigger asChild>
                    <button
                      type="button"
                      className="flex justify-between items-center p-3 text-sm text-left"
                    >
                      <Markdown
                        components={{
                          p: ({ children }: PropsWithChildren) => <p className="m-0">{children}</p>,
                        }}
                      >
                        {faq.question}
                      </Markdown>
                      <ChevronUp className="transition data-open-parent:rotate-0 data-closed-parent:rotate-180" />
                    </button>
                  </CollapsibleTrigger>
                  <CollapsibleContent>
                    <div className="text-foreground flex flex-col justify-start items-center px-3 pb-4 text-sm">
                      <Markdown
                        components={{
                          code: (props: any) => <CodeBlock hideLineNumbers {...props} />,
                        }}
                      >
                        {faq.answer}
                      </Markdown>
                    </div>
                  </CollapsibleContent>
                </Collapsible>
              ))}
            </>
          )}
        </div>
      </div>
    </div>
  )
}

// TODO: this was copied from studio - find a way to share it between sites
function getTheme(isDarkMode: boolean): editor.IStandaloneThemeData {
  return {
    base: isDarkMode ? 'vs-dark' : 'vs', // can also be vs-dark or hc-black
    inherit: true, // can also be false to completely replace the builtin rules
    rules: [
      {
        token: '',
        background: isDarkMode ? '1f1f1f' : 'f0f0f0',
        foreground: isDarkMode ? 'd4d4d4' : '444444',
      },
      { token: 'string.sql', foreground: '24b47e' },
      { token: 'comment', foreground: '666666' },
      { token: 'predefined.sql', foreground: isDarkMode ? 'D4D4D4' : '444444' },
    ],
    colors: { 'editor.background': isDarkMode ? '#1f1f1f' : '#f0f0f0' },
  }
}
