import { useState, useEffect, useCallback } from 'react'
import {
  Save, RotateCcw, RefreshCw, Plus, Trash2, ChevronDown, ChevronRight,
  Check, X, Eye as EyeIcon, EyeOff, Send, Clock, Zap,
  Calendar, AlertTriangle, Copy, AlertCircle, Layers,
  Wifi, WifiOff, Mail, Globe, Radio, MessageSquare,
  Activity, Cloud, Flame, Car, Snowflake, Mountain, MapPin, Satellite
} from 'lucide-react'
import ChannelPicker from '@/components/ChannelPicker'
import NodePicker from '@/components/NodePicker'
import { useDirty } from '@/context/DirtyContext'

// Types
interface NotificationRuleConfig {
  name: string
  enabled: boolean
  trigger_type: 'condition' | 'schedule'
  categories: string[]
  min_severity: string
  schedule_frequency: 'daily' | 'twice_daily' | 'weekly'
  schedule_time: string
  schedule_time_2: string
  schedule_days: string[]
  message_type: string
  custom_message: string
  delivery_type: string
  broadcast_channel: number
  node_ids: string[]
  smtp_host: string
  smtp_port: number
  smtp_user: string
  smtp_password: string
  smtp_tls: boolean
  from_address: string
  recipients: string[]
  webhook_url: string
  webhook_headers: Record<string, string>
  cooldown_minutes: number
  region_scope: string[]
}

export interface NotificationToggle {
  name: string
  enabled: boolean
  min_severity: string
  regions: string[]
  severity_channels: Record<string, string[]>
  freshness_seconds?: number
  cooldown_seconds?: number
  broadcast_channel: number | null
  meshcore_channel?: string | null
  node_ids: string[]
  meshcore_dm_contacts: string[]
  smtp_host: string
  smtp_port: number
  smtp_user: string
  smtp_password: string
  smtp_tls: boolean
  from_address: string
  recipients: string[]
  webhook_url: string
  webhook_headers: Record<string, string>
}

export interface NotificationsConfig {
  enabled: boolean
  cold_start_grace_seconds?: number
  band_conditions_enabled?: boolean
  band_conditions_schedule?: string[]
  rules: NotificationRuleConfig[]
  toggles?: Record<string, NotificationToggle>
}

interface AlertCategory {
  id: string
  name: string
  description: string
  default_severity: string
  example_message: string
  toggle?: string
}

interface RegionInfo {
  name: string
  local_name?: string
}

interface RuleStats {
  last_fired: number | null
  last_test: number | null
  fire_count: number
}

interface SourceHealth {
  [category: string]: {
    enabled: boolean
    active_events: number
    source: string
    status: 'ok' | 'disabled' | 'no_data'
  }
}

interface TestResult {
  live_data_summary?: string[]
  conditions_matched?: number
  preview_messages?: string[]
  is_example?: boolean
  conditions_below_threshold?: number
  below_threshold_summary?: string
  below_threshold_events?: { headline: string; severity: string }[]
  suggestion?: string
  delivered?: boolean
  delivery_method?: string
  delivery_result?: string
  delivery_error?: string
  can_send_live?: boolean
  source_health?: SourceHealth
  rule_stats?: RuleStats
  // Legacy
  success?: boolean
  message?: string
}

interface ChannelTestResult {
  success: boolean
  message: string
  error: string
  details: Record<string, unknown>
}

// Severity levels with descriptions
const SEVERITY_OPTIONS = [
  { value: 'routine', label: 'Routine', description: 'Informational, no time pressure (ducting, new node, weather advisory, battery declining)' },
  { value: 'priority', label: 'Priority', description: 'Needs attention soon (severe weather, fire nearby, node offline, HF blackout)' },
  { value: 'immediate', label: 'Immediate', description: 'Act now, drop everything (fire at infrastructure, extreme weather, region blackout)' },
]

// Notification rule templates
const RULE_TEMPLATES = [
  {
    id: "mesh_health",
    name: "Mesh Health Monitoring",
    description: "Infrastructure problems - offline nodes, low battery, channel congestion",
    rule: {
      name: "Mesh Health Monitoring",
      enabled: true,
      trigger_type: "condition" as const,
      categories: ["infra_offline", "critical_node_down", "infra_recovery", "battery_warning", "battery_critical", "battery_emergency", "high_utilization", "packet_flood", "mesh_score_low"],
      min_severity: "routine",
      delivery_type: "mesh_broadcast",
      broadcast_channel: 0,
      cooldown_minutes: 30,
      schedule_frequency: "daily" as const,
      schedule_time: "07:00",
      schedule_time_2: "",
      schedule_days: [] as string[],
      message_type: "",
      custom_message: "",
      node_ids: [] as string[],
      smtp_host: "",
      smtp_port: 587,
      smtp_user: "",
      smtp_password: "",
      smtp_tls: true,
      from_address: "",
      recipients: [] as string[],
      webhook_url: "",
      webhook_headers: {} as Record<string, string>,
    }
  },
  {
    id: "weather_fire",
    name: "Weather & Fire Alerts",
    description: "Environmental threats - severe weather, nearby wildfires, new ignitions, flooding",
    rule: {
      name: "Weather & Fire Alerts",
      enabled: true,
      trigger_type: "condition" as const,
      categories: ["weather_warning", "fire_proximity", "new_ignition", "stream_flood_warning"],
      min_severity: "priority",
      delivery_type: "mesh_broadcast",
      broadcast_channel: 0,
      cooldown_minutes: 15,
      schedule_frequency: "daily" as const,
      schedule_time: "07:00",
      schedule_time_2: "",
      schedule_days: [] as string[],
      message_type: "",
      custom_message: "",
      node_ids: [] as string[],
      smtp_host: "",
      smtp_port: 587,
      smtp_user: "",
      smtp_password: "",
      smtp_tls: true,
      from_address: "",
      recipients: [] as string[],
      webhook_url: "",
      webhook_headers: {} as Record<string, string>,
    }
  },
  {
    id: "rf_conditions",
    name: "RF Conditions",
    description: "Propagation changes - solar events, HF blackouts, tropospheric ducting",
    rule: {
      name: "RF Conditions",
      enabled: true,
      trigger_type: "condition" as const,
      categories: ["hf_blackout", "tropospheric_ducting", "geomagnetic_storm"],
      min_severity: "routine",
      delivery_type: "mesh_broadcast",
      broadcast_channel: 0,
      cooldown_minutes: 60,
      schedule_frequency: "daily" as const,
      schedule_time: "07:00",
      schedule_time_2: "",
      schedule_days: [] as string[],
      message_type: "",
      custom_message: "",
      node_ids: [] as string[],
      smtp_host: "",
      smtp_port: 587,
      smtp_user: "",
      smtp_password: "",
      smtp_tls: true,
      from_address: "",
      recipients: [] as string[],
      webhook_url: "",
      webhook_headers: {} as Record<string, string>,
    }
  },
  {
    id: "road_traffic",
    name: "Road & Traffic",
    description: "Road closures and severe congestion",
    rule: {
      name: "Road & Traffic",
      enabled: true,
      trigger_type: "condition" as const,
      categories: ["road_closure", "traffic_congestion"],
      min_severity: "routine",
      delivery_type: "mesh_broadcast",
      broadcast_channel: 0,
      cooldown_minutes: 30,
      schedule_frequency: "daily" as const,
      schedule_time: "07:00",
      schedule_time_2: "",
      schedule_days: [] as string[],
      message_type: "",
      custom_message: "",
      node_ids: [] as string[],
      smtp_host: "",
      smtp_port: 587,
      smtp_user: "",
      smtp_password: "",
      smtp_tls: true,
      from_address: "",
      recipients: [] as string[],
      webhook_url: "",
      webhook_headers: {} as Record<string, string>,
    }
  },
  {
    id: "everything_critical",
    name: "Everything Critical",
    description: "All emergency-level events regardless of type",
    rule: {
      name: "Everything Critical",
      enabled: true,
      trigger_type: "condition" as const,
      categories: [] as string[],
      min_severity: "immediate",
      delivery_type: "mesh_broadcast",
      broadcast_channel: 0,
      cooldown_minutes: 5,
      schedule_frequency: "daily" as const,
      schedule_time: "07:00",
      schedule_time_2: "",
      schedule_days: [] as string[],
      message_type: "",
      custom_message: "",
      node_ids: [] as string[],
      smtp_host: "",
      smtp_port: 587,
      smtp_user: "",
      smtp_password: "",
      smtp_tls: true,
      from_address: "",
      recipients: [] as string[],
      webhook_url: "",
      webhook_headers: {} as Record<string, string>,
    }
  },
  {
    id: "morning_briefing",
    name: "Morning Briefing",
    description: "Daily health and conditions summary at 7am",
    rule: {
      name: "Morning Briefing",
      enabled: true,
      trigger_type: "schedule" as const,
      categories: [] as string[],
      min_severity: "routine",
      schedule_frequency: "daily" as const,
      schedule_time: "07:00",
      schedule_time_2: "",
      schedule_days: [] as string[],
      message_type: "mesh_health_summary",
      custom_message: "",
      delivery_type: "mesh_broadcast",
      broadcast_channel: 0,
      cooldown_minutes: 0,
      node_ids: [] as string[],
      smtp_host: "",
      smtp_port: 587,
      smtp_user: "",
      smtp_password: "",
      smtp_tls: true,
      from_address: "",
      recipients: [] as string[],
      webhook_url: "",
      webhook_headers: {} as Record<string, string>,
    }
  },
]

