import {
  ActionIcon,
  AppShell,
  Avatar,
  Badge,
  Box,
  Burger,
  Button,
  Card,
  Center,
  Container,
  Group,
  Loader,
  Menu,
  Modal,
  NavLink,
  Pagination,
  Paper,
  SegmentedControl,
  Select,
  SimpleGrid,
  Stack,
  Table,
  Text,
  ThemeIcon,
  Title,
  Tooltip,
} from '@mantine/core'
import { useDisclosure, useMediaQuery } from '@mantine/hooks'
import { modals } from '@mantine/modals'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
import {
  Background,
  Controls,
  type Edge,
  Handle,
  MarkerType,
  type Node,
  Position,
  ReactFlow,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import ELK from 'elkjs/lib/elk.bundled.js'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  TbActivity,
  TbApps,
  TbBug,
  TbChevronRight,
  TbCircleFilled,
  TbCode,
  TbDatabase,
  TbDots,
  TbFileText,
  TbLayoutDashboard,
  TbLayoutSidebarLeftCollapse,
  TbLayoutSidebarLeftExpand,
  TbLogout,
  TbRefresh,
  TbServer,
  TbSettings,
  TbSitemap,
  TbTrash,
  TbUser,
  TbUserSearch,
  TbUsers,
} from 'react-icons/tb'
import { type Role, useLogout, useSession } from '@/frontend/hooks/useAuth'
import { usePresence } from '@/frontend/hooks/usePresence'

const validTabs = ['overview', 'operators', 'bugs', 'app-logs', 'activity-logs', 'database', 'project', 'settings'] as const

export const Route = createFileRoute('/dev')({
  validateSearch: (search: Record<string, unknown>) => ({
    tab: validTabs.includes(search.tab as any) ? (search.tab as string) : 'overview',
  }),
  beforeLoad: async ({ context }) => {
    try {
      const data = await context.queryClient.ensureQueryData({
        queryKey: ['auth', 'session'],
        queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
      })
      if (!data?.user) throw redirect({ to: '/login' })
      if (data.user.role !== 'DEVELOPER') throw redirect({ to: '/dashboard' })
    } catch (e) {
      if (e instanceof Error) throw redirect({ to: '/login' })
      throw e
    }
  },
  component: DevPage,
})

interface AdminUser {
  id: string
  name: string
  email: string
  role: Role
  active: boolean
  image?: string | null
  createdAt: string
}

const navItems = [
  { label: 'Overview', icon: TbLayoutDashboard, key: 'overview' },
  { label: 'Operators', icon: TbUsers, key: 'operators' },
  { label: 'Bugs', icon: TbBug, key: 'bugs' },
  { label: 'App Logs', icon: TbServer, key: 'app-logs', disabled: true },
  // { label: 'Activity Logs', icon: TbActivity, key: 'activity-logs' },
  { label: 'Database', icon: TbDatabase, key: 'database' },
  { label: 'Project', icon: TbSitemap, key: 'project' },
  { label: 'Settings', icon: TbSettings, key: 'settings' },
]

function DevPage() {
  const { data } = useSession()
  const logout = useLogout()
  const user = data?.user
  const { tab: active } = Route.useSearch()
  const navigate = useNavigate()
  const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure(false)
  const isMobile = useMediaQuery('(max-width: 48em)')
  const setActive = (key: string) => {
    navigate({ to: '/dev', search: { tab: key } })
    closeMobile()
  }
  const [collapsed, setCollapsed] = useState(() => localStorage.getItem('dev:sidebar') === 'collapsed')
  const toggleSidebar = () => {
    setCollapsed((prev) => {
      const next = !prev
      localStorage.setItem('dev:sidebar', next ? 'collapsed' : 'open')
      return next
    })
  }
  const confirmLogout = () =>
    modals.openConfirmModal({
      title: 'Logout',
      children: <Text size="sm">Yakin ingin logout?</Text>,
      labels: { confirm: 'Logout', cancel: 'Batal' },
      confirmProps: { color: 'red' },
      onConfirm: () => logout.mutate(),
    })

  return (
    <AppShell
      header={{ height: 56, collapsed: !isMobile }}
      navbar={{ width: collapsed ? 60 : 260, breakpoint: 'sm', collapsed: { mobile: !mobileOpened } }}
      padding="md"
    >
      <AppShell.Header px="md" hiddenFrom="sm">
        <Group h="100%" justify="space-between">
          <Group gap="xs">
            <Burger opened={mobileOpened} onClick={toggleMobile} size="sm" />
            <ThemeIcon size="md" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }}>
              <TbCode size={16} />
            </ThemeIcon>
            <Text fw={700} size="sm">Dev Console</Text>
          </Group>
        </Group>
      </AppShell.Header>

      <AppShell.Navbar p={collapsed ? 'xs' : 'md'}>
        <AppShell.Section>
          <Group gap="xs" mb="md" justify={collapsed ? 'center' : 'space-between'}>
            {collapsed ? (
              <Tooltip label="Expand sidebar" position="right">
                <ActionIcon variant="subtle" color="gray" size="lg" onClick={toggleSidebar}>
                  <TbLayoutSidebarLeftExpand size={18} />
                </ActionIcon>
              </Tooltip>
            ) : (
              <>
                <Group gap="xs">
                  <ThemeIcon size="lg" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }}>
                    <TbCode size={18} />
                  </ThemeIcon>
                  <div>
                    <Text fw={700} size="sm">Dev Console</Text>
                    <Text size="xs" c="dimmed">Developer</Text>
                  </div>
                </Group>
                <Tooltip label="Minimize sidebar">
                  <ActionIcon variant="subtle" color="gray" size="sm" onClick={toggleSidebar}>
                    <TbLayoutSidebarLeftCollapse size={18} />
                  </ActionIcon>
                </Tooltip>
              </>
            )}
          </Group>
        </AppShell.Section>

        <AppShell.Section grow>
          <Stack gap={4}>
            {navItems.map((item) => {
              const Icon = item.icon
              if (collapsed) {
                return (
                  <Tooltip key={item.key} label={item.label} position="right">
                    <ActionIcon
                      variant={active === item.key ? 'filled' : 'subtle'}
                      color={active === item.key ? 'blue' : 'gray'}
                      size="lg"
                      disabled={item.disabled}
                      onClick={() => !item.disabled && setActive(item.key)}
                    >
                      <Icon size={18} />
                    </ActionIcon>
                  </Tooltip>
                )
              }
              return (
                <NavLink
                  key={item.key}
                  label={item.label}
                  leftSection={<Icon size={16} />}
                  rightSection={active === item.key ? <TbChevronRight size={14} /> : undefined}
                  active={active === item.key}
                  disabled={item.disabled}
                  onClick={() => !item.disabled && setActive(item.key)}
                  style={{ borderRadius: 6 }}
                />
              )
            })}
          </Stack>
        </AppShell.Section>

        <AppShell.Section>
          {!collapsed && user && (
            <Box mb="sm">
              <Group gap="sm">
                <Avatar size="sm" color="blue" radius="xl">
                  {user.name.charAt(0).toUpperCase()}
                </Avatar>
                <Box style={{ flex: 1, minWidth: 0 }}>
                  <Text size="sm" fw={500} truncate>{user.name}</Text>
                  <Text size="xs" c="dimmed" truncate>{user.email}</Text>
                </Box>
              </Group>
            </Box>
          )}
          <Group gap="xs" justify={collapsed ? 'center' : 'space-between'}>
            {!collapsed && (
              <Tooltip label="Logout">
                <ActionIcon variant="subtle" color="red" onClick={confirmLogout} loading={logout.isPending}>
                  <TbLogout size={18} />
                </ActionIcon>
              </Tooltip>
            )}
            {collapsed && (
              <Tooltip label="Logout" position="right">
                <ActionIcon variant="subtle" color="red" onClick={confirmLogout} loading={logout.isPending}>
                  <TbLogout size={18} />
                </ActionIcon>
              </Tooltip>
            )}
          </Group>
        </AppShell.Section>
      </AppShell.Navbar>

      <AppShell.Main>
        <Container size="xl" px={0}>
          {active === 'overview' && <OverviewPanel />}
          {active === 'operators' && <OperatorsPanel currentUserId={user?.id ?? ''} />}
          {active === 'bugs' && <BugsPanel />}
          {active === 'app-logs' && <AppLogsPanel />}
          {active === 'activity-logs' && <ActivityLogsPanel />}
          {active === 'database' && <DatabasePanel />}
          {active === 'project' && <ProjectPanel />}
          {active === 'settings' && <SettingsPanel />}
        </Container>
      </AppShell.Main>
    </AppShell>
  )
}

