import React, { useEffect, useMemo, useRef, useState } from 'react'

import { IconChevronDown, IconChevronRight } from '@tabler/icons-react'
import {
  Accordion,
  Button,
  Checkbox,
  Group,
  NumberInput,
  Select,
  Stack,
  Switch,
  Text,
  TextInput,
  Textarea,
  Tooltip
} from '@tidbcloud/uikit'
import { IconInfoCircle } from '@tidbcloud/uikit/icons'

import type {
  HealthcheckHealthCheckStrategyReq,
  HealthcheckHealthCheckStrategyResp
} from '../types'

type MemoryUnit = 'B' | 'KB' | 'MiB' | 'GiB'
type ParamVisibility = 'primary' | 'advanced' | 'hidden'
type ParamVisibleWhen = {
  key: string
  includes: string
}

type ParamDef =
  | {
      type: 'number'
      key: string
      label: string
      tooltip: string
      min: number
      max?: number
      defaultValue: number
      step?: number
      integer?: boolean
      visibility?: ParamVisibility
      visibleWhen?: ParamVisibleWhen
    }
  | {
      type: 'boolean'
      key: string
      label: string
      tooltip: string
      defaultValue: boolean
      visibility?: ParamVisibility
      visibleWhen?: ParamVisibleWhen
    }
  | {
      type: 'memory'
      key: string
      label: string
      tooltip: string
      defaultValue: number
      defaultUnit: MemoryUnit
      maxBytes: number
      visibility?: ParamVisibility
      visibleWhen?: ParamVisibleWhen
    }
  | {
      type: 'multiselect'
      key: string
      label: string
      tooltip: string
      data: { value: string; label: string }[]
      defaultValue: string[]
      minSelected?: number
      visibility?: ParamVisibility
      visibleWhen?: ParamVisibleWhen
    }
  | {
      type: 'select'
      key: string
      label: string
      tooltip: string
      data: { value: string; label: string }[]
      defaultValue: string
      visibility?: ParamVisibility
      visibleWhen?: ParamVisibleWhen
    }
  | {
      type: 'text'
      key: string
      label: string
      tooltip: string
      defaultValue: string
      placeholder?: string
      visibility?: ParamVisibility
      visibleWhen?: ParamVisibleWhen
    }

interface CheckItemDef {
  name: string
  displayName: string
  checkType: string
  description: string
  params: ParamDef[]
}

interface StrategyFormProps {
  initialStrategy?: HealthcheckHealthCheckStrategyResp
  provider?: string
  submitLabel: string
  submitting?: boolean
  onCancel: () => void
  onSubmit: (strategy: HealthcheckHealthCheckStrategyReq) => void
}

type ItemState = {
  enabled: boolean
  params: Record<string, ParamValue>
}

type ParamValue = number | boolean | string | string[]

const PARAM_GRID_STYLE: React.CSSProperties = {
  display: 'grid',
  gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
  gap: '16px 24px',
  alignItems: 'start',
  maxWidth: 760
}

const PARAM_INPUT_STYLE = { width: '100%', maxWidth: 360 }