// Helper to format relative time
function formatRelativeTime(timestamp: number | null): string {
  if (!timestamp) return 'Never'
  const now = Date.now() / 1000
  const diff = now - timestamp
  if (diff < 60) return 'Just now'
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
  if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`
  return new Date(timestamp * 1000).toLocaleDateString()
}

// InfoButton component
export function InfoButton({ info }: { info: string }) {
  const [open, setOpen] = useState(false)

  return (
    <div className="relative inline-block">
      <button
        type="button"
        onClick={(e) => { e.stopPropagation(); setOpen(!open) }}
        className="ml-1.5 w-4 h-4 rounded-full bg-slate-700 hover:bg-slate-600 text-slate-400 hover:text-slate-200 inline-flex items-center justify-center text-xs transition-colors"
        title="More info"
      >
        ?
      </button>
      {open && (
        <>
          <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
          <div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] shadow-xl text-xs text-slate-300 leading-relaxed">
            {info}
          </div>
        </>
      )}
    </div>
  )
}

// Form components
export function TextInput({ label, value, onChange, type = 'text', placeholder = '', helper = '', info = '' }: {
  label: string
  value: string
  onChange: (v: string) => void
  type?: string
  placeholder?: string
  helper?: string
  info?: string
}) {
  const [showPassword, setShowPassword] = useState(false)
  const isPassword = type === 'password'

  return (
    <div className="space-y-1">
      <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        {label}
        {info && <InfoButton info={info} />}
      </label>
      <div className="relative">
        <input
          type={isPassword && !showPassword ? 'password' : 'text'}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          placeholder={placeholder}
          className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent placeholder-slate-600"
        />
        {isPassword && (
          <button
            type="button"
            onClick={() => setShowPassword(!showPassword)}
            className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300"
          >
            {showPassword ? <EyeOff size={16} /> : <EyeIcon size={16} />}
          </button>
        )}
      </div>
      {helper && <p className="text-xs text-slate-600">{helper}</p>}
    </div>
  )
}

export function NumberInput({ label, value, onChange, min, max, step = 1, helper = '', info = '' }: {
  label: string
  value: number
  onChange: (v: number) => void
  min?: number
  max?: number
  step?: number
  helper?: string
  info?: string
}) {
  return (
    <div className="space-y-1">
      <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        {label}
        {info && <InfoButton info={info} />}
      </label>
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
        min={min}
        max={max}
        step={step}
        className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent"
      />
      {helper && <p className="text-xs text-slate-600">{helper}</p>}
    </div>
  )
}

export function Toggle({ label, checked, onChange, helper = '', info = '' }: {
  label: string
  checked: boolean
  onChange: (v: boolean) => void
  helper?: string
  info?: string
}) {
  return (
    <div className="flex items-center justify-between py-2">
      <div>
        <span className="flex items-center text-sm text-slate-300">
          {label}
          {info && <InfoButton info={info} />}
        </span>
        {helper && <p className="text-xs text-slate-600">{helper}</p>}
      </div>
      <button
        type="button"
        onClick={() => onChange(!checked)}
        className={`relative w-11 h-6 rounded-full transition-colors ${
          checked ? 'bg-accent' : 'bg-[#1e2a3a]'
        }`}
      >
        <span
          className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform ${
            checked ? 'translate-x-5' : ''
          }`}
        />
      </button>
    </div>
  )
}

export function TimeInput({ label, value, onChange, helper = '', info = '' }: {
  label: string
  value: string
  onChange: (v: string) => void
  helper?: string
  info?: string
}) {
  return (
    <div className="space-y-1">
      <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        {label}
        {info && <InfoButton info={info} />}
      </label>
      <input
        type="time"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 focus:outline-none focus:border-accent"
      />
      {helper && <p className="text-xs text-slate-600">{helper}</p>}
    </div>
  )
}

export function ListInput({ label, value, onChange, placeholder = 'Add item...', helper = '', info = '' }: {
  label: string
  value: string[]
  onChange: (v: string[]) => void
  placeholder?: string
  helper?: string
  info?: string
}) {
  const [inputValue, setInputValue] = useState('')

  const addItem = () => {
    if (inputValue.trim() && !value.includes(inputValue.trim())) {
      onChange([...value, inputValue.trim()])
      setInputValue('')
    }
  }

  const removeItem = (index: number) => {
    onChange(value.filter((_, i) => i !== index))
  }

  return (
    <div className="space-y-1">
      <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        {label}
        {info && <InfoButton info={info} />}
      </label>
      <div className="flex gap-2">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addItem())}
          className="flex-1 px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent"
          placeholder={placeholder}
        />
        <button
          type="button"
          onClick={addItem}
          className="px-3 py-2 bg-accent hover:bg-accent/80 rounded text-sm text-white transition-colors"
        >
          <Plus size={16} />
        </button>
      </div>
      {value.length > 0 && (
        <div className="flex flex-wrap gap-2 mt-2">
          {value.map((item, i) => (
            <span
              key={i}
              className="inline-flex items-center gap-1 px-2 py-1 bg-[#1e2a3a] rounded text-sm text-slate-300"
            >
              {item}
              <button
                type="button"
                onClick={() => removeItem(i)}
                className="text-slate-500 hover:text-red-400"
              >
                <X size={14} />
              </button>
            </span>
          ))}
        </div>
      )}
      {helper && <p className="text-xs text-slate-600">{helper}</p>}
    </div>
  )
}

