import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { Dayjs } from 'dayjs'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectTrigger,
  SelectValue,
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
import {
  formatDateForOutput,
  toDayjs,
} from '@/app/components/base/date-and-time-picker/utils/dayjs'
import Input from '@/app/components/base/input'

const DATA_FORMAT = {
  TEXT: 'text',
  JSON: 'json',
} as const

const SUPPORTED_TAGS = {
  LABEL: 'label',
  INPUT: 'input',
  TEXTAREA: 'textarea',
  BUTTON: 'button',
} as const

const SUPPORTED_TYPES = {
  TEXT: 'text',
  PASSWORD: 'password',
  EMAIL: 'email',
  NUMBER: 'number',
  DATE: 'date',
  TIME: 'time',
  DATETIME: 'datetime',
  CHECKBOX: 'checkbox',
  SELECT: 'select',
  HIDDEN: 'hidden',
} as const

type SupportedType = (typeof SUPPORTED_TYPES)[keyof typeof SUPPORTED_TYPES]

const SUPPORTED_TYPES_SET = new Set<string>(Object.values(SUPPORTED_TYPES))

const SAFE_NAME_RE = (() => {
  try {
    return new RegExp('^\\p{L}[\\p{L}\\p{M}\\p{N}_()!*&（）！＊＆－-]*$', 'u')
  } catch {
    // Fallback for browsers without Unicode property escape support.
    return /^[a-z][\w-]*$/i
  }
})()
const PROTOTYPE_POISON_KEYS = new Set(['__proto__', 'constructor', 'prototype'])

function isSafeName(name: unknown): name is string {
  return (
    typeof name === 'string' &&
    name.length > 0 &&
    name.length <= 128 &&
    SAFE_NAME_RE.test(name) &&
    !PROTOTYPE_POISON_KEYS.has(name)
  )
}

const VALID_BUTTON_VARIANTS = new Set<string>([
  'primary',
  'warning',
  'secondary',
  'secondary-accent',
  'ghost',
  'ghost-accent',
  'tertiary',
])
const VALID_BUTTON_SIZES = new Set<string>(['small', 'medium', 'large'])

type HastText = {
  type: 'text'
  value: string
}

type HastElement = {
  type: 'element'
  tagName: string
  properties: Record<string, unknown>
  children: Array<HastElement | HastText>
}

type FormValue = string | boolean | Dayjs | undefined
type FormValues = Record<string, FormValue>
type EditState = {
  source: HastElement[]
  edits: FormValues
}

function getTextContent(node: HastElement): string {
  const textChild = node.children.find((c): c is HastText => c.type === 'text')
  return textChild?.value ?? ''
}

function getLabelTarget(node: HastElement): string {
  return str(node.properties.htmlFor || node.properties.for || node.properties.name)
}

function str(val: unknown): string {
  if (val == null) return ''
  return String(val)
}

function computeInitialFormValues(children: HastElement[]): FormValues {
  const init: FormValues = Object.create(null) as FormValues
  for (const child of children) {
    if (child.tagName !== SUPPORTED_TAGS.INPUT && child.tagName !== SUPPORTED_TAGS.TEXTAREA)
      continue
    const name = child.properties.name
    if (!isSafeName(name)) continue

    const type = child.tagName === SUPPORTED_TAGS.INPUT ? str(child.properties.type) : ''

    if (type === SUPPORTED_TYPES.HIDDEN) {
      init[name] = str(child.properties.value)
    } else if (
      type === SUPPORTED_TYPES.DATE ||
      type === SUPPORTED_TYPES.DATETIME ||
      type === SUPPORTED_TYPES.TIME
    ) {
      const raw = child.properties.value
      init[name] = raw != null ? toDayjs(String(raw)) : undefined
    } else if (type === SUPPORTED_TYPES.CHECKBOX) {
      const { checked, value } = child.properties
      init[name] = !!checked || value === true || value === 'true'
    } else {
      init[name] = child.properties.value != null ? str(child.properties.value) : undefined
    }
  }
  return init
}