const DISK_CHECK_ITEM_NAME = 'disk_check'
const HOTSPOT_CHECK_ITEM_NAMES = new Set([
  'sql_tikv_high_duration',
  'sql_tikv_high_cpu'
])
const SLOW_LOG_TOP_DIGEST_DIMENSIONS = [
  { value: 'count', label: 'Count' },
  { value: 'total_latency', label: 'Total Latency' },
  { value: 'total_keys', label: 'SUM Total Keys' },
  { value: 'process_keys', label: 'SUM Process Keys' },
  { value: 'request_count', label: 'SUM Request Count' },
  { value: 'sum_ru', label: 'SUM RU' }
]
const MEMORY_UNITS: MemoryUnit[] = ['B', 'KB', 'MiB', 'GiB']
const MEMORY_UNIT_TO_BYTES: Record<MemoryUnit, number> = {
  B: 1,
  KB: 1024,
  MiB: 1024 * 1024,
  GiB: 1024 * 1024 * 1024
}
const HOTSPOT_COMMON_PARAMS: ParamDef[] = [
  {
    type: 'number',
    key: 'window_min_duration_seconds',
    label: 'Minimum Window Duration (s)',
    tooltip:
      'Minimum anomaly duration to report. Lower this value when you want to catch short spikes.',
    min: 0,
    defaultValue: 180,
    step: 30,
    integer: true
  },
  {
    type: 'number',
    key: 'critical_window_duration_seconds',
    label: 'Critical Window Duration (s)',
    tooltip:
      'Long anomaly windows at or above this duration are marked more severe.',
    min: 0,
    defaultValue: 600,
    step: 60,
    integer: true
  },
  {
    type: 'number',
    key: 'topsql_cpu_percent_threshold',
    label: 'Min TopSQL Share (%)',
    tooltip:
      'Only report a SQL digest when it contributes at least this share of TiKV TopSQL CPU in the anomaly window.',
    min: 0,
    max: 100,
    defaultValue: 10,
    step: 1
  },
  {
    type: 'number',
    key: 'topsql_dominance_ratio_threshold',
    label: 'Dominance Ratio',
    tooltip:
      'Stop reporting lower-ranked SQL digests when the current digest CPU is at least this many times the next one.',
    min: 1,
    defaultValue: 2,
    step: 0.1
  }
]
const TIKV_HIGH_DURATION_PARAMS: ParamDef[] = [
  {
    type: 'number',
    key: 'duration_threshold_ms',
    label: 'Duration Alert Threshold (ms)',
    tooltip:
      'TiKV coprocessor p99 duration threshold used to detect abnormal nodes.',
    min: 0,
    defaultValue: 1000,
    step: 100
  },
  {
    type: 'number',
    key: 'critical_duration_threshold_ms',
    label: 'Critical Duration Threshold (ms)',
    tooltip:
      'Duration threshold used to mark a high-duration window as critical.',
    min: 0,
    defaultValue: 4000,
    step: 100
  },
  {
    type: 'text',
    key: 'grpc_type',
    label: 'gRPC Type',
    tooltip: 'TiKV gRPC type used for duration detection.',
    defaultValue: 'coprocessor',
    placeholder: 'coprocessor',
    visibility: 'hidden'
  },
  {
    type: 'text',
    key: 'exclude_grpc_type_regex',
    label: 'Exclude gRPC Type Regex',
    tooltip: 'gRPC type regex to exclude from duration detection.',
    defaultValue: 'kv_gc',
    placeholder: 'kv_gc',
    visibility: 'hidden'
  }
]
const TIKV_HIGH_CPU_PARAMS: ParamDef[] = [
  {
    type: 'number',
    key: 'absolute_cpu_threshold_percent',
    label: 'CPU Alert Threshold (%)',
    tooltip:
      'TiKV node CPU percentage above which a node is considered high CPU.',
    min: 0,
    max: 100,
    defaultValue: 75,
    step: 1
  },
  {
    type: 'number',
    key: 'relative_cpu_delta_threshold_percent',
    label: 'Relative CPU Delta (%)',
    tooltip:
      'CPU percentage above cluster P50 required to mark a node as relative outlier.',
    min: 0,
    max: 100,
    defaultValue: 10,
    step: 1
  },
  {
    type: 'number',
    key: 'min_cpu_for_relative_outlier_percent',
    label: 'Min CPU for Relative Outlier (%)',
    tooltip:
      'Minimum node CPU required before applying the relative outlier rule.',
    min: 0,
    max: 100,
    defaultValue: 30,
    step: 1,
    visibility: 'hidden'
  },
  {
    type: 'number',
    key: 'critical_cpu_threshold_percent',
    label: 'Critical CPU Threshold (%)',
    tooltip: 'CPU percentage used to mark a high-CPU window as critical.',
    min: 0,
    max: 100,
    defaultValue: 85,
    step: 1
  }
]