// Severity selector with descriptions
function SeveritySelector({ value, onChange }: {
  value: string
  onChange: (v: string) => void
}) {
  const [isOpen, setIsOpen] = useState(false)
  const selected = SEVERITY_OPTIONS.find(s => s.value === value) || SEVERITY_OPTIONS[0]

  return (
    <div className="space-y-1">
      <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        Severity Threshold
        <InfoButton info="Only alerts at or above this severity trigger this rule. ROUTINE = informational, PRIORITY = needs attention, IMMEDIATE = act now." />
      </label>
      <div className="relative">
        <button
          type="button"
          onClick={() => setIsOpen(!isOpen)}
          className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-left flex items-center justify-between hover:border-accent transition-colors"
        >
          <div>
            <span className="text-slate-200">{selected.label}</span>
            <span className="text-slate-500 ml-2">- {selected.description}</span>
          </div>
          <ChevronDown size={16} className={`text-slate-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
        </button>
        {isOpen && (
          <>
            <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
            <div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#0a0e17] border border-[#1e2a3a] shadow-xl overflow-hidden">
              {SEVERITY_OPTIONS.map((opt) => (
                <button
                  key={opt.value}
                  type="button"
                  onClick={() => { onChange(opt.value); setIsOpen(false) }}
                  className={`w-full px-3 py-2.5 text-left text-sm hover:bg-[#1e2a3a] transition-colors ${
                    value === opt.value ? 'bg-accent/10' : ''
                  }`}
                >
                  <div className="font-medium text-slate-200">{opt.label}</div>
                  <div className="text-xs text-slate-500">{opt.description}</div>
                </button>
              ))}
            </div>
          </>
        )}
      </div>
      <p className="text-xs text-slate-600">Lower = more notifications. "Warning" recommended for most rules.</p>
    </div>
  )
}

// Channel Test Button Component
function ChannelTestButton({ rule }: {
  rule: NotificationRuleConfig
}) {
  const [testing, setTesting] = useState(false)
  const [result, setResult] = useState<ChannelTestResult | null>(null)

  const handleTest = async () => {
    setTesting(true)
    setResult(null)

    try {
      // Build channel config from rule
      let channelConfig: Record<string, unknown> = { type: rule.delivery_type }

      if (rule.delivery_type === 'mesh_broadcast') {
        channelConfig.channel_index = rule.broadcast_channel
      } else if (rule.delivery_type === 'mesh_dm') {
        channelConfig.node_ids = rule.node_ids
      } else if (rule.delivery_type === 'email') {
        channelConfig = {
          type: 'email',
          smtp_host: rule.smtp_host,
          smtp_port: rule.smtp_port,
          smtp_user: rule.smtp_user,
          smtp_password: rule.smtp_password,
          smtp_tls: rule.smtp_tls,
          from_address: rule.from_address,
          recipients: rule.recipients,
        }
      } else if (rule.delivery_type === 'webhook') {
        channelConfig = {
          type: 'webhook',
          url: rule.webhook_url,
          headers: rule.webhook_headers,
        }
      }

      const res = await fetch('/api/notifications/channels/test', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(channelConfig),
      })
      const data = await res.json()
      setResult(data)
    } catch (err) {
      setResult({
        success: false,
        message: 'Test failed',
        error: err instanceof Error ? err.message : 'Unknown error',
        details: {}
      })
    } finally {
      setTesting(false)
    }
  }

  if (!rule.delivery_type) return null

  const icon = {
    mesh_broadcast: <Radio size={14} />,
    mesh_dm: <MessageSquare size={14} />,
    email: <Mail size={14} />,
    webhook: <Globe size={14} />,
  }[rule.delivery_type] || <Wifi size={14} />

  return (
    <div className="space-y-2">
      <button
        type="button"
        onClick={handleTest}
        disabled={testing}
        className="flex items-center gap-2 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 rounded text-sm disabled:opacity-50"
      >
        {testing ? (
          <>
            <RefreshCw size={14} className="animate-spin" />
            Testing...
          </>
        ) : (
          <>
            {icon}
            Test Channel
          </>
        )}
      </button>

      {result && (
        <div className={`p-2 rounded text-xs ${
          result.success
            ? 'bg-green-500/10 border border-green-500/30 text-green-400'
            : 'bg-red-500/10 border border-red-500/30 text-red-400'
        }`}>
          <div className="flex items-start gap-2">
            {result.success ? <Check size={14} className="mt-0.5 flex-shrink-0" /> : <X size={14} className="mt-0.5 flex-shrink-0" />}
            <div>
              <div className="font-medium">{result.message}</div>
              {result.error && <div className="mt-1 text-red-300">{result.error}</div>}
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

// Notification Rule Card Component
function NotificationRuleCard({
  rule,
  ruleIndex,
  categories,
  regions,
  onChange,
  onDelete,
  onDuplicate,
  onTest,
}: {
  rule: NotificationRuleConfig
  ruleIndex: number
  categories: AlertCategory[]
  regions: RegionInfo[]
  onChange: (r: NotificationRuleConfig) => void
  onDelete: () => void
  onDuplicate: () => void
  onTest: () => void
}) {
  const [expanded, setExpanded] = useState(!rule.name)
  const [testing, setTesting] = useState(false)
  const [ruleStats, setRuleStats] = useState<RuleStats | null>(null)
  const [sourceHealth, setSourceHealth] = useState<SourceHealth | null>(null)

  // Fetch rule stats on mount
  useEffect(() => {
    if (rule.name && ruleIndex >= 0) {
      fetch(`/api/notifications/rules/${ruleIndex}/stats`)
        .then(res => res.json())
        .then(data => setRuleStats(data))
        .catch(() => {})

      if (rule.categories?.length) {
        fetch('/api/notifications/rules/sources', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ categories: rule.categories })
        })
          .then(res => res.json())
          .then(data => setSourceHealth(data))
          .catch(() => {})
      }
    }
  }, [rule.name, ruleIndex, rule.categories])

  const deliveryOptions = [
    { value: '', label: '(None)', description: 'Rule matches but does not deliver' },
    { value: 'mesh_broadcast', label: 'Mesh Broadcast', description: 'Send to a mesh radio channel' },
    { value: 'mesh_dm', label: 'Mesh DM', description: 'Direct message to specific nodes' },
    { value: 'email', label: 'Email', description: 'Send via SMTP' },
    { value: 'webhook', label: 'Webhook', description: 'POST to any URL' },
  ]

  const frequencyOptions = [
    { value: 'daily', label: 'Daily' },
    { value: 'twice_daily', label: 'Twice Daily' },
    { value: 'weekly', label: 'Weekly' },
  ]

  const messageTypeOptions = [
    { value: 'mesh_health_summary', label: 'Mesh Health Summary', description: 'Current health score, pillar breakdown, problem nodes' },
    { value: 'rf_propagation_report', label: 'RF Propagation Report', description: 'Solar indices, Kp, ducting conditions' },
    { value: 'alerts_digest', label: 'Active Alerts Digest', description: 'Summary of all active environmental alerts' },
    { value: 'environmental_conditions', label: 'Environmental Conditions', description: 'Full conditions: weather, fire, streams, roads' },
    { value: 'custom', label: 'Custom Message', description: 'Write your own with template tokens' },
  ]

  const dayOptions = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']

  const toggleCategory = (catId: string) => {
    const current = rule.categories || []
    if (current.includes(catId)) {
      onChange({ ...rule, categories: current.filter(c => c !== catId) })
    } else {
      onChange({ ...rule, categories: [...current, catId] })
    }
  }

  const selectManyCategories = (catIds: string[], action: 'add' | 'remove') => {
    const current = rule.categories || []
    if (action === 'add') {
      const merged = Array.from(new Set([...current, ...catIds]))
      onChange({ ...rule, categories: merged })
    } else {
      const drop = new Set(catIds)
      onChange({ ...rule, categories: current.filter(c => !drop.has(c)) })
    }
  }

  const toggleRegion = (regionName: string) => {
    const current = rule.region_scope || []
    if (current.includes(regionName)) {
      onChange({ ...rule, region_scope: current.filter(r => r !== regionName) })
    } else {
      onChange({ ...rule, region_scope: [...current, regionName] })
    }
  }

  const toggleDay = (day: string) => {
    const current = rule.schedule_days || []
    if (current.includes(day)) {
      onChange({ ...rule, schedule_days: current.filter(d => d !== day) })
    } else {
      onChange({ ...rule, schedule_days: [...current, day] })
    }
  }

  const handleTest = async () => {
    setTesting(true)
    await onTest()
    setTesting(false)
  }

  // Get example message for display
  const getExampleMessage = (): string => {
    if (rule.trigger_type === 'schedule') {
      return '[Scheduled report preview would appear here]'
    }
    const ruleCats = rule.categories || []
    if (ruleCats.length === 0 && categories.length > 0) {
      return categories[0].example_message || 'Alert notification'
    }
    const firstCat = categories.find(c => ruleCats.includes(c.id))
    return firstCat?.example_message || 'Alert notification'
  }

  // Generate summary for collapsed view
  const getSummary = () => {
    const parts: string[] = []

    if (rule.trigger_type === 'schedule') {
      const freq = frequencyOptions.find(f => f.value === rule.schedule_frequency)?.label || rule.schedule_frequency
      const msgType = messageTypeOptions.find(m => m.value === rule.message_type)?.label || rule.message_type
      parts.push(`${freq} at ${rule.schedule_time || '??:??'}`)
      parts.push(msgType)
    } else {
      const catCount = rule.categories?.length || 0
      const catText = catCount === 0 ? 'All' : categories.filter(c => rule.categories?.includes(c.id)).map(c => c.name).slice(0, 2).join(', ') + (catCount > 2 ? ` +${catCount - 2}` : '')
      const severity = SEVERITY_OPTIONS.find(s => s.value === rule.min_severity)?.label || rule.min_severity
      parts.push(`${catText} at ${severity}+`)
    }

    // Delivery summary
    if (!rule.delivery_type) {
      parts.push('No delivery')
    } else {
      const delivery = deliveryOptions.find(d => d.value === rule.delivery_type)?.label || rule.delivery_type
      let target = ''
      if (rule.delivery_type === 'mesh_broadcast') {
        target = `Ch ${rule.broadcast_channel}`
      } else if (rule.delivery_type === 'mesh_dm') {
        target = `${rule.node_ids?.length || 0} nodes`
      } else if (rule.delivery_type === 'email') {
        target = rule.recipients?.length ? rule.recipients[0] + (rule.recipients.length > 1 ? ` +${rule.recipients.length - 1}` : '') : 'no recipients'
      } else if (rule.delivery_type === 'webhook') {
        try {
          const url = new URL(rule.webhook_url)
          target = url.hostname
        } catch {
          target = rule.webhook_url?.slice(0, 20) || 'no URL'
        }
      }
      parts.push(`${delivery}${target ? ` (${target})` : ''}`)
    }

    return parts.join(' -> ')
  }

  // Get source status indicators
  const getSourceIndicators = () => {
    if (!sourceHealth || !rule.categories?.length) return null

    const sources = new Map<string, { enabled: boolean; events: number }>()
    for (const [, health] of Object.entries(sourceHealth)) {
      const existing = sources.get(health.source)
      if (existing) {
        existing.events += health.active_events
        existing.enabled = existing.enabled && health.enabled
      } else {
        sources.set(health.source, { enabled: health.enabled, events: health.active_events })
      }
    }

    return Array.from(sources.entries()).map(([source, { enabled, events }]) => (
      <span
        key={source}
        className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs ${
          enabled
            ? 'bg-green-500/10 text-green-400'
            : 'bg-red-500/10 text-red-400'
        }`}
        title={enabled ? `${events} active` : 'Not enabled'}
      >
        {enabled ? <Wifi size={10} /> : <WifiOff size={10} />}
        {source.toUpperCase()}
        {enabled && events > 0 && ` (${events})`}
      </span>
    ))
  }

  return (
    <div className={`border overflow-hidden ${rule.enabled ? 'border-[#1e2a3a]' : 'border-slate-700 opacity-60'}`}>
      {/* Header */}
      <div
        className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
        onClick={() => setExpanded(!expanded)}
      >
        <div className="flex items-center gap-3 min-w-0 flex-1">
          {expanded ? <ChevronDown size={16} className="text-slate-500 flex-shrink-0" /> : <ChevronRight size={16} className="text-slate-500 flex-shrink-0" />}
          <button
            onClick={(e) => { e.stopPropagation(); onChange({ ...rule, enabled: !rule.enabled }) }}
            className={`w-2 h-2 rounded-full flex-shrink-0 ${rule.enabled ? 'bg-green-500' : 'bg-slate-500'}`}
            title={rule.enabled ? 'Enabled' : 'Disabled'}
          />
          {rule.trigger_type === 'schedule' ? (
            <Clock size={14} className="text-[#f59e0b] flex-shrink-0" />
          ) : (
            <Zap size={14} className="text-yellow-400 flex-shrink-0" />
          )}
          <span className="font-medium text-slate-200 truncate" title={rule.name || undefined}>{rule.name || 'New Rule'}</span>
          {!expanded && (
            <span className={`text-xs truncate hidden sm:block ${!rule.delivery_type ? 'text-amber-400' : 'text-slate-500'}`}>
              {getSummary()}
            </span>
          )}
        </div>
        <div className="flex items-center gap-1 flex-shrink-0">
          {/* Activity badge */}
          {!expanded && (() => {
            const badgeBase = 'hidden sm:inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs mr-2'
            if (!rule.enabled) {
              return <span className={`${badgeBase} bg-slate-800 text-slate-500`}>Disabled</span>
            }
            if (!ruleStats) return null
            const count = ruleStats.fire_count || 0
            const last = ruleStats.last_fired
            const sevenDaysAgo = (Date.now() / 1000) - 7 * 86400
            if (count > 0 && last && last >= sevenDaysAgo) {
              return (
                <span className={`${badgeBase} bg-green-500/10 text-green-400`} title={`Last fired ${formatRelativeTime(last)}`}>
                  Active
                </span>
              )
            }
            if (count > 0 && last) {
              return (
                <span className={`${badgeBase} bg-yellow-500/10 text-yellow-400`} title={`Last fired ${formatRelativeTime(last)}`}>
                  Idle (no recent activity)
                </span>
              )
            }
            return <span className={`${badgeBase} bg-slate-800 text-slate-400`}>No activity yet</span>
          })()}
          {/* Source indicators */}
          {!expanded && (
            <div className="hidden md:flex items-center gap-1 mr-2">
              {getSourceIndicators()}
            </div>
          )}
          <button
            onClick={(e) => { e.stopPropagation(); handleTest() }}
            disabled={testing || !rule.name}
            className="p-1.5 text-[#f59e0b] hover:text-[#d97706] hover:bg-[#f59e0b]/10 rounded disabled:opacity-50"
            title="Test rule"
          >
            <Send size={14} />
          </button>
          <button
            onClick={(e) => { e.stopPropagation(); onDuplicate() }}
            className="p-1.5 text-slate-400 hover:text-slate-200 hover:bg-slate-500/10 rounded"
            title="Duplicate"
          >
            <Copy size={14} />
          </button>
          <button
            onClick={(e) => { e.stopPropagation(); onDelete() }}
            className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded"
            title="Delete"
          >
            <Trash2 size={14} />
          </button>
        </div>
      </div>

      {/* Status line (collapsed view) */}
      {!expanded && rule.name && (
        <div className="px-3 pb-2 pt-0 bg-[#0a0e17] flex items-center gap-2 flex-wrap text-xs">
          {!rule.delivery_type && (
            <span className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-amber-500/10 text-amber-400 rounded">
              <AlertCircle size={10} />
              No delivery method
            </span>
          )}
          {ruleStats?.fire_count !== undefined && ruleStats.fire_count > 0 && (
            <span className="text-slate-500">
              Fired {ruleStats.fire_count}x
            </span>
          )}
        </div>
      )}

      {/* Expanded content */}
      {expanded && (
        <div className="p-4 space-y-6 border-t border-[#1e2a3a]">
          {/* Rule name */}
          <TextInput
            label="Rule Name"
            value={rule.name}
            onChange={(v) => onChange({ ...rule, name: v })}
            placeholder="e.g., Emergency Broadcast, Daily Health Report"
            helper="A descriptive name for this rule"
          />

          {/* Trigger type toggle */}
          <div className="space-y-2">
            <label className="text-xs text-slate-500 uppercase tracking-wide">Trigger Type</label>
            <div className="flex gap-2">
              <button
                type="button"
                onClick={() => onChange({ ...rule, trigger_type: 'condition' })}
                className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 border transition-colors ${
                  rule.trigger_type !== 'schedule'
                    ? 'bg-accent/10 border-accent text-accent'
                    : 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200'
                }`}
              >
                <Zap size={16} />
                <span>Condition</span>
              </button>
              <button
                type="button"
                onClick={() => onChange({ ...rule, trigger_type: 'schedule' })}
                className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 border transition-colors ${
                  rule.trigger_type === 'schedule'
                    ? 'bg-accent/10 border-accent text-accent'
                    : 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200'
                }`}
              >
                <Clock size={16} />
                <span>Schedule</span>
              </button>
            </div>
            <p className="text-xs text-slate-600">
              {rule.trigger_type === 'schedule'
                ? 'Send reports on a schedule (daily briefings, weekly digests)'
                : 'React to alert conditions (fires, outages, weather warnings)'}
            </p>
          </div>

          {/* WHEN section - Condition trigger */}
          {rule.trigger_type !== 'schedule' && (
            <div className="space-y-4 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
              <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
                <AlertTriangle size={14} />
                WHEN (Condition)
              </div>

              <SeveritySelector
                value={rule.min_severity}
                onChange={(v) => onChange({ ...rule, min_severity: v })}
              />

              <div className="space-y-2">
                <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
                  Alert Categories
                  <InfoButton info="Select which types of alerts trigger this rule. Leave all unchecked to match ALL categories. Categories are grouped by family — use the 'All' / 'Clear' buttons in each header to bulk-toggle." />
                </label>
                <div className="text-xs text-slate-500 mb-2">
                  {(rule.categories?.length || 0) === 0 ? 'All categories (none selected)' : `${rule.categories?.length} selected`}
                </div>
                <GroupedCategoryPicker
                  categories={categories}
                  selected={rule.categories || []}
                  onToggle={toggleCategory}
                  onSelectMany={selectManyCategories}
                />
              </div>

              {/* Source health display */}
              {sourceHealth && Object.keys(sourceHealth).length > 0 && (
                <div className="space-y-2">
                  <label className="text-xs text-slate-500 uppercase tracking-wide">Data Sources</label>
                  <div className="flex flex-wrap gap-2">
                    {getSourceIndicators()}
                  </div>
                </div>
              )}
            </div>
          )}

          {/* WHEN section - Schedule trigger */}
          {rule.trigger_type === 'schedule' && (
            <div className="space-y-4 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
              <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
                <Calendar size={14} />
                WHEN (Schedule)
              </div>

              <div className="space-y-1">
                <label className="text-xs text-slate-500 uppercase tracking-wide">Frequency</label>
                <select
                  value={rule.schedule_frequency || 'daily'}
                  onChange={(e) => onChange({ ...rule, schedule_frequency: e.target.value as 'daily' | 'twice_daily' | 'weekly' })}
                  className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 focus:outline-none focus:border-accent"
                >
                  {frequencyOptions.map(opt => (
                    <option key={opt.value} value={opt.value}>{opt.label}</option>
                  ))}
                </select>
              </div>

              <div className="grid grid-cols-2 gap-4">
                <TimeInput
                  label="Time"
                  value={rule.schedule_time || '07:00'}
                  onChange={(v) => onChange({ ...rule, schedule_time: v })}
                />
                {rule.schedule_frequency === 'twice_daily' && (
                  <TimeInput
                    label="Second Time"
                    value={rule.schedule_time_2 || '19:00'}
                    onChange={(v) => onChange({ ...rule, schedule_time_2: v })}
                  />
                )}
              </div>

              {rule.schedule_frequency === 'weekly' && (
                <div className="space-y-2">
                  <label className="text-xs text-slate-500 uppercase tracking-wide">Days</label>
                  <div className="flex flex-wrap gap-2">
                    {dayOptions.map((day) => (
                      <button
                        key={day}
                        type="button"
                        onClick={() => toggleDay(day)}
                        className={`px-3 py-1.5 rounded text-sm capitalize transition-colors ${
                          rule.schedule_days?.includes(day)
                            ? 'bg-accent text-white'
                            : 'bg-[#1e2a3a] text-slate-400 hover:text-slate-200'
                        }`}
                      >
                        {day.slice(0, 3)}
                      </button>
                    ))}
                  </div>
                </div>
              )}

              <div className="space-y-1">
                <label className="text-xs text-slate-500 uppercase tracking-wide">Report Type</label>
                <select
                  value={rule.message_type || 'mesh_health_summary'}
                  onChange={(e) => onChange({ ...rule, message_type: e.target.value })}
                  className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 focus:outline-none focus:border-accent"
                >
                  {messageTypeOptions.map(opt => (
                    <option key={opt.value} value={opt.value}>{opt.label}</option>
                  ))}
                </select>
                <p className="text-xs text-slate-600">
                  {messageTypeOptions.find(m => m.value === rule.message_type)?.description}
                </p>
              </div>

              {rule.message_type === 'custom' && (
                <div className="space-y-1">
                  <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
                    Custom Message
                    <InfoButton info="Available tokens: {MESH_SCORE}, {NODE_COUNT}, {NODES_ONLINE}, {ACTIVE_ALERTS}, {KP}, {SFI}, {DATE}, {TIME}" />
                  </label>
                  <textarea
                    value={rule.custom_message || ''}
                    onChange={(e) => onChange({ ...rule, custom_message: e.target.value })}
                    rows={4}
                    placeholder="Good morning! Mesh health: {MESH_SCORE}/100 with {NODE_COUNT} nodes online."
                    className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 font-mono focus:outline-none focus:border-accent placeholder-slate-600"
                  />
                </div>
              )}
            </div>
          )}

          {/* REGIONS section — scope rule to specific regions; empty = all regions */}
          <div className="space-y-2 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
            <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
              <MapPin size={14} />
              REGIONS
              <InfoButton info="Limit this rule to alerts from specific regions. Empty selection = all regions (backward compatible). Region names come from /api/regions." />
            </div>
            <div className="text-xs text-slate-500">
              {(rule.region_scope?.length || 0) === 0
                ? 'All regions (none selected)'
                : `${rule.region_scope.length} of ${regions.length} selected`}
            </div>
            {regions.length === 0 ? (
              <div className="text-xs text-slate-600 italic">No regions configured.</div>
            ) : (
              <div className="flex flex-wrap gap-2">
                {regions.map(r => {
                  const on = (rule.region_scope || []).includes(r.name)
                  return (
                    <button
                      key={r.name}
                      type="button"
                      onClick={() => toggleRegion(r.name)}
                      className={`px-3 py-1.5 rounded text-sm transition-colors ${
                        on
                          ? 'bg-accent text-white'
                          : 'bg-[#1e2a3a] text-slate-400 hover:text-slate-200'
                      }`}
                      title={r.local_name || r.name}
                    >
                      {r.local_name || r.name}
                    </button>
                  )
                })}
              </div>
            )}
          </div>

          {/* SEND VIA section */}
          <div className="space-y-4 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
            <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
              <Send size={14} />
              SEND VIA
            </div>

            <div className="space-y-1">
              <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
                Delivery Method
                <InfoButton info="Where this notification gets delivered. Select (None) to save the rule without delivery - it will match conditions but won't send until you configure a delivery method." />
              </label>
              <select
                value={rule.delivery_type || ''}
                onChange={(e) => onChange({ ...rule, delivery_type: e.target.value })}
                className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 focus:outline-none focus:border-accent"
              >
                {deliveryOptions.map(opt => (
                  <option key={opt.value} value={opt.value}>{opt.label}</option>
                ))}
              </select>
              <p className="text-xs text-slate-600">
                {deliveryOptions.find(d => d.value === (rule.delivery_type || ''))?.description}
              </p>
            </div>

            {/* No delivery warning */}
            {!rule.delivery_type && (
              <div className="flex items-start gap-2 p-3 bg-amber-500/10 border border-amber-500/20">
                <AlertCircle size={16} className="text-amber-400 mt-0.5 flex-shrink-0" />
                <div className="text-sm text-amber-300">
                  Rule will log matches but not deliver until a delivery method is configured.
                </div>
              </div>
            )}

            {/* Mesh Broadcast fields */}
            {rule.delivery_type === 'mesh_broadcast' && (
              <>
                <ChannelPicker
                  label="Broadcast Channel"
                  value={rule.broadcast_channel ?? 0}
                  onChange={(v) => onChange({ ...rule, broadcast_channel: v })}
                  helper="Select the mesh radio channel"
                  mode="single"
                />
                <ChannelTestButton rule={rule} />
              </>
            )}

            {/* Mesh DM fields */}
            {rule.delivery_type === 'mesh_dm' && (
              <>
                <NodePicker
                  label="Recipient Nodes"
                  value={rule.node_ids || []}
                  onChange={(v) => onChange({ ...rule, node_ids: v })}
                  helper="Nodes that receive direct messages"
                  valueType="node_id_hex"
                />
                <ChannelTestButton rule={rule} />
              </>
            )}

            {/* Email fields */}
            {rule.delivery_type === 'email' && (
              <div className="space-y-4">
                <ListInput
                  label="Recipients"
                  value={rule.recipients || []}
                  onChange={(v) => onChange({ ...rule, recipients: v })}
                  placeholder="email@example.com"
                  helper="Email addresses to receive alerts"
                />

                <details className="group">
                  <summary className="flex items-center gap-2 cursor-pointer text-sm text-slate-400 hover:text-slate-200">
                    <ChevronRight size={14} className="group-open:rotate-90 transition-transform" />
                    SMTP Configuration
                  </summary>
                  <div className="mt-4 space-y-4 pl-6 border-l border-[#1e2a3a]">
                    <div className="grid grid-cols-2 gap-4">
                      <TextInput
                        label="SMTP Host"
                        value={rule.smtp_host || ''}
                        onChange={(v) => onChange({ ...rule, smtp_host: v })}
                        placeholder="smtp.gmail.com"
                      />
                      <NumberInput
                        label="SMTP Port"
                        value={rule.smtp_port ?? 587}
                        onChange={(v) => onChange({ ...rule, smtp_port: v })}
                        min={1}
                        max={65535}
                      />
                    </div>
                    <div className="grid grid-cols-2 gap-4">
                      <TextInput
                        label="Username"
                        value={rule.smtp_user || ''}
                        onChange={(v) => onChange({ ...rule, smtp_user: v })}
                      />
                      <TextInput
                        label="Password"
                        value={rule.smtp_password || ''}
                        onChange={(v) => onChange({ ...rule, smtp_password: v })}
                        type="password"
                        info="Gmail users: use an App Password from myaccount.google.com/apppasswords"
                      />
                    </div>
                    <Toggle
                      label="Use TLS"
                      checked={rule.smtp_tls ?? true}
                      onChange={(v) => onChange({ ...rule, smtp_tls: v })}
                    />
                    <TextInput
                      label="From Address"
                      value={rule.from_address || ''}
                      onChange={(v) => onChange({ ...rule, from_address: v })}
                      placeholder="alerts@yourdomain.com"
                    />
                  </div>
                </details>

                <ChannelTestButton rule={rule} />
              </div>
            )}

            {/* Webhook fields */}
            {rule.delivery_type === 'webhook' && (
              <>
                <TextInput
                  label="Webhook URL"
                  value={rule.webhook_url || ''}
                  onChange={(v) => onChange({ ...rule, webhook_url: v })}
                  placeholder="https://discord.com/api/webhooks/..."
                  helper="POST alert as JSON"
                  info="Works with Discord webhooks, ntfy.sh, Slack, Home Assistant, Pushover, or any HTTP POST endpoint."
                />
                <ChannelTestButton rule={rule} />
              </>
            )}
          </div>

          {/* Behavior section */}
          <div className="grid grid-cols-2 gap-4">
            <NumberInput
              label="Cooldown (minutes)"
              value={rule.cooldown_minutes ?? 10}
              onChange={(v) => onChange({ ...rule, cooldown_minutes: v })}
              min={0}
              helper="Min time between repeat sends"
              info="Prevents alert spam. Same condition won't re-trigger this rule within this window."
            />          </div>

          {/* Rule statistics */}
          {ruleStats && (
            <div className="flex items-center gap-4 text-xs text-slate-500">
              <span>Last fired: {formatRelativeTime(ruleStats.last_fired)}</span>
              <span>Last tested: {formatRelativeTime(ruleStats.last_test)}</span>
              <span>Total fires: {ruleStats.fire_count}</span>
            </div>
          )}

          {/* Example message */}
          {rule.trigger_type !== 'schedule' && (
            <div className="space-y-2">
              <label className="text-xs text-slate-500 uppercase tracking-wide">Example Message</label>
              <div className="p-3 bg-[#1e2a3a]/50 border border-[#1e2a3a]">
                <p className="text-sm text-slate-300 font-mono">{getExampleMessage()}</p>
              </div>
              <p className="text-xs text-slate-600">This is an example of what this rule would send.</p>
            </div>
          )}
        </div>
      )}
    </div>
  )
}

