import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
import { seoPlugin } from '@payloadcms/plugin-seo'
import type {
  ArrayField,
  CheckboxField,
  Field,
  GroupField,
  RichTextField,
  SelectField,
  TextField,
  TextareaField,
} from 'payload'
import { Plugin } from 'payload'
import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
import { Page } from '@/payload-types'
import { getServerSideURL } from '@/utilities/getURL'
import { PhoneField } from '@/blocks/Form/Phone/config'
import Redis from 'ioredis'
import { adminOnly, adminOrEditor, anyRole, anyRoleAdmin } from '@/access/roles'
import { logCollectionChange, logCollectionDelete } from '@/hooks/auditLog'

const submissionRateLimit = 3
const submissionRateWindowSec = 60
const submissionMaxFieldLength = 5000
const submissionMaxFields = 100

const redis: Redis | null =
  process.env.REDIS_HOST && process.env.REDIS_PORT
    ? new Redis({
      host: process.env.REDIS_HOST,
      port: Number(process.env.REDIS_PORT),
      username: process.env.REDIS_USERNAME,
      password: process.env.REDIS_PASSWORD,
      connectTimeout: Number(process.env.REDIS_CONNECTION_TIMEOUT),
      lazyConnect: true,
      maxRetriesPerRequest: 1,
      enableReadyCheck: false,
    })
    : null

const inMemoryRate = new Map<string, { count: number; resetAt: number }>()

const getHeader = (req: any, name: string) => {
  if (!req?.headers) return null
  if (typeof req.headers.get === 'function') return req.headers.get(name)
  const key = name.toLowerCase()
  return req.headers[key] ?? req.headers[name] ?? null
}

const getClientIp = (req: any) => {
  const forwarded = getHeader(req, 'x-forwarded-for')
  if (typeof forwarded === 'string' && forwarded.length > 0) {
    return forwarded.split(',')[0].trim()
  }
  const realIp = getHeader(req, 'x-real-ip')
  if (typeof realIp === 'string' && realIp.length > 0) {
    return realIp.trim()
  }
  return req?.ip || 'unknown'
}

const checkRateLimit = async (
  ip: string,
  logger?: { warn?: (msg: string, meta?: unknown) => void },
) => {
  if (!submissionRateLimit || submissionRateLimit <= 0) return true

  if (redis) {
    try {
      if (redis.status === 'wait') {
        await redis.connect()
      }
      const key = `form-submissions:rate:${ip}`
      const count = await redis.incr(key)
      if (count === 1) {
        await redis.expire(key, submissionRateWindowSec)
      }
      return count <= submissionRateLimit
    } catch (_err) {
      logger?.warn?.('[form-submissions] redis rate limit failed, falling back to memory')
    }
  }

  const now = Date.now()
  const entry = inMemoryRate.get(ip)
  if (!entry || now >= entry.resetAt) {
    inMemoryRate.set(ip, { count: 1, resetAt: now + submissionRateWindowSec * 1000 })
    return true
  }
  entry.count += 1
  return entry.count <= submissionRateLimit
}

const isMalicious = (value: string) => {
  return /<\s*script|javascript:|data:text\/html/i.test(value)
}

const normalizeSubmissionField = (value: unknown) => {
  if (typeof value !== 'string') return ''
  return value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '')
}

const getSubmissionValue = (submissionData: unknown, fieldNames: string[]) => {
  if (!Array.isArray(submissionData)) return ''
  const normalizedNames = new Set(fieldNames.map(normalizeSubmissionField).filter(Boolean))
  if (normalizedNames.size === 0) return ''

  for (const entry of submissionData) {
    if (!entry || typeof entry !== 'object') continue
    const field = normalizeSubmissionField((entry as { field?: unknown }).field)
    if (!field || !normalizedNames.has(field)) continue
    const value = (entry as { value?: unknown }).value
    if (value === null || typeof value === 'undefined') return ''
    return String(value)
  }

  return ''
}