const CHECK_ITEMS: CheckItemDef[] = [
  {
    name: 'metrics_healthcheck',
    displayName: 'Key Metrics Check',
    checkType: 'metrics',
    description:
      'Compares current metrics against 14-day baseline to detect anomalies in CPU, memory, QPS, latency, and disk usage',
    params: []
  },
  {
    name: 'slow_query_hugeResourceUsage',
    displayName: 'Extreme Slow Query',
    checkType: 'sql',
    description:
      'Detects slow queries with high memory usage or long execution time',
    params: [
      {
        type: 'memory',
        key: 'memMaxThreshold',
        label: 'Memory Threshold',
        tooltip:
          'Maximum memory consumption allowed per single SQL query (1 MiB = 1024 KB, 1 GiB = 1024 MiB)',
        defaultValue: 10 * 1024 * 1024 * 1024,
        defaultUnit: 'GiB',
        maxBytes: 100 * 1024 * 1024 * 1024
      },
      {
        type: 'number',
        key: 'queryTimeThreshold',
        label: 'Query Time Threshold (s)',
        tooltip: 'Maximum execution time allowed per single SQL query',
        min: 1,
        max: 300,
        defaultValue: 5
      }
    ]
  },
  {
    name: 'tablefullscan_slow_query',
    displayName: 'TableFullScan Slow Query',
    checkType: 'sql',
    description:
      'Detects SQL queries with `tablefullscan` operations that scan large amounts of rows',
    params: [
      {
        type: 'number',
        key: 'scan_rows_threshold',
        label: 'Scan Rows Threshold',
        tooltip: 'Minimum number of scanned rows to trigger this check',
        min: 100,
        max: 10000000,
        defaultValue: 10000,
        step: 100
      },
      {
        type: 'number',
        key: 'topN',
        label: 'Top N',
        tooltip:
          'Maximum number of result entries to return, ordered by total scanned rows',
        min: 1,
        defaultValue: 30,
        step: 1,
        integer: true
      },
      {
        type: 'number',
        key: 'sample_top_size',
        label: 'Sample Top Size',
        tooltip: 'Number of sample slow logs to include for each result',
        min: 1,
        max: 10,
        defaultValue: 5
      },
      {
        type: 'boolean',
        key: 'ignore_dml',
        label: 'Ignore DML (INSERT/UPDATE/DELETE)',
        tooltip: 'Whether to ignore DML statements (INSERT/UPDATE/DELETE)',
        defaultValue: true
      },
      {
        type: 'boolean',
        key: 'ignore_tiflash',
        label: 'Ignore TiFlash',
        tooltip: 'Whether to ignore `tablefullscan` operations on TiFlash',
        defaultValue: true
      }
    ]
  },
  {
    name: 'slowlog_top_digest',
    displayName: 'Slow Log Top Digest',
    checkType: 'sql',
    description:
      'Detects top slow log digests by selected aggregate dimensions',
    params: [
      {
        type: 'number',
        key: 'topN',
        label: 'Top N',
        tooltip:
          'Maximum number of digest entries to return for each selected dimension',
        min: 1,
        max: 50,
        defaultValue: 10,
        step: 1,
        integer: true
      },
      {
        type: 'multiselect',
        key: 'dimensions',
        label: 'Top Dimensions',
        tooltip: 'Aggregate dimensions used to rank slow log digests',
        data: SLOW_LOG_TOP_DIGEST_DIMENSIONS,
        defaultValue: SLOW_LOG_TOP_DIGEST_DIMENSIONS.map(
          (dimension) => dimension.value
        ),
        minSelected: 1
      },
      {
        type: 'boolean',
        key: 'ignore_dml',
        label: 'Ignore DML (INSERT/UPDATE/DELETE)',
        tooltip:
          'Exclude INSERT, UPDATE, and DELETE slow logs. This helps focus on slow read SQL instead of batch write statements.',
        defaultValue: false
      },
      {
        type: 'number',
        key: 'min_count',
        label: 'Min Count',
        tooltip:
          'Ignore digests whose slow log count is less than this value across all selected dimensions.',
        min: 0,
        max: 1000000,
        defaultValue: 0,
        step: 1,
        integer: true
      },
      {
        type: 'number',
        key: 'min_total_keys',
        label: 'Min Total Keys',
        tooltip:
          'Ignore individual slow logs whose total_keys is less than this value before aggregating digests.',
        min: 0,
        max: 1000000000000,
        defaultValue: 0,
        step: 1000,
        integer: true
      }
    ]
  },
  {
    name: 'sql_tikv_high_duration',
    displayName: 'SQL TiKV High Duration',
    checkType: 'sql',
    description:
      'Detects prominent TopSQL digests during TiKV coprocessor duration anomaly windows, with slow logs as enrichment',
    params: [...TIKV_HIGH_DURATION_PARAMS, ...HOTSPOT_COMMON_PARAMS]
  },
  {
    name: 'sql_tikv_high_cpu',
    displayName: 'SQL TiKV High CPU',
    checkType: 'sql',
    description:
      'Detects prominent TopSQL digests during high or outlier TiKV CPU windows, with slow logs as enrichment',
    params: [...TIKV_HIGH_CPU_PARAMS, ...HOTSPOT_COMMON_PARAMS]
  },
  {
    name: DISK_CHECK_ITEM_NAME,
    displayName: 'Disk Anomaly Check',
    checkType: 'metrics',
    description:
      'Detects disk I/O anomalies such as latency spikes and EBS throughput/IOPS limit exceeded during the inspection time range',
    params: []
  },
  {
    name: 'config_check',
    displayName: 'Config Change Check',
    checkType: 'config',
    description:
      'Checks whether any configuration changes were made during the inspection time range',
    params: []
  }
]

function toBytes(value: number, unit: MemoryUnit) {
  return value * MEMORY_UNIT_TO_BYTES[unit]
}

function fromBytes(bytes: number, unit: MemoryUnit) {
  return bytes / MEMORY_UNIT_TO_BYTES[unit]
}

function maxForUnit(maxBytes: number, unit: MemoryUnit) {
  return Math.floor(maxBytes / MEMORY_UNIT_TO_BYTES[unit])
}

function getAvailableCheckItems(provider?: string) {
  const shouldHideDiskCheck = provider != null && provider !== 'aws'
  if (!shouldHideDiskCheck) return CHECK_ITEMS
  return CHECK_ITEMS.filter((item) => item.name !== DISK_CHECK_ITEM_NAME)
}

function parseParams(params: unknown) {
  if (!params) return {}
  if (typeof params !== 'string') {
    return typeof params === 'object' ? (params as Record<string, unknown>) : {}
  }
  try {
    const parsed = JSON.parse(params)
    return parsed && typeof parsed === 'object'
      ? (parsed as Record<string, unknown>)
      : {}
  } catch {
    return {}
  }
}

function normalizeNumberValue(
  param: Extract<ParamDef, { type: 'number' }>,
  value: unknown
) {
  const numeric = Number(value)
  if (!Number.isFinite(numeric)) {
    return param.defaultValue
  }
  const normalized = param.integer ? Math.trunc(numeric) : numeric
  const maxClamped =
    param.max == null ? normalized : Math.min(param.max, normalized)
  return Math.max(param.min, maxClamped)
}

