// AdminSecurityMalware — admin Security tab "Malware" sub-tab (M33).
// ADR-0072 (Tetragon removed 2026-04-30 by M39). Surfaces the
// LMD + YARA-X + signature-base stack.
//
// Sub-tabs: Overview / Quarantine / Events / ManualScan / YARA / Settings.
import { useTranslation } from "react-i18next";
import {
  Alert,
  Button,
  Card,
  Col,
  Descriptions,
  Drawer,
  Form,
  InputNumber,
  Popconfirm,
  Radio,
  Row,
  Select,
  Space,
  Switch,
  Table,
  Tabs,
  Tag,
  Tooltip,
  Typography,
  message,
} from "antd";
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
import { SearchableTableStringQ } from "../../../components/SearchableTable";

import { Input } from "antd";
import {
  BugOutlined,
  DeleteOutlined,
  RotateCcwOutlined,
  EyeOutlined,
  FileTextOutlined,
  ServerOutlined,
  ThunderboltOutlined,
} from "@icons";
import { RowActions } from "../../../components/RowActions";
import { useListQuery } from "../../../hooks/useQueries";

// Mirror of the User shape used in shells/admin/users/UserList.tsx — we
// only need the username + display fields for the multi-user scan
// picker, so a narrow local copy avoids cross-shell coupling.
type ScanUser = {
  id: string;
  email: string;
  username?: string | null;
  name_first: string;
  name_last: string;
  is_admin: boolean;
};

import {
  type MalwareQuarantineRow,
  type MalwareScanner,
  type MalwareScanStatus,
  type MalwareSeverity,
  useDeleteQuarantine,
  useDeleteYARARule,
  useMalwareEvents,
  useMalwareQuarantine,
  useMalwareScanStatus,
  useMalwareSettings,
  useMalwareStatus,
  useMalwareUserScans,
  useRestoreQuarantine,
  useAuditRecent,
  useStartMalwareScan,
  useToggleYARARule,
  useUpdateMalwareSettings,
  useUpdateMalwareSignatures,
  useUploadYARARule,
  useYARARules,
} from "../../../hooks/useSecurityMalware";

const SCANNER_COLOR: Record<MalwareScanner, string> = {
  maldet: "orange",
  clamav: "blue",
  yara: "purple",
};

const SEVERITY_COLOR: Record<MalwareSeverity, string> = {
  info: "blue",
  warn: "orange",
  critical: "red",
};

const displayPath = (full: string, user?: string): string => {
  if (user && full.startsWith(`/home/${user}/`)) {
    return `~/${full.slice(`/home/${user}/`.length)}`;
  }
  return full;
};

// truncatePath collapses long file paths with a middle ellipsis so the
// quarantine + events tables stay readable. Keeps the start (parent
// dirs the operator scans for) AND the filename (the actionable bit)
// — both are higher-signal than the middle for /tmp/systemd-private-…
// or /home/<user>/domains/<domain>/wp-content/uploads/<sha> noise.
const truncatePath = (full: string, max = 64): string => {
  if (!full || full.length <= max) return full;
  const keep = Math.floor((max - 1) / 2);
  return `${full.slice(0, keep)}…${full.slice(full.length - keep)}`;
};

const formatBytes = (n: number): string => {
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
  return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
};

const formatTime = (s?: string): string => {
  if (!s) return "—";
  try {
    return new Date(s).toLocaleString();
  } catch {
    return s;
  }
};