// Main Notifications Page Component
export const TOGGLE_FAMILY_META: { key: string; label: string; Icon: typeof Activity }[] = [
  { key: 'mesh_health', label: 'Mesh Health', Icon: Activity },
  { key: 'weather', label: 'Weather', Icon: Cloud },
  { key: 'fire', label: 'Fire', Icon: Flame },
  { key: 'rf_propagation', label: 'RF Propagation', Icon: Radio },
  { key: 'roads', label: 'Roads', Icon: Car },
  { key: 'avalanche', label: 'Avalanche', Icon: Snowflake },
  { key: 'satpass', label: 'Satellite Passes', Icon: Satellite },
  { key: 'seismic', label: 'Seismic', Icon: Mountain },
  { key: 'tracking', label: 'Tracking', Icon: MapPin },
]

// Grouped category picker — shows categories family-by-family using TOGGLE_FAMILY_META
// for icons/labels/order. Each family has a Select-all / Clear control and a count.
// Categories whose `toggle` field doesn't match a known family fall into "Other".
function GroupedCategoryPicker({
  categories,
  selected,
  onToggle,
  onSelectMany,
}: {
  categories: AlertCategory[]
  selected: string[]
  onToggle: (catId: string) => void
  onSelectMany: (catIds: string[], action: 'add' | 'remove') => void
}) {
  const FAMILY_KEYS = new Set(TOGGLE_FAMILY_META.map(f => f.key))
  // Group by toggle, preserving family order; collect unknowns into "other".
  const byFamily = new Map<string, AlertCategory[]>()
  TOGGLE_FAMILY_META.forEach(f => byFamily.set(f.key, []))
  const other: AlertCategory[] = []
  for (const cat of categories) {
    const fam = cat.toggle
    if (fam && FAMILY_KEYS.has(fam)) byFamily.get(fam)!.push(cat)
    else other.push(cat)
  }

  // Track which families are expanded. Default: any family that has selected items
  // starts expanded so users can see what's checked.
  const initialOpen = new Set<string>()
  for (const [fam, cats] of byFamily) {
    if (cats.some(c => selected.includes(c.id))) initialOpen.add(fam)
  }
  if (other.some(c => selected.includes(c.id))) initialOpen.add('other')
  const [openFamilies, setOpenFamilies] = useState<Set<string>>(initialOpen)
  const toggleOpen = (key: string) => {
    setOpenFamilies(prev => {
      const next = new Set(prev)
      if (next.has(key)) next.delete(key); else next.add(key)
      return next
    })
  }

  const renderGroup = (key: string, label: string, Icon: typeof Activity | null, cats: AlertCategory[]) => {
    if (!cats.length) return null
    const isOpen = openFamilies.has(key)
    const ids = cats.map(c => c.id)
    const selectedInFamily = ids.filter(id => selected.includes(id)).length
    return (
      <div key={key} className="border border-[#1e2a3a] rounded">
        <div className="flex items-center justify-between px-2 py-1.5 bg-[#0d1420]">
          <button
            type="button"
            onClick={() => toggleOpen(key)}
            className="flex items-center gap-2 text-sm text-slate-200 flex-1 min-w-0"
          >
            {isOpen ? <ChevronDown size={14} className="text-slate-500 flex-shrink-0" /> : <ChevronRight size={14} className="text-slate-500 flex-shrink-0" />}
            {Icon && <Icon size={14} className="text-slate-400 flex-shrink-0" />}
            <span className="truncate">{label} ({cats.length})</span>
            {selectedInFamily > 0 && (
              <span className="ml-1 text-xs text-accent">{selectedInFamily} selected</span>
            )}
          </button>
          <div className="flex items-center gap-1 flex-shrink-0">
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); onSelectMany(ids, 'add') }}
              className="text-xs px-2 py-0.5 rounded text-slate-400 hover:text-accent hover:bg-accent/10"
              title="Select all in family"
            >
              All
            </button>
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); onSelectMany(ids, 'remove') }}
              className="text-xs px-2 py-0.5 rounded text-slate-400 hover:text-red-400 hover:bg-red-500/10"
              title="Clear family"
            >
              Clear
            </button>
          </div>
        </div>
        {isOpen && (
          <div className="p-1 space-y-1">
            {cats.map(cat => (
              <label
                key={cat.id}
                onClick={() => onToggle(cat.id)}
                className="flex items-start gap-2 p-2 rounded hover:bg-[#1e2a3a]/50 cursor-pointer"
              >
                <div className={`w-4 h-4 mt-0.5 rounded border flex items-center justify-center flex-shrink-0 ${
                  selected.includes(cat.id) ? 'bg-accent border-accent' : 'border-slate-600'
                }`}>
                  {selected.includes(cat.id) && <Check size={12} className="text-white" />}
                </div>
                <div className="flex-1 min-w-0">
                  <div className="text-sm text-slate-200">{cat.name}</div>
                  <div className="text-xs text-slate-500">{cat.description}</div>
                </div>
              </label>
            ))}
          </div>
        )}
      </div>
    )
  }

  return (
    <div className="max-h-96 overflow-y-auto border border-[#1e2a3a] p-2 space-y-2">
      {TOGGLE_FAMILY_META.map(f => renderGroup(f.key, f.label, f.Icon, byFamily.get(f.key) || []))}
      {renderGroup('other', 'Other', null, other)}
    </div>
  )
}
// Per-mesh channel groups for the split severity matrix
const MT_CHANNELS = ['mesh_broadcast', 'mesh_dm'] as const
export const MC_CHANNELS = ['meshcore_broadcast', 'meshcore_dm'] as const
const OTHER_CHANNELS = ['digest', 'email', 'webhook'] as const
const TOGGLE_SEVERITIES = ['routine', 'priority', 'immediate']