// ─── Overview Panel ────────────────────────────────────────────────────────────

function OverviewPanel() {
  const { onlineUserIds } = usePresence()
  const { data } = useQuery({
    queryKey: ['admin', 'stats'],
    queryFn: () => fetch('/api/admin/stats', { credentials: 'include' }).then((r) => r.json()),
    refetchInterval: 10_000,
  })
  const stats = data ?? {}

  const cards = [
    { label: 'Total Apps', value: stats.totalApps ?? '—', icon: TbApps, color: 'blue' },
    { label: 'Open Bugs', value: stats.openBugs ?? '—', icon: TbBug, color: 'red' },
    { label: 'Total Operators', value: stats.totalOperators ?? '—', icon: TbUsers, color: 'green' },
    { label: 'Online Now', value: onlineUserIds.length, icon: TbCircleFilled, color: 'teal' },
  ]

  return (
    <Stack>
      <Title order={3}>Overview</Title>
      <SimpleGrid cols={{ base: 1, sm: 2, md: 4 }}>
        {cards.map((c) => {
          const Icon = c.icon
          return (
            <Card key={c.label} withBorder shadow="sm" radius="md" p="lg">
              <Group justify="space-between" mb="xs">
                <Text size="sm" c="dimmed" fw={500}>{c.label}</Text>
                <ThemeIcon color={c.color} variant="light" size="md">
                  <Icon size={16} />
                </ThemeIcon>
              </Group>
              <Text fw={700} size="xl">{String(c.value)}</Text>
            </Card>
          )
        })}
      </SimpleGrid>
    </Stack>
  )
}

// ─── Operators Panel ───────────────────────────────────────────────────────────

function OperatorsPanel({ currentUserId }: { currentUserId: string }) {
  const { onlineUserIds } = usePresence()
  const qc = useQueryClient()
  const { data, isLoading } = useQuery({
    queryKey: ['admin', 'users'],
    queryFn: () => fetch('/api/admin/users', { credentials: 'include' }).then((r) => r.json()),
  })
  const users: AdminUser[] = data?.users ?? []

  const roleMutation = useMutation({
    mutationFn: ({ id, role }: { id: string; role: string }) =>
      fetch(`/api/admin/users/${id}/role`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }) }).then((r) => r.json()),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'users'] }),
  })
  const activateMutation = useMutation({
    mutationFn: ({ id, active }: { id: string; active: boolean }) =>
      fetch(`/api/admin/users/${id}/activate`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ active }) }).then((r) => r.json()),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'users'] }),
  })

  const roleColor: Record<string, string> = { DEVELOPER: 'violet', ADMIN: 'blue', USER: 'gray' }

  return (
    <Stack>
      <Group justify="space-between">
        <Title order={3}>Operators</Title>
        <Text size="sm" c="dimmed">{users.length} total</Text>
      </Group>
      {isLoading ? <Center><Loader /></Center> : (
        <Table.ScrollContainer minWidth={600}>
          <Table striped highlightOnHover>
            <Table.Thead>
              <Table.Tr>
                <Table.Th>Operator</Table.Th>
                <Table.Th>Role</Table.Th>
                <Table.Th>Status</Table.Th>
                <Table.Th>Joined</Table.Th>
                <Table.Th />
              </Table.Tr>
            </Table.Thead>
            <Table.Tbody>
              {users.map((u) => {
                const isOnline = onlineUserIds.includes(u.id)
                const isSelf = u.id === currentUserId
                const isDeveloper = u.role === 'DEVELOPER'
                return (
                  <Table.Tr key={u.id}>
                    <Table.Td>
                      <Group gap="sm">
                        <Box pos="relative">
                          <Avatar size="sm" color="blue" radius="xl">{u.name.charAt(0).toUpperCase()}</Avatar>
                          {isOnline && (
                            <Box pos="absolute" bottom={0} right={0} w={8} h={8} bg="green" style={{ borderRadius: '50%', border: '1.5px solid white' }} />
                          )}
                        </Box>
                        <div>
                          <Text size="sm" fw={500}>{u.name} {isSelf && <Text span size="xs" c="dimmed">(you)</Text>}</Text>
                          <Text size="xs" c="dimmed">{u.email}</Text>
                        </div>
                      </Group>
                    </Table.Td>
                    <Table.Td><Badge color={roleColor[u.role] ?? 'gray'} variant="light">{u.role}</Badge></Table.Td>
                    <Table.Td>
                      {!u.active ? <Badge color="red" variant="light">Inactive</Badge>
                        : isOnline ? <Badge color="green" variant="light">Online</Badge>
                        : <Badge color="gray" variant="light">Offline</Badge>}
                    </Table.Td>
                    <Table.Td><Text size="xs" c="dimmed">{new Date(u.createdAt).toLocaleDateString('id-ID')}</Text></Table.Td>
                    <Table.Td>
                      {!isSelf && !isDeveloper && (
                        <Menu shadow="md" withinPortal>
                          <Menu.Target>
                            <ActionIcon variant="subtle" color="gray"><TbDots size={16} /></ActionIcon>
                          </Menu.Target>
                          <Menu.Dropdown>
                            <Menu.Label>Ganti Role</Menu.Label>
                            {(['USER', 'ADMIN'] as const).filter((r) => r !== u.role).map((r) => (
                              <Menu.Item key={r} leftSection={<TbUser size={14} />} onClick={() => roleMutation.mutate({ id: u.id, role: r })}>
                                Jadikan {r}
                              </Menu.Item>
                            ))}
                            <Menu.Divider />
                            {u.active ? (
                              <Menu.Item color="red" leftSection={<TbUserSearch size={14} />}
                                onClick={() => modals.openConfirmModal({
                                  title: 'Nonaktifkan Operator',
                                  children: <Text size="sm">Nonaktifkan {u.name}? Semua sesi aktif akan dihapus.</Text>,
                                  labels: { confirm: 'Nonaktifkan', cancel: 'Batal' },
                                  confirmProps: { color: 'red' },
                                  onConfirm: () => activateMutation.mutate({ id: u.id, active: false }),
                                })}>
                                Nonaktifkan
                              </Menu.Item>
                            ) : (
                              <Menu.Item color="green" leftSection={<TbUserSearch size={14} />}
                                onClick={() => activateMutation.mutate({ id: u.id, active: true })}>
                                Aktifkan
                              </Menu.Item>
                            )}
                          </Menu.Dropdown>
                        </Menu>
                      )}
                    </Table.Td>
                  </Table.Tr>
                )
              })}
            </Table.Tbody>
          </Table>
        </Table.ScrollContainer>
      )}
    </Stack>
  )
}

// ─── Bugs Panel ────────────────────────────────────────────────────────────────

const BUG_STATUSES = ['all', 'OPEN', 'ON_HOLD', 'IN_PROGRESS', 'RESOLVED', 'RELEASED', 'CLOSED'] as const
const BUG_STATUS_COLOR: Record<string, string> = {
  OPEN: 'red', ON_HOLD: 'orange', IN_PROGRESS: 'blue', RESOLVED: 'teal', RELEASED: 'green', CLOSED: 'gray',
}
const BUG_SOURCE_COLOR: Record<string, string> = { QC: 'violet', SYSTEM: 'blue', USER: 'gray' }