// ---- Overview card ----
//
// Renders the malware-stack health as a grid of icon tiles instead of
// a flat Descriptions list. Each tile is its own Card so the eye lands
// on one fact at a time; tints follow the same scheme as the CrowdSec
// metrics overview (blue/green = healthy, amber/red = attention).
function OverviewCard() {
  const { t } = useTranslation();
  const { data, isLoading } = useMalwareStatus();
  const { data: settings } = useMalwareSettings();
  const updateSigs = useUpdateMalwareSignatures();

  if (isLoading || !data) {
    return <Typography.Text type="secondary">Loading…</Typography.Text>;
  }

  const activeTint = "#52c41a";
  const warnTint = "#faad14";
  const infoTint = "#1677ff";
  const offTint = "#bfbfbf";
  const realtimeOptedIn = settings?.realtime_enabled === true;

  return (
    <Space direction="vertical" size={12} style={{ width: "100%" }}>
      <Space wrap style={{ justifyContent: "space-between", width: "100%", rowGap: 8 }}>
        <Typography.Title level={5} style={{ margin: 0 }}>
          Stack health
        </Typography.Title>
        <Button
          size="small"
          type="primary"
          loading={updateSigs.isPending}
          onClick={() => {
            updateSigs.mutate(undefined, {
              onSuccess: () => message.success("Signature update started"),
              onError: () => message.error("Signature update failed"),
            });
          }}
        >
          Update signatures
        </Button>
      </Space>

      {!realtimeOptedIn && (
        <Alert
          type="info"
          showIcon
          message={t("adminsecuritymalware.real_time_scanning_is_off")}
          description={t("adminsecuritymalware.the_inotify_monitor_is_not_running_daily_sch")}
        />
      )}

      {/* Row 1 (4 tiles, lg=6): live state. Row 2 (3 tiles, lg=8): version/date. */}
      <Row gutter={[12, 12]}>
        <StatusTile
          lg={6}
          icon={<EyeOutlined />}
          tint={
            data.monitor_active
              ? activeTint
              : realtimeOptedIn
                ? warnTint
                : offTint
          }
          label={t("adminsecuritymalware.realtime_monitor")}
          value={
            data.monitor_active
              ? "active"
              : realtimeOptedIn
                ? "stopped"
                : "off (opt-in)"
          }
          hint={
            realtimeOptedIn
              ? "LMD inotify watcher on user docroots"
              : "Disabled by admin — enable in Settings tab"
          }
        />
        <StatusTile
          lg={6}
          icon={<ServerOutlined />}
          tint={data.signature_base_present ? activeTint : warnTint}
          label="signature-base"
          value={
            data.signature_base_present
              ? `${data.signature_base_files ?? 0} files`
              : "missing"
          }
          hint="Neo23x0/signature-base — webshell + crime YARA rules. git-pulled daily."
        />
        <StatusTile
          lg={6}
          icon={<FileTextOutlined />}
          tint={infoTint}
          label="maldet sig"
          value={data.maldet_sig_version || "—"}
          hint="Last rfxn signature-pack version"
        />
        <StatusTile
          lg={8}
          icon={<BugOutlined />}
          tint={data.yara_engine_version ? activeTint : warnTint}
          label={t("adminsecuritymalware.yara_engine")}
          value={data.yara_engine_version || "missing"}
          hint="Scan engine for .yar rules (yara-x preferred, legacy yara fallback)"
        />
        <StatusTile
          lg={8}
          icon={<ThunderboltOutlined />}
          tint={infoTint}
          label={t("adminsecuritymalware.inotify_watches")}
          value={`${data.watch_count_configured ?? 0} / ${data.watch_ceiling ?? 0}`}
          hint="Active inotify watches vs kernel ceiling"
        />
        <StatusTile
          lg={8}
          icon={<BugOutlined />}
          tint={infoTint}
          label="maldet version"
          value={data.maldet_version || "not installed"}
          hint="Linux Malware Detect engine version"
        />
      </Row>
    </Space>
  );
}

// StatusTile is the malware-overview equivalent of CrowdSec's MetricTile,
// adapted to status strings instead of numbers. Same icon-badge layout.
function StatusTile({
  icon,
  tint,
  label,
  value,
  hint,
  lg,
}: {
  icon: ReactNode;
  tint: string;
  label: string;
  value: string;
  hint: string;
  lg?: number;
}) {
  const colProps = lg
    ? { xs: 24, sm: 12, lg }
    : { flex: "1 1 220px" as const, style: { minWidth: 200 } };
  return (
    <Col {...colProps}>
      <Tooltip title={hint}>
        <Card
          size="small"
          hoverable
          styles={{ body: { padding: 12 } }}
          style={{ height: "100%" }}
        >
          <Space size={12} align="center" style={{ width: "100%" }}>
            <div
              style={{
                width: 44,
                height: 44,
                borderRadius: 8,
                background: `${tint}22`,
                color: tint,
                fontSize: 22,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                flexShrink: 0,
              }}
            >
              {icon}
            </div>
            <div style={{ minWidth: 0, flex: 1 }}>
              <Typography.Text type="secondary" style={{ fontSize: 12, display: "block" }}>
                {label}
              </Typography.Text>
              <Typography.Text
                strong
                style={{ fontSize: 16, lineHeight: 1.2, display: "block" }}
                ellipsis={{ tooltip: value }}
              >
                {value}
              </Typography.Text>
            </div>
          </Space>
        </Card>
      </Tooltip>
    </Col>
  );
}