function normalizeMultiselectValue(
  param: Extract<ParamDef, { type: 'multiselect' }>,
  value: unknown
) {
  const allowedValues = new Set(param.data.map((item) => item.value))
  const rawValues = Array.isArray(value)
    ? value
    : typeof value === 'string'
    ? value.split(',')
    : value && typeof value === 'object'
    ? Object.entries(value as Record<string, unknown>)
        .filter(([, selected]) => selected === true)
        .map(([key]) => key)
    : []
  const normalized = Array.from(
    new Set(
      rawValues
        .map((item) => String(item).trim())
        .filter((item) => allowedValues.has(item))
    )
  )

  if (normalized.length >= (param.minSelected ?? 0)) {
    return normalized
  }

  return [...param.defaultValue]
}

function getDefaultParamValue(param: ParamDef) {
  if (param.type === 'memory') {
    return fromBytes(param.defaultValue, param.defaultUnit)
  }
  if (param.type === 'multiselect') {
    return [...param.defaultValue]
  }
  return param.defaultValue
}

function getInitialParamValue(param: ParamDef, value: unknown) {
  if (param.type === 'boolean') {
    return typeof value === 'boolean' ? value : param.defaultValue
  }
  if (param.type === 'memory') {
    const bytes = Number(value)
    return Number.isFinite(bytes) && bytes > 0
      ? fromBytes(bytes, param.defaultUnit)
      : fromBytes(param.defaultValue, param.defaultUnit)
  }
  if (param.type === 'multiselect') {
    return normalizeMultiselectValue(param, value)
  }
  if (param.type === 'select') {
    const rawValue = String(value ?? '')
    return param.data.some((option) => option.value === rawValue)
      ? rawValue
      : param.defaultValue
  }
  if (param.type === 'text') {
    return typeof value === 'string' && value.trim()
      ? value
      : param.defaultValue
  }
  return normalizeNumberValue(param, value)
}

function isParamVisible(param: ParamDef, state?: ItemState) {
  if (!param.visibleWhen) return true
  const value = state?.params[param.visibleWhen.key]
  return Array.isArray(value) && value.includes(param.visibleWhen.includes)
}

function buildDefaultItemStates(
  checkItems: CheckItemDef[],
  initialStrategy?: HealthcheckHealthCheckStrategyResp
) {
  const itemStates: Record<string, ItemState> = {}

  checkItems.forEach((item) => {
    const params: Record<string, ParamValue> = {}
    item.params.forEach((param) => {
      params[param.key] = getDefaultParamValue(param)
    })
    itemStates[item.name] = { enabled: false, params }
  })

  initialStrategy?.items?.forEach((item) => {
    const itemDef = checkItems.find((checkItem) => checkItem.name === item.name)
    if (!itemDef) return

    const parsedParams = parseParams(item.params)
    const params = { ...itemStates[item.name].params }
    itemDef.params.forEach((param) => {
      params[param.key] = getInitialParamValue(param, parsedParams[param.key])
    })

    itemStates[item.name] = {
      enabled: item.enabled !== false,
      params
    }
  })

  return itemStates
}

function buildDefaultMemoryUnits(checkItems: CheckItemDef[]) {
  const units: Record<string, MemoryUnit> = {}
  checkItems.forEach((item) => {
    item.params.forEach((param) => {
      if (param.type === 'memory') {
        units[param.key] = param.defaultUnit
      }
    })
  })
  return units
}

const pad2 = (n: number) => String(n).padStart(2, '0')

// parseClock parses a strict "HH:MM" inspection-window value into hour/minute.
// Returns null for any other value (legacy "-86400"/"0", empty, etc.).
function parseClock(s?: string): { hour: number; minute: number } | null {
  const m = s?.match(/^(\d{2}):(\d{2})$/)
  if (!m) return null
  const hour = Number(m[1])
  const minute = Number(m[2])
  if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null
  return { hour, minute }
}

function parseInitialSchedule(
  initialStrategy?: HealthcheckHealthCheckStrategyResp
) {
  if (initialStrategy?.scheduleType !== 'cron') {
    return {
      scheduleType: 'manual' as const,
      dailyHour: 0,
      dailyMinute: 0,
      windowStartHour: 0,
      windowStartMinute: 0,
      windowEndHour: 0,
      windowEndMinute: 0,
      windowCustomized: false
    }
  }

  const cronMatch = initialStrategy.scheduleExpr?.match(
    /^(?:CRON_TZ=(\S+)\s+)?(\d+)\s+(\d+)\s+\*\s+\*\s+\*$/
  )
  const rawHour = Number(cronMatch?.[3])
  const rawMinute = Number(cronMatch?.[2])
  const dailyHour =
    Number.isFinite(rawHour) && rawHour >= 0 && rawHour <= 23 ? rawHour : 0
  const dailyMinute =
    Number.isFinite(rawMinute) && rawMinute >= 0 && rawMinute <= 59
      ? Math.floor(rawMinute / 5) * 5
      : 0

  // Inspection window: prefer the stored "HH:MM" values; otherwise fall back to
  // the trigger time, which makes a full-24h window === the legacy "past 24h".
  const start = parseClock(initialStrategy.timeRangeAStart)
  const end = parseClock(initialStrategy.timeRangeAEnd)
  const windowCustomized = start !== null && end !== null

  return {
    scheduleType: 'cron' as const,
    dailyHour,
    dailyMinute,
    windowStartHour: start?.hour ?? dailyHour,
    windowStartMinute: start?.minute ?? dailyMinute,
    windowEndHour: end?.hour ?? dailyHour,
    windowEndMinute: end?.minute ?? dailyMinute,
    windowCustomized
  }
}