const getSubmissionIdentity = (submissionData: unknown) => {
  return {
    firstName: getSubmissionValue(submissionData, [
      'firstName',
      'first_name',
      'first name',
      'givenName',
      'given_name',
    ]),
    lastName: getSubmissionValue(submissionData, [
      'lastName',
      'last_name',
      'last name',
      'surname',
      'familyName',
      'family_name',
    ]),
    email: getSubmissionValue(submissionData, ['email', 'emailAddress', 'email_address', 'e-mail']),
  }
}

const verifyRecaptcha = async (token: string, logger?: { warn?: (msg: string, meta?: unknown) => void }) => {
  const secretKey = process.env.RECAPTCHA_SECRET_KEY
  if (!secretKey) return true

  try {
    const response = await fetch(
      `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${token}`,
      {
        method: 'POST',
      },
    )
    const data = (await response.json()) as { 
      success: boolean;
      score?: number; // v3 specific
      action?: string; // v3 specific
    }

    if (!data.success) return false

    // Threshold: 0.3 for dev/staging, 0.5 for production
    const threshold = process.env.NODE_ENV === 'production' ? 0.5 : 0.3
    if (typeof data.score === 'number' && data.score < threshold) {
      logger?.warn?.(`[reCAPTCHA] Submission rejected: score ${data.score} < ${threshold}`)
      return false
    }

    return true
  } catch (error) {
    logger?.warn?.(`[reCAPTCHA] Verification error: ${error instanceof Error ? error.message : String(error)}`)
    return false
  }
}

const keyValuePairToHtmlTable = (data: Record<string, unknown>) => {
  const rows = Object.entries(data)
    .map(([key, value]) => `<tr><td>${key}</td><td>${value ?? ''}</td></tr>`)
    .join('')
  return `<table>${rows}</table>`
}

const replaceDoubleCurlys = (
  str: string,
  variables: Array<{ field: string; value: unknown }> = [],
) => {
  const regex = /\{\{(.+?)\}\}/g
  return str.replace(regex, (_, variable: string) => {
    if (variable.includes('*')) {
      if (variable === '*') {
        return variables.map(({ field, value }) => `${field} : ${value}`).join(' <br /> ')
      }
      if (variable === '*:table') {
        const tableData = variables.reduce<Record<string, unknown>>((acc, { field, value }) => {
          acc[field] = value
          return acc
        }, {})
        return keyValuePairToHtmlTable(tableData)
      }
    } else {
      const found = variables.find(({ field }) => variable === field)
      if (found) return String(found.value ?? '')
    }

    return variable
  })
}

const withAdminCondition = <T extends Field>(
  field: T,
  condition: (_: unknown, siblingData: Record<string, unknown>) => boolean,
): T => {
  const base = field as unknown as { admin?: Record<string, unknown> }
  return {
    ...(field as unknown as Record<string, unknown>),
    admin: {
      ...(base.admin || {}),
      condition,
    },
  } as T
}

const unwrapOuterTag = (html: string, tag: string) => {
  const pattern = new RegExp(`^\\s*<${tag}[^>]*>([\\s\\S]*)<\\/${tag}>\\s*$`, 'i')
  const match = html.match(pattern)
  return match ? match[1] : html
}

const cleanEmailHtml = (html: string) => {
  let out = html
  const looksLikeFullHtml = /<!doctype\s+html|<html[\s>]/i.test(out)
  out = unwrapOuterTag(out, 'div')
  out = unwrapOuterTag(out, 'p')

  if (looksLikeFullHtml) {
    out = out.replace(/<p>\s*(?=<)/gi, '')
    out = out.replace(/>\s*<\/p>/gi, '>')
    out = out.replace(/<\/p>\s*<p>/gi, '')
    out = out.replace(/^<\/p>\s*/i, '')
    out = out.replace(/\s*<p>\s*$/i, '')
  }

  return out
}

const generateTitle: GenerateTitle<Page> = ({ doc }) => {
  return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template'
}

