import { zodResolver } from '@hookform/resolvers/zod'
import {
  ident,
  joinSqlFragments,
  safeSql,
  type SafeSqlFragment,
} from '@supabase/pg-meta/src/pg-format'
import { useParams } from 'common'
import randomBytes from 'randombytes'
import { useEffect, useMemo } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
  Button,
  Form,
  FormControl,
  FormField,
  Input,
  RadioGroupStacked,
  RadioGroupStackedItem,
  Separator,
  Sheet,
  SheetContent,
  SheetFooter,
  SheetHeader,
  SheetSection,
  SheetTitle,
  Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import * as z from 'zod'

import { Hook, HOOK_DEFINITION_TITLE, HOOKS_DEFINITIONS } from './hooks.constants'
import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils'
import { convertArgumentTypes } from '@/components/interfaces/Database/Functions/Functions.utils'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
import { DocsButton } from '@/components/ui/DocsButton'
import FunctionSelector from '@/components/ui/FunctionSelector'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { AuthConfigResponse } from '@/data/auth/auth-config-query'
import { useAuthHooksUpdateMutation } from '@/data/auth/auth-hooks-update-mutation'
import { executeSql } from '@/data/sql/execute-sql-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { DOCS_URL } from '@/lib/constants'

interface CreateHookSheetProps {
  visible: boolean
  title: HOOK_DEFINITION_TITLE | null
  authConfig: AuthConfigResponse
  onClose: () => void
  onDelete: () => void
}

export function generateAuthHookSecret() {
  const secretByteLength = 60
  const buffer = randomBytes(secretByteLength)
  const base64String = buffer.toString('base64')
  return `v1,whsec_${base64String}`
}

const FORM_ID = 'create-edit-auth-hook'

const FormSchema = z
  .object({
    hookType: z.string(),
    enabled: z.boolean(),
    selectedType: z.union([z.literal('https'), z.literal('postgres')]),
    httpsValues: z.object({
      url: z.string(),
      secret: z.string(),
    }),
    postgresValues: z.object({
      schema: z.string(),
      functionName: z.string(),
    }),
  })
  .superRefine((data, ctx) => {
    if (data.selectedType === 'https') {
      if (!data.httpsValues.url.startsWith('https://')) {
        ctx.addIssue({
          path: ['httpsValues', 'url'],
          code: z.ZodIssueCode.custom,
          message: 'The URL must start with https://',
        })
      }
      if (!data.httpsValues.secret) {
        ctx.addIssue({
          path: ['httpsValues', 'secret'],
          code: z.ZodIssueCode.custom,
          message: 'Missing secret value',
        })
      }
    }
    if (data.selectedType === 'postgres') {
      if (!data.postgresValues.schema) {
        ctx.addIssue({
          path: ['postgresValues', 'schema'],
          code: z.ZodIssueCode.custom,
          message: 'You must select a schema',
        })
      }
      if (!data.postgresValues.functionName) {
        ctx.addIssue({
          path: ['postgresValues', 'functionName'],
          code: z.ZodIssueCode.custom,
          message: 'You must select a Postgres function',
        })
      }
    }
    return true
  })