// ---- Quarantine card ----
function QuarantineCard() {
  const { t } = useTranslation();
  const [page, setPage] = useState(1);
  const { data, isLoading } = useMalwareQuarantine(page, 50);
  const restore = useRestoreQuarantine();
  const del = useDeleteQuarantine();
  const [restoreOpen, setRestoreOpen] = useState<MalwareQuarantineRow | null>(null);
  const [restoreReason, setRestoreReason] = useState("");

  const columns = useMemo(
    () => [
      {
        title: "File path",
        dataIndex: "original_path",
        key: "original_path",
        width: 460,
        render: (_: string, row: MalwareQuarantineRow) => {
          const friendly = displayPath(row.original_path, row.user_id);
          return (
            <Tooltip title={row.original_path}>
              <span
                style={{
                  display: "inline-block",
                  maxWidth: 440,
                  whiteSpace: "nowrap",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                  verticalAlign: "middle",
                }}
              >
                {truncatePath(friendly, 60)}
              </span>
            </Tooltip>
          );
        },
      },
      {
        title: "User",
        key: "user",
        width: 140,
        render: (_: unknown, row: MalwareQuarantineRow) => {
          if (row.username) {
            return (
              <Tooltip title={row.user_id}>
                <span>{row.username}</span>
              </Tooltip>
            );
          }
          if (row.user_id) {
            // ULID present but lookup miss (deleted user). Show short id.
            return (
              <Tooltip title={row.user_id}>
                <span style={{ opacity: 0.7 }}>{row.user_id.slice(-6)}</span>
              </Tooltip>
            );
          }
          return <Tag>system</Tag>;
        },
      },
      {
        title: "Signature",
        dataIndex: "signature",
        key: "signature",
        ellipsis: { showTitle: false } as const,
        render: (v: string) => <Tooltip title={v}><span>{v}</span></Tooltip>,
      },
      {
        title: "Scanner",
        dataIndex: "scanner",
        key: "scanner",
        width: 100,
        render: (v: MalwareScanner) => <Tag color={SCANNER_COLOR[v]}>{v}</Tag>,
      },
      {
        title: "Detected",
        dataIndex: "detected_at",
        key: "detected_at",
        width: 180,
        render: (v: string) => formatTime(v),
      },
      {
        title: "Size",
        dataIndex: "size_bytes",
        key: "size_bytes",
        width: 90,
        render: (v: number) => formatBytes(v),
      },
      {
        title: "Actions",
        key: "actions",
        width: 200,
        render: (_: unknown, row: MalwareQuarantineRow) => (
          <RowActions
            actions={[
              {
                key: "restore",
                label: "Restore",
                icon: <RotateCcwOutlined />,
                onClick: () => {
                  setRestoreOpen(row);
                  setRestoreReason("");
                },
              },
              {
                key: "delete",
                label: "Delete",
                icon: <DeleteOutlined />,
                danger: true,
                onClick: () =>
                  del.mutate(row.id, {
                    onSuccess: () => message.success("Deleted"),
                    onError: () => message.error("Delete failed"),
                  }),
                confirm: {
                  title: "Delete quarantined file permanently?",
                  description: "The file will be removed from disk and cannot be recovered.",
                  okText: "Delete",
                },
              },
            ]}
          />
        ),
      },
    ],
    [del],
  );

  return (
    <>
      <Card title={t("adminsecuritymalware.quarantine")} size="small">
        <Table
          rowKey="id"
          loading={isLoading}
          dataSource={data?.data || []}
          columns={columns}
          tableLayout="fixed"
          scroll={{ x: "max-content" }}
          pagination={{
            current: page,
            pageSize: 50,
            total: data?.total ?? 0,
            onChange: setPage,
            showSizeChanger: false,
          }}
          size="small"
        />
      </Card>

      <Drawer
        open={restoreOpen !== null}
        title={t("adminsecuritymalware.restore_from_quarantine")}
        onClose={() => setRestoreOpen(null)}
        footer={
          <Space>
            <Button onClick={() => setRestoreOpen(null)}>Cancel</Button>
            <Button
              type="primary"
              danger
              loading={restore.isPending}
              disabled={restoreReason.trim().length < 5}
              onClick={() => {
                if (!restoreOpen) return;
                restore.mutate(
                  { id: restoreOpen.id, reason: restoreReason },
                  {
                    onSuccess: () => {
                      message.success("Restored");
                      setRestoreOpen(null);
                    },
                    onError: () => message.error("Restore failed"),
                  },
                );
              }}
            >
              Restore
            </Button>
          </Space>
        }
        width={520}
      >
        {restoreOpen && (
          <Space direction="vertical" size={12} style={{ width: "100%" }}>
            <Alert
              type="warning"
              showIcon
              message={t("adminsecuritymalware.restoring_re_introduces_the_file_to_disk")}
              description={`Signature: ${restoreOpen.signature}. Confirm only if this is a known false positive.`}
            />
            <Descriptions size="small" column={1} bordered>
              <Descriptions.Item label={t("adminsecuritymalware.original")}>{restoreOpen.original_path}</Descriptions.Item>
              <Descriptions.Item label={t("adminsecuritymalware.quarantine")}>{restoreOpen.quarantine_path}</Descriptions.Item>
              <Descriptions.Item label={t("adminsecuritymalware.sha256")}>
                <code style={{ fontSize: 11 }}>{restoreOpen.sha256}</code>
              </Descriptions.Item>
            </Descriptions>
            <Form.Item label={t("adminsecuritymalware.reason_audit")} required>
              <Input.TextArea
                rows={3}
                value={restoreReason}
                onChange={(e) => setRestoreReason(e.target.value)}
                placeholder={t("adminsecuritymalware.explain_why_this_restore_is_safe")}
              />
            </Form.Item>
          </Space>
        )}
      </Drawer>
    </>
  );
}