function BugsPanel() {
  const [status, setStatus] = useState('all')
  const [page, setPage] = useState(1)
  const PER_PAGE = 25

  const { data, isLoading } = useQuery({
    queryKey: ['admin', 'bugs', status, page],
    queryFn: () => {
      const params = new URLSearchParams({ page: String(page), limit: String(PER_PAGE) })
      if (status !== 'all') params.set('status', status)
      return fetch(`/api/bugs?${params}`, { credentials: 'include' }).then((r) => r.json())
    },
    refetchInterval: 15_000,
  })

  const bugs = data?.data ?? []
  const totalPages = data?.totalPages ?? 1
  const totalItems = data?.totalItems ?? 0

  return (
    <Stack>
      <Group justify="space-between">
        <Title order={3}>Bugs</Title>
        <Text size="sm" c="dimmed">{totalItems} total</Text>
      </Group>
      <SegmentedControl
        value={status}
        onChange={(v) => { setStatus(v); setPage(1) }}
        data={BUG_STATUSES.map((s) => ({ label: s === 'all' ? 'All' : s.replace('_', ' '), value: s }))}
        size="xs"
      />
      {isLoading ? <Center><Loader /></Center> : (
        <>
          <Table.ScrollContainer minWidth={700}>
            <Table striped highlightOnHover>
              <Table.Thead>
                <Table.Tr>
                  <Table.Th>Status</Table.Th>
                  <Table.Th>Source</Table.Th>
                  <Table.Th>App</Table.Th>
                  <Table.Th>Description</Table.Th>
                  <Table.Th>Version</Table.Th>
                  <Table.Th>Reporter</Table.Th>
                  <Table.Th>Date</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                {bugs.map((b: any) => (
                  <Table.Tr key={b.id}>
                    <Table.Td><Badge color={BUG_STATUS_COLOR[b.status] ?? 'gray'} variant="light" size="sm">{b.status.replace('_', ' ')}</Badge></Table.Td>
                    <Table.Td><Badge color={BUG_SOURCE_COLOR[b.source] ?? 'gray'} variant="dot" size="sm">{b.source}</Badge></Table.Td>
                    <Table.Td><Text size="xs" c="dimmed">{b.app?.name ?? b.appId ?? '—'}</Text></Table.Td>
                    <Table.Td><Text size="sm" lineClamp={1} style={{ maxWidth: 240 }}>{b.description}</Text></Table.Td>
                    <Table.Td><Text size="xs" c="dimmed">{b.affectedVersion}</Text></Table.Td>
                    <Table.Td><Text size="xs">{b.user?.name ?? '—'}</Text></Table.Td>
                    <Table.Td><Text size="xs" c="dimmed">{new Date(b.createdAt).toLocaleDateString('id-ID')}</Text></Table.Td>
                  </Table.Tr>
                ))}
                {bugs.length === 0 && (
                  <Table.Tr>
                    <Table.Td colSpan={7}><Center py="xl"><Text c="dimmed">Tidak ada bug ditemukan</Text></Center></Table.Td>
                  </Table.Tr>
                )}
              </Table.Tbody>
            </Table>
          </Table.ScrollContainer>
          {totalPages > 1 && <Center><Pagination total={totalPages} value={page} onChange={setPage} size="sm" /></Center>}
        </>
      )}
    </Stack>
  )
}

// ─── App Logs Panel ────────────────────────────────────────────────────────────

const LOG_LEVELS = ['all', 'info', 'warn', 'error'] as const
const LOG_LEVEL_COLOR: Record<string, string> = { info: 'blue', warn: 'orange', error: 'red' }

function AppLogsPanel() {
  const [level, setLevel] = useState('all')
  const [page, setPage] = useState(1)
  const PER_PAGE = 25
  const qc = useQueryClient()

  const { data, isLoading } = useQuery({
    queryKey: ['admin', 'logs', 'app', level],
    queryFn: () => {
      const params = new URLSearchParams({ limit: '200' })
      if (level !== 'all') params.set('level', level)
      return fetch(`/api/admin/logs/app?${params}`, { credentials: 'include' }).then((r) => r.json())
    },
    refetchInterval: 5_000,
  })

  const allLogs = data?.logs ?? []
  const redisDisabled = data?.redisDisabled

  const pageLogs = useMemo(() => {
    const start = (page - 1) * PER_PAGE
    return allLogs.slice(start, start + PER_PAGE)
  }, [allLogs, page])

  const totalPages = Math.ceil(allLogs.length / PER_PAGE)

  const clearMutation = useMutation({
    mutationFn: () => fetch('/api/admin/logs/app', { method: 'DELETE', credentials: 'include' }).then((r) => r.json()),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'logs', 'app'] }),
  })

  return (
    <Stack>
      <Group justify="space-between">
        <Title order={3}>App Logs</Title>
        <Group gap="xs">
          <ActionIcon variant="subtle" color="gray" onClick={() => qc.invalidateQueries({ queryKey: ['admin', 'logs', 'app'] })}>
            <TbRefresh size={16} />
          </ActionIcon>
          <ActionIcon variant="subtle" color="red"
            onClick={() => modals.openConfirmModal({
              title: 'Hapus semua app logs',
              children: <Text size="sm">Semua log Redis akan dihapus. Tindakan ini tidak bisa dibatalkan.</Text>,
              labels: { confirm: 'Hapus', cancel: 'Batal' },
              confirmProps: { color: 'red' },
              onConfirm: () => clearMutation.mutate(),
            })}>
            <TbTrash size={16} />
          </ActionIcon>
        </Group>
      </Group>

      {redisDisabled && (
        <Paper withBorder p="md" bg="yellow.0">
          <Text size="sm" c="orange">Redis tidak dikonfigurasi (REDIS_URL kosong). App Logs tidak tersedia.</Text>
        </Paper>
      )}

      <SegmentedControl
        value={level}
        onChange={(v) => { setLevel(v); setPage(1) }}
        data={LOG_LEVELS.map((l) => ({ label: l === 'all' ? 'All' : l.toUpperCase(), value: l }))}
        size="xs"
      />

      {isLoading ? <Center><Loader /></Center> : (
        <>
          <Table.ScrollContainer minWidth={600}>
            <Table striped highlightOnHover fz="xs">
              <Table.Thead>
                <Table.Tr>
                  <Table.Th>Time</Table.Th>
                  <Table.Th>Level</Table.Th>
                  <Table.Th>Message</Table.Th>
                  <Table.Th>Detail</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                {pageLogs.map((log: any) => (
                  <Table.Tr key={log.id}>
                    <Table.Td style={{ whiteSpace: 'nowrap' }}>{new Date(log.timestamp).toLocaleTimeString('id-ID')}</Table.Td>
                    <Table.Td><Badge color={LOG_LEVEL_COLOR[log.level] ?? 'gray'} variant="light" size="xs">{log.level.toUpperCase()}</Badge></Table.Td>
                    <Table.Td>{log.message}</Table.Td>
                    <Table.Td c="dimmed">{log.detail ?? ''}</Table.Td>
                  </Table.Tr>
                ))}
                {pageLogs.length === 0 && !redisDisabled && (
                  <Table.Tr><Table.Td colSpan={4}><Center py="xl"><Text c="dimmed">Belum ada log</Text></Center></Table.Td></Table.Tr>
                )}
              </Table.Tbody>
            </Table>
          </Table.ScrollContainer>
          {totalPages > 1 && <Center><Pagination total={totalPages} value={page} onChange={setPage} size="sm" /></Center>}
        </>
      )}
    </Stack>
  )
}

// ─── Activity Logs Panel ───────────────────────────────────────────────────────

const LOG_TYPES = ['all', 'LOGIN', 'LOGOUT', 'CREATE', 'UPDATE', 'DELETE'] as const
const LOG_TYPE_COLOR: Record<string, string> = { LOGIN: 'green', LOGOUT: 'gray', CREATE: 'blue', UPDATE: 'yellow', DELETE: 'red' }