function getElementKey(child: HastElement, index: number): string {
  const tag = child.tagName
  const name = str(child.properties.name)
  const htmlFor = str(child.properties.htmlFor)
  const type = str(child.properties.type)

  if (tag === SUPPORTED_TAGS.LABEL) return `label-${index}-${htmlFor || name}`
  if (tag === SUPPORTED_TAGS.INPUT) return `input-${index}-${type}-${name}`
  if (tag === SUPPORTED_TAGS.TEXTAREA) return `textarea-${index}-${name}`
  if (tag === SUPPORTED_TAGS.BUTTON) return `button-${index}-${getTextContent(child)}`
  return `${tag}-${index}`
}

const MarkdownForm = ({ node }: { node: HastElement }) => {
  const typedNode = node
  const { onSend } = useChatContext()
  const [isSubmitting, setIsSubmitting] = useState(false)

  const elementChildren = useMemo(
    () => typedNode.children.filter((c): c is HastElement => c.type === 'element'),
    [typedNode.children],
  )

  const baseFormValues = useMemo(() => computeInitialFormValues(elementChildren), [elementChildren])

  const [editState, setEditState] = useState<EditState>(() => ({
    source: elementChildren,
    edits: {},
  }))

  const formValues = useMemo<FormValues>(() => {
    if (editState.source === elementChildren) return { ...baseFormValues, ...editState.edits }
    return baseFormValues
  }, [editState, baseFormValues, elementChildren])

  const updateValue = useCallback(
    (name: string, value: FormValue) => {
      if (!isSafeName(name)) return
      setEditState((prev) => ({
        source: elementChildren,
        edits: {
          ...(prev.source === elementChildren ? prev.edits : {}),
          [name]: value,
        },
      }))
    },
    [elementChildren],
  )

  const getFormOutput = useCallback((): Record<string, string | boolean | undefined> => {
    const out = Object.create(null) as Record<string, string | boolean | undefined>
    for (const child of elementChildren) {
      if (child.tagName !== SUPPORTED_TAGS.INPUT && child.tagName !== SUPPORTED_TAGS.TEXTAREA)
        continue
      const name = child.properties.name
      if (!isSafeName(name)) continue
      let value: FormValue = formValues[name]
      if (
        child.tagName === SUPPORTED_TAGS.INPUT &&
        (child.properties.type === SUPPORTED_TYPES.DATE ||
          child.properties.type === SUPPORTED_TYPES.DATETIME) &&
        value != null &&
        typeof value === 'object' &&
        'format' in value
      ) {
        const includeTime = child.properties.type === SUPPORTED_TYPES.DATETIME
        value = formatDateForOutput(value as Dayjs, includeTime)
      }
      if (typeof value === 'boolean') out[name] = value
      else out[name] = value != null ? String(value) : undefined
    }
    return out
  }, [elementChildren, formValues])

  const onSubmit = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault()
      if (isSubmitting) return
      setIsSubmitting(true)
      try {
        const format = str(typedNode.properties.dataFormat) || DATA_FORMAT.TEXT
        const result = getFormOutput()
        if (format === DATA_FORMAT.JSON) {
          onSend?.(JSON.stringify(result))
        } else {
          const textResult = Object.entries(result)
            .map(([key, value]) => `${key}: ${value}`)
            .join('\n')
          onSend?.(textResult)
        }
      } catch {
        setIsSubmitting(false)
      }
    },
    [isSubmitting, typedNode.properties.dataFormat, getFormOutput, onSend],
  )

  return (
    <form
      autoComplete="off"
      className="flex flex-col self-stretch"
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
      }}
    >
      {elementChildren.map((child, index) => {
        const key = getElementKey(child, index)
        if (child.tagName === SUPPORTED_TAGS.LABEL) {
          return (
            <label
              key={key}
              htmlFor={getLabelTarget(child)}
              className="my-2 system-md-semibold text-text-secondary"
            >
              {getTextContent(child)}
            </label>
          )
        }

        if (
          child.tagName === SUPPORTED_TAGS.INPUT &&
          SUPPORTED_TYPES_SET.has(str(child.properties.type))
        ) {
          const name = str(child.properties.name)
          if (!isSafeName(name)) return null

          const type = str(child.properties.type) as SupportedType

          if (type === SUPPORTED_TYPES.DATE || type === SUPPORTED_TYPES.DATETIME) {
            return (
              <DatePicker
                key={key}
                value={formValues[name] as Dayjs | undefined}
                needTimePicker={type === SUPPORTED_TYPES.DATETIME}
                onChange={(date) => updateValue(name, date)}
                onClear={() => updateValue(name, undefined)}
              />
            )
          }
          if (type === SUPPORTED_TYPES.TIME) {
            return (
              <TimePicker
                key={key}
                value={formValues[name] as Dayjs | string | undefined}
                onChange={(time) => updateValue(name, time)}
                onClear={() => updateValue(name, undefined)}
              />
            )
          }
          if (type === SUPPORTED_TYPES.CHECKBOX) {
            const label = str(child.properties.dataTip || child.properties['data-tip'])
            const hasExternalLabel = elementChildren.some(
              (node) => node.tagName === SUPPORTED_TAGS.LABEL && getLabelTarget(node) === name,
            )
            const checkboxAriaLabel = label || (hasExternalLabel ? undefined : name)
            return (
              <div className="mt-2 flex h-6 items-center space-x-2" key={key}>
                <Checkbox
                  id={name}
                  checked={!!formValues[name]}
                  aria-label={checkboxAriaLabel}
                  onCheckedChange={(checked) => updateValue(name, checked)}
                />
                {label && <span>{label}</span>}
              </div>
            )
          }
          if (type === SUPPORTED_TYPES.SELECT) {
            const rawOptions =
              child.properties.dataOptions || child.properties['data-options'] || []
            let options: string[] = []
            if (typeof rawOptions === 'string') {
              try {
                const parsed: unknown = JSON.parse(rawOptions)
                if (Array.isArray(parsed))
                  options = parsed.filter((o): o is string => typeof o === 'string')
              } catch (error) {
                console.error('Failed to parse data-options JSON:', rawOptions, error)
                options = []
              }
            } else if (Array.isArray(rawOptions)) {
              options = rawOptions.filter((o): o is string => typeof o === 'string')
            }
            return (
              <Select
                key={key}
                defaultValue={formValues[name] as string | undefined}
                onValueChange={(val) => {
                  if (val != null) updateValue(name, val)
                }}
              >
                <SelectTrigger className="w-full">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {options.map((option) => (
                    <SelectItem key={option} value={option}>
                      <SelectItemText>{option}</SelectItemText>
                      <SelectItemIndicator />
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            )
          }

          if (type === SUPPORTED_TYPES.HIDDEN) {
            return (
              <input
                key={key}
                type="hidden"
                name={name}
                value={str(formValues[name] ?? child.properties.value)}
              />
            )
          }

          return (
            <Input
              key={key}
              type={type}
              name={name}
              placeholder={str(child.properties.placeholder)}
              value={str(formValues[name])}
              onChange={(e) => updateValue(name, e.target.value)}
            />
          )
        }

        if (child.tagName === SUPPORTED_TAGS.TEXTAREA) {
          const name = str(child.properties.name)
          if (!isSafeName(name)) return null
          return (
            <Textarea
              aria-label={name}
              key={key}
              name={name}
              placeholder={str(child.properties.placeholder)}
              value={str(formValues[name])}
              onValueChange={(value) => updateValue(name, value)}
            />
          )
        }

        if (child.tagName === SUPPORTED_TAGS.BUTTON) {
          const rawVariant = str(child.properties.dataVariant)
          const rawSize = str(child.properties.dataSize)
          const variant = VALID_BUTTON_VARIANTS.has(rawVariant)
            ? (rawVariant as ButtonProps['variant'])
            : undefined
          const size = VALID_BUTTON_SIZES.has(rawSize)
            ? (rawSize as ButtonProps['size'])
            : undefined

          return (
            <Button
              variant={variant}
              size={size}
              className="mt-4"
              key={key}
              disabled={isSubmitting}
              onClick={onSubmit}
            >
              <span className="text-[13px]">{getTextContent(child)}</span>
            </Button>
          )
        }

        return (
          <p key={key}>
            Unsupported tag:
            {child.tagName}
          </p>
        )
      })}
    </form>
  )
}
MarkdownForm.displayName = 'MarkdownForm'
export default MarkdownForm