// ---- Manual scan card ----
//
// Two modes:
//   - "path"  — admin types a directory; one scan session.
//   - "users" — admin picks one or more users from the table; we fan
//     out one start.mutate per selected user and stack a
//     <ScanProgress> card per resulting session id. The User table
//     reuses the existing /api/v1/users list endpoint with
//     is_admin=false so the picker matches the Users page.
function ManualScanCard() {
  const { t } = useTranslation();
  const [mode, setMode] = useState<"path" | "users">("users");
  const [pathValue, setPathValue] = useState("");
  const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
  const [sessions, setSessions] = useState<{ id: string; label: string }[]>([]);
  const [userSearch, setUserSearch] = useState("");
  const start = useStartMalwareScan();

  const usersQ = useListQuery<ScanUser>({
    resource: "users",
    params: { is_admin: false, page_size: 200 },
    enabled: mode === "users",
  });
  const scannableUsers = useMemo(
    () => usersQ.items.filter((u) => !!u.username),
    [usersQ.items],
  );
  const filteredUsers = useMemo(() => {
    const q = userSearch.trim().toLowerCase();
    if (!q) return scannableUsers;
    return scannableUsers.filter((u) => {
      const hay = [
        u.username,
        u.email,
        u.name_first,
        u.name_last,
      ]
        .filter(Boolean)
        .join(" ")
        .toLowerCase();
      return hay.includes(q);
    });
  }, [scannableUsers, userSearch]);

  const submitPath = () => {
    const value = pathValue.trim();
    if (!value) {
      message.warning("Provide a path");
      return;
    }
    start.mutate(
      { path: value },
      {
        onSuccess: (resp) => {
          setSessions((prev) => [...prev, { id: resp.session_id, label: value }]);
          message.success("Scan started");
        },
        onError: () => message.error("Failed to start scan"),
      },
    );
  };

  const submitUsers = () => {
    if (selectedUsernames.length === 0) {
      message.warning("Select at least one user");
      return;
    }
    const toScan = selectedUsernames.filter((u) => !runningUsernames.has(u));
    const skipped = selectedUsernames.length - toScan.length;
    if (toScan.length === 0) {
      message.warning("All selected users already have a scan running");
      return;
    }
    toScan.forEach((username) => {
      start.mutate(
        { user: username },
        {
          onSuccess: (resp) => {
            setSessions((prev) => [...prev, { id: resp.session_id, label: `user: ${username}` }]);
          },
          onError: () => message.error(`Failed to start scan for ${username}`),
        },
      );
    });
    message.success(
      skipped > 0
        ? `Started ${toScan.length} scan(s) — ${skipped} skipped (already running)`
        : `Started ${toScan.length} scan(s)`,
    );
    setSelectedUsernames([]);
  };

  const userScansQ = useMalwareUserScans();
  const scanByUsername = useMemo(() => {
    const m = new Map<string, NonNullable<typeof userScansQ.data>[number]>();
    (userScansQ.data ?? []).forEach((r) => m.set(r.username, r));
    return m;
  }, [userScansQ.data]);
  const runningUsernames = useMemo(
    () =>
      new Set(
        (userScansQ.data ?? [])
          .filter((r) => r.state === "running")
          .map((r) => r.username),
      ),
    [userScansQ.data],
  );

  const userColumns = [
    {
      title: "Username",
      dataIndex: "username",
      key: "username",
      render: (v: string | null | undefined) => v ?? "—",
    },
    {
      title: "Name",
      key: "name",
      render: (_: unknown, r: ScanUser) =>
        [r.name_first, r.name_last].filter(Boolean).join(" ") || "—",
    },
    { title: "Email", dataIndex: "email", key: "email" },
    {
      title: "Last scanned",
      key: "last_scanned",
      render: (_: unknown, r: ScanUser) => {
        const row = r.username ? scanByUsername.get(r.username) : undefined;
        if (!row) return <Typography.Text type="secondary">—</Typography.Text>;
        const ts = row.finished_at ?? row.started_at;
        const d = new Date(ts);
        return (
          <Tooltip title={d.toISOString()}>
            <span>{d.toLocaleString()}</span>
          </Tooltip>
        );
      },
    },
    {
      title: "Status",
      key: "scan_status",
      render: (_: unknown, r: ScanUser) => {
        const row = r.username ? scanByUsername.get(r.username) : undefined;
        if (!row) return <Tag>idle</Tag>;
        if (row.state === "running") return <Tag color="processing">running</Tag>;
        if (row.state === "failed") return <Tag color="error">failed</Tag>;
        return <Tag color={row.hits > 0 ? "warning" : "success"}>
          done{row.hits > 0 ? ` · ${row.hits} hit${row.hits === 1 ? "" : "s"}` : ""}
        </Tag>;
      },
    },
  ];

  return (
    <Card title={t("adminsecuritymalware.manual_scan")} size="small">
      <Space direction="vertical" size={12} style={{ width: "100%" }}>
        <Radio.Group
          value={mode}
          onChange={(e) => setMode(e.target.value)}
        >
          <Radio.Button value="users">Users</Radio.Button>
          <Radio.Button value="path">Path</Radio.Button>
        </Radio.Group>

        {mode === "path" && (
          <Space.Compact style={{ width: "100%" }}>
            <Input
              placeholder="/home/<user>/domains/<domain>/public_html"
              value={pathValue}
              onChange={(e) => setPathValue(e.target.value)}
              onPressEnter={submitPath}
            />
            <Button type="primary" loading={start.isPending} onClick={submitPath}>
              Scan
            </Button>
          </Space.Compact>
        )}

        {mode === "users" && (
          <Space direction="vertical" size={8} style={{ width: "100%" }}>
            <Space wrap style={{ justifyContent: "space-between", width: "100%", rowGap: 8 }}>
              <Input.Search
                placeholder={t("adminsecuritymalware.search_username_name_or_email")}
                allowClear
                value={userSearch}
                onChange={(e) => setUserSearch(e.target.value)}
                style={{ maxWidth: 360 }}
              />
              <Space wrap>
                <Typography.Text type="secondary">
                  {selectedUsernames.length > 0
                    ? `${selectedUsernames.length} selected`
                    : "Select users to scan"}
                </Typography.Text>
                <Button
                  type="primary"
                  disabled={selectedUsernames.length === 0}
                  loading={start.isPending}
                  onClick={submitUsers}
                >
                  Scan selected
                </Button>
              </Space>
            </Space>
            <Table<ScanUser>
              size="small"
              rowKey="id"
              loading={usersQ.isLoading}
              dataSource={filteredUsers}
              columns={userColumns}
              pagination={{ pageSize: 10, size: "small" }}
              scroll={{ x: "max-content" }}
              rowSelection={{
                selectedRowKeys: filteredUsers
                  .filter((u) => u.username && selectedUsernames.includes(u.username))
                  .map((u) => u.id),
                onChange: (_keys, rows) =>
                  setSelectedUsernames(
                    rows.map((r) => r.username).filter((u): u is string => !!u),
                  ),
                preserveSelectedRowKeys: true,
                getCheckboxProps: (r) => ({
                  disabled: !!r.username && runningUsernames.has(r.username),
                }),
              }}
            />
          </Space>
        )}

        {sessions.map((s) => (
          <SessionProgress key={s.id} sessionId={s.id} label={s.label} />
        ))}
      </Space>
    </Card>
  );
}