// Reusable severity × channel matrix.
// Toggling a single channel only touches that exact string in the severity row —
// all other channels (including those owned by the other mesh section) are preserved.
// This makes every checkbox merge-safe: Meshtastic toggles never clobber MeshCore
// entries and vice-versa.
export function SeverityChannelMatrix({
  channels,
  severityChannels,
  onChange,
}: {
  channels: readonly string[]
  severityChannels: Record<string, string[]>
  onChange: (updated: Record<string, string[]>) => void
}) {
  const colLabel = (c: string) =>
    c.replace('meshcore_', 'mc_').replace('mesh_', '').replace(/_/g, ' ')
  return (
    <table className="text-xs w-full">
      <thead>
        <tr>
          <th className="text-left text-slate-600 font-normal w-20">severity</th>
          {channels.map((c) => (
            <th key={c} className="text-slate-500 font-normal px-1 whitespace-nowrap">{colLabel(c)}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {TOGGLE_SEVERITIES.map((sev) => (
          <tr key={sev}>
            <td className="text-slate-400 pr-2 whitespace-nowrap">{sev}</td>
            {channels.map((ch) => {
              const on = (severityChannels[sev] || []).includes(ch)
              return (
                <td key={ch} className="text-center">
                  <input
                    type="checkbox"
                    checked={on}
                    onChange={(e) => {
                      // Shallow-copy the dict so we don't mutate state, then
                      // only modify the specific channel being toggled.
                      const cur: Record<string, string[]> = { ...severityChannels }
                      const arr = new Set(cur[sev] || [])
                      if (e.target.checked) arr.add(ch)
                      else arr.delete(ch)
                      cur[sev] = Array.from(arr)
                      onChange(cur)
                    }}
                  />
                </td>
              )
            })}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

// Merge only the MT+Other-channel-owned delivery fields of `mine` into `fresh`,
// preserving MeshCore entries (meshcore_*) and all gating fields
// (enabled/min_severity/freshness_seconds/cooldown_seconds/regions) that now live
// on the Data Feeds page.
function mergeMeshtasticAndOtherFields(
  fresh: NotificationToggle | undefined,
  mine: NotificationToggle,
  key: string,
): NotificationToggle {
  const base: NotificationToggle = fresh ? { ...fresh } : { ...mine, name: key }
  base.name = base.name || key

  // Merge severity_channels: keep meshcore_* from fresh, overlay non-meshcore from mine.
  const freshSC = fresh?.severity_channels || {}
  const mineSC = mine.severity_channels || {}
  const severities = new Set([...Object.keys(freshSC), ...Object.keys(mineSC)])
  const mergedSC: Record<string, string[]> = {}
  severities.forEach((sev) => {
    const meshcoreOnly = (freshSC[sev] || []).filter((c) => c.startsWith('meshcore_'))
    const nonMeshcore = (mineSC[sev] || []).filter((c) => !c.startsWith('meshcore_'))
    mergedSC[sev] = [...meshcoreOnly, ...nonMeshcore]
  })
  base.severity_channels = mergedSC

  // Overlay MT delivery fields
  base.broadcast_channel = mine.broadcast_channel
  base.node_ids = mine.node_ids
  // Overlay Other channels delivery fields
  base.smtp_host = mine.smtp_host
  base.smtp_port = mine.smtp_port
  base.smtp_user = mine.smtp_user
  base.smtp_password = mine.smtp_password
  base.smtp_tls = mine.smtp_tls
  base.from_address = mine.from_address
  base.recipients = mine.recipients
  base.webhook_url = mine.webhook_url
  base.webhook_headers = mine.webhook_headers

  // NOTE: do NOT overlay gating fields (enabled, min_severity, freshness_seconds,
  // cooldown_seconds, regions) — those are managed by Data Feeds > Family Settings.
  return base
}

// Always-visible per-family Meshtastic delivery grid.
// Structure mirrors MeshCoreRouting.tsx: pure delivery, no enable toggle, no expander.
function MeshtasticDeliveryGrid({
  toggles,
  onChange,
}: {
  toggles: Record<string, NotificationToggle>
  onChange: (t: Record<string, NotificationToggle>) => void
}) {
  const upd = (fam: string, patch: Partial<NotificationToggle>) =>
    onChange({ ...toggles, [fam]: { ...(toggles[fam] || {}), name: fam, ...patch } as NotificationToggle })

  return (
    <div className="space-y-3">
      <div className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        Meshtastic Delivery
        <InfoButton info="Per-family Meshtastic delivery matrix. Choose which channels fire at each severity, the broadcast channel index, and DM node IDs. Family on/off and severity threshold are configured on the Data Feeds page." />
      </div>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
        {TOGGLE_FAMILY_META.map(({ key, label, Icon }) => {
          const t = toggles[key] || ({} as NotificationToggle)
          return (
            <div key={key} className="border border-[#1e2a3a] p-3 space-y-3">
              <div className="flex items-center gap-2 text-sm text-slate-200">
                <Icon size={15} /> {label}
              </div>
              <div className="space-y-3 p-3 bg-[#0a0e17] border border-[#1e2a3a]">
                <div className="flex items-center gap-2 text-xs font-medium text-slate-300">
                  <Radio size={13} />
                  Meshtastic
                </div>
                <SeverityChannelMatrix
                  channels={MT_CHANNELS}
                  severityChannels={t.severity_channels || {}}
                  onChange={(sc) => upd(key, { severity_channels: sc })}
                />
                <NumberInput
                  label="Broadcast channel"
                  value={t.broadcast_channel ?? 0}
                  onChange={(v) => upd(key, { broadcast_channel: v })}
                  min={0}
                  helper="Meshtastic channel index (0 = LongFast primary)"
                  info="The Meshtastic channel index used for mesh_broadcast delivery. 0 = primary channel."
                />
                <ListInput
                  label="DM node IDs"
                  value={t.node_ids || []}
                  onChange={(v) => upd(key, { node_ids: v })}
                  placeholder="!hex_id"
                  helper="Meshtastic DM recipients (hex node IDs)"
                  info="Hex node IDs for mesh_dm delivery (e.g. !a1b2c3d4). Used when mesh_dm is enabled for a severity."
                />
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

// Per-family Other Channels (email / webhook / digest) delivery grid.
// Kept on the Meshtastic Routing page so all non-MeshCore delivery lives in one place.
function OtherChannelsGrid({
  toggles,
  onChange,
}: {
  toggles: Record<string, NotificationToggle>
  onChange: (t: Record<string, NotificationToggle>) => void
}) {
  const upd = (fam: string, patch: Partial<NotificationToggle>) =>
    onChange({ ...toggles, [fam]: { ...(toggles[fam] || {}), name: fam, ...patch } as NotificationToggle })

  return (
    <div className="space-y-3">
      <div className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
        Other Channels (Email / Webhook / Digest)
        <InfoButton info="Per-family delivery via email, webhook, and digest. The severity matrix selects which of these channels fire at each level. Email SMTP settings are behind the collapsible below each family." />
      </div>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
        {TOGGLE_FAMILY_META.map(({ key, label, Icon }) => {
          const t = toggles[key] || ({} as NotificationToggle)
          return (
            <div key={key} className="border border-[#1e2a3a] p-3 space-y-3">
              <div className="flex items-center gap-2 text-sm text-slate-200">
                <Icon size={15} /> {label}
              </div>
              <div className="space-y-3 p-3 bg-[#0a0e17] border border-[#1e2a3a]">
                <div className="flex items-center gap-2 text-xs font-medium text-slate-300">
                  <Mail size={13} />
                  Other channels
                </div>
                <SeverityChannelMatrix
                  channels={OTHER_CHANNELS}
                  severityChannels={t.severity_channels || {}}
                  onChange={(sc) => upd(key, { severity_channels: sc })}
                />
                <ListInput
                  label="Email recipients"
                  value={t.recipients || []}
                  onChange={(v) => upd(key, { recipients: v })}
                  placeholder="ops@example.com"
                />
                <details className="group">
                  <summary className="flex items-center gap-2 cursor-pointer text-xs text-slate-400 hover:text-slate-200">
                    <ChevronRight size={12} className="group-open:rotate-90 transition-transform" />
                    SMTP settings
                  </summary>
                  <div className="mt-2 space-y-2 pl-4 border-l border-[#1e2a3a]">
                    <TextInput label="SMTP host" value={t.smtp_host || ''} onChange={(v) => upd(key, { smtp_host: v })} placeholder="smtp.example.com" />
                    <NumberInput label="SMTP port" value={t.smtp_port ?? 587} onChange={(v) => upd(key, { smtp_port: v })} />
                    <TextInput label="Username" value={t.smtp_user || ''} onChange={(v) => upd(key, { smtp_user: v })} />
                    <TextInput label="Password" value={t.smtp_password || ''} onChange={(v) => upd(key, { smtp_password: v })} type="password" />
                    <Toggle label="Use TLS" checked={t.smtp_tls ?? true} onChange={(v) => upd(key, { smtp_tls: v })} />
                    <TextInput label="From address" value={t.from_address || ''} onChange={(v) => upd(key, { from_address: v })} placeholder="alerts@example.com" />
                  </div>
                </details>
                <TextInput
                  label="Webhook URL"
                  value={t.webhook_url || ''}
                  onChange={(v) => upd(key, { webhook_url: v })}
                  placeholder="https://..."
                  helper="POST alert as JSON"
                />
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}


export default function Notifications() {
  const { setDirty } = useDirty()
  const [config, setConfig] = useState<NotificationsConfig | null>(null)
  const [originalConfig, setOriginalConfig] = useState<NotificationsConfig | null>(null)
  const [categories, setCategories] = useState<AlertCategory[]>([])
  const [regions, setRegions] = useState<RegionInfo[]>([])
  const [loading, setLoading] = useState(true)
  const [saving, setSaving] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [success, setSuccess] = useState<string | null>(null)
  const [testResult, setTestResult] = useState<TestResult | null>(null)
  const [testDialog, setTestDialog] = useState<{ open: boolean; ruleIndex: number; loading: boolean; action: string }>({ open: false, ruleIndex: -1, loading: false, action: '' })
  const [showTemplates, setShowTemplates] = useState(false)
  const [hasChanges, setHasChanges] = useState(false)

  const fetchConfig = useCallback(async () => {
    try {
      const [configRes, categoriesRes, regionsRes] = await Promise.all([
        fetch('/api/config/notifications'),
        fetch('/api/notifications/categories'),
        fetch('/api/regions'),
      ])
      if (!configRes.ok) throw new Error('Failed to fetch notifications config')
      const configData = await configRes.json()
      const categoriesData = await categoriesRes.json()
      // /api/regions is best-effort — don't block the page if it fails.
      const regionsData: RegionInfo[] = regionsRes.ok ? await regionsRes.json() : []
      setConfig(configData)
      setOriginalConfig(JSON.parse(JSON.stringify(configData)))
      setCategories(categoriesData)
      setRegions(Array.isArray(regionsData) ? regionsData : [])
      setHasChanges(false)
      setError(null)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => {
    document.title = 'Notifications - MeshAI'
    fetchConfig()
  }, [fetchConfig])

  useEffect(() => {
    if (config && originalConfig) {
      setHasChanges(JSON.stringify(config) !== JSON.stringify(originalConfig))
    }
  }, [config, originalConfig])

  useEffect(() => {
    setDirty(hasChanges)
    return () => setDirty(false)
  }, [hasChanges, setDirty])

  const saveConfig = async () => {
    if (!config) return

    setSaving(true)
    setError(null)
    setSuccess(null)

    try {
      // Re-fetch the live config and merge ONLY MT+Other delivery fields so we never
      // clobber gating fields (enabled/min_severity/freshness/cooldown) managed by
      // Data Feeds, nor MeshCore fields managed by the MeshCore Routing page.
      const freshRes = await fetch('/api/config/notifications')
      if (!freshRes.ok) throw new Error('Failed to re-fetch notifications config')
      const fresh: NotificationsConfig = await freshRes.json()

      const merged: NotificationsConfig = {
        ...fresh,
        enabled: config.enabled,   // global master switch lives on this page
        rules: config.rules,       // rules are only edited on this page
        toggles: { ...(fresh.toggles || {}) },
      }
      const myToggles = config.toggles || {}
      for (const { key } of TOGGLE_FAMILY_META) {
        const mine = myToggles[key]
        if (!mine) continue
        merged.toggles![key] = mergeMeshtasticAndOtherFields(
          (fresh.toggles || {})[key],
          mine,
          key,
        )
      }

      const res = await fetch('/api/config/notifications', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(merged),
      })

      const result = await res.json()

      if (!res.ok) {
        throw new Error(result.detail || 'Save failed')
      }

      setConfig(merged)
      setOriginalConfig(JSON.parse(JSON.stringify(merged)))
      setHasChanges(false)
      setDirty(false)
      setSuccess('Meshtastic routing saved successfully')
      setTimeout(() => setSuccess(null), 3000)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Save failed')
    } finally {
      setSaving(false)
    }
  }

  const discardChanges = () => {
    if (originalConfig) {
      setConfig(JSON.parse(JSON.stringify(originalConfig)))
      setHasChanges(false)
    }
  }

  const createDefaultRule = (): NotificationRuleConfig => ({
    name: '',
    enabled: true,
    trigger_type: 'condition',
    categories: [],
    min_severity: 'routine',
    schedule_frequency: 'daily',
    schedule_time: '07:00',
    schedule_time_2: '19:00',
    schedule_days: ['monday'],
    message_type: 'mesh_health_summary',
    custom_message: '',
    delivery_type: '',
    broadcast_channel: 0,
    node_ids: [],
    smtp_host: '',
    smtp_port: 587,
    smtp_user: '',
    smtp_password: '',
    smtp_tls: true,
    from_address: '',
    recipients: [],
    webhook_url: '',
    webhook_headers: {},
    cooldown_minutes: 10,
    region_scope: [],
  })

  const addRule = () => {
    if (!config) return
    setConfig({ ...config, rules: [...(config.rules || []), createDefaultRule()] })
  }

  const addFromTemplate = (templateId: string) => {
    if (!config) return
    const template = RULE_TEMPLATES.find(t => t.id === templateId)
    if (!template) return
    // Merge over defaults so future NotificationRuleConfig fields (e.g., region_scope)
    // don't have to be back-filled into every literal template.
    setConfig({ ...config, rules: [...(config.rules || []), { ...createDefaultRule(), ...template.rule }] })
    setShowTemplates(false)
  }

  const duplicateRule = (index: number) => {
    if (!config) return
    const original = config.rules[index]
    const duplicate = { ...JSON.parse(JSON.stringify(original)), name: `${original.name} (copy)` }
    const newRules = [...config.rules]
    newRules.splice(index + 1, 0, duplicate)
    setConfig({ ...config, rules: newRules })
  }

  const testRule = async (index: number) => {
    setTestDialog({ open: true, ruleIndex: index, loading: true, action: '' })
    try {
      const res = await fetch(`/api/notifications/rules/${index}/test`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'preview' })
      })
      const result = await res.json()
      setTestResult(result)
      setTestDialog(d => ({ ...d, loading: false }))
    } catch {
      setTestResult({ success: false, message: 'Failed to get preview' })
      setTestDialog(d => ({ ...d, loading: false }))
    }
  }

  const sendTestAction = async (action: string) => {
    const index = testDialog.ruleIndex
    setTestDialog(d => ({ ...d, loading: true, action }))
    try {
      const res = await fetch(`/api/notifications/rules/${index}/test`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action })
      })
      const result = await res.json()
      setTestResult(result)
      setTestDialog(d => ({ ...d, loading: false }))
    } catch {
      setTestResult({ success: false, message: `Failed to ${action}` })
      setTestDialog(d => ({ ...d, loading: false }))
    }
  }

  const closeTestDialog = () => {
    setTestDialog({ open: false, ruleIndex: -1, loading: false, action: '' })
    setTestResult(null)
  }

  if (loading) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="text-slate-400">Loading notifications config...</div>
      </div>
    )
  }

  if (!config) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="text-red-400">Failed to load notifications config</div>
      </div>
    )
  }

  return (
    <div className="max-w-4xl mx-auto space-y-6">
      {/* Test Dialog */}
      {testDialog.open && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
          <div className="bg-[#1a2332] border border-[#2a3a4a] shadow-xl max-w-2xl w-full mx-4 max-h-[85vh] overflow-auto">
            <div className="p-4 border-b border-[#2a3a4a] flex items-center justify-between sticky top-0 bg-[#1a2332]">
              <h3 className="text-lg font-semibold">Test Notification Rule</h3>
              <button onClick={closeTestDialog} className="text-slate-500 hover:text-slate-300">
                <X size={20} />
              </button>
            </div>
            <div className="p-4 space-y-4">
              {testDialog.loading ? (
                <div className="flex items-center justify-center py-8">
                  <RefreshCw size={20} className="animate-spin text-slate-400 mr-2" />
                  <div className="text-slate-400">
                    {testDialog.action ? `${testDialog.action.replace('_', ' ').replace('send ', 'Sending ')}...` : 'Loading current data...'}
                  </div>
                </div>
              ) : testResult ? (
                <>
                  {/* Section 1: Current Data */}
                  <div className="space-y-2">
                    <div className="text-sm font-medium text-slate-400 uppercase tracking-wide">Current Data</div>
                    {testResult.live_data_summary && testResult.live_data_summary.length > 0 ? (
                      <div className="p-3 bg-slate-800/50 rounded space-y-1">
                        {testResult.live_data_summary.map((line, i) => (
                          <div
                            key={i}
                            className={`text-sm font-mono ${line.startsWith('[!]') ? 'text-amber-400' : ''}`}
                          >
                            {line}
                          </div>
                        ))}
                      </div>
                    ) : (
                      <div className="p-3 bg-slate-800/50 rounded text-sm text-slate-500">
                        No live data available for this rule's categories
                      </div>
                    )}
                  </div>

                  {/* Section 2: Alert Conditions */}
                  <div className="space-y-2">
                    <div className="text-sm font-medium text-slate-400 uppercase tracking-wide">Rule Matching</div>
                    <div className="flex items-center gap-2 flex-wrap">
                      {testResult.conditions_matched && testResult.conditions_matched > 0 ? (
                        <span className="px-2 py-1 bg-green-500/20 text-green-400 rounded text-sm">
                          {testResult.conditions_matched} condition{testResult.conditions_matched !== 1 ? 's' : ''} match - this rule WOULD fire
                        </span>
                      ) : (
                        <span className="px-2 py-1 bg-slate-700 text-slate-400 rounded text-sm">
                          No conditions trigger this rule right now
                        </span>
                      )}
                      {testResult.conditions_below_threshold && testResult.conditions_below_threshold > 0 && (
                        <span className="px-2 py-1 bg-yellow-500/20 text-yellow-400 rounded text-sm">
                          {testResult.conditions_below_threshold} below threshold
                        </span>
                      )}
                    </div>

                    {/* Near-miss explanation */}
                    {testResult.conditions_below_threshold && testResult.conditions_below_threshold > 0 && (
                      <div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded text-sm space-y-2">
                        <div className="text-yellow-300">{testResult.below_threshold_summary}</div>
                        {testResult.below_threshold_events && testResult.below_threshold_events.length > 0 && (
                          <div className="space-y-1 text-yellow-200/80">
                            {testResult.below_threshold_events.slice(0, 3).map((evt, i) => (
                              <div key={i} className="flex items-center gap-2">
                                <span className="text-xs px-1.5 py-0.5 bg-yellow-500/20 rounded">{evt.severity}</span>
                                <span>{evt.headline}</span>
                              </div>
                            ))}
                          </div>
                        )}
                        {testResult.suggestion && (
                          <div className="text-yellow-400 text-xs mt-2">Tip: {testResult.suggestion}</div>
                        )}
                      </div>
                    )}
                  </div>

                  {/* Section 3: Preview Messages */}
                  <div className="space-y-2">
                    <div className="text-sm font-medium text-slate-400 uppercase tracking-wide">
                      {testResult.is_example ? 'Example Messages' : 'Messages That Would Fire'}
                    </div>
                    {testResult.preview_messages?.map((msg, i) => (
                      <div key={i} className="p-3 bg-slate-800 rounded text-sm font-mono break-words">
                        {msg}
                      </div>
                    ))}
                  </div>

                  {/* Delivery result */}
                  {testResult.delivered !== undefined && testResult.delivery_result && (
                    <div className={`p-3 rounded text-sm ${
                      testResult.delivered
                        ? 'bg-green-500/10 border border-green-500/30 text-green-400'
                        : 'bg-red-500/10 border border-red-500/30 text-red-400'
                    }`}>
                      <div className="flex items-start gap-2">
                        {testResult.delivered ? <Check size={16} className="mt-0.5" /> : <X size={16} className="mt-0.5" />}
                        <div>
                          <div>{testResult.delivery_result}</div>
                          {testResult.delivery_error && (
                            <div className="mt-1 text-red-300">{testResult.delivery_error}</div>
                          )}
                        </div>
                      </div>
                    </div>
                  )}

                  {/* Legacy format support */}
                  {testResult.message && !testResult.preview_messages && (
                    <div className={`p-3 rounded text-sm ${testResult.success ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}`}>
                      {testResult.message}
                    </div>
                  )}
                </>
              ) : null}
            </div>
            <div className="p-4 border-t border-[#2a3a4a] flex justify-between sticky bottom-0 bg-[#1a2332]">
              <button
                onClick={closeTestDialog}
                className="px-4 py-2 text-slate-400 hover:text-slate-200"
              >
                Close
              </button>
              {testResult && !testResult.delivered && (
                <div className="flex gap-2">
                  {!testResult.delivery_method ? (
                    <span className="px-3 py-2 text-amber-400 text-sm">
                      Configure a delivery method to send test messages
                    </span>
                  ) : (
                    <>
                      {testResult.live_data_summary && testResult.live_data_summary.length > 0 && (
                        <button
                          onClick={() => sendTestAction('send_status')}
                          disabled={testDialog.loading}
                          className="px-3 py-2 bg-slate-700 hover:bg-slate-600 rounded text-sm disabled:opacity-50"
                          title="Send current conditions summary"
                        >
                          Send Current Conditions
                        </button>
                      )}
                      <button
                        onClick={() => sendTestAction('send_test')}
                        disabled={testDialog.loading}
                        className="px-3 py-2 bg-slate-700 hover:bg-slate-600 rounded text-sm disabled:opacity-50"
                        title="Send example alert message"
                      >
                        Send Example Alert
                      </button>
                      {testResult.can_send_live && (
                        <button
                          onClick={() => sendTestAction('send_live')}
                          disabled={testDialog.loading}
                          className="px-3 py-2 bg-accent hover:bg-accent/80 rounded text-sm disabled:opacity-50"
                          title="Send actual live alert"
                        >
                          Send Live Alert
                        </button>
                      )}
                    </>
                  )}
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <p className="text-sm text-slate-500">
            Per-family Meshtastic delivery and other-channel delivery. Family gating (enable, severity threshold, freshness/cooldown) is on <a href="/environment" className="text-accent hover:underline">Data Feeds</a>. MeshCore delivery is on the <a href="/meshcore/routing" className="text-accent hover:underline">MeshCore Routing</a> page.
          </p>
        </div>
        <div className="flex items-center gap-2">
          <button
            onClick={fetchConfig}
            className="p-2 text-slate-400 hover:text-slate-200 hover:bg-bg-hover rounded transition-colors"
            title="Refresh"
          >
            <RefreshCw size={18} />
          </button>
          <button
            onClick={discardChanges}
            disabled={!hasChanges}
            className="flex items-center gap-2 px-3 py-2 text-slate-400 hover:text-slate-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
          >
            <RotateCcw size={16} />
            Discard
          </button>
          <button
            onClick={saveConfig}
            disabled={saving || !hasChanges}
            className="flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent/80 disabled:bg-slate-700 disabled:cursor-not-allowed rounded text-white transition-colors"
          >
            <Save size={16} />
            {saving ? 'Saving...' : 'Save'}
          </button>
        </div>
      </div>

      {/* Status messages */}
      {error && (
        <div className="p-3 text-sm bg-red-500/10 text-red-400 border border-red-500/20">
          {error}
        </div>
      )}
      {success && (
        <div className="p-3 text-sm bg-green-500/10 text-green-400 border border-green-500/20">
          <Check size={14} className="inline mr-2" />
          {success}
        </div>
      )}

      {/* Main content */}
      <div className="bg-bg-card border border-border p-6 space-y-6">
        <Toggle
          label="Enable Notifications"
          checked={config.enabled}
          onChange={(v) => setConfig({ ...config, enabled: v })}
          helper="Master switch for all notification delivery"
          info="When disabled, no alerts or scheduled messages will be delivered. Alerts still get recorded to history."
        />

        {/* Meshtastic delivery grids — always visible regardless of master switch */}
        {config.toggles && (
          <>
            <MeshtasticDeliveryGrid
              toggles={config.toggles}
              onChange={(t) => setConfig({ ...config, toggles: t })}
            />
            <OtherChannelsGrid
              toggles={config.toggles}
              onChange={(t) => setConfig({ ...config, toggles: t })}
            />
          </>
        )}

        {config.enabled && (
          <>
            {/* Rules Section */}
            <div className="space-y-3">
              <div className="flex items-center justify-between">
                <label className="flex items-center text-xs text-slate-500 uppercase tracking-wide">
                  Notification Rules
                  <InfoButton info="Each rule is self-contained: define what triggers it (condition or schedule), where to send it (mesh, email, webhook), and behavior settings." />
                </label>
                <span className="text-xs text-slate-500">
                  {config.rules?.length || 0} rule{(config.rules?.length || 0) !== 1 ? 's' : ''}
                </span>
              </div>

              {(config.rules || []).map((rule, i) => (
                <NotificationRuleCard
                  key={i}
                  rule={rule}
                  ruleIndex={i}
                  categories={categories}
                  regions={regions}                  onChange={(r) => {
                    const newRules = [...(config.rules || [])]
                    newRules[i] = r
                    setConfig({ ...config, rules: newRules })
                  }}
                  onDelete={() => {
                    if (confirm(`Delete rule "${rule.name || 'New Rule'}"?`)) {
                      setConfig({ ...config, rules: (config.rules || []).filter((_, j) => j !== i) })
                    }
                  }}
                  onDuplicate={() => duplicateRule(i)}
                  onTest={() => testRule(i)}
                />
              ))}

              <div className="flex gap-2">
                <button
                  onClick={addRule}
                  className="flex-1 py-3 border border-dashed border-[#1e2a3a] text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
                >
                  <Plus size={16} /> Add Rule
                </button>
                <div className="relative">
                  <button
                    onClick={() => setShowTemplates(!showTemplates)}
                    className="py-3 px-4 border border-dashed border-[#1e2a3a] text-slate-500 hover:text-slate-300 hover:border-accent flex items-center gap-2 transition-colors"
                  >
                    <Layers size={16} /> Add from Template
                  </button>
                  {showTemplates && (
                    <>
                      <div className="fixed inset-0 z-40" onClick={() => setShowTemplates(false)} />
                      <div className="absolute right-0 top-full mt-2 z-50 w-80 bg-[#1a2332] border border-[#2a3a4a] shadow-xl overflow-hidden">
                        <div className="p-2 border-b border-[#2a3a4a] text-xs text-slate-500 uppercase">Rule Templates</div>
                        {RULE_TEMPLATES.map((t) => (
                          <button
                            key={t.id}
                            onClick={() => addFromTemplate(t.id)}
                            className="w-full p-3 text-left hover:bg-[#2a3a4a] transition-colors"
                          >
                            <div className="font-medium text-slate-200">{t.name}</div>
                            <div className="text-xs text-slate-500 mt-0.5">{t.description}</div>
                          </button>
                        ))}
                      </div>
                    </>
                  )}
                </div>
              </div>
            </div>
          </>
        )}
      </div>

    </div>
  )
}