function ActivityLogsPanel() {
  const [type, setType] = useState('all')
  const [userId, setUserId] = useState('all')
  const [page, setPage] = useState(1)
  const PER_PAGE = 25
  const qc = useQueryClient()

  const { data: usersData } = useQuery({
    queryKey: ['admin', 'users'],
    queryFn: () => fetch('/api/admin/users', { credentials: 'include' }).then((r) => r.json()),
  })
  const users: AdminUser[] = usersData?.users ?? []

  const { data, isLoading } = useQuery({
    queryKey: ['admin', 'logs', 'audit', type, userId],
    queryFn: () => {
      const params = new URLSearchParams({ limit: '200' })
      if (type !== 'all') params.set('type', type)
      if (userId !== 'all') params.set('userId', userId)
      return fetch(`/api/admin/logs/audit?${params}`, { credentials: 'include' }).then((r) => r.json())
    },
    refetchInterval: 10_000,
  })

  const allLogs = data?.logs ?? []
  const pageLogs = useMemo(() => {
    const start = (page - 1) * PER_PAGE
    return allLogs.slice(start, start + PER_PAGE)
  }, [allLogs, page])
  const totalPages = Math.ceil(allLogs.length / PER_PAGE)

  const clearMutation = useMutation({
    mutationFn: () => fetch('/api/admin/logs/audit', { method: 'DELETE', credentials: 'include' }).then((r) => r.json()),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'logs', 'audit'] }),
  })

  return (
    <Stack>
      <Group justify="space-between">
        <Title order={3}>Activity Logs</Title>
        <Group gap="xs">
          <ActionIcon variant="subtle" color="gray" onClick={() => qc.invalidateQueries({ queryKey: ['admin', 'logs', 'audit'] })}>
            <TbRefresh size={16} />
          </ActionIcon>
          <ActionIcon variant="subtle" color="red"
            onClick={() => modals.openConfirmModal({
              title: 'Hapus semua activity logs',
              children: <Text size="sm">Semua log aktivitas akan dihapus permanen.</Text>,
              labels: { confirm: 'Hapus', cancel: 'Batal' },
              confirmProps: { color: 'red' },
              onConfirm: () => clearMutation.mutate(),
            })}>
            <TbTrash size={16} />
          </ActionIcon>
        </Group>
      </Group>
      <Group gap="sm">
        <Select
          placeholder="Filter operator"
          value={userId}
          onChange={(v) => { setUserId(v ?? 'all'); setPage(1) }}
          data={[{ value: 'all', label: 'Semua operator' }, ...users.map((u) => ({ value: u.id, label: u.name }))]}
          size="xs"
          w={180}
          clearable
        />
        <SegmentedControl
          value={type}
          onChange={(v) => { setType(v); setPage(1) }}
          data={LOG_TYPES.map((t) => ({ label: t === 'all' ? 'All' : t, value: t }))}
          size="xs"
        />
      </Group>
      {isLoading ? <Center><Loader /></Center> : (
        <>
          <Table.ScrollContainer minWidth={600}>
            <Table striped highlightOnHover fz="xs">
              <Table.Thead>
                <Table.Tr>
                  <Table.Th>Time</Table.Th>
                  <Table.Th>Operator</Table.Th>
                  <Table.Th>Type</Table.Th>
                  <Table.Th>Message</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                {pageLogs.map((log: any) => (
                  <Table.Tr key={log.id}>
                    <Table.Td style={{ whiteSpace: 'nowrap' }}>{new Date(log.createdAt).toLocaleString('id-ID')}</Table.Td>
                    <Table.Td>
                      {log.user ? (
                        <div>
                          <Text size="xs" fw={500}>{log.user.name}</Text>
                          <Text size="xs" c="dimmed">{log.user.email}</Text>
                        </div>
                      ) : <Text size="xs" c="dimmed">—</Text>}
                    </Table.Td>
                    <Table.Td><Badge color={LOG_TYPE_COLOR[log.type] ?? 'gray'} variant="light" size="xs">{log.type}</Badge></Table.Td>
                    <Table.Td><Text size="xs" lineClamp={1}>{log.message}</Text></Table.Td>
                  </Table.Tr>
                ))}
                {pageLogs.length === 0 && (
                  <Table.Tr><Table.Td colSpan={4}><Center py="xl"><Text c="dimmed">Belum ada log aktivitas</Text></Center></Table.Td></Table.Tr>
                )}
              </Table.Tbody>
            </Table>
          </Table.ScrollContainer>
          {totalPages > 1 && <Center><Pagination total={totalPages} value={page} onChange={setPage} size="sm" /></Center>}
        </>
      )}
    </Stack>
  )
}

// ─── ELK Layout Utility ────────────────────────────────────────────────────────

const elk = new ELK()

async function applyElkLayout(
  nodes: Node[],
  edges: Edge[],
  direction: 'DOWN' | 'RIGHT' | 'UP' | 'LEFT' = 'RIGHT',
): Promise<{ nodes: Node[]; edges: Edge[] }> {
  if (nodes.length === 0) return { nodes, edges }
  const elkNodes = nodes.map((n) => ({ id: n.id, width: (n.measured?.width ?? n.data?.width ?? 200) as number, height: (n.measured?.height ?? n.data?.height ?? 60) as number }))
  const elkEdges = edges.map((e) => ({ id: e.id, sources: [e.source], targets: [e.target] }))
  const graph = await elk.layout({
    id: 'root',
    layoutOptions: {
      'elk.algorithm': 'layered',
      'elk.direction': direction,
      'elk.spacing.nodeNode': '40',
      'elk.layered.spacing.nodeNodeBetweenLayers': '60',
    },
    children: elkNodes,
    edges: elkEdges,
  })
  const positioned = new Map(graph.children?.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]) ?? [])
  return {
    nodes: nodes.map((n) => ({ ...n, position: positioned.get(n.id) ?? n.position })),
    edges,
  }
}

function useFlowAutoSave(key: string) {
  const savePositions = useCallback((nodes: Node[]) => {
    const pos: Record<string, { x: number; y: number }> = {}
    for (const n of nodes) pos[n.id] = n.position
    localStorage.setItem(`dev:flow:${key}:positions`, JSON.stringify(pos))
  }, [key])
  const saveViewport = useCallback((vp: { x: number; y: number; zoom: number }) => {
    localStorage.setItem(`dev:flow:${key}:viewport`, JSON.stringify(vp))
  }, [key])
  const loadPositions = useCallback((): Record<string, { x: number; y: number }> => {
    try { return JSON.parse(localStorage.getItem(`dev:flow:${key}:positions`) ?? '{}') } catch { return {} }
  }, [key])
  const loadViewport = useCallback(() => {
    try { return JSON.parse(localStorage.getItem(`dev:flow:${key}:viewport`) ?? 'null') } catch { return null }
  }, [key])
  return { savePositions, saveViewport, loadPositions, loadViewport }
}

// ─── Database Panel ────────────────────────────────────────────────────────────

interface SchemaField { name: string; type: string; isId: boolean; isUnique: boolean; isOptional: boolean; isList: boolean; isRelation: boolean; default?: string }
interface SchemaRelation { from: string; fromField: string; to: string; toField: string; onDelete?: string }
interface SchemaModel { name: string; tableName: string; fields: SchemaField[] }
interface SchemaEnum { name: string; values: string[] }
interface ParsedSchema { models: SchemaModel[]; enums: SchemaEnum[]; relations: SchemaRelation[] }

function ModelNode({ data }: { data: any }) {
  return (
    <Paper withBorder shadow="sm" p={0} style={{ minWidth: 180, fontSize: 12 }}>
      <Handle type="target" position={Position.Left} />
      <Box bg="blue.6" px="xs" py={4} style={{ borderRadius: '4px 4px 0 0' }}>
        <Text size="xs" fw={700} c="white">{data.name}</Text>
        {data.tableName !== data.name && <Text size="xs" c="blue.1">@map({data.tableName})</Text>}
      </Box>
      <Stack gap={0}>
        {data.fields?.map((f: SchemaField) => (
          <Box key={f.name} px="xs" py={2} style={{ borderBottom: '1px solid var(--mantine-color-default-border)', display: 'flex', justifyContent: 'space-between', gap: 8 }}>
            <Group gap={4}>
              {f.isId && <Badge size="xs" color="yellow" variant="filled" p={2}>PK</Badge>}
              {f.isUnique && !f.isId && <Badge size="xs" color="teal" variant="filled" p={2}>UQ</Badge>}
              <Text size="xs" fw={f.isId ? 700 : 400} c={f.isRelation ? 'blue' : undefined}>{f.name}{f.isOptional ? '?' : ''}</Text>
            </Group>
            <Text size="xs" c="dimmed">{f.type}</Text>
          </Box>
        ))}
      </Stack>
      <Handle type="source" position={Position.Right} />
    </Paper>
  )
}

function EnumNode({ data }: { data: any }) {
  return (
    <Paper withBorder shadow="sm" p={0} style={{ minWidth: 140, fontSize: 12 }}>
      <Box bg="violet.6" px="xs" py={4} style={{ borderRadius: '4px 4px 0 0' }}>
        <Text size="xs" fw={700} c="white">enum {data.name}</Text>
      </Box>
      <Stack gap={0}>
        {data.values?.map((v: string) => (
          <Box key={v} px="xs" py={2} style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
            <Text size="xs">{v}</Text>
          </Box>
        ))}
      </Stack>
    </Paper>
  )
}