export const CreateHookSheet = ({
  visible,
  title,
  authConfig,
  onClose,
  onDelete,
}: CreateHookSheetProps) => {
  const { ref: projectRef } = useParams()
  const { data: project } = useSelectedProjectQuery()

  const definition = useMemo(
    () => HOOKS_DEFINITIONS.find((d) => d.title === title) || HOOKS_DEFINITIONS[0],
    [title]
  )

  const supportedReturnTypes =
    definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED'
      ? ['json', 'jsonb', 'void']
      : ['json', 'jsonb']

  const hook: Hook = useMemo(() => {
    return {
      ...definition,
      enabled: authConfig?.[definition.enabledKey] || false,
      method: extractMethod(
        authConfig?.[definition.uriKey] || '',
        authConfig?.[definition.secretsKey] || ''
      ),
    }
  }, [definition, authConfig])

  // if the hook has all parameters, then it is not being created.
  const isCreating = !isValidHook(hook)

  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema as any),
    defaultValues: {
      hookType: title || '',
      enabled: true,
      selectedType: 'postgres',
      httpsValues: {
        url: '',
        secret: '',
      },
      postgresValues: {
        schema: 'public',
        functionName: '',
      },
    },
  })

  const isDirty = form.formState.isDirty
  const values = form.watch()
  const {
    confirmOnClose,
    handleOpenChange,
    modalProps: discardChangesModalProps,
  } = useConfirmOnClose({
    checkIsDirty: () => isDirty,
    onClose,
  })

  const statements = useMemo(() => {
    let permissionChanges: Array<SafeSqlFragment> = []
    if (hook.method.type === 'postgres') {
      if (
        hook.method.schema !== '' &&
        hook.method.functionName !== '' &&
        hook.method.functionName !== values.postgresValues.functionName
      ) {
        permissionChanges = getRevokePermissionStatements(
          hook.method.schema,
          hook.method.functionName
        )
      }
    }

    if (values.postgresValues.functionName !== '') {
      const schema = values.postgresValues.schema
      const functionName = values.postgresValues.functionName
      permissionChanges = [
        ...permissionChanges,
        safeSql`-- Grant access to function to briven_auth_admin
grant execute on function ${ident(schema)}.${ident(functionName)} to briven_auth_admin;`,
        safeSql`-- Grant access to schema to briven_auth_admin
grant usage on schema ${ident(schema)} to briven_auth_admin;`,
        safeSql`-- Revoke function permissions from authenticated, anon and public
revoke execute on function ${ident(schema)}.${ident(functionName)} from authenticated, anon, public;`,
      ]
    }
    return permissionChanges
  }, [hook, values.postgresValues.schema, values.postgresValues.functionName])

  const { mutate: updateAuthHooks, isPending: isUpdatingAuthHooks } = useAuthHooksUpdateMutation({
    onSuccess: () => {
      toast.success(`Successfully created ${values.hookType}.`)
      if (statements.length > 0) {
        executeSql({
          projectRef,
          connectionString: project!.connectionString,
          sql: joinSqlFragments(statements, '\n'),
        })
      }
      onClose()
    },
    onError: (error) => {
      toast.error(`Failed to create hook: ${error.message}`)
    },
  })

  const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
    if (!project) return console.error('Project is required')
    const definition = HOOKS_DEFINITIONS.find((d) => values.hookType === d.title)

    if (!definition) {
      return
    }

    const enabledLabel = definition.enabledKey
    const uriLabel = definition.uriKey
    const secretsLabel = definition.secretsKey

    let url = ''
    if (values.selectedType === 'postgres') {
      url = `pg-functions://postgres/${values.postgresValues.schema}/${values.postgresValues.functionName}`
    } else {
      url = values.httpsValues.url
    }

    const payload = {
      [enabledLabel]: values.enabled,
      [uriLabel]: url,
      [secretsLabel]: values.selectedType === 'https' ? values.httpsValues.secret : null,
    }

    updateAuthHooks({ projectRef: projectRef!, config: payload })
  }

  useEffect(() => {
    if (visible) {
      if (definition) {
        const values = extractMethod(
          authConfig?.[definition.uriKey] || '',
          authConfig?.[definition.secretsKey] || ''
        )

        form.reset({
          hookType: definition.title,
          enabled: isCreating ? true : authConfig?.[definition.enabledKey],
          selectedType: values.type,
          httpsValues: {
            url: (values.type === 'https' && values.url) || '',
            secret: (values.type === 'https' && values.secret) || '',
          },
          postgresValues: {
            schema: (values.type === 'postgres' && values.schema) || 'public',
            functionName: (values.type === 'postgres' && values.functionName) || '',
          },
        })
      } else {
        form.reset({
          hookType: title || '',
          enabled: true,
          selectedType: 'postgres',
          httpsValues: {
            url: '',
            secret: '',
          },
          postgresValues: {
            schema: 'public',
            functionName: '',
          },
        })
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [authConfig, title, visible, definition])

  return (
    <Sheet open={visible} onOpenChange={handleOpenChange}>
      <SheetContent
        aria-describedby={undefined}
        size="lg"
        showClose={false}
        className="flex flex-col gap-0"
      >
        <SheetHeader className="py-3 flex flex-row justify-between items-center border-b-0">
          <SheetTitle className="truncate">
            {isCreating ? `Add ${title}` : `Update ${title}`}
          </SheetTitle>
          <DocsButton href={`${DOCS_URL}/guides/auth/auth-hooks/${hook.docSlug}`} />
        </SheetHeader>
        <Separator />
        <SheetSection className="overflow-auto grow px-0">
          <Form {...form}>
            <form
              id={FORM_ID}
              className="space-y-6 w-full py-5 flex-1"
              onSubmit={form.handleSubmit(onSubmit)}
            >
              <FormField
                key="enabled"
                name="enabled"
                control={form.control}
                render={({ field }) => (
                  <FormItemLayout
                    layout="flex"
                    className="px-5"
                    label={`Enable ${values.hookType}`}
                    description={
                      values.hookType === 'Send SMS hook'
                        ? 'SMS Provider settings will be disabled in favor of SMS hooks'
                        : undefined
                    }
                  >
                    <FormControl>
                      <Switch
                        checked={field.value}
                        onCheckedChange={field.onChange}
                        disabled={field.disabled}
                      />
                    </FormControl>
                  </FormItemLayout>
                )}
              />
              <Separator />
              <FormField
                control={form.control}
                name="selectedType"
                render={({ field }) => (
                  <FormItemLayout label="Hook type" className="px-5">
                    <FormControl>
                      <RadioGroupStacked
                        value={field.value}
                        onValueChange={(value) => field.onChange(value)}
                      >
                        <RadioGroupStackedItem
                          value="postgres"
                          id="postgres"
                          key="postgres"
                          label="Postgres"
                          description="Used to call a Postgres function."
                        />
                        <RadioGroupStackedItem
                          value="https"
                          id="https"
                          key="https"
                          label="HTTPS"
                          description="Used to call any HTTPS endpoint."
                        />
                      </RadioGroupStacked>
                    </FormControl>
                  </FormItemLayout>
                )}
              />
              {values.selectedType === 'postgres' ? (
                <>
                  <div className="grid grid-cols-2 gap-8 px-5">
                    <FormField
                      key="postgresValues.schema"
                      control={form.control}
                      name="postgresValues.schema"
                      render={({ field }) => (
                        <FormItemLayout
                          label="Postgres Schema"
                          description="Postgres schema where the function is defined"
                        >
                          <FormControl>
                            <SchemaSelector
                              size="small"
                              showError={false}
                              stopScrollPropagation
                              selectedSchemaName={field.value}
                              onSelectSchema={(name) => field.onChange(name)}
                              disabled={field.disabled}
                            />
                          </FormControl>
                        </FormItemLayout>
                      )}
                    />
                    <FormField
                      key="postgresValues.functionName"
                      control={form.control}
                      name="postgresValues.functionName"
                      render={({ field }) => (
                        <FormItemLayout
                          label="Postgres function"
                          description="This function will be called by Briven Auth each time the hook is triggered"
                        >
                          <FormControl>
                            <FunctionSelector
                              size="small"
                              schema={values.postgresValues.schema}
                              value={field.value}
                              stopScrollPropagation
                              onChange={field.onChange}
                              disabled={field.disabled}
                              filterFunction={(func) => {
                                if (supportedReturnTypes.includes(func.return_type)) {
                                  const { value } = convertArgumentTypes(func.argument_types)
                                  if (value.length !== 1) return false
                                  return value[0].type === 'json' || value[0].type === 'jsonb'
                                }
                                return false
                              }}
                              noResultsLabel={
                                <span>
                                  No function with a single JSON/B argument
                                  <br />
                                  and JSON/B
                                  {definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED'
                                    ? ' or void'
                                    : ''}{' '}
                                  return type found in this schema.
                                </span>
                              }
                            />
                          </FormControl>
                        </FormItemLayout>
                      )}
                    />
                  </div>

                  {statements.length > 0 && (
                    <div className="h-72 w-full gap-3 flex flex-col">
                      <p className="text-sm text-foreground-light px-5">
                        The following statements will be executed on the selected function:
                      </p>
                      <CodeEditor
                        isReadOnly
                        id="postgres-hook-editor"
                        language="pgsql"
                        value={statements.join('\n\n')}
                      />
                    </div>
                  )}
                </>
              ) : (
                <div className="flex flex-col gap-4 px-5">
                  <FormField
                    key="httpsValues.url"
                    control={form.control}
                    name="httpsValues.url"
                    render={({ field }) => (
                      <FormItemLayout
                        label="URL"
                        description="Briven Auth will send a HTTPS POST request to this URL each time the hook is triggered."
                      >
                        <FormControl>
                          <Input {...field} />
                        </FormControl>
                      </FormItemLayout>
                    )}
                  />
                  <FormField
                    key="httpsValues.secret"
                    control={form.control}
                    name="httpsValues.secret"
                    render={({ field }) => (
                      <FormItemLayout
                        label="Secret"
                        description={
                          <div className="flex items-center gap-x-2">
                            <p>
                              Should be a base64 encoded hook secret with a prefix{' '}
                              <code className="text-code-inline">v1,whsec_</code>.
                            </p>
                            <InfoTooltip side="bottom" className="w-60 text-center">
                              <code className="text-code-inline">v1</code> denotes the signature
                              version and <code className="text-code-inline">whsec_</code> signifies
                              a symmetric secret.
                            </InfoTooltip>
                          </div>
                        }
                      >
                        <FormControl>
                          <div className="flex flex-row">
                            <Input {...field} className="rounded-r-none border-r-0" />
                            <Button
                              type="default"
                              size="small"
                              className="rounded-l-none text-xs"
                              onClick={() => {
                                const authHookSecret = generateAuthHookSecret()
                                form.setValue('httpsValues.secret', authHookSecret, {
                                  shouldDirty: true,
                                })
                              }}
                            >
                              Generate secret
                            </Button>
                          </div>
                        </FormControl>
                      </FormItemLayout>
                    )}
                  />
                </div>
              )}
            </form>
          </Form>
        </SheetSection>
        <SheetFooter>
          {!isCreating && (
            <div className="flex-1">
              <Button type="danger" onClick={() => onDelete()}>
                Delete hook
              </Button>
            </div>
          )}

          <Button disabled={isUpdatingAuthHooks} type="default" onClick={confirmOnClose}>
            Cancel
          </Button>
          <Button
            form={FORM_ID}
            htmlType="submit"
            disabled={isUpdatingAuthHooks}
            loading={isUpdatingAuthHooks}
          >
            {isCreating ? 'Create hook' : 'Update hook'}
          </Button>
        </SheetFooter>
      </SheetContent>
      <DiscardChangesConfirmationDialog {...discardChangesModalProps} />
    </Sheet>
  )
}