// SessionProgress wraps useMalwareScanStatus + ScanProgress so each
// active session gets its own polling hook (rules-of-hooks compliant
// — we cannot call useMalwareScanStatus inside a .map).
function SessionProgress({ sessionId, label }: { sessionId: string; label: string }) {
  const status = useMalwareScanStatus(sessionId);
  return (
    <ScanProgress
      sessionId={`${label} — ${sessionId}`}
      status={status.data}
      loading={status.isFetching}
    />
  );
}

function ScanProgress({
  sessionId,
  status,
  loading,
}: {
  sessionId: string;
  status?: MalwareScanStatus;
  loading: boolean;
}) {
  return (
    <Card size="small" type="inner" title={`Scan ${sessionId}`}>
      <Space direction="vertical" size={4} style={{ width: "100%" }}>
        <Tag color={status?.done ? (status.exit_code === 0 ? "green" : "red") : "blue"}>
          {status?.state || (loading ? "starting" : "—")}
          {status?.done ? ` (exit ${status.exit_code})` : ""}
        </Tag>
        <pre
          style={{
            background: "#0b1020",
            color: "#cfe3ff",
            padding: 8,
            maxHeight: 320,
            overflow: "auto",
            fontSize: 12,
            margin: 0,
          }}
        >
          {(status?.log_tail || []).slice(-200).join("\n") || "(waiting for output…)"}
        </pre>
      </Space>
    </Card>
  );
}

// ---- Events card ----
function EventsCard() {
  const { t } = useTranslation();
  const [src, setSrc] = useState<MalwareScanner | undefined>();
  const [sev, setSev] = useState<MalwareSeverity | undefined>();
  const { data, isLoading } = useMalwareEvents({ source: src, severity: sev });
  const [drawerRow, setDrawerRow] = useState<unknown>(null);

  return (
    <>
      <Card
        title={t("adminsecuritymalware.recent_events")}
        size="small"
        extra={
          <Space size={4}>
            <Select
              size="small"
              style={{ width: 120 }}
              allowClear
              placeholder="source"
              value={src}
              onChange={setSrc}
              options={[
                { value: "maldet", label: "maldet" },
                { value: "clamav", label: "clamav" },
                { value: "yara", label: "yara" },
              ]}
            />
            <Select
              size="small"
              style={{ width: 120 }}
              allowClear
              placeholder="severity"
              value={sev}
              onChange={setSev}
              options={[
                { value: "info", label: "info" },
                { value: "warn", label: "warn" },
                { value: "critical", label: "critical" },
              ]}
            />
          </Space>
        }
      >
        <Table
          rowKey="id"
          loading={isLoading}
          dataSource={data?.data || []}
          tableLayout="fixed"
          scroll={{ x: "max-content" }}
          size="small"
          onRow={(row) => ({ onClick: () => setDrawerRow(row), style: { cursor: "pointer" } })}
          columns={[
            { title: "Time", dataIndex: "created_at", width: 180, render: formatTime },
            {
              title: "Source",
              dataIndex: "source",
              width: 100,
              render: (v: MalwareScanner) => <Tag color={SCANNER_COLOR[v]}>{v}</Tag>,
            },
            {
              title: "Severity",
              dataIndex: "severity",
              width: 100,
              render: (v: MalwareSeverity) => <Tag color={SEVERITY_COLOR[v]}>{v}</Tag>,
            },
            { title: "Type", dataIndex: "event_type", width: 160 },
            {
              title: "Path",
              dataIndex: "target_path",
              width: 460,
              render: (v?: string) =>
                v ? (
                  <Tooltip title={v}>
                    <span
                      style={{
                        display: "inline-block",
                        maxWidth: 440,
                        whiteSpace: "nowrap",
                        overflow: "hidden",
                        textOverflow: "ellipsis",
                        verticalAlign: "middle",
                      }}
                    >
                      {truncatePath(v, 60)}
                    </span>
                  </Tooltip>
                ) : (
                  "—"
                ),
            },
            {
              title: "Signature",
              dataIndex: "signature",
              width: 200,
              ellipsis: { showTitle: false } as const,
              render: (v?: string) => (v ? <Tooltip title={v}>{v}</Tooltip> : "—"),
            },
          ]}
        />
      </Card>

      <Drawer
        open={drawerRow !== null}
        onClose={() => setDrawerRow(null)}
        title={t("adminsecuritymalware.event_detail")}
        width={640}
      >
        <pre style={{ fontSize: 12 }}>{JSON.stringify(drawerRow, null, 2)}</pre>
      </Drawer>
    </>
  );
}