const DB_NODE_TYPES = { model: ModelNode, enum: EnumNode }

function DatabaseFlowInner({ schema }: { schema: ParsedSchema }) {
  const { fitView, setViewport } = useReactFlow()
  const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
  const { savePositions, saveViewport, loadPositions, loadViewport } = useFlowAutoSave('db')
  const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

  useEffect(() => {
    const savedPos = loadPositions()
    const hasSaved = Object.keys(savedPos).length > 0

    const newNodes: Node[] = [
      ...schema.models.map((m, i) => ({
        id: m.name, type: 'model',
        position: savedPos[m.name] ?? { x: i * 220, y: 0 },
        data: { name: m.name, tableName: m.tableName, fields: m.fields },
      })),
      ...schema.enums.map((e, i) => ({
        id: `enum_${e.name}`, type: 'enum',
        position: savedPos[`enum_${e.name}`] ?? { x: i * 160, y: 400 },
        data: { name: e.name, values: e.values },
      })),
    ]

    const newEdges: Edge[] = schema.relations.map((r) => ({
      id: `${r.from}-${r.fromField}-${r.to}`,
      source: r.from, target: r.to,
      label: `${r.fromField} → ${r.toField}${r.onDelete ? ` [${r.onDelete}]` : ''}`,
      markerEnd: { type: MarkerType.ArrowClosed },
      style: { stroke: 'var(--mantine-color-blue-5)' },
    }))

    if (hasSaved) {
      setNodes(newNodes)
      setEdges(newEdges)
      const savedVp = loadViewport()
      if (savedVp) setTimeout(() => setViewport(savedVp), 50)
      else setTimeout(() => fitView({ padding: 0.2 }), 50)
    } else {
      applyElkLayout(newNodes, newEdges, 'RIGHT').then(({ nodes: ln, edges: le }) => {
        setNodes(ln)
        setEdges(le)
        setTimeout(() => fitView({ padding: 0.2 }), 100)
      })
    }
  }, [schema])

  const onNodeDragStop = useCallback((_: any, __: any, allNodes: Node[]) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => savePositions(allNodes), 500)
  }, [savePositions])

  const onMoveEnd = useCallback((_: any, vp: any) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => saveViewport(vp), 500)
  }, [saveViewport])

  return (
    <ReactFlow
      nodes={nodes} edges={edges}
      onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
      onNodeDragStop={onNodeDragStop} onMoveEnd={onMoveEnd}
      nodeTypes={DB_NODE_TYPES}
      fitView minZoom={0.05} maxZoom={5}
    >
      <Background /><Controls />
    </ReactFlow>
  )
}

function DatabasePanel() {
  const { data, isLoading, refetch } = useQuery({
    queryKey: ['admin', 'schema'],
    queryFn: () => fetch('/api/admin/schema', { credentials: 'include' }).then((r) => r.json()),
  })
  const schema: ParsedSchema | null = data?.schema ?? null

  return (
    <Stack h="calc(100vh - 100px)">
      <Group justify="space-between">
        <Title order={3}>Database Schema</Title>
        <ActionIcon variant="subtle" color="gray" onClick={() => refetch()}><TbRefresh size={16} /></ActionIcon>
      </Group>
      {isLoading ? <Center flex={1}><Loader /></Center> : schema ? (
        <Box flex={1}>
          <ReactFlowProvider>
            <DatabaseFlowInner schema={schema} />
          </ReactFlowProvider>
        </Box>
      ) : <Text c="dimmed">Schema tidak tersedia</Text>}
    </Stack>
  )
}

// ─── Project Panel ─────────────────────────────────────────────────────────────

const PROJECT_VIEWS = [
  { group: 'Architecture', items: [
    { value: 'api-routes', label: 'API Routes' },
    { value: 'file-structure', label: 'File Structure' },
    { value: 'user-flow', label: 'User Flow' },
    { value: 'data-flow', label: 'Data Flow' },
  ]},
  { group: 'DevOps', items: [
    { value: 'env-map', label: 'Env Variables' },
    { value: 'test-coverage', label: 'Test Coverage' },
    { value: 'dependencies', label: 'Dependencies' },
    { value: 'migrations', label: 'Migrations' },
  ]},
  { group: 'Live', items: [
    { value: 'sessions', label: 'Sessions' },
    { value: 'live-requests', label: 'Live Requests' },
  ]},
]

function GenericFlowPanel({ queryKey, queryFn, buildGraph }: {
  queryKey: string[]
  queryFn: () => Promise<any>
  buildGraph: (data: any) => { nodes: Node[]; edges: Edge[] }
}) {
  const { data, isLoading, refetch } = useQuery({ queryKey, queryFn, refetchInterval: queryKey.includes('sessions') ? 10_000 : undefined })
  const { fitView, setViewport } = useReactFlow()
  const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
  const flowKey = queryKey.join('-')
  const { savePositions, saveViewport, loadPositions, loadViewport } = useFlowAutoSave(flowKey)
  const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

  useEffect(() => {
    if (!data) return
    const { nodes: newNodes, edges: newEdges } = buildGraph(data)
    const savedPos = loadPositions()
    const hasSaved = Object.keys(savedPos).length > 0

    if (hasSaved) {
      const withPos = newNodes.map((n) => ({ ...n, position: savedPos[n.id] ?? n.position }))
      setNodes(withPos); setEdges(newEdges)
      const savedVp = loadViewport()
      if (savedVp) setTimeout(() => setViewport(savedVp), 50)
      else setTimeout(() => fitView({ padding: 0.15 }), 50)
    } else {
      applyElkLayout(newNodes, newEdges, 'RIGHT').then(({ nodes: ln, edges: le }) => {
        setNodes(ln); setEdges(le)
        setTimeout(() => fitView({ padding: 0.15 }), 100)
      })
    }
  }, [data])

  const onNodeDragStop = useCallback((_: any, __: any, all: Node[]) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => savePositions(all), 500)
  }, [savePositions])
  const onMoveEnd = useCallback((_: any, vp: any) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => saveViewport(vp), 500)
  }, [saveViewport])

  if (isLoading) return <Center flex={1}><Loader /></Center>
  return (
    <Box style={{ position: 'absolute', inset: 0 }}>
      <Box pos="absolute" top={8} right={8} style={{ zIndex: 10 }}>
        <ActionIcon variant="subtle" style={{ background: 'white', boxShadow: '0 1px 3px rgba(0,0,0,.15)' }} onClick={() => { refetch() }}><TbRefresh size={14} /></ActionIcon>
      </Box>
      <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
        onNodeDragStop={onNodeDragStop} onMoveEnd={onMoveEnd} fitView minZoom={0.05} maxZoom={5}>
        <Background /><Controls />
      </ReactFlow>
    </Box>
  )
}

function SimpleNode({ data }: { data: any }) {
  const methodColor: Record<string, string> = { GET: 'green', POST: 'blue', PUT: 'orange', PATCH: 'yellow', DELETE: 'red', WS: 'violet', PAGE: 'gray' }
  const authColor: Record<string, string> = { public: 'gray', developer: 'violet', admin: 'blue', authenticated: 'teal', apiKeyOrSession: 'orange' }
  return (
    <Paper withBorder shadow="xs" px="sm" py="xs" style={{ minWidth: 200, maxWidth: 300, fontSize: 12 }}>
      <Handle type="target" position={Position.Left} />
      <Group gap={4} mb={2}>
        {data.method && <Badge size="xs" color={methodColor[data.method] ?? 'gray'} variant="filled">{data.method}</Badge>}
        {data.auth && <Badge size="xs" color={authColor[data.auth] ?? 'gray'} variant="light">{data.auth}</Badge>}
      </Group>
      <Text size="xs" fw={600} style={{ fontFamily: 'monospace' }}>{data.path ?? data.label}</Text>
      {data.description && <Text size="xs" c="dimmed" lineClamp={1}>{data.description}</Text>}
      <Handle type="source" position={Position.Right} />
    </Paper>
  )
}

const SIMPLE_NODE_TYPES = { simple: SimpleNode }

function buildApiRoutesGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const routes: any[] = data?.routes ?? []
  const nodes: Node[] = routes.map((r, i) => ({
    id: r.path, type: 'simple',
    position: { x: i * 30, y: i * 30 },
    data: { label: r.path, method: r.method, path: r.path, auth: r.auth, description: r.description },
  }))
  const loginNode = nodes.find((n) => n.id === '/login')
  const edges: Edge[] = []
  if (loginNode) {
    for (const n of nodes) {
      if (['/dev', '/dashboard', '/profile'].includes(n.id)) {
        edges.push({ id: `login-${n.id}`, source: '/login', target: n.id, label: '', markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: 'var(--mantine-color-blue-5)', strokeDasharray: '4' } })
      }
    }
  }
  return { nodes, edges }
}

function buildFileStructureGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const files: any[] = data?.files ?? []
  const catColor: Record<string, string> = { route: 'blue', hook: 'cyan', component: 'teal', frontend: 'green', lib: 'orange', backend: 'red', prisma: 'violet', 'test-unit': 'yellow', 'test-integration': 'yellow', test: 'yellow', config: 'gray' }
  const nodes: Node[] = files.map((f, i) => ({
    id: f.path, type: 'simple',
    position: { x: i * 30, y: i * 30 },
    data: { label: f.path.split('/').pop(), path: f.path, description: `${f.lines} lines • ${f.exports.length} exports`, auth: f.category },
    style: { '--badge-color': catColor[f.category] ?? 'gray' } as React.CSSProperties,
  }))
  const edges: Edge[] = []
  for (const f of files) {
    for (const imp of f.imports) {
      if (files.find((x) => x.path === imp.from)) {
        edges.push({ id: `${f.path}-${imp.from}`, source: f.path, target: imp.from, markerEnd: { type: MarkerType.ArrowClosed }, style: { stroke: 'var(--mantine-color-gray-5)', opacity: 0.5 } })
      }
    }
  }
  return { nodes, edges }
}

function buildEnvMapGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const vars: any[] = data?.variables ?? []
  const nodes: Node[] = [
    ...vars.map((v, i) => ({
      id: `env_${v.name}`, type: 'simple',
      position: { x: 0, y: i * 70 },
      data: { label: v.name, path: v.name, method: v.required ? 'REQ' : 'OPT', auth: v.isSet ? 'set' : 'unset', description: v.description },
    })),
    ...[...new Set(vars.flatMap((v) => v.usedBy))].map((f: string, i) => ({
      id: `file_${f}`, type: 'simple',
      position: { x: 400, y: i * 60 },
      data: { label: f.split('/').pop(), path: f, description: f },
    })),
  ]
  const edges: Edge[] = vars.flatMap((v) =>
    v.usedBy.map((f: string) => ({ id: `${v.name}-${f}`, source: `env_${v.name}`, target: `file_${f}`, markerEnd: { type: MarkerType.ArrowClosed } }))
  )
  return { nodes, edges }
}

function buildTestCoverageGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const src: any[] = data?.sourceFiles ?? []
  const tests: any[] = data?.testFiles ?? []
  const covColor: Record<string, string> = { covered: 'green', partial: 'yellow', uncovered: 'red' }
  const nodes: Node[] = [
    ...src.map((f, i) => ({
      id: f.path, type: 'simple',
      position: { x: 0, y: i * 60 },
      data: { label: f.path.split('/').pop(), path: f.path, auth: f.coverage, description: `${f.lines} lines • ${f.coverage}` },
    })),
    ...tests.map((t, i) => ({
      id: t.path, type: 'simple',
      position: { x: 400, y: i * 60 },
      data: { label: t.path.split('/').pop(), path: t.path, description: `${t.lines} lines • ${t.type}` },
    })),
  ]
  const edges: Edge[] = tests.flatMap((t) =>
    t.targets.map((target: string) => ({ id: `${t.path}-${target}`, source: target, target: t.path, markerEnd: { type: MarkerType.ArrowClosed } }))
  )
  return { nodes, edges }
}

function buildDependenciesGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const pkgs: any[] = data?.packages ?? []
  const catColor: Record<string, string> = { server: 'orange', ui: 'blue', database: 'green', storage: 'cyan', build: 'gray', other: 'gray' }
  const nodes: Node[] = [
    ...pkgs.map((p, i) => ({
      id: `pkg_${p.name}`, type: 'simple',
      position: { x: 0, y: i * 55 },
      data: { label: p.name, method: p.type === 'runtime' ? 'RT' : 'DEV', auth: p.category, description: p.version },
    })),
    ...[...new Set(pkgs.flatMap((p) => p.usedBy))].map((f: string, i) => ({
      id: `file_${f}`, type: 'simple',
      position: { x: 350, y: i * 60 },
      data: { label: f.split('/').pop(), path: f, description: f },
    })),
  ]
  const edges: Edge[] = pkgs.flatMap((p) =>
    p.usedBy.map((f: string) => ({ id: `${p.name}-${f}`, source: `pkg_${p.name}`, target: `file_${f}`, markerEnd: { type: MarkerType.ArrowClosed } }))
  )
  return { nodes, edges }
}

function buildMigrationsGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const migrations: any[] = data?.migrations ?? []
  const nodes: Node[] = migrations.map((m, i) => ({
    id: m.folder, type: 'simple',
    position: { x: i * 250, y: 100 },
    data: { label: m.name, description: `${m.changes.length} changes • ${new Date(m.createdAt).toLocaleDateString('id-ID')}` },
  }))
  const edges: Edge[] = migrations.slice(1).map((m, i) => ({
    id: `mig-${i}`, source: migrations[i].folder, target: m.folder, markerEnd: { type: MarkerType.ArrowClosed },
  }))
  return { nodes, edges }
}

function buildSessionsGraph(data: any): { nodes: Node[]; edges: Edge[] } {
  const sessions: any[] = data?.sessions ?? []
  const roleColor: Record<string, string> = { DEVELOPER: 'violet', ADMIN: 'blue', USER: 'gray' }
  const uniqueUsers = [...new Map(sessions.map((s) => [s.userId, s])).values()]
  const roles = [...new Set(sessions.map((s) => s.userRole))]
  const nodes: Node[] = [
    ...uniqueUsers.map((s, i) => ({
      id: `user_${s.userId}`, type: 'simple',
      position: { x: 0, y: i * 70 },
      data: { label: s.userName, method: s.isOnline ? 'ON' : 'OFF', auth: s.userRole.toLowerCase(), description: s.userEmail },
    })),
    ...roles.map((r, i) => ({
      id: `role_${r}`, type: 'simple',
      position: { x: 350, y: i * 100 },
      data: { label: r, description: `Access: ${r === 'DEVELOPER' ? '/dev, /dashboard' : r === 'ADMIN' ? '/dashboard' : '/profile'}` },
    })),
  ]
  const edges: Edge[] = uniqueUsers.map((s) => ({
    id: `${s.userId}-${s.userRole}`, source: `user_${s.userId}`, target: `role_${s.userRole}`, markerEnd: { type: MarkerType.ArrowClosed },
  }))
  return { nodes, edges }
}

// Static flow graphs
function buildUserFlowGraph(): { nodes: Node[]; edges: Edge[] } {
  const nodes: Node[] = [
    { id: 'visit', type: 'simple', position: { x: 0, y: 0 }, data: { label: 'Visit App', description: 'User opens browser' } },
    { id: 'login', type: 'simple', position: { x: 200, y: 0 }, data: { label: '/login', description: 'Authentication page' } },
    { id: 'auth-check', type: 'simple', position: { x: 400, y: 0 }, data: { label: 'Auth Check', description: 'Validate session' } },
    { id: 'dev', type: 'simple', position: { x: 600, y: -100 }, data: { label: '/dev', auth: 'developer', description: 'Dev Console (DEVELOPER)' } },
    { id: 'dashboard', type: 'simple', position: { x: 600, y: 0 }, data: { label: '/dashboard', auth: 'admin', description: 'Admin Dashboard (ADMIN+)' } },
    { id: 'profile', type: 'simple', position: { x: 600, y: 100 }, data: { label: '/profile', auth: 'authenticated', description: 'User Profile (all)' } },
  ]
  const edges: Edge[] = [
    { id: 'v-l', source: 'visit', target: 'login', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'l-a', source: 'login', target: 'auth-check', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'a-d', source: 'auth-check', target: 'dev', label: 'DEVELOPER', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'a-da', source: 'auth-check', target: 'dashboard', label: 'ADMIN', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'a-p', source: 'auth-check', target: 'profile', label: 'USER', markerEnd: { type: MarkerType.ArrowClosed } },
  ]
  return { nodes, edges }
}