const generateURL: GenerateURL<Page> = ({ doc }) => {
  const url = getServerSideURL()

  return doc?.slug ? `${url}/${doc.slug}` : url
}

export const plugins: Plugin[] = [
  redirectsPlugin({
    collections: ['pages'],
  }),
  seoPlugin({
    generateTitle,
    generateURL,
  }),
  formBuilderPlugin({
    beforeEmail: async (emails, beforeChangeParams) => {
      const submissionData = Array.isArray(beforeChangeParams?.data?.submissionData)
        ? beforeChangeParams?.data?.submissionData
        : []
      const formId = beforeChangeParams?.data?.form

      let emailConfigs: Array<{
        messageFormat?: 'richText' | 'html'
        messageHTML?: unknown
      }> = []

      if (formId && beforeChangeParams?.req?.payload) {
        try {
          const form = await beforeChangeParams.req.payload.findByID({
            collection: 'forms',
            id: formId,
            depth: 0,
            req: beforeChangeParams.req,
          })
          emailConfigs = Array.isArray((form as any)?.emails) ? (form as any).emails : []
        } catch (error) {
          beforeChangeParams?.req?.payload?.logger?.warn?.(
            `[form-submissions] failed to load form email config for HTML mode: ${String(error)}`,
          )
        }
      }

      const cleaned = emails.map((email, index) => {
        const config = emailConfigs[index]
        const messageFormat = config?.messageFormat || 'richText'

        if (messageFormat === 'html') {
          const rawHtml = typeof config?.messageHTML === 'string' ? config.messageHTML : ''
          return {
            ...email,
            html: replaceDoubleCurlys(rawHtml, submissionData),
          }
        }

        if (!email?.html || typeof email.html !== 'string') return email
        return {
          ...email,
          html: cleanEmailHtml(email.html),
        }
      })

      return cleaned
    },
    fields: {
      payment: false,
    },
    formOverrides: {
      access: {
        admin: anyRoleAdmin,
        create: adminOrEditor,
        read: anyRole,
        update: adminOrEditor,
        delete: adminOnly,
      },
      hooks: {
        afterChange: [logCollectionChange('forms')],
        afterDelete: [logCollectionDelete('forms')],
      },
      fields: ({ defaultFields }: { defaultFields: Field[] }): Field[] => {
        const updatedFields: Field[] = [
          {
            name: 'description',
            type: 'textarea',
            label: 'Description / Subtitle',
            admin: {
              description: 'This text will appear below the form title on the website.',
            },
          },
        ]

        for (const field of defaultFields) {


          if ('name' in field && field.name === 'submitButtonLabel') {
            updatedFields.push(field)
            updatedFields.push({
              name: 'submitButtonId',
              type: 'text',
              label: 'Submit Button ID',
              admin: {
                description: 'Optional HTML id attribute for the submit button (e.g. "scilly-submit-btn").',
              },
            } as TextField)
            continue
          }

          if ('name' in field && field.name === 'confirmationMessage') {
            updatedFields.push({
              ...field,
              editor: lexicalEditor({
                features: ({ rootFeatures }) => [
                  ...rootFeatures,
                  FixedToolbarFeature(),
                  HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
                ],
              }),
            } as RichTextField)
            continue
          }

          if ('name' in field && field.name === 'fields' && field.type === 'blocks') {
            updatedFields.push({
              ...field,
              blocks: [...(field.blocks || []), PhoneField],
            } as Field)
            continue
          }

          if ('name' in field && field.name === 'emails' && field.type === 'array') {
            const emailField = field as ArrayField
            const existingFields: Field[] = (emailField.fields || []) as Field[]

            const updatedEmailFields: Field[] = existingFields.flatMap((subField): Field[] => {
              if ('name' in subField && subField.name === 'message') {
                const messageFormatField: SelectField = {
                  name: 'messageFormat',
                  type: 'select',
                  label: 'Message Type',
                  defaultValue: 'richText',
                  options: [
                    { label: 'Rich Text', value: 'richText' },
                    { label: 'HTML (EDM)', value: 'html' },
                  ],
                  required: true,
                }

                const messageField = withAdminCondition(
                  subField,
                  (_: unknown, siblingData) => siblingData?.messageFormat !== 'html',
                )

                const messageHtmlField: TextareaField = {
                  name: 'messageHTML',
                  type: 'textarea',
                  label: 'HTML (EDM)',
                  maxLength: 200000,
                  localized: true,
                  admin: {
                    condition: (_: unknown, siblingData: Record<string, unknown>) =>
                      siblingData?.messageFormat === 'html',
                    description:
                      'Paste full HTML. Use {{fieldName}}, {{*}} or {{*:table}} for form data.',
                    rows: 20,
                  },
                }

                return [messageFormatField, messageField, messageHtmlField]
              }

              return [subField]
            })

            updatedFields.push({
              ...emailField,
              fields: updatedEmailFields,
            } as ArrayField)
            continue
          }

          if ('name' in field && field.name === 'redirect' && field.type === 'group') {
            updatedFields.push({
              name: 'successMessage',
              type: 'text',
              label: 'Success Message',
              localized: true,
              admin: {
                description: 'Message after successful submission.',
              },
            })

            const groupField = field as GroupField
            const existingFields: Field[] = (groupField.fields || []) as Field[]

            updatedFields.push({
              ...groupField,
              fields: existingFields
                .flatMap((subField): Field[] => {
                  if ('name' in subField && subField.name === 'url') {
                    const redirectTypeField: SelectField = {
                      name: 'redirectType',
                      type: 'select',
                      label: 'Redirection Type',
                      defaultValue: 'external',
                      options: [
                        { label: 'External Redirection', value: 'external' },
                        { label: 'Internal Redirection', value: 'internal' },
                      ],
                      required: true,
                    }

                    const urlField = withAdminCondition(
                      subField,
                      (_: unknown, siblingData) => siblingData?.redirectType === 'external',
                    )

                    const slugField: TextField = {
                      name: 'slug',
                      type: 'text',
                      label: 'Slug',
                      admin: {
                        condition: (_: unknown, siblingData: Record<string, unknown>) =>
                          siblingData?.redirectType === 'internal',
                      },
                    }

                    const headField: TextareaField = {
                      name: 'head',
                      type: 'textarea',
                      label: 'Head',
                      admin: {
                        condition: (_: unknown, siblingData: Record<string, unknown>) =>
                          siblingData?.redirectType === 'internal',
                      },
                    }

                    const bodyTopField: TextareaField = {
                      name: 'bodyTop',
                      type: 'textarea',
                      label: 'Body Top',
                      admin: {
                        condition: (_: unknown, siblingData: Record<string, unknown>) =>
                          siblingData?.redirectType === 'internal',
                      },
                    }

                    const bodyBottomField: TextareaField = {
                      name: 'bodyBottom',
                      type: 'textarea',
                      label: 'Body Bottom',
                      admin: {
                        condition: (_: unknown, siblingData: Record<string, unknown>) =>
                          siblingData?.redirectType === 'internal',
                      },
                    }

                    return [
                      redirectTypeField,
                      urlField,
                      slugField,
                      headField,
                      bodyTopField,
                      bodyBottomField,
                    ]
                  }

                  return [subField]
                })
                .concat([
                  {
                    name: 'openInNewTab',
                    type: 'checkbox',
                    label: 'Open In New Tab',
                    defaultValue: false,
                  } as CheckboxField,
                ]),
            } as GroupField)

            continue
          }
          updatedFields.push(field)
        }

        return updatedFields
      },
    },
    formSubmissionOverrides: {
      access: {
        admin: anyRoleAdmin,
        create: () => true,
        read: adminOrEditor,
        update: adminOnly,
        delete: adminOnly,
      },
      admin: {
        defaultColumns: [
          'id',
          'firstName',
          'lastName',
          'email',
          'form',
          'submissionData',
          'createdAt',
        ],
        components: {
          beforeList: ['@/components/FormSubmissionsExport'],
        },
      },
      fields: ({ defaultFields }) => {
        return [
          ...defaultFields,
          {
            name: 'firstName',
            type: 'text',
            label: 'First Name',
            virtual: true,
            admin: {
              readOnly: true,
            },
          },
          {
            name: 'lastName',
            type: 'text',
            label: 'Last Name',
            virtual: true,
            admin: {
              readOnly: true,
            },
          },
          {
            name: 'email',
            type: 'text',
            label: 'Email',
            virtual: true,
            admin: {
              readOnly: true,
            },
          },
          {
            name: 'recaptchaToken',
            type: 'text',
            admin: {
              hidden: true,
            },
          },
        ]
      },
      hooks: {
        beforeValidate: [
          async ({ data, req, operation }) => {
            if (operation !== 'create') return data

            const ip = getClientIp(req)
            const allowed = await checkRateLimit(ip, req?.payload?.logger)
            if (!allowed) {
              throw new Error('Too many submissions. Please try again later.')
            }

            const token = data?.recaptchaToken
            if (!token && operation === 'create') {
              throw new Error('reCAPTCHA token is missing.')
            }

            if (token) {
              const isValid = await verifyRecaptcha(token, req?.payload?.logger)
              if (!isValid) {
                throw new Error('reCAPTCHA verification failed. Please try again.')
              }
            }

            if (!data?.form) {
              throw new Error('Form is required.')
            }

            if (!Array.isArray(data?.submissionData)) {
              throw new Error('Invalid submission data.')
            }

            if (data.submissionData.length > submissionMaxFields) {
              throw new Error('Too many fields in submission.')
            }

            const form = await req.payload.findByID({
              id: data.form,
              collection: 'forms',
              req,
              depth: 0,
            })

            const formFields = Array.isArray(form?.fields) ? form.fields : []
            const formFieldNames = new Set<string>()
            const requiredFieldNames = new Set<string>()

            for (const field of formFields) {
              if (field && typeof field === 'object' && 'name' in field) {
                const name = String((field as { name?: unknown }).name || '').trim()
                if (name) {
                  formFieldNames.add(name)
                  if ((field as { required?: boolean }).required) {
                    requiredFieldNames.add(name)
                  }
                }
              }
            }

            const presentFields = new Set<string>()

            for (const entry of data.submissionData) {
              const fieldName = String(entry?.field || '').trim()
              if (!fieldName || !formFieldNames.has(fieldName)) {
                throw new Error('Submission contains unknown fields.')
              }

              const rawValue = entry?.value
              if (
                typeof rawValue !== 'string' &&
                typeof rawValue !== 'number' &&
                typeof rawValue !== 'boolean' &&
                rawValue !== null &&
                typeof rawValue !== 'undefined'
              ) {
                throw new Error('Invalid field value.')
              }

              const value = String(rawValue ?? '')
              if (value.length > submissionMaxFieldLength) {
                throw new Error(`Field "${fieldName}" is too long.`)
              }

              if (isMalicious(value)) {
                throw new Error('Submission contains disallowed content.')
              }

              if (value.trim().length > 0) {
                presentFields.add(fieldName)
              }
            }

            for (const requiredName of requiredFieldNames) {
              if (!presentFields.has(requiredName)) {
                throw new Error(`Missing required field "${requiredName}".`)
              }
            }

            return data
          },
        ],
        afterChange: [logCollectionChange('form-submissions')],
        afterDelete: [logCollectionDelete('form-submissions')],
        afterRead: [
          ({ doc }) => {
            if (!doc || typeof doc !== 'object') return doc
            const submissionData = (doc as { submissionData?: unknown }).submissionData
            const { firstName, lastName, email } = getSubmissionIdentity(submissionData)

            const typedDoc = doc as {
              firstName?: string
              lastName?: string
              email?: string
            }

            typedDoc.firstName = firstName
            typedDoc.lastName = lastName
            typedDoc.email = email

            return doc
          },
        ],
      },
    },
  }),
]
