/**
 * JailsTab — list/detail layout for per-jail configuration editing.
 *
 * Left pane: jail names with Active/Inactive badges, sorted with active on
 * top.  Right pane: editable form for the selected jail plus a collapsible
 * raw-config editor.
 */

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  Badge,
  Button,
  Combobox,
  Field,
  Input,
  MessageBar,
  MessageBarBody,
  Option,
  Select,
  Skeleton,
  SkeletonItem,
  Spinner,
  Switch,
  Text,
  tokens,
} from "@fluentui/react-components";
import {
  Add24Regular,
  Dismiss24Regular,
  LockClosed24Regular,
  LockOpen24Regular,
  Play24Regular,
} from "@fluentui/react-icons";
import { ApiError } from "../../api/client";
import { handleFetchError } from "../../utils/fetchError";
import type {
  AddLogPathRequest,
  BackendType,
  DNSMode,
  InactiveJail,
  JailConfig,
  JailConfigUpdate,
  JailValidationIssue,
  JailValidationResult,
  LogEncoding,
} from "../../types/config";
import { useAutoSave } from "../../hooks/useAutoSave";
import { useConfigActiveStatus } from "../../hooks/useConfigActiveStatus";
import { useJailConfigs } from "../../hooks/useJailConfigs";
import { useJailAdmin } from "../../hooks/useJailAdmin";
import { useJailConfigOperations } from "../../hooks/useJailConfigOperations";
import { ActivateJailDialog } from "./ActivateJailDialog";
import { AutoSaveIndicator } from "./AutoSaveIndicator";
import { ConfigListDetail } from "./ConfigListDetail";
import { CreateJailDialog } from "./CreateJailDialog";
import { RawConfigSection } from "./RawConfigSection";
import { RegexList } from "./RegexList";
import { useConfigStyles } from "./configStyles";

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const BACKENDS = [
  { value: "auto", label: "auto — pyinotify, then polling" },
  { value: "polling", label: "polling — standard polling algorithm" },
  { value: "pyinotify", label: "pyinotify — requires pyinotify library" },
  { value: "systemd", label: "systemd — uses systemd journal" },
  { value: "gamin", label: "gamin — legacy file alteration monitor" },
] as const;

const LOG_ENCODINGS = [
  { value: "auto", label: "auto — use system locale" },
  { value: "ascii", label: "ascii" },
  { value: "utf-8", label: "utf-8" },
  { value: "latin-1", label: "latin-1 (ISO 8859-1)" },
] as const;

const DATE_PATTERN_PRESETS = [
  { value: "", label: "auto-detect (leave blank)" },
  { value: "{^LN-BEG}", label: "{^LN-BEG} — line beginning" },
  { value: "%%Y-%%m-%%d %%H:%%M:%%S", label: "YYYY-MM-DD HH:MM:SS" },
  { value: "%%d/%%b/%%Y:%%H:%%M:%%S", label: "DD/Mon/YYYY:HH:MM:SS (Apache)" },
  { value: "%%b %%d %%H:%%M:%%S", label: "Mon DD HH:MM:SS (syslog)" },
  { value: "EPOCH", label: "EPOCH — Unix timestamp" },
  { value: "%%Y-%%m-%%dT%%H:%%M:%%S", label: "ISO 8601" },
  { value: "TAI64N", label: "TAI64N" },
] as const;

// ---------------------------------------------------------------------------
// JailConfigDetail
// ---------------------------------------------------------------------------

interface JailConfigDetailProps {
  jail: JailConfig;
  onSave: (name: string, update: JailConfigUpdate) => Promise<void>;
  onDeactivate?: () => void;
  /** When true all fields are read-only and auto-save is suppressed. */
  readOnly?: boolean;
  /** When provided (and readOnly=true) shows an Activate Jail button. */
  onActivate?: () => void;
  /** When provided (and readOnly=true) shows a Validate Config button. */
  onValidate?: () => void;
  /** Whether validation is currently running (shows spinner on Validate button). */
  validating?: boolean;
}

/**
 * Editable configuration form for a single fail2ban jail.
 *
 * Contains all config fields plus a collapsible raw-config editor at the
 * bottom.  Previously known as JailAccordionPanel.
 *
 * @param props - Component props.
 * @returns JSX element.
 */