function buildDataFlowGraph(): { nodes: Node[]; edges: Edge[] } {
  const nodes: Node[] = [
    { id: 'client', type: 'simple', position: { x: 0, y: 0 }, data: { label: 'Client (React)', description: 'TanStack Router + Query' } },
    { id: 'elysia', type: 'simple', position: { x: 200, y: 0 }, data: { label: 'Elysia.js', description: 'HTTP framework (Bun)' } },
    { id: 'auth-mw', type: 'simple', position: { x: 400, y: -80 }, data: { label: 'Auth Middleware', description: 'Session cookie / API Key' } },
    { id: 'handler', type: 'simple', position: { x: 400, y: 80 }, data: { label: 'Route Handler', description: 'Business logic' } },
    { id: 'prisma', type: 'simple', position: { x: 600, y: 0 }, data: { label: 'Prisma ORM', description: 'PostgreSQL queries' } },
    { id: 'minio', type: 'simple', position: { x: 600, y: 120 }, data: { label: 'MinIO', description: 'Object storage (images)' } },
    { id: 'redis', type: 'simple', position: { x: 600, y: -120 }, data: { label: 'Redis', description: 'App log ring buffer' } },
    { id: 'response', type: 'simple', position: { x: 800, y: 0 }, data: { label: 'JSON Response', description: 'Back to client' } },
  ]
  const edges: Edge[] = [
    { id: 'c-e', source: 'client', target: 'elysia', label: 'HTTP / WS', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'e-a', source: 'elysia', target: 'auth-mw', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'e-h', source: 'elysia', target: 'handler', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'h-p', source: 'handler', target: 'prisma', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'h-m', source: 'handler', target: 'minio', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'h-r', source: 'handler', target: 'redis', markerEnd: { type: MarkerType.ArrowClosed } },
    { id: 'p-res', source: 'prisma', target: 'response', markerEnd: { type: MarkerType.ArrowClosed } },
  ]
  return { nodes, edges }
}

// Live Requests sub-view
function LiveRequestsPanel() {
  const [requests, setRequests] = useState<any[]>([])
  const [paused, setPaused] = useState(false)
  const wsRef = useRef<WebSocket | null>(null)

  useEffect(() => {
    const proto = location.protocol === 'https:' ? 'wss' : 'ws'
    const ws = new WebSocket(`${proto}://${location.host}/ws/presence`)
    wsRef.current = ws
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data)
      if (msg.type === 'request' && !paused) {
        setRequests((prev) => [msg, ...prev].slice(0, 100))
      }
    }
    return () => { ws.close() }
  }, [paused])

  const statusColor = (s: number) => s >= 500 ? 'red' : s >= 400 ? 'orange' : s >= 300 ? 'yellow' : 'green'

  return (
    <Stack>
      <Group justify="space-between">
        <Text fw={600}>Live Requests</Text>
        <Group gap="xs">
          <Button size="xs" variant={paused ? 'filled' : 'light'} color={paused ? 'green' : 'orange'} onClick={() => setPaused((p) => !p)}>
            {paused ? 'Resume' : 'Pause'}
          </Button>
          <Button size="xs" variant="subtle" color="red" onClick={() => setRequests([])}>Clear</Button>
        </Group>
      </Group>
      <Table.ScrollContainer minWidth={500}>
        <Table fz="xs" striped>
          <Table.Thead>
            <Table.Tr>
              <Table.Th>Time</Table.Th>
              <Table.Th>Method</Table.Th>
              <Table.Th>Path</Table.Th>
              <Table.Th>Status</Table.Th>
              <Table.Th>Duration</Table.Th>
            </Table.Tr>
          </Table.Thead>
          <Table.Tbody>
            {requests.map((r, i) => (
              <Table.Tr key={i}>
                <Table.Td style={{ whiteSpace: 'nowrap' }}>{new Date(r.timestamp).toLocaleTimeString('id-ID')}</Table.Td>
                <Table.Td><Badge size="xs" color={({ GET: 'green', POST: 'blue', PUT: 'orange', PATCH: 'yellow', DELETE: 'red' } as Record<string, string>)[r.method] ?? 'gray'} variant="filled">{r.method}</Badge></Table.Td>
                <Table.Td style={{ fontFamily: 'monospace' }}>{r.path}</Table.Td>
                <Table.Td><Badge size="xs" color={statusColor(r.status)} variant="light">{r.status}</Badge></Table.Td>
                <Table.Td c="dimmed">{r.duration}ms</Table.Td>
              </Table.Tr>
            ))}
            {requests.length === 0 && (
              <Table.Tr><Table.Td colSpan={5}><Center py="md"><Text c="dimmed" size="xs">Menunggu request masuk...</Text></Center></Table.Td></Table.Tr>
            )}
          </Table.Tbody>
        </Table>
      </Table.ScrollContainer>
    </Stack>
  )
}