export const StrategyForm: React.FC<StrategyFormProps> = ({
  initialStrategy,
  provider,
  submitLabel,
  submitting,
  onCancel,
  onSubmit
}) => {
  const availableCheckItems = useMemo(
    () => getAvailableCheckItems(provider),
    [provider]
  )
  const initialSchedule = useMemo(
    () => parseInitialSchedule(initialStrategy),
    [initialStrategy]
  )

  const [name, setName] = useState(initialStrategy?.name ?? '')
  const [description, setDescription] = useState(
    initialStrategy?.description ?? ''
  )
  const [scheduleType, setScheduleType] = useState<'manual' | 'cron'>(
    initialSchedule.scheduleType
  )
  const [dailyHour, setDailyHour] = useState<number>(initialSchedule.dailyHour)
  const [dailyMinute, setDailyMinute] = useState<number>(
    initialSchedule.dailyMinute
  )
  const [windowStartHour, setWindowStartHour] = useState<number>(
    initialSchedule.windowStartHour
  )
  const [windowStartMinute, setWindowStartMinute] = useState<number>(
    initialSchedule.windowStartMinute
  )
  const [windowEndHour, setWindowEndHour] = useState<number>(
    initialSchedule.windowEndHour
  )
  const [windowEndMinute, setWindowEndMinute] = useState<number>(
    initialSchedule.windowEndMinute
  )
  // Until the user edits the inspection window, keep it tracking the trigger
  // time so the default stays equivalent to the legacy "past 24h" behaviour.
  const windowTouched = useRef(initialSchedule.windowCustomized)
  useEffect(() => {
    if (scheduleType === 'cron' && !windowTouched.current) {
      setWindowStartHour(dailyHour)
      setWindowStartMinute(dailyMinute)
      setWindowEndHour(dailyHour)
      setWindowEndMinute(dailyMinute)
    }
  }, [dailyHour, dailyMinute, scheduleType])
  const [itemStates, setItemStates] = useState<Record<string, ItemState>>(() =>
    buildDefaultItemStates(availableCheckItems, initialStrategy)
  )
  const [memoryUnits, setMemoryUnits] = useState<Record<string, MemoryUnit>>(
    () => buildDefaultMemoryUnits(availableCheckItems)
  )
  const [expandedItems, setExpandedItems] = useState<string[]>(
    () =>
      initialStrategy?.items
        ?.filter((item) => item.enabled !== false)
        .map((item) => item.name ?? '')
        .filter(Boolean) ?? []
  )
  const [expandedAdvancedItems, setExpandedAdvancedItems] = useState<string[]>(
    []
  )

  const toggleItem = (itemName: string, enabled: boolean) => {
    setItemStates((prev) => ({
      ...prev,
      [itemName]: { ...prev[itemName], enabled }
    }))
    if (enabled) {
      setExpandedItems((prev) =>
        prev.includes(itemName) ? prev : [...prev, itemName]
      )
    } else {
      setExpandedItems((prev) => prev.filter((value) => value !== itemName))
    }
  }

  const setAllItemsEnabled = (enabled: boolean) => {
    setItemStates((prev) => {
      const next = { ...prev }
      availableCheckItems.forEach((item) => {
        next[item.name] = { ...next[item.name], enabled }
      })
      return next
    })

    if (!enabled) {
      setExpandedItems([])
      setExpandedAdvancedItems([])
    }
  }

  const setParamValue = (
    itemName: string,
    paramKey: string,
    value: ParamValue
  ) => {
    setItemStates((prev) => ({
      ...prev,
      [itemName]: {
        ...prev[itemName],
        params: { ...prev[itemName].params, [paramKey]: value }
      }
    }))
  }

  const toggleAdvancedParams = (itemName: string) => {
    setExpandedAdvancedItems((prev) =>
      prev.includes(itemName)
        ? prev.filter((value) => value !== itemName)
        : [...prev, itemName]
    )
  }

  const renderParamField = (item: CheckItemDef, param: ParamDef) => {
    const state = itemStates[item.name]
    const labelWithTip = (
      <Group gap={4} align='center'>
        {param.label}
        <Tooltip label={param.tooltip}>
          <IconInfoCircle size={14} color='gray' />
        </Tooltip>
      </Group>
    )

    if (param.type === 'boolean') {
      return (
        <Switch
          label={labelWithTip}
          checked={(state?.params[param.key] ?? param.defaultValue) as boolean}
          onChange={(event) =>
            setParamValue(item.name, param.key, event.currentTarget.checked)
          }
          disabled={!state?.enabled}
        />
      )
    }

    if (param.type === 'memory') {
      const unit = memoryUnits[param.key] ?? param.defaultUnit
      const displayValue =
        (state?.params[param.key] as number) ??
        fromBytes(param.defaultValue, param.defaultUnit)

      return (
        <div>
          <Text size='sm' fw={500} mb={4}>
            {labelWithTip}
          </Text>
          <Group gap='sm' wrap='nowrap'>
            <NumberInput
              value={displayValue}
              onChange={(value) =>
                setParamValue(item.name, param.key, Number(value) || 1)
              }
              min={1}
              max={maxForUnit(param.maxBytes, unit)}
              step={1}
              style={{ width: 200 }}
              disabled={!state?.enabled}
            />
            <Select
              data={MEMORY_UNITS}
              value={unit}
              onChange={(value) => {
                if (!value) return

                const newUnit = value as MemoryUnit
                const oldBytes = toBytes(displayValue, unit)
                const newMax = maxForUnit(param.maxBytes, newUnit)
                const newValue = Math.min(
                  Math.max(1, Math.round(fromBytes(oldBytes, newUnit))),
                  newMax
                )

                setMemoryUnits((prev) => ({
                  ...prev,
                  [param.key]: newUnit
                }))
                setParamValue(item.name, param.key, newValue)
              }}
              style={{ width: 100 }}
              disabled={!state?.enabled}
            />
          </Group>
        </div>
      )
    }

    if (param.type === 'multiselect') {
      const selectedValues = normalizeMultiselectValue(
        param,
        state?.params[param.key] ?? param.defaultValue
      )
      const minSelected = param.minSelected ?? 0

      return (
        <div>
          <Text size='sm' fw={500} mb={6}>
            {labelWithTip}
          </Text>
          <Group gap='md' wrap='wrap'>
            {param.data.map((option) => {
              const checked = selectedValues.includes(option.value)
              const isOnlyRequiredSelection =
                checked && selectedValues.length <= minSelected

              return (
                <Checkbox
                  key={option.value}
                  label={option.label}
                  checked={checked}
                  onChange={(event) => {
                    const nextValues = event.currentTarget.checked
                      ? [...selectedValues, option.value]
                      : selectedValues.filter((value) => value !== option.value)

                    setParamValue(
                      item.name,
                      param.key,
                      normalizeMultiselectValue(param, nextValues)
                    )
                  }}
                  disabled={!state?.enabled || isOnlyRequiredSelection}
                />
              )
            })}
          </Group>
        </div>
      )
    }

    if (param.type === 'select') {
      return (
        <Select
          label={labelWithTip}
          data={param.data}
          value={(state?.params[param.key] ?? param.defaultValue) as string}
          onChange={(value) => {
            if (!value) return
            setParamValue(item.name, param.key, value)
          }}
          style={PARAM_INPUT_STYLE}
          disabled={!state?.enabled}
        />
      )
    }

    if (param.type === 'text') {
      return (
        <TextInput
          label={labelWithTip}
          value={(state?.params[param.key] ?? param.defaultValue) as string}
          placeholder={param.placeholder}
          onChange={(event) =>
            setParamValue(item.name, param.key, event.currentTarget.value)
          }
          style={PARAM_INPUT_STYLE}
          disabled={!state?.enabled}
        />
      )
    }

    return (
      <NumberInput
        label={labelWithTip}
        value={(state?.params[param.key] ?? param.defaultValue) as number}
        onChange={(value) =>
          setParamValue(
            item.name,
            param.key,
            normalizeNumberValue(param, value)
          )
        }
        min={param.min}
        max={param.max}
        step={param.step}
        allowDecimal={!param.integer}
        style={PARAM_INPUT_STYLE}
        disabled={!state?.enabled}
      />
    )
  }

  const renderParams = (
    item: CheckItemDef,
    params: ParamDef[],
    useHotspotLayout: boolean
  ) => {
    if (!useHotspotLayout) {
      return (
        <Stack gap='md'>
          {params.map((param) => (
            <div key={param.key}>{renderParamField(item, param)}</div>
          ))}
        </Stack>
      )
    }

    const inlineParams = params.filter(
      (param) =>
        param.type === 'number' ||
        param.type === 'memory' ||
        param.type === 'select' ||
        param.type === 'text'
    )
    const standaloneParams = params.filter(
      (param) => param.type === 'boolean' || param.type === 'multiselect'
    )

    return (
      <Stack gap='md'>
        {inlineParams.length > 0 && (
          <div style={PARAM_GRID_STYLE}>
            {inlineParams.map((param) => (
              <div key={param.key}>{renderParamField(item, param)}</div>
            ))}
          </div>
        )}
        {standaloneParams.length > 0 && (
          <Stack gap='sm'>
            {standaloneParams.map((param) => (
              <div key={param.key}>{renderParamField(item, param)}</div>
            ))}
          </Stack>
        )}
      </Stack>
    )
  }

  const hasEnabledItems = availableCheckItems.some(
    (item) => itemStates[item.name]?.enabled
  )

  const handleSubmit = () => {
    if (!name.trim() || !hasEnabledItems) return

    const items = availableCheckItems
      .filter((item) => itemStates[item.name]?.enabled)
      .map((item) => {
        const rawParams = { ...itemStates[item.name]?.params }

        item.params.forEach((param) => {
          if (param.type === 'memory') {
            const unit = memoryUnits[param.key] ?? param.defaultUnit
            const displayValue =
              (rawParams[param.key] as number) ??
              fromBytes(param.defaultValue, unit)
            rawParams[param.key] = toBytes(displayValue, unit)
          }
        })

        return {
          name: item.name,
          checkType: item.checkType,
          description: item.description,
          enabled: true,
          params: JSON.stringify(rawParams)
        }
      })

    const strategy: HealthcheckHealthCheckStrategyReq = {
      name: name.trim(),
      description: description.trim() || undefined,
      scheduleType,
      items
    }

    if (scheduleType === 'cron') {
      strategy.scheduleExpr = `CRON_TZ=${
        Intl.DateTimeFormat().resolvedOptions().timeZone
      } ${dailyMinute} ${dailyHour} * * *`
      // Inspection window as time-of-day; the backend resolves it to the most
      // recent occurrence before each trigger (in the CRON_TZ timezone).
      strategy.timeRangeAStart = `${pad2(windowStartHour)}:${pad2(
        windowStartMinute
      )}`
      strategy.timeRangeAEnd = `${pad2(windowEndHour)}:${pad2(windowEndMinute)}`
    }

    onSubmit(strategy)
  }

  return (
    <Stack gap='xl'>
      <TextInput
        label='Name'
        placeholder='Enter strategy name'
        required
        maxLength={250}
        value={name}
        onChange={(event) => setName(event.currentTarget.value)}
        style={{ maxWidth: 480 }}
      />

      <Textarea
        label='Description'
        placeholder='Describe what this strategy does'
        value={description}
        onChange={(event) => setDescription(event.currentTarget.value)}
        autosize
        minRows={2}
        maxRows={4}
        style={{ maxWidth: 480 }}
      />

      <div>
        <Select
          label='Trigger Mode'
          data={[
            { value: 'manual', label: 'Manual Only' },
            { value: 'cron', label: 'Daily' }
          ]}
          value={scheduleType}
          onChange={(value) => {
            if (value === 'manual' || value === 'cron') {
              setScheduleType(value)
            }
          }}
          style={{ maxWidth: 240 }}
        />

        {scheduleType === 'cron' && (
          <Stack gap='xs' mt='sm'>
            <Text size='sm' fw={500}>
              Run At ({Intl.DateTimeFormat().resolvedOptions().timeZone})
            </Text>
            <Group gap='sm' align='flex-start'>
              <Select
                label='Hour'
                data={Array.from({ length: 24 }, (_, index) => ({
                  value: String(index),
                  label: String(index).padStart(2, '0')
                }))}
                value={String(dailyHour)}
                onChange={(value) => setDailyHour(Number(value))}
                style={{ maxWidth: 160 }}
              />
              <Select
                label='Minute'
                data={Array.from({ length: 12 }, (_, index) => {
                  const minute = index * 5
                  return {
                    value: String(minute),
                    label: String(minute).padStart(2, '0')
                  }
                })}
                value={String(dailyMinute)}
                onChange={(value) => setDailyMinute(Number(value))}
                style={{ maxWidth: 120 }}
              />
            </Group>
            <Text size='sm' fw={500} mt='xs'>
              Inspection Window (
              {Intl.DateTimeFormat().resolvedOptions().timeZone})
            </Text>
            <Group gap='sm' align='flex-end'>
              <Select
                label='Start Hour'
                data={Array.from({ length: 24 }, (_, index) => ({
                  value: String(index),
                  label: String(index).padStart(2, '0')
                }))}
                value={String(windowStartHour)}
                onChange={(value) => {
                  windowTouched.current = true
                  setWindowStartHour(Number(value))
                }}
                style={{ maxWidth: 130 }}
              />
              <Select
                label='Start Minute'
                data={Array.from({ length: 12 }, (_, index) => {
                  const minute = index * 5
                  return {
                    value: String(minute),
                    label: String(minute).padStart(2, '0')
                  }
                })}
                value={String(windowStartMinute)}
                onChange={(value) => {
                  windowTouched.current = true
                  setWindowStartMinute(Number(value))
                }}
                style={{ maxWidth: 110 }}
              />
              <Text mb={6}>–</Text>
              <Select
                label='End Hour'
                data={Array.from({ length: 24 }, (_, index) => ({
                  value: String(index),
                  label: String(index).padStart(2, '0')
                }))}
                value={String(windowEndHour)}
                onChange={(value) => {
                  windowTouched.current = true
                  setWindowEndHour(Number(value))
                }}
                style={{ maxWidth: 130 }}
              />
              <Select
                label='End Minute'
                data={Array.from({ length: 12 }, (_, index) => {
                  const minute = index * 5
                  return {
                    value: String(minute),
                    label: String(minute).padStart(2, '0')
                  }
                })}
                value={String(windowEndMinute)}
                onChange={(value) => {
                  windowTouched.current = true
                  setWindowEndMinute(Number(value))
                }}
                style={{ maxWidth: 110 }}
              />
            </Group>
            <Text size='xs' c='dimmed'>
              {(() => {
                const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
                const at = `${pad2(dailyHour)}:${pad2(dailyMinute)}`
                const s = `${pad2(windowStartHour)}:${pad2(windowStartMinute)}`
                const e = `${pad2(windowEndHour)}:${pad2(windowEndMinute)}`
                const sTotal = windowStartHour * 60 + windowStartMinute
                const eTotal = windowEndHour * 60 + windowEndMinute
                if (eTotal === sTotal) {
                  return `Runs daily at ${at} (${tz}), inspecting the most recent 24 hours before each run.`
                }
                const cross = eTotal < sTotal ? ' next day' : ''
                return `Runs daily at ${at} (${tz}), inspecting the most recent ${s}–${e}${cross} window before each run.`
              })()}
              <br />
              {(() => {
                // Mirror the backend resolver to show a concrete example: the
                // next trigger time and the absolute window that run inspects.
                const now = new Date()
                const nextRun = new Date(now)
                nextRun.setHours(dailyHour, dailyMinute, 0, 0)
                if (nextRun.getTime() <= now.getTime()) {
                  nextRun.setDate(nextRun.getDate() + 1)
                }
                const absEnd = new Date(nextRun)
                absEnd.setHours(windowEndHour, windowEndMinute, 0, 0)
                if (absEnd.getTime() > nextRun.getTime()) {
                  absEnd.setDate(absEnd.getDate() - 1)
                }
                const startTOD = windowStartHour * 60 + windowStartMinute
                const endTOD = windowEndHour * 60 + windowEndMinute
                const absStart = new Date(absEnd)
                absStart.setHours(windowStartHour, windowStartMinute, 0, 0)
                if (startTOD >= endTOD) {
                  absStart.setDate(absStart.getDate() - 1)
                }
                const fmt = (d: Date) =>
                  `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(
                    d.getDate()
                  )} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`
                return `For example, the next run at ${fmt(
                  nextRun
                )} will inspect ${fmt(absStart)} to ${fmt(absEnd)}.`
              })()}
            </Text>
          </Stack>
        )}
      </div>

      <div>
        <Group justify='space-between' mb='sm'>
          <Text size='sm' fw={500}>
            Check Items
          </Text>
          <Group gap='xs'>
            <Button
              variant='default'
              size='xs'
              onClick={() => setAllItemsEnabled(true)}
            >
              Enable All
            </Button>
            <Button
              variant='default'
              size='xs'
              onClick={() => setAllItemsEnabled(false)}
            >
              Disable All
            </Button>
          </Group>
        </Group>
        <Accordion
          multiple
          value={expandedItems}
          onChange={setExpandedItems}
          variant='contained'
          chevronPosition='left'
        >
          {availableCheckItems.map((item) => {
            const state = itemStates[item.name]
            const visibleParams = item.params.filter((param) =>
              isParamVisible(param, state)
            )
            const primaryParams = visibleParams.filter(
              (param) => (param.visibility ?? 'primary') === 'primary'
            )
            const advancedParams = visibleParams.filter(
              (param) => param.visibility === 'advanced'
            )
            const advancedExpanded = expandedAdvancedItems.includes(item.name)
            const isHotspotItem = HOTSPOT_CHECK_ITEM_NAMES.has(item.name)

            return (
              <Accordion.Item key={item.name} value={item.name}>
                <Accordion.Control>
                  <Group justify='space-between' pr='xs'>
                    <div>
                      <Text size='sm' fw={500}>
                        {item.displayName}
                      </Text>
                      <Text size='xs' c='dimmed'>
                        {item.description}
                      </Text>
                    </div>
                    <Switch
                      checked={state?.enabled ?? false}
                      onChange={(event) => {
                        event.stopPropagation()
                        toggleItem(item.name, event.currentTarget.checked)
                      }}
                      onClick={(event) => event.stopPropagation()}
                    />
                  </Group>
                </Accordion.Control>
                <Accordion.Panel>
                  <Stack gap='md' ml={28}>
                    {primaryParams.length > 0 &&
                      renderParams(item, primaryParams, isHotspotItem)}

                    {advancedParams.length > 0 && (
                      <Stack gap='sm'>
                        <Button
                          variant='subtle'
                          size='xs'
                          style={{
                            alignSelf: 'flex-start',
                            color: 'var(--mantine-color-blue-6)',
                            paddingLeft: 0
                          }}
                          onClick={() => toggleAdvancedParams(item.name)}
                        >
                          <Group gap={4} wrap='nowrap'>
                            {advancedExpanded ? (
                              <IconChevronDown size={14} />
                            ) : (
                              <IconChevronRight size={14} />
                            )}
                            <span>
                              {advancedExpanded
                                ? 'Hide advanced parameters'
                                : `Show advanced parameters (${advancedParams.length})`}
                            </span>
                          </Group>
                        </Button>
                        {advancedExpanded &&
                          renderParams(item, advancedParams, isHotspotItem)}
                      </Stack>
                    )}
                  </Stack>
                </Accordion.Panel>
              </Accordion.Item>
            )
          })}
        </Accordion>
      </div>

      <Group justify='flex-end'>
        <Button variant='default' onClick={onCancel}>
          Cancel
        </Button>
        <Button
          loading={submitting}
          disabled={!name.trim() || !hasEnabledItems}
          onClick={handleSubmit}
        >
          {submitLabel}
        </Button>
      </Group>
    </Stack>
  )
}