// ---- Settings card ----
function SettingsCard() {
  const { t } = useTranslation();
  const { data, isLoading } = useMalwareSettings();
  const update = useUpdateMalwareSettings();
  const [form] = Form.useForm();

  if (isLoading || !data) {
    return (
      <Card title={t("adminsecuritymalware.settings")} size="small">
        <Typography.Text type="secondary">Loading…</Typography.Text>
      </Card>
    );
  }

  return (
    <Card title={t("adminsecuritymalware.settings")} size="small">
      <Form
        form={form}
        layout="vertical"
        initialValues={data}
        onFinish={(values) => {
          update.mutate({ ...data, ...values }, {
            onSuccess: () => message.success("Settings saved"),
            onError: () => message.error("Save failed"),
          });
        }}
      >
        <Form.Item
          label={t("adminsecuritymalware.daily_scan_schedule_cron")}
          name="schedule_cron"
          rules={[{ required: true, message: "cron expression required" }]}
        >
          <Input placeholder="0 3 * * *" />
        </Form.Item>
        <Form.Item label={t("adminsecuritymalware.quarantine_retention_days")} name="quarantine_retain_days">
          <InputNumber min={1} max={365} style={{ width: 160 }} />
        </Form.Item>
        <Form.Item label={t("adminsecuritymalware.max_scan_size_per_file_mb")} name="max_scan_mb">
          <InputNumber min={1} max={10240} style={{ width: 160 }} />
        </Form.Item>
        <Form.Item label={t("adminsecuritymalware.notification_severity_threshold")} name="notify_threshold">
          <Select
            style={{ width: 200 }}
            options={[
              { value: "info", label: "info (everything)" },
              { value: "warn", label: "warn (default)" },
              { value: "critical", label: "critical only" },
            ]}
          />
        </Form.Item>
        <Form.Item
          label={t("adminsecuritymalware.real_time_scanning")}
          name="realtime_enabled"
          valuePropName="checked"
          extra="Watches user docroots via inotify and scans changed files in batches with maldet's native HEX/MD5/SHA-256 + YARA scanner. Lightweight (~50MB RAM during scans). Off by default — enable on production hosts where you want immediate webshell-upload detection."
        >
          <Switch />
        </Form.Item>
        <Typography.Title level={5} style={{ marginTop: 16 }}>
          Mail YARA scanner (M33.2 — async, ADR-0079)
        </Typography.Title>
        <Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
          Walks Stalwart mailboxes via JMAP every 5 min, scans attachments with
          yara-x against the same rule packs as the filesystem scanner. Hits move
          to a per-account "Malware" mailbox + fire admin notifications. Zero
          SMTP-time latency (post-delivery scan).
        </Typography.Paragraph>

        <Form.Item
          label={t("adminsecuritymalware.enable_mail_attachment_scanning")}
          name="mail_scan_enabled"
          valuePropName="checked"
          extra="Off by default. Reads /etc/jabali-panel/stalwart-admin.token for JMAP."
        >
          <Switch />
        </Form.Item>
        <Form.Item
          label={t("adminsecuritymalware.scan_all_folders_not_just_inbox")}
          name="mail_scan_all_folders"
          valuePropName="checked"
          extra="Skips Sent / Trash / Drafts / Junk / Spam regardless."
        >
          <Switch />
        </Form.Item>
        <Form.Item
          label={t("adminsecuritymalware.skip_these_recipient_addresses_comma_separat")}
          name="mail_scan_skip_addresses"
          extra="Per-mailbox skip list — e.g. samples@panel-host for sysadmin sample analysis."
        >
          <Input placeholder="samples@example.com, lab@example.com" />
        </Form.Item>
        <Form.Item label={t("adminsecuritymalware.max_attachment_size_mb")} name="mail_scan_max_attachment_mb">
          <InputNumber min={1} max={500} style={{ width: 160 }} />
        </Form.Item>
        <Form.Item label={t("adminsecuritymalware.per_attachment_scan_timeout_seconds")} name="mail_scan_timeout_sec">
          <InputNumber min={1} max={120} style={{ width: 160 }} />
        </Form.Item>
        <Form.Item label={t("adminsecuritymalware.per_tick_budget_mailboxes_scanned_each_cycle")} name="mail_scan_per_tick_budget">
          <InputNumber min={10} max={2000} style={{ width: 160 }} />
        </Form.Item>

        <Button type="primary" htmlType="submit" loading={update.isPending}>
          Save settings
        </Button>
      </Form>
    </Card>
  );
}