function JailConfigDetail({
  jail,
  onSave,
  onDeactivate,
  readOnly = false,
  onActivate,
  onValidate,
  validating = false,
}: JailConfigDetailProps): React.JSX.Element {
  const styles = useConfigStyles();
  const [banTime, setBanTime] = useState(String(jail.ban_time));
  const [findTime, setFindTime] = useState(String(jail.find_time));
  const [maxRetry, setMaxRetry] = useState(String(jail.max_retry));
  const [failRegex, setFailRegex] = useState<string[]>(jail.fail_regex);
  const [ignoreRegex, setIgnoreRegex] = useState<string[]>(jail.ignore_regex);
  const [logPaths, setLogPaths] = useState<string[]>(jail.log_paths);
  const [datePattern, setDatePattern] = useState(jail.date_pattern ?? "");
  const [dnsMode, setDnsMode] = useState<DNSMode>(jail.use_dns);
  const [backend, setBackend] = useState<BackendType>(jail.backend);
  const [logEncoding, setLogEncoding] = useState<LogEncoding>(jail.log_encoding);
  const [prefRegex, setPrefRegex] = useState(jail.prefregex);
  const [deletingPath, setDeletingPath] = useState<string | null>(null);
  const [newLogPath, setNewLogPath] = useState("");
  const [newLogPathTail, setNewLogPathTail] = useState(true);
  const [addingLogPath, setAddingLogPath] = useState(false);
  const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);

  // Ban-time escalation state
  const esc0 = jail.bantime_escalation;
  const [escEnabled, setEscEnabled] = useState(esc0?.increment ?? false);
  const [escFactor, setEscFactor] = useState(
    esc0?.factor != null ? String(esc0.factor) : "",
  );
  const [escFormula, setEscFormula] = useState(esc0?.formula ?? "");
  const [escMultipliers, setEscMultipliers] = useState(
    esc0?.multipliers ?? "",
  );
  const [escMaxTime, setEscMaxTime] = useState(
    esc0?.max_time != null ? String(esc0.max_time) : "",
  );
  const [escRndTime, setEscRndTime] = useState(
    esc0?.rnd_time != null ? String(esc0.rnd_time) : "",
  );
  const [escOverallJails, setEscOverallJails] = useState(
    esc0?.overall_jails ?? false,
  );

  const {
    addLogPath: addLogPathToJail,
    deleteLogPath: deleteJailLogPath,
    fetchRawContent,
    saveRawContent,
  } = useJailConfigOperations(jail.name);

  const handleDeleteLogPath = useCallback(
    async (path: string) => {
      setDeletingPath(path);
      setMsg(null);
      try {
        await deleteJailLogPath(path);
        setLogPaths((prev) => prev.filter((p) => p !== path));
        setMsg({ text: `Removed log path: ${path}`, ok: true });
      } catch (err: unknown) {
        setMsg({
          text: err instanceof ApiError ? err.message : "Delete failed.",
          ok: false,
        });
      } finally {
        setDeletingPath(null);
      }
    },
    [deleteJailLogPath],
  );

  const handleAddLogPath = useCallback(async () => {
    const trimmed = newLogPath.trim();
    if (!trimmed) return;
    setAddingLogPath(true);
    setMsg(null);
    try {
      const req: AddLogPathRequest = { log_path: trimmed, tail: newLogPathTail };
      await addLogPathToJail(req);
      setLogPaths((prev) => [...prev, trimmed]);
      setNewLogPath("");
      setMsg({ text: `Added log path: ${trimmed}`, ok: true });
    } catch (err: unknown) {
      setMsg({
        text: err instanceof ApiError ? err.message : "Failed to add log path.",
        ok: false,
      });
    } finally {
      setAddingLogPath(false);
    }
  }, [addLogPathToJail, newLogPath, newLogPathTail]);

  const autoSavePayload = useMemo<JailConfigUpdate>(
    () => ({
      ban_time: banTime.trim() === "" || Number.isNaN(Number(banTime)) ? jail.ban_time : Number(banTime),
      find_time: findTime.trim() === "" || Number.isNaN(Number(findTime)) ? jail.find_time : Number(findTime),
      max_retry: maxRetry.trim() === "" || Number.isNaN(Number(maxRetry)) ? jail.max_retry : Number(maxRetry),
      fail_regex: failRegex,
      ignore_regex: ignoreRegex,
      date_pattern: datePattern !== "" ? datePattern : null,
      dns_mode: dnsMode,
      log_encoding: logEncoding,
      prefregex: prefRegex !== "" ? prefRegex : null,
      bantime_escalation: {
        increment: escEnabled,
        factor: escFactor !== "" ? Number(escFactor) : null,
        formula: escFormula !== "" ? escFormula : null,
        multipliers: escMultipliers !== "" ? escMultipliers : null,
        max_time: escMaxTime !== "" ? Number(escMaxTime) : null,
        rnd_time: escRndTime !== "" ? Number(escRndTime) : null,
        overall_jails: escOverallJails,
      },
    }),
    [
      banTime, findTime, maxRetry, failRegex, ignoreRegex, datePattern,
      dnsMode, logEncoding, prefRegex, escEnabled, escFactor,
      escFormula, escMultipliers, escMaxTime, escRndTime, escOverallJails,
      jail.ban_time, jail.find_time, jail.max_retry,
    ],
  );

  const saveCurrent = useCallback(
    async (update: JailConfigUpdate): Promise<void> => {
      if (readOnly) return;
      await onSave(jail.name, update);
    },
    [jail.name, onSave, readOnly],
  );

  const { status: saveStatus, errorText: saveErrorText, retry: retrySave } =
    useAutoSave(autoSavePayload, saveCurrent);

  // Ref to track the last jail object we synced from. When the incoming jail
  // prop is a new object (indicating a server refresh), and autoSave is idle
  // with no pending changes, we reset the form to the new server state.
  const lastSyncedJailRef = useRef<JailConfig>(jail);

  // Resync form fields when the jail prop changes identity (server refresh)
  // but only if autoSave has completed and there are no pending changes.
  useEffect(() => {
    if (
      lastSyncedJailRef.current !== jail &&
      saveStatus === "idle" &&
      !saveErrorText
    ) {
      // Payload is equal to the server state, so we can safely reset.
      if (
        JSON.stringify(autoSavePayload) === JSON.stringify({
          ban_time: lastSyncedJailRef.current.ban_time,
          find_time: lastSyncedJailRef.current.find_time,
          max_retry: lastSyncedJailRef.current.max_retry,
          fail_regex: lastSyncedJailRef.current.fail_regex,
          ignore_regex: lastSyncedJailRef.current.ignore_regex,
          date_pattern: lastSyncedJailRef.current.date_pattern ?? null,
          dns_mode: lastSyncedJailRef.current.use_dns,
          log_encoding: lastSyncedJailRef.current.log_encoding,
          prefregex: lastSyncedJailRef.current.prefregex,
          bantime_escalation: {
            increment: lastSyncedJailRef.current.bantime_escalation?.increment ?? false,
            factor: lastSyncedJailRef.current.bantime_escalation?.factor ?? null,
            formula: lastSyncedJailRef.current.bantime_escalation?.formula ?? null,
            multipliers: lastSyncedJailRef.current.bantime_escalation?.multipliers ?? null,
            max_time: lastSyncedJailRef.current.bantime_escalation?.max_time ?? null,
            rnd_time: lastSyncedJailRef.current.bantime_escalation?.rnd_time ?? null,
            overall_jails: lastSyncedJailRef.current.bantime_escalation?.overall_jails ?? false,
          },
        })
      ) {
        // Reset all form state to new jail prop values.
        setBanTime(String(jail.ban_time));
        setFindTime(String(jail.find_time));
        setMaxRetry(String(jail.max_retry));
        setFailRegex(jail.fail_regex);
        setIgnoreRegex(jail.ignore_regex);
        setLogPaths(jail.log_paths);
        setDatePattern(jail.date_pattern ?? "");
        setDnsMode(jail.use_dns);
        setBackend(jail.backend);
        setLogEncoding(jail.log_encoding);
        setPrefRegex(jail.prefregex);
        setEscEnabled(jail.bantime_escalation?.increment ?? false);
        setEscFactor(
          jail.bantime_escalation?.factor != null
            ? String(jail.bantime_escalation.factor)
            : "",
        );
        setEscFormula(jail.bantime_escalation?.formula ?? "");
        setEscMultipliers(jail.bantime_escalation?.multipliers ?? "");
        setEscMaxTime(
          jail.bantime_escalation?.max_time != null
            ? String(jail.bantime_escalation.max_time)
            : "",
        );
        setEscRndTime(
          jail.bantime_escalation?.rnd_time != null
            ? String(jail.bantime_escalation.rnd_time)
            : "",
        );
        setEscOverallJails(jail.bantime_escalation?.overall_jails ?? false);
      }
      lastSyncedJailRef.current = jail;
    }
  }, [jail, saveStatus, saveErrorText, autoSavePayload]);

  // Raw config file fetch/save helpers — uses jail.d/<name>.conf convention.
  const fetchRaw = useCallback(async (): Promise<string> => {
    return await fetchRawContent();
  }, [fetchRawContent]);

  const saveRaw = useCallback(
    async (content: string): Promise<void> => {
      await saveRawContent(content);
    },
    [saveRawContent],
  );

  return (
    <div>
      {msg && (
        <MessageBar intent={msg.ok ? "success" : "error"}>
          <MessageBarBody>{msg.text}</MessageBarBody>
        </MessageBar>
      )}
      <div className={styles.fieldRowThree}>
        <Field label="Ban Time (s)">
          <Input
            type="number"
            value={banTime}
            readOnly={readOnly}
            onChange={(_e, d) => {
              setBanTime(d.value);
            }}
          />
        </Field>
        <Field label="Find Time (s)">
          <Input
            type="number"
            value={findTime}
            readOnly={readOnly}
            onChange={(_e, d) => {
              setFindTime(d.value);
            }}
          />
        </Field>
        <Field label="Max Retry">
          <Input
            type="number"
            value={maxRetry}
            readOnly={readOnly}
            onChange={(_e, d) => {
              setMaxRetry(d.value);
            }}
          />
        </Field>
      </div>
      <div className={styles.fieldRow}>
        <Field label="Backend">
          <Select
            value={backend}
            disabled={readOnly}
            onChange={(_e, d) => {
              setBackend(d.value as BackendType);
            }}
          >
            {BACKENDS.map((b) => (
              <option key={b.value} value={b.value}>{b.label}</option>
            ))}
          </Select>
        </Field>
        <Field label="Log Encoding">
          <Select
            value={logEncoding.toLowerCase()}
            disabled={readOnly}
            onChange={(_e, d) => {
              setLogEncoding(d.value as LogEncoding);
            }}
          >
            {LOG_ENCODINGS.map((e) => (
              <option key={e.value} value={e.value}>{e.label}</option>
            ))}
          </Select>
        </Field>
      </div>
      <div className={styles.fieldRow}>
        <Field label="Date Pattern" hint="Leave blank for auto-detect.">
          <Combobox
            className={styles.codeFont}
            placeholder="auto-detect"
            value={datePattern}
            selectedOptions={[datePattern]}
            freeform
            disabled={readOnly}
            onOptionSelect={(_e, d) => {
              setDatePattern(d.optionValue ?? "");
            }}
            onChange={(e) => {
              setDatePattern(e.target.value);
            }}
          >
            {DATE_PATTERN_PRESETS.map((p) => (
              <Option key={p.value} value={p.value}>{p.label}</Option>
            ))}
          </Combobox>
        </Field>
        <Field label="DNS Mode" hint={"\u00A0"}>
          <Select
            value={dnsMode}
            disabled={readOnly}
            onChange={(_e, d) => {
              setDnsMode(d.value as DNSMode);
            }}
          >
            <option value="yes">yes — resolve hostnames</option>
            <option value="warn">warn — resolve and warn</option>
            <option value="no">no — skip hostname resolution</option>
            <option value="raw">raw — use value as-is</option>
          </Select>
        </Field>
      </div>
      <Field
        label="Prefix Regex"
        hint="Prepended to every failregex for pre-filtering. Leave blank to disable."
      >
        <Input
          className={styles.codeFont}
          placeholder="e.g. ^%(__prefix_line)s"
          value={prefRegex}
          readOnly={readOnly}
          onChange={(_e, d) => {
            setPrefRegex(d.value);
          }}
        />
      </Field>
      <Field label="Log Paths">
        {logPaths.length === 0 ? (
          <Text className={styles.infoText} size={200}>
            (none)
          </Text>
        ) : (
          logPaths.map((p, i) => (
            <div key={p} className={styles.regexItem}>
              <Input
                className={styles.codeFont}
                style={{ flexGrow: 1 }}
                value={p}
                readOnly={readOnly}
                onChange={(_e, d) => {
                  setLogPaths((prev) => prev.map((v, j) => (j === i ? d.value : v)));
                }}
              />
              {!readOnly && (
                <Button
                  appearance="subtle"
                  icon={<Dismiss24Regular />}
                  size="small"
                  disabled={deletingPath === p}
                  title="Remove log path"
                  onClick={() => void handleDeleteLogPath(p)}
                />
              )}
            </div>
          ))
        )}
        {/* Add log path inline form — hidden in read-only mode */}
        {!readOnly && (
        <div className={styles.regexItem} style={{ marginTop: tokens.spacingVerticalXS }}>
          <Input
            className={styles.codeFont}
            style={{ flexGrow: 1 }}
            placeholder="/var/log/example.log"
            value={newLogPath}
            disabled={addingLogPath}
            aria-label="New log path"
            onChange={(_e, d) => {
              setNewLogPath(d.value);
            }}
          />
          <Switch
            label={newLogPathTail ? "tail" : "head"}
            checked={newLogPathTail}
            onChange={(_e, d) => {
              setNewLogPathTail(d.checked);
            }}
          />
          <Button
            appearance="primary"
            size="small"
            aria-label="Add log path"
            disabled={addingLogPath || !newLogPath.trim()}
            onClick={() => void handleAddLogPath()}
          >
            {addingLogPath ? "Adding…" : "Add"}
          </Button>
        </div>
        )}
      </Field>
      <div style={{ marginTop: tokens.spacingVerticalS }}>
        <RegexList
          label="Fail Regex"
          patterns={failRegex}
          onChange={setFailRegex}
          readOnly={readOnly}
        />
      </div>
      <div style={{ marginTop: tokens.spacingVerticalS }}>
        <RegexList
          label="Ignore Regex"
          patterns={ignoreRegex}
          onChange={setIgnoreRegex}
          readOnly={readOnly}
        />
      </div>
      {jail.actions.length > 0 && (
        <Field label="Actions">
          <div>
            {jail.actions.map((a) => (
              <Badge
                key={a}
                appearance="tint"
                color="informative"
                style={{ marginRight: tokens.spacingHorizontalXS }}
              >
                {a}
              </Badge>
            ))}
          </div>
        </Field>
      )}

      {/* Ban-time Escalation */}
      <div style={{ marginTop: tokens.spacingVerticalM }}>
        <Text weight="semibold" size={400} block>
          Ban-time Escalation
        </Text>
        <Switch
          label="Enable incremental banning"
          checked={escEnabled}
          disabled={readOnly}
          onChange={(_e, d) => {
            setEscEnabled(d.checked);
          }}
        />
        {escEnabled && (
          <div>
            <div className={styles.fieldRowThree}>
              <Field label="Factor">
                <Input
                  type="number"
                  value={escFactor}
                  readOnly={readOnly}
                  onChange={(_e, d) => {
                    setEscFactor(d.value);
                  }}
                />
              </Field>
              <Field label="Max Time (s)">
                <Input
                  type="number"
                  value={escMaxTime}
                  readOnly={readOnly}
                  onChange={(_e, d) => {
                    setEscMaxTime(d.value);
                  }}
                />
              </Field>
              <Field label="Random Jitter (s)">
                <Input
                  type="number"
                  value={escRndTime}
                  readOnly={readOnly}
                  onChange={(_e, d) => {
                    setEscRndTime(d.value);
                  }}
                />
              </Field>
            </div>
            <Field label="Formula">
              <Input
                value={escFormula}
                readOnly={readOnly}
                onChange={(_e, d) => {
                  setEscFormula(d.value);
                }}
              />
            </Field>
            <Field label="Multipliers (space-separated)">
              <Input
                value={escMultipliers}
                readOnly={readOnly}
                onChange={(_e, d) => {
                  setEscMultipliers(d.value);
                }}
              />
            </Field>
            <Switch
              label="Count repeat offences across all jails"
              checked={escOverallJails}
              disabled={readOnly}
              onChange={(_e, d) => {
                setEscOverallJails(d.checked);
              }}
            />
          </div>
        )}
      </div>

      {!readOnly && (
        <div style={{ marginTop: tokens.spacingVerticalS }}>
          <AutoSaveIndicator
            status={saveStatus}
            errorText={saveErrorText}
            onRetry={retrySave}
          />
        </div>
      )}

      {!readOnly && onDeactivate !== undefined && (
        <div style={{ marginTop: tokens.spacingVerticalM }}>
          <Button
            appearance="secondary"
            icon={<LockOpen24Regular />}
            onClick={onDeactivate}
          >
            Deactivate Jail
          </Button>
        </div>
      )}

      {readOnly && (onActivate !== undefined || onValidate !== undefined || onDeactivate !== undefined) && (
        <div style={{ marginTop: tokens.spacingVerticalM, display: "flex", gap: tokens.spacingHorizontalS, flexWrap: "wrap" }}>
          {onValidate !== undefined && (
            <Button
              appearance="secondary"
              icon={validating ? <Spinner size="tiny" /> : undefined}
              onClick={onValidate}
              disabled={validating}
            >
              {validating ? "Validating…" : "Validate Config"}
            </Button>
          )}
          {onDeactivate !== undefined && (
            <Button
              appearance="secondary"
              icon={<LockOpen24Regular />}
              onClick={onDeactivate}
            >
              Deactivate Jail
            </Button>
          )}
          {onActivate !== undefined && (
            <Button
              appearance="primary"
              icon={<Play24Regular />}
              onClick={onActivate}
            >
              Activate Jail
            </Button>
          )}
        </div>
      )}

      {/* Raw Configuration — hidden in read-only (inactive jail) mode */}
      {!readOnly && (
        <div style={{ marginTop: tokens.spacingVerticalL }}>
          <RawConfigSection
            fetchContent={fetchRaw}
            saveContent={saveRaw}
            label="Raw Jail Configuration"
          />
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// InactiveJailDetail
// ---------------------------------------------------------------------------

interface InactiveJailDetailProps {
  jail: InactiveJail;
  onActivate: () => void;
  /** Called when the user requests removal of the .local override file. */
  onDeactivate?: () => void;
  onValidate: () => Promise<JailValidationResult>;
}

/**
 * Detail view for an inactive jail.
 *
 * Maps the parsed config fields to a JailConfig-compatible object and renders
 * JailConfigDetail in read-only mode, so the UI is identical to the active
 * jail view but with all fields disabled and an Activate button instead of
 * a Deactivate button.
 *
 * @param props - Component props.
 * @returns JSX element.
 */
function InactiveJailDetail({
  jail,
  onActivate,
  onDeactivate,
  onValidate,
}: InactiveJailDetailProps): React.JSX.Element {
  const styles = useConfigStyles();
  const [validating, setValidating] = useState(false);
  const [validationResult, setValidationResult] = useState<JailValidationResult | null>(null);
  const [validationError, setValidationError] = useState<string | null>(null);

  const handleValidate = useCallback((): void => {
    setValidating(true);
    setValidationResult(null);
    setValidationError(null);
    onValidate()
      .then((result) => { setValidationResult(result); })
      .catch((err: unknown) => {
        handleFetchError(err, setValidationError, "Validation request failed.");
      })
      .finally(() => { setValidating(false); });
  }, [onValidate]);

  const blockingIssues: JailValidationIssue[] =
    validationResult?.issues.filter((i) => i.field !== "logpath") ?? [];
  const advisoryIssues: JailValidationIssue[] =
    validationResult?.issues.filter((i) => i.field === "logpath") ?? [];

  const jailConfig = useMemo<JailConfig>(
    () => ({
      name: jail.name,
      ban_time: jail.ban_time_seconds,
      find_time: jail.find_time_seconds,
      max_retry: jail.maxretry ?? 5,
      fail_regex: jail.fail_regex,
      ignore_regex: jail.ignore_regex,
      log_paths: jail.logpath,
      date_pattern: jail.date_pattern,
      log_encoding: jail.log_encoding,
      backend: jail.backend,
      use_dns: jail.use_dns,
      prefregex: jail.prefregex,
      actions: jail.actions,
      bantime_escalation: jail.bantime_escalation,
    }),
    [jail],
  );

  return (
    <div>
      <div className={styles.fieldRow} style={{ marginBottom: tokens.spacingVerticalS }}>
        <Field label="Filter">
          <Input readOnly value={jail.filter || "(none)"} className={styles.codeFont} />
        </Field>
        <Field label="Port">
          <Input readOnly value={jail.port ?? "(auto)"} />
        </Field>
      </div>
      <Field label="Source file" style={{ marginBottom: tokens.spacingVerticalM }}>
        <Input readOnly value={jail.source_file} className={styles.codeFont} />
      </Field>

      {/* Validation error panel */}
      {validationError !== null && (
        <div style={{ marginBottom: tokens.spacingVerticalM }}>
          <MessageBar intent="error">
            <MessageBarBody>{validationError}</MessageBarBody>
          </MessageBar>
        </div>
      )}

      {/* Validation result panel */}
      {validationResult !== null && validationError === null && (
        <div style={{ marginBottom: tokens.spacingVerticalM }}>
          {blockingIssues.length === 0 && advisoryIssues.length === 0 ? (
            <MessageBar intent="success">
              <MessageBarBody>Configuration is valid.</MessageBarBody>
            </MessageBar>
          ) : null}
          {blockingIssues.length > 0 && (
            <MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalXS }}>
              <MessageBarBody>
                <strong>Errors:</strong>
                <ul style={{ margin: `4px 0 0 0`, paddingLeft: "1.2em" }}>
                  {blockingIssues.map((issue, idx) => (
                    <li key={`${issue.field}-${String(idx)}`}><em>{issue.field}:</em> {issue.message}</li>
                  ))}
                </ul>
              </MessageBarBody>
            </MessageBar>
          )}
          {advisoryIssues.length > 0 && (
            <MessageBar intent="warning">
              <MessageBarBody>
                <strong>Warnings:</strong>
                <ul style={{ margin: `4px 0 0 0`, paddingLeft: "1.2em" }}>
                  {advisoryIssues.map((issue, idx) => (
                    <li key={`${issue.field}-${String(idx)}`}><em>{issue.field}:</em> {issue.message}</li>
                  ))}
                </ul>
              </MessageBarBody>
            </MessageBar>
          )}
        </div>
      )}

      <JailConfigDetail
        jail={jailConfig}
        onSave={async () => { /* read-only — never called */ }}
        readOnly
        onActivate={onActivate}
        onDeactivate={jail.has_local_override ? onDeactivate : undefined}
        onValidate={handleValidate}
        validating={validating}
      />
    </div>
  );
}

// ---------------------------------------------------------------------------
// JailsTab
// ---------------------------------------------------------------------------

/**
 * Tab component showing all fail2ban jails in a list/detail layout with
 * editable configuration forms.
 *
 * @returns JSX element.
 */
interface JailsTabProps {
  /** Jail name to pre-select when the component mounts. */
  initialJail?: string;
}

export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
  const styles = useConfigStyles();
  const { jails, loading, error, refresh, updateJail } =
    useJailConfigs();
  const { activeJails } = useConfigActiveStatus();
  const {
    inactiveJails,
    inactiveLoading,
    refreshInactiveJails,
    deactivateJail,
    deleteJailLocalOverride,
    validateJailConfig,
    activateJail,
    createJailConfigFile,
  } = useJailAdmin();
  const [selectedName, setSelectedName] = useState<string | null>(null);
  const [createDialogOpen, setCreateDialogOpen] = useState(false);
  const [activateTarget, setActivateTarget] = useState<InactiveJail | null>(null);

  const handleDeactivate = useCallback(
    async (name: string): Promise<void> => {
      try {
        await deactivateJail(name);
        setSelectedName(null);
        refresh();
        refreshInactiveJails();
      } catch {
        /* non-critical — list refreshes on next load */ }
    },
    [deactivateJail, refresh, refreshInactiveJails],
  );

  const handleDeactivateInactive = useCallback(
    async (name: string): Promise<void> => {
      try {
        await deleteJailLocalOverride(name);
        setSelectedName(null);
        refreshInactiveJails();
      } catch {
        /* non-critical — list refreshes on next load */ }
    },
    [deleteJailLocalOverride, refreshInactiveJails],
  );

  const handleActivated = useCallback((): void => {
    setActivateTarget(null);
    setSelectedName(null);
    refresh();
    refreshInactiveJails();
  }, [refresh, refreshInactiveJails]);

  const listItems = useMemo<Array<{ name: string; kind: "active" | "inactive" }>>(() => {
    const activeItems = jails.map((j) => ({ name: j.name, kind: "active" as const }));
    const activeNames = new Set(jails.map((j) => j.name));
    const inactiveItems = inactiveJails
      .filter((j) => !activeNames.has(j.name))
      .map((j) => ({ name: j.name, kind: "inactive" as const }));
    return [...activeItems, ...inactiveItems];
  }, [jails, inactiveJails]);

  useEffect(() => {
    if (!initialJail || selectedName) return;
    if (listItems.some((item) => item.name === initialJail)) {
      setSelectedName(initialJail);
    }
  }, [initialJail, listItems, selectedName]);

  const activeJailMap = useMemo(
    () => new Map(jails.map((j) => [j.name, j])),
    [jails],
  );
  const inactiveJailMap = useMemo(
    () => new Map(inactiveJails.map((j) => [j.name, j])),
    [inactiveJails],
  );

  const selectedListItem = listItems.find((item) => item.name === selectedName);
  const selectedActiveJail =
    selectedListItem?.kind === "active"
      ? activeJailMap.get(selectedListItem.name)
      : undefined;
  const selectedInactiveJail =
    selectedListItem?.kind === "inactive"
      ? inactiveJailMap.get(selectedListItem.name)
      : undefined;

  if (loading && listItems.length === 0) {
    return (
      <Skeleton aria-label="Loading jail configs…">
        {[0, 1, 2].map((i) => (
          <SkeletonItem key={i} size={40} style={{ marginBottom: 4 }} />
        ))}
      </Skeleton>
    );
  }

  if (error) {
    return (
      <MessageBar intent="error">
        <MessageBarBody>{error}</MessageBarBody>
      </MessageBar>
    );
  }

  if (listItems.length === 0 && !loading && !inactiveLoading) {
    return (
      <div className={styles.emptyState}>
        <LockClosed24Regular
          style={{ fontSize: 48, color: tokens.colorNeutralForeground3 }}
          aria-hidden
        />
        <Text size={400} style={{ color: tokens.colorNeutralForeground3 }}>
          No jails found.
        </Text>
        <Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
          Ensure fail2ban is running and jails are configured.
        </Text>
      </div>
    );
  }

  const listHeader = (
    <Button
      appearance="outline"
      icon={<Add24Regular />}
      size="small"
      onClick={() => { setCreateDialogOpen(true); }}
    >
      Create Config
    </Button>
  );

  return (
    <div className={styles.tabContent}>
      <div style={{ marginTop: tokens.spacingVerticalM }}>
        <ConfigListDetail
          items={listItems}
          isActive={(item) => item.kind === "active" && activeJails.has(item.name)}
          selectedName={selectedName}
          onSelect={setSelectedName}
          loading={false}
          error={null}
          listHeader={listHeader}
        >
          {selectedActiveJail !== undefined ? (
            <JailConfigDetail
              key={selectedActiveJail.name}
              jail={selectedActiveJail}
              onSave={updateJail}
              onDeactivate={() => { void handleDeactivate(selectedActiveJail.name); }}
            />
          ) : selectedInactiveJail !== undefined ? (
            <InactiveJailDetail
              key={selectedInactiveJail.name}
              jail={selectedInactiveJail}
              onActivate={() => { setActivateTarget(selectedInactiveJail); }}
              onDeactivate={
                selectedInactiveJail.has_local_override
                  ? (): void => { void handleDeactivateInactive(selectedInactiveJail.name); }
                  : undefined
              }
              onValidate={async () => validateJailConfig(selectedInactiveJail.name)}
            />
          ) : null}
        </ConfigListDetail>
      </div>

      <ActivateJailDialog
        jail={activateTarget}
        open={activateTarget !== null}
        onClose={() => { setActivateTarget(null); }}
        onActivated={handleActivated}
        onValidate={async () => {
          if (!activateTarget) {
            return { jail_name: "", valid: false, issues: [] };
          }
          return await validateJailConfig(activateTarget.name);
        }}
        onActivate={async (payload) => {
          if (!activateTarget) {
            return {
              name: "",
              active: false,
              message: "No jail selected.",
              fail2ban_running: false,
              validation_warnings: [],
            };
          }
          return await activateJail(activateTarget.name, payload);
        }}
      />

      <CreateJailDialog
        open={createDialogOpen}
        onClose={() => { setCreateDialogOpen(false); }}
        onCreateJail={async (payload) => {
          await createJailConfigFile(payload);
        }}
        onCreated={() => {
          setCreateDialogOpen(false);
          refresh();
          refreshInactiveJails();
        }}
      />
    </div>
  );
}