function ProjectPanel() {
  const [view, setView] = useState('api-routes')

  const viewData = useMemo(() => {
    switch (view) {
      case 'api-routes': return {
        queryKey: ['admin', 'routes'],
        queryFn: () => fetch('/api/admin/routes', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildApiRoutesGraph,
      }
      case 'file-structure': return {
        queryKey: ['admin', 'project-structure'],
        queryFn: () => fetch('/api/admin/project-structure', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildFileStructureGraph,
      }
      case 'env-map': return {
        queryKey: ['admin', 'env-map'],
        queryFn: () => fetch('/api/admin/env-map', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildEnvMapGraph,
      }
      case 'test-coverage': return {
        queryKey: ['admin', 'test-coverage'],
        queryFn: () => fetch('/api/admin/test-coverage', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildTestCoverageGraph,
      }
      case 'dependencies': return {
        queryKey: ['admin', 'dependencies'],
        queryFn: () => fetch('/api/admin/dependencies', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildDependenciesGraph,
      }
      case 'migrations': return {
        queryKey: ['admin', 'migrations'],
        queryFn: () => fetch('/api/admin/migrations', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildMigrationsGraph,
      }
      case 'sessions': return {
        queryKey: ['admin', 'sessions'],
        queryFn: () => fetch('/api/admin/sessions', { credentials: 'include' }).then((r) => r.json()),
        buildGraph: buildSessionsGraph,
      }
      default: return null
    }
  }, [view])

  const isStaticView = view === 'user-flow' || view === 'data-flow'
  const isLiveView = view === 'live-requests'

  const staticGraph = useMemo(() => {
    if (view === 'user-flow') return buildUserFlowGraph()
    if (view === 'data-flow') return buildDataFlowGraph()
    return null
  }, [view])

  return (
    <Stack h="calc(100vh - 100px)">
      <Group justify="space-between">
        <Title order={3}>Project</Title>
        <Select
          value={view}
          onChange={(v) => v && setView(v)}
          data={PROJECT_VIEWS.map((g) => ({ group: g.group, items: g.items }))}
          size="xs"
          w={200}
        />
      </Group>
      <Box flex={1} pos="relative">
        {isLiveView && <LiveRequestsPanel />}
        {isStaticView && staticGraph && (
          <ReactFlowProvider>
            <StaticFlowPanel graph={staticGraph} flowKey={view} />
          </ReactFlowProvider>
        )}
        {!isLiveView && !isStaticView && viewData && (
          <ReactFlowProvider>
            <GenericFlowPanelWrapper key={view} {...viewData} />
          </ReactFlowProvider>
        )}
      </Box>
    </Stack>
  )
}

function GenericFlowPanelWrapper(props: { queryKey: string[]; queryFn: () => Promise<any>; buildGraph: (d: any) => { nodes: Node[]; edges: Edge[] } }) {
  return (
    <Box style={{ position: 'absolute', inset: 0 }}>
      <GenericFlowInner {...props} />
    </Box>
  )
}

function GenericFlowInner({ queryKey, queryFn, buildGraph }: { queryKey: string[]; queryFn: () => Promise<any>; buildGraph: (d: any) => { nodes: Node[]; edges: Edge[] } }) {
  const { fitView, setViewport } = useReactFlow()
  const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
  const flowKey = queryKey.join('-')
  const { savePositions, saveViewport, loadPositions, loadViewport } = useFlowAutoSave(flowKey)
  const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

  const { data, isLoading, refetch } = useQuery({ queryKey, queryFn, refetchInterval: queryKey.includes('sessions') ? 10_000 : undefined })

  useEffect(() => {
    if (!data) return
    const { nodes: newNodes, edges: newEdges } = buildGraph(data)
    const savedPos = loadPositions()
    const hasSaved = Object.keys(savedPos).length > 0
    if (hasSaved) {
      setNodes(newNodes.map((n) => ({ ...n, position: savedPos[n.id] ?? n.position })))
      setEdges(newEdges)
      const vp = loadViewport()
      if (vp) setTimeout(() => setViewport(vp), 50)
      else setTimeout(() => fitView({ padding: 0.15 }), 50)
    } else {
      applyElkLayout(newNodes, newEdges, 'RIGHT').then(({ nodes: ln, edges: le }) => {
        setNodes(ln); setEdges(le)
        setTimeout(() => fitView({ padding: 0.15 }), 100)
      })
    }
  }, [data])

  const onNodeDragStop = useCallback((_: any, __: any, all: Node[]) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => savePositions(all), 500)
  }, [savePositions])
  const onMoveEnd = useCallback((_: any, vp: any) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => saveViewport(vp), 500)
  }, [saveViewport])

  if (isLoading) return <Center style={{ position: 'absolute', inset: 0 }}><Loader /></Center>
  return (
    <>
      <Box pos="absolute" top={8} right={8} style={{ zIndex: 10 }}>
        <ActionIcon variant="subtle" style={{ background: 'white', boxShadow: '0 1px 3px rgba(0,0,0,.15)' }} onClick={() => { refetch() }}><TbRefresh size={14} /></ActionIcon>
      </Box>
      <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
        onNodeDragStop={onNodeDragStop} onMoveEnd={onMoveEnd} nodeTypes={SIMPLE_NODE_TYPES}
        fitView minZoom={0.05} maxZoom={5}>
        <Background /><Controls />
      </ReactFlow>
    </>
  )
}

function StaticFlowPanel({ graph, flowKey }: { graph: { nodes: Node[]; edges: Edge[] }; flowKey: string }) {
  const { fitView, setViewport } = useReactFlow()
  const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
  const { savePositions, saveViewport, loadPositions, loadViewport } = useFlowAutoSave(flowKey)
  const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

  useEffect(() => {
    const savedPos = loadPositions()
    const hasSaved = Object.keys(savedPos).length > 0
    if (hasSaved) {
      setNodes(graph.nodes.map((n) => ({ ...n, position: savedPos[n.id] ?? n.position })))
      setEdges(graph.edges)
      const vp = loadViewport()
      if (vp) setTimeout(() => setViewport(vp), 50)
      else setTimeout(() => fitView({ padding: 0.15 }), 50)
    } else {
      applyElkLayout(graph.nodes, graph.edges, 'RIGHT').then(({ nodes: ln, edges: le }) => {
        setNodes(ln); setEdges(le)
        setTimeout(() => fitView({ padding: 0.15 }), 100)
      })
    }
  }, [])

  const onNodeDragStop = useCallback((_: any, __: any, all: Node[]) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => savePositions(all), 500)
  }, [savePositions])
  const onMoveEnd = useCallback((_: any, vp: any) => {
    clearTimeout(saveTimer.current)
    saveTimer.current = setTimeout(() => saveViewport(vp), 500)
  }, [saveViewport])

  return (
    <Box style={{ position: 'absolute', inset: 0 }}>
      <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
        onNodeDragStop={onNodeDragStop} onMoveEnd={onMoveEnd} nodeTypes={SIMPLE_NODE_TYPES}
        fitView minZoom={0.05} maxZoom={5}>
        <Background /><Controls />
      </ReactFlow>
    </Box>
  )
}

// ─── Settings Panel ────────────────────────────────────────────────────────────

interface AppConfigEntry { key: string; value: string; updatedAt: string }

const CONFIG_DEFINITIONS: { key: string; label: string; description: string; placeholder: string }[] = [
  {
    key: 'URL_API_DESA_PLUS',
    label: 'URL API Desa Plus',
    description: 'Base URL untuk API eksternal Desa Plus. Semua request dari frontend akan diproxy melalui server ke URL ini.',
    placeholder: 'https://api.desa-plus.example.com',
  },
]

function SettingsPanel() {
  const qc = useQueryClient()
  const [values, setValues] = useState<Record<string, string>>({})
  const [saved, setSaved] = useState<Record<string, boolean>>({})

  const { data, isLoading } = useQuery({
    queryKey: ['admin', 'config'],
    queryFn: () => fetch('/api/admin/config', { credentials: 'include' }).then((r) => r.json()),
  })

  const configs: AppConfigEntry[] = data?.configs ?? []

  useEffect(() => {
    const initial: Record<string, string> = {}
    for (const def of CONFIG_DEFINITIONS) {
      const existing = configs.find((c) => c.key === def.key)
      initial[def.key] = existing?.value ?? ''
    }
    setValues(initial)
  }, [configs])

  const saveMutation = useMutation({
    mutationFn: ({ key, value }: { key: string; value: string }) =>
      fetch('/api/admin/config', {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key, value }),
      }).then((r) => r.json()),
    onSuccess: (_, { key }) => {
      qc.invalidateQueries({ queryKey: ['admin', 'config'] })
      setSaved((prev) => ({ ...prev, [key]: true }))
      setTimeout(() => setSaved((prev) => ({ ...prev, [key]: false })), 2000)
    },
  })

  return (
    <Stack>
      <Title order={3}>Settings</Title>
      <Text size="sm" c="dimmed">Konfigurasi runtime — perubahan langsung berlaku tanpa rebuild atau redeploy.</Text>

      {isLoading ? <Center><Loader /></Center> : (
        <Stack gap="md">
          {CONFIG_DEFINITIONS.map((def) => {
            const existing = configs.find((c) => c.key === def.key)
            return (
              <Paper key={def.key} withBorder p="lg" radius="md">
                <Stack gap="xs">
                  <Group justify="space-between" align="flex-start">
                    <div>
                      <Text fw={600} size="sm">{def.label}</Text>
                      <Text size="xs" c="dimmed" ff="monospace">{def.key}</Text>
                    </div>
                    {existing && (
                      <Text size="xs" c="dimmed">
                        Diupdate {new Date(existing.updatedAt).toLocaleString('id-ID')}
                      </Text>
                    )}
                  </Group>
                  <Text size="xs" c="dimmed">{def.description}</Text>
                  <Group gap="xs" align="flex-end">
                    <Box style={{ flex: 1 }}>
                      <input
                        style={{
                          width: '100%',
                          padding: '8px 12px',
                          borderRadius: 6,
                          border: '1px solid var(--mantine-color-default-border)',
                          background: 'var(--mantine-color-body)',
                          color: 'var(--mantine-color-text)',
                          fontSize: 13,
                          fontFamily: 'monospace',
                        }}
                        value={values[def.key] ?? ''}
                        onChange={(e) => setValues((prev) => ({ ...prev, [def.key]: e.target.value }))}
                        placeholder={def.placeholder}
                      />
                    </Box>
                    <Button
                      size="sm"
                      color={saved[def.key] ? 'green' : 'blue'}
                      loading={saveMutation.isPending && saveMutation.variables?.key === def.key}
                      onClick={() => saveMutation.mutate({ key: def.key, value: values[def.key] ?? '' })}
                    >
                      {saved[def.key] ? 'Tersimpan' : 'Simpan'}
                    </Button>
                  </Group>
                  {!existing && (
                    <Badge color="orange" variant="light" size="xs">Belum dikonfigurasi — data tidak akan ter-load</Badge>
                  )}
                </Stack>
              </Paper>
            )
          })}
        </Stack>
      )}
    </Stack>
  )
}

// ─── Unused imports fix ────────────────────────────────────────────────────────
// Box, Container, Card, Modal, Paper, Select, SimpleGrid, Stack, Table, Text, ThemeIcon, Title, Tooltip — all used above
// TbDots is used in OperatorsPanel menu
void TbFileText
void TbCode
void TbUser
void TbUserSearch