// ---- Built-in YARA rule packs (read-only) ----
function BuiltinYARACard() {
  const { t } = useTranslation();
  const { data: status, isLoading } = useMalwareStatus();
  const updateSigs = useUpdateMalwareSignatures();

  const rows = [
    {
      key: "rfxn",
      name: "rfxn.yara",
      description: "Shipped + refreshed by maldet (rfxn.com signature pack)",
      present: status?.rfxn_rules_present ?? false,
      count: status?.rfxn_rules_count ?? 0,
      updated: status?.rfxn_rules_updated,
      path: status?.rfxn_rules_path ?? "/usr/local/maldetect/sigs/rfxn.yara",
    },
    {
      key: "signature-base",
      name: "signature-base",
      description:
        "Neo23x0/signature-base — webshell + crime YARA rules; git-pulled daily.",
      present: status?.signature_base_present ?? false,
      count: status?.signature_base_files ?? 0,
      updated: status?.signature_base_updated,
      path: status?.signature_base_path ?? "/opt/jabali/signature-base/yara",
    },
  ];

  return (
    <Card
      title={t("adminsecuritymalware.built_in_rule_packs")}
      size="small"
      style={{ marginBottom: 16 }}
      extra={
        <Button
          size="small"
          type="primary"
          loading={updateSigs.isPending}
          onClick={() =>
            updateSigs.mutate(undefined, {
              onSuccess: () => message.success("Signature update started"),
              onError: () => message.error("Signature update failed"),
            })
          }
        >
          Update now
        </Button>
      }
    >
      <Table
        rowKey="key"
        loading={isLoading}
        dataSource={rows}
        pagination={false}
        columns={[
          {
            title: "Pack",
            dataIndex: "name",
            key: "name",
            render: (_v: string, r) => (
              <Space direction="vertical" size={0}>
                <Typography.Text strong>{r.name}</Typography.Text>
                <Typography.Text type="secondary" style={{ fontSize: 12 }}>
                  {r.description}
                </Typography.Text>
              </Space>
            ),
          },
          {
            title: "Rules",
            dataIndex: "count",
            key: "count",
            width: 120,
            render: (_v: number, r) =>
              r.present ? (
                <Tag color="blue">{r.count.toLocaleString()}</Tag>
              ) : (
                <Tag color="red">missing</Tag>
              ),
          },
          {
            title: "Updated",
            dataIndex: "updated",
            key: "updated",
            width: 200,
            render: (v?: string) => formatTime(v) || "—",
          },
          {
            title: "Path",
            dataIndex: "path",
            key: "path",
            render: (v: string) => (
              <Typography.Text code copyable={{ text: v }} style={{ fontSize: 12 }}>
                {v}
              </Typography.Text>
            ),
          },
        ]}
      />
    </Card>
  );
}

// ---- YARA rules card (M33 Step 8) ----
function YARACard() {
  const { t } = useTranslation();
  const { data, isLoading } = useYARARules();
  const [ruleQuery, setRuleQuery] = useState("");
  const upload = useUploadYARARule();
  const toggle = useToggleYARARule();
  const del = useDeleteYARARule();
  const [uploadOpen, setUploadOpen] = useState(false);
  const [filename, setFilename] = useState("");
  const [content, setContent] = useState("");

  const reset = () => {
    setFilename("");
    setContent("");
    setUploadOpen(false);
  };

  return (
    <>
      <BuiltinYARACard />
      <Card
        title={t("adminsecuritymalware.custom_yara_rules")}
        size="small"
        extra={
          <Button size="small" type="primary" onClick={() => setUploadOpen(true)}>
            Upload rule
          </Button>
        }
      >
        <SearchableTableStringQ
          onSearchChange={setRuleQuery}
          searchPlaceholder="Search filename…"
          rowKey="filename"
          loading={isLoading}
          dataSource={(data || []).filter(
            (r) => !ruleQuery || r.filename.toLowerCase().includes(ruleQuery.toLowerCase()),
          )}
          tableLayout="fixed"
          columns={[
            { title: "Filename", dataIndex: "filename", ellipsis: { showTitle: false } as const,
              render: (v: string) => <Tooltip title={v}><code>{v}</code></Tooltip> },
            {
              title: "Enabled",
              dataIndex: "enabled",
              width: 110,
              render: (v: boolean, row) => (
                <Switch
                  size="small"
                  checked={v}
                  onChange={(checked) =>
                    toggle.mutate(
                      { filename: row.filename, enabled: checked },
                      { onError: () => message.error("Toggle failed") },
                    )
                  }
                />
              ),
            },
            { title: "Size", dataIndex: "size_bytes", width: 90, render: (v: number) => formatBytes(v) },
            { title: "SHA256", dataIndex: "sha256", width: 200, ellipsis: { showTitle: false } as const,
              render: (v: string) => <Tooltip title={v}><code style={{ fontSize: 11 }}>{v.slice(0, 16)}…</code></Tooltip> },
            { title: "Modified", dataIndex: "last_modified", width: 180, render: formatTime },
            {
              title: "Actions",
              key: "actions",
              width: 110,
              render: (_, row) => (
                <Popconfirm
                  title={`Delete ${row.filename}?`}
                  onConfirm={() =>
                    del.mutate(row.filename, {
                      onSuccess: () => message.success("Deleted"),
                      onError: () => message.error("Delete failed"),
                    })
                  }
                >
                  <Button size="small" danger>
                    Delete
                  </Button>
                </Popconfirm>
              ),
            },
          ]}
        />
      </Card>

      <Drawer
        open={uploadOpen}
        title={t("adminsecuritymalware.upload_yara_rule")}
        onClose={reset}
        width={640}
        footer={
          <Space>
            <Button onClick={reset}>Cancel</Button>
            <Button
              type="primary"
              loading={upload.isPending}
              disabled={!filename.endsWith(".yar") || content.trim().length === 0}
              onClick={() =>
                upload.mutate(
                  { filename, content },
                  {
                    onSuccess: () => {
                      message.success("Uploaded + parse-validated");
                      reset();
                    },
                    onError: (err: unknown) => {
                      const detail =
                        err instanceof Error ? err.message : "Upload failed";
                      message.error(detail);
                    },
                  },
                )
              }
            >
              Upload
            </Button>
          </Space>
        }
      >
        <Space direction="vertical" size={12} style={{ width: "100%" }}>
          <Alert
            type="info"
            showIcon
            message={t("adminsecuritymalware.pre_flight_parsed_via_yara_w_before_install")}
            description={t("adminsecuritymalware.upload_fails_with_the_parser_error_if_the_ru")}
          />
          <Form.Item label={t("adminsecuritymalware.filename")} required>
            <Input
              placeholder="my-custom-rule.yar"
              value={filename}
              onChange={(e) => setFilename(e.target.value)}
            />
          </Form.Item>
          <Form.Item label={t("adminsecuritymalware.rule_content")} required>
            <Input.TextArea
              rows={14}
              value={content}
              onChange={(e) => setContent(e.target.value)}
              placeholder="rule example { condition: true }"
              style={{ fontFamily: "monospace", fontSize: 12 }}
            />
          </Form.Item>
        </Space>
      </Drawer>
    </>
  );
}
// ---- Exec audit card (M39 ADR-0085) ----
function ExecAuditCard() {
  const { t } = useTranslation();
  const { data, isLoading, refetch } = useAuditRecent(100);

  return (
    <Card
      title={t("adminsecuritymalware.exec_audit_auditd_jabali_susp_exec")}
      size="small"
      extra={
        <Button size="small" onClick={() => refetch()}>
          Refresh
        </Button>
      }
    >
      <Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
        Suspicious-binary execve trail. Tagged{" "}
        <code>jabali_susp_exec</code>; auid&nbsp;&geq;&nbsp;1000 only.
      </Typography.Paragraph>
      <Table
        rowKey={(r, idx) => `${r.ts}-${r.pid}-${idx}`}
        dataSource={data?.events ?? []}
        loading={isLoading}
        tableLayout="fixed"
        scroll={{ x: "max-content" }}
        size="small"
        pagination={{ pageSize: 50 }}
        columns={[
          { title: "Time", dataIndex: "ts", width: 200, render: (v: string) => v || "—" },
          { title: "User", dataIndex: "username", width: 140, render: (v?: string, r?) => v || r?.auid || "—" },
          { title: "Binary", dataIndex: "comm", width: 140, render: (v?: string) => v ? <code>{v}</code> : "—" },
          { title: "Exe", dataIndex: "exe", ellipsis: { showTitle: false } as const, render: (v?: string) => v ? <Tooltip title={v}><code>{v}</code></Tooltip> : "—" },
          { title: "PID", dataIndex: "pid", width: 90 },
          { title: "PPID", dataIndex: "ppid", width: 90 },
        ]}
      />
    </Card>
  );
}

// ---- Page shell with sub-tabs ----
const SUB_KEYS = ["overview", "quarantine", "events", "scan", "yara", "exec-audit", "settings"] as const;
type SubKey = (typeof SUB_KEYS)[number];

export const AdminSecurityMalware = () => {
  const [active, setActive] = useState<SubKey>("overview");

  return (
    <Tabs
      activeKey={active}
      onChange={(k) => setActive(k as SubKey)}
      items={[
        { key: "overview", label: "Overview", children: <OverviewCard /> },
        { key: "quarantine", label: "Quarantine", children: <QuarantineCard /> },
        { key: "events", label: "Events", children: <EventsCard /> },
        { key: "scan", label: "Manual scan", children: <ManualScanCard /> },
        { key: "yara", label: "YARA rules", children: <YARACard /> },
        { key: "exec-audit", label: "Exec audit", children: <ExecAuditCard /> },
        { key: "settings", label: "Settings", children: <SettingsCard /> },
      ]}
    />
  );
};
