import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { api } from '../api.js';
import {
  accessTabForKey,
  connectionAction,
  connectionDurationParts,
  copyText,
  isSubscriptionUrlValid,
  localProxyUrls,
  subscriptionDomain,
  subscriptionDaysLeft,
  subscriptionUsage,
} from '../utils/clientControls.js';
import { formatBytes } from '../utils/format.js';
import { instructionBlocks } from '../instructions.js';
import { operationBlocked } from '../state/operations.js';
import { ConfirmationPopup } from './ConfirmationPopup.jsx';
import { canAppendRouteRule } from '../../shared/routingRules.js';
import {
  HARBOR_VERSIONS,
  parseVersion,
  versionCompatibility,
} from '../../shared/versions.js';

const SUBSCRIPTION_REVEAL_DELAY_MS = 1350;
const DURATION_MODE_STORAGE_KEY = 'harbor-duration-mode';
const ACCESS_TABS = [
  ['gateway', 'Gateway'],
  ['proxy', 'Gateway Proxy'],
];

function CloudTooltip({ children }) {
  return <span className="client-tooltip" role="tooltip">{children}</span>;
}

const VERSION_PARTS = [
  ['major', 'Major'],
  ['minor', 'Minor'],
  ['hotfix', 'Hotfix'],
];

function VersionBadge({ code, component, componentKey, version, runtime, incompatible = false }) {
  const parsed = parseVersion(version);
  const values = parsed ? VERSION_PARTS.map(([key]) => parsed[key]) : ['–', '–', '–'];

  function description(key) {
    if (key === 'major') {
      return 'Уровень совместимости всей экосистемы. Все совместно работающие компоненты и клиенты должны иметь одинаковый major.';
    }
    if (key === 'minor') {
      return 'Уровень конкретного компонента и тесно связанной с ним части системы. Остальные клиенты с тем же major могут обновляться отдельно.';
    }
    return 'Локальное совместимое исправление этой версии. Не требует обновлять связанные компоненты или другие клиенты.';
  }

  return (
    <div className={`harbor-version${incompatible ? ' is-incompatible' : ''}`}>
      <span className="harbor-version-code" aria-hidden="true">{code}</span>
      <span className="harbor-version-number" aria-label={`${component} ${version || 'не определена'}`}>
        {VERSION_PARTS.map(([key, label], index) => {
          const tooltipId = `harbor-version-${componentKey}-${key}`;
          return <React.Fragment key={key}>
            {index > 0 && <span className="harbor-version-dot" aria-hidden="true">.</span>}
            <span
              className="harbor-version-part"
              tabIndex="0"
              aria-describedby={tooltipId}
            >
              {values[index]}
              <span className="harbor-version-tooltip" id={tooltipId} role="tooltip">
                <strong>{component} · {label} {values[index]}</strong>
                <span>{description(key)}</span>
                {runtime && <small>{runtime}</small>}
                {incompatible && <small className="is-warning">Версии Gateway несовместимы.</small>}
              </span>
            </span>
          </React.Fragment>;
        })}
      </span>
    </div>
  );
}

function VersionDisplay({ isGateway, versionInfo }) {
  const runtimeSingBox = versionInfo?.runtime?.singBox;
  if (!isGateway) {
    return <aside className="harbor-versions" aria-label="Версия Harbor">
      <VersionBadge
        code="M"
        component="Mac client"
        componentKey="macClient"
        version={versionInfo?.components?.macClient || HARBOR_VERSIONS.macClient}
        runtime={runtimeSingBox ? `Runtime: sing-box ${runtimeSingBox}` : null}
      />
    </aside>;
  }

  const backendVersion = versionInfo?.components?.gatewayBackend;
  const dataplaneVersion = versionInfo?.runtime?.dataplaneVersion;
  const compatibility = backendVersion && versionCompatibility({
    ...HARBOR_VERSIONS,
    gatewayBackend: backendVersion,
  });
  const incompatible = compatibility && !compatibility.compatible;
  return <aside className="harbor-versions" aria-label="Версии Harbor Gateway">
    <VersionBadge
      code="C"
      component="Gateway client UI"
      componentKey="gatewayClient"
      version={HARBOR_VERSIONS.gatewayClient}
      incompatible={incompatible}
    />
    <VersionBadge
      code="B"
      component="Gateway control backend"
      componentKey="gatewayBackend"
      version={backendVersion}
      incompatible={incompatible}
    />
    <VersionBadge
      code="D"
      component="Gateway dataplane"
      componentKey="gatewayDataplane"
      version={dataplaneVersion}
      runtime={runtimeSingBox ? `Runtime: sing-box ${runtimeSingBox}` : null}
    />
  </aside>;
}

function InlineError({ error, context }) {
  if (!error || error.context !== context) return null;
  return (
    <div className={`client-inline-error is-${context}`} role="alert">
      <span>{error.message}</span>
      {error.correlationId && (
        <small title={error.correlationId}>Код: {error.correlationId.slice(0, 8)}</small>
      )}
      {error.retry && <button type="button" onClick={error.retry}>Повторить</button>}
    </div>
  );
}

const operationProgress = {
  connection: ['connection', 'Меняем состояние подключения…'],
  serverApply: ['connection', 'Применяем сервер…'],
  subscriptionImport: ['subscription', 'Загружаем подписку…'],
  subscriptionDelete: ['subscription', 'Удаляем подписку…'],
  routeRules: ['routing', 'Применяем локальные правила…'],
};

function InlineProgress({ operations, context }) {
  const active = Object.entries(operationProgress).find(([key, [operationContext]]) => (
    operationContext === context && operations[key]?.status === 'running'
  ));
  if (!active) return null;
  return (
    <div className={`client-inline-error client-operation-progress is-${context}`} role="status">
      <span>{active[1][1]}</span>
    </div>
  );
}

function InstructionStep({ step }) {
  if (typeof step === 'string') return step;
  return (
    <>
      {step.before}
      <a href={step.link[1]} target="_blank" rel="noreferrer">{step.link[0]}</a>
      {step.after}
    </>
  );
}

function InstructionBlock({ block, open, onToggle }) {
  return (
    <section
      className={`client-instruction-block${open ? ' is-open' : ''}`}
      style={{ viewTransitionName: `instruction-${block.id}` }}
    >
      <button
        className="client-instruction-summary"
        type="button"
        aria-expanded={open}
        onClick={onToggle}
      >
        <span>{block.label}</span>
        <strong>{block.title}</strong>
        <small>{block.summary}</small>
        <i aria-hidden="true" />
      </button>
      <div className="client-instruction-reveal" aria-hidden={!open} inert={!open ? true : undefined}>
        <div className="client-instruction-body">
          {block.paragraphs?.map((paragraph) => <p key={paragraph}>{paragraph}</p>)}
          {block.steps && (
            <ol>
              {block.steps.map((step) => (
                <li key={typeof step === 'string' ? step : step.link[1]}>
                  <InstructionStep step={step} />
                </li>
              ))}
            </ol>
          )}
          {block.code && <code>{block.code}</code>}
          {block.note && <p className="client-instruction-note">{block.note}</p>}
        </div>
      </div>
    </section>
  );
}

const ROUTE_RULE_OPTIONS = [
  ['domain', 'Точный домен'],
  ['domain_suffix', 'Суффикс'],
  ['domain_keyword', 'Содержит'],
];

const ROUTE_RULE_PLACEHOLDERS = {
  domain: 'example.com или полный URL',
  domain_suffix: 'example.org',
  domain_keyword: 'cdn',
};

let localRuleDraftId = 0;
const createLocalRuleDraft = (rule) => ({
  ...rule,
  enabled: rule?.enabled !== false,
  _key: `route-rule-${localRuleDraftId += 1}`,
});
const localRuleValues = (rules) => rules
  .filter((rule) => !rule.removing)
  .map(({ type, value, enabled }) => ({ type, value, enabled }));
const localRulesSignature = (rules) => JSON.stringify(localRuleValues(rules));
const localRuleKey = ({ type, value, enabled }) => `${type}:${String(value || '').trim().toLowerCase()}:${enabled}`;

function localRuleStatus(rule, savedRules, activeRules) {
  const key = localRuleKey(rule);
  if (!savedRules.some((saved) => localRuleKey(saved) === key)) return ['unsaved', 'Не сохранено'];
  if (!rule.enabled) return ['disabled', 'Выключено'];
  if (activeRules.some((active) => localRuleKey(active) === key)) return ['active', 'Активно'];
  return ['pending', 'Ждёт перезапуска'];
}

function RuleTypePicker({ value, ruleKey, index, disabled, onChange }) {
  const [open, setOpen] = useState(false);
  const rootRef = useRef(null);
  const triggerRef = useRef(null);
  const optionRefs = useRef([]);
  const listId = `${ruleKey}-types`;
  const selectedIndex = Math.max(0, ROUTE_RULE_OPTIONS.findIndex(([type]) => type === value));

  useEffect(() => {
    if (!open) return undefined;
    optionRefs.current[selectedIndex]?.focus();
    const close = (event) => {
      if (event.type === 'keydown' && event.key !== 'Escape') return;
      if (rootRef.current?.contains(event.target)) return;
      setOpen(false);
    };
    document.addEventListener('pointerdown', close);
    document.addEventListener('keydown', close);
    return () => {
      document.removeEventListener('pointerdown', close);
      document.removeEventListener('keydown', close);
    };
  }, [open, selectedIndex]);

  function choose(type) {
    onChange(type);
    setOpen(false);
    triggerRef.current?.focus();
  }

  function moveOption(event, offset) {
    if (!['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'].includes(event.key)) return;
    event.preventDefault();
    if (event.key === 'Escape') {
      setOpen(false);
      triggerRef.current?.focus();
      return;
    }
    const current = optionRefs.current.indexOf(document.activeElement);
    const next = event.key === 'Home'
      ? 0
      : event.key === 'End'
        ? ROUTE_RULE_OPTIONS.length - 1
        : (current + offset + ROUTE_RULE_OPTIONS.length) % ROUTE_RULE_OPTIONS.length;
    optionRefs.current[next]?.focus();
  }

  return (
    <div className={`client-rule-type${open ? ' is-open' : ''}`} ref={rootRef}>
      <button
        ref={triggerRef}
        className="client-rule-type-trigger"
        type="button"
        aria-label={`Тип правила ${index + 1}`}
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-controls={listId}
        disabled={disabled}
        onClick={() => setOpen((current) => !current)}
        onKeyDown={(event) => {
          if (event.key === 'Escape' && open) {
            event.preventDefault();
            setOpen(false);
            return;
          }
          if (!['ArrowDown', 'ArrowUp'].includes(event.key)) return;
          event.preventDefault();
          setOpen(true);
        }}
      >
        <span>{ROUTE_RULE_OPTIONS[selectedIndex][1]}</span>
        <svg viewBox="0 0 12 8" aria-hidden="true"><path d="m1 1 5 5 5-5" /></svg>
      </button>
      <div className="client-rule-type-list" id={listId} role="listbox" aria-hidden={!open}>
        {ROUTE_RULE_OPTIONS.map(([type, label], optionIndex) => (
          <button
            ref={(node) => { optionRefs.current[optionIndex] = node; }}
            type="button"
            role="option"
            aria-selected={type === value}
            tabIndex={open ? 0 : -1}
            key={type}
            onClick={() => choose(type)}
            onKeyDown={(event) => moveOption(event, event.key === 'ArrowUp' ? -1 : 1)}
          >
            {label}
          </button>
        ))}
      </div>
    </div>
  );
}

function LocalRulesPanel({
  open,
  rules,
  savedRules,
  activeRules,
  blocked,
  dirty,
  restartPending,
  error,
  operations,
  panelRef,
  closeRef,
  onAdd,
  onChange,
  onRemove,
  onRemoveComplete,
  onClose,
  onSave,
}) {
  const draftRules = rules.filter((rule) => !rule.removing);
  const canAdd = canAppendRouteRule(draftRules) && !blocked;
  const incomplete = draftRules.some((rule) => !String(rule.value || '').trim());

  return (
    <aside
      ref={panelRef}
      id="client-local-rules"
      className={`client-local-rules${open ? ' is-open' : ''}`}
      aria-labelledby="local-rules-title"
      aria-hidden={!open}
      inert={!open ? true : undefined}
    >
      <div className="client-local-rules-sheet">
        <button
          ref={closeRef}
          className="client-drawer-close"
          type="button"
          aria-label="Закрыть локальные правила"
          onClick={onClose}
        >×</button>
        <header className="client-local-rules-header">
          <span>Маршрутизация</span>
          <button
            className="client-local-rules-save"
            type="submit"
            form="client-local-rules-form"
            disabled={blocked || !dirty}
          >
            Сохранить
          </button>
          <h2 id="local-rules-title">Локальные правила</h2>
          <p>Эти домены идут напрямую. Остальной трафик — через выбранный VPN.</p>
          {restartPending && (
            <p className="client-local-rules-runtime" role="status">
              Правила сохранены, но начнут работать после запуска или перезапуска sing-box.
            </p>
          )}
        </header>

        <form id="client-local-rules-form" className="client-local-rules-form" onSubmit={onSave}>
          <section className="client-local-rules-group" aria-labelledby="local-rules-list-title">
            <span id="local-rules-list-title">Правила</span>
            <div className="client-local-rules-list">
              {rules.map((rule, index) => {
                const [status, statusLabel] = localRuleStatus(rule, savedRules, activeRules);
                return (
                <div
                  className={`client-local-rule client-deletable-row is-${status}${rule.enabled ? '' : ' is-disabled'}${rule.removing ? ' is-removing' : ''}`}
                  key={rule._key}
                  style={{ viewTransitionName: rule.removing ? 'none' : rule._key }}
                  inert={rule.removing ? true : undefined}
                >
                  <button
                    className="client-local-rule-enabled"
                    type="button"
                    role="switch"
                    aria-checked={rule.enabled}
                    aria-label={`${rule.enabled ? 'Выключить' : 'Включить'} правило ${index + 1}`}
                    onClick={() => onChange(index, 'enabled', !rule.enabled)}
                  >
                    <svg viewBox="0 0 20 20" aria-hidden="true">
                      <circle cx="10" cy="10" r="6" />
                      <path className="client-rule-check" d="m6.8 10.1 2.1 2.2 4.5-5" />
                    </svg>
                  </button>
                  <RuleTypePicker
                    value={rule.type}
                    ruleKey={rule._key}
                    index={index}
                    disabled={rule.removing}
                    onChange={(type) => onChange(index, 'type', type)}
                  />
                  <input
                    type="text"
                    inputMode="url"
                    autoComplete="off"
                    spellCheck={false}
                    required
                    aria-label={`Значение правила ${index + 1}`}
                    placeholder={ROUTE_RULE_PLACEHOLDERS[rule.type]}
                    value={rule.value}
                    onChange={(event) => onChange(index, 'value', event.target.value)}
                  />
                  <span className="client-local-rule-status" role="status">{statusLabel}</span>
                  <button
                    className="client-local-rule-delete"
                    type="button"
                    aria-label={`Удалить правило ${index + 1}`}
                    onClick={() => onRemove(index)}
                  >
                    ×
                  </button>
                  <span
                    className="client-delete-strike"
                    aria-hidden="true"
                    onAnimationEnd={() => onRemoveComplete(rule._key)}
                  />
                </div>
                );
              })}
              {!rules.length && <p className="client-local-rules-empty">Правил пока нет. Весь трафик идёт через VPN.</p>}
            </div>
            <div className="client-local-rule-add-slot">
              <button className="client-local-rule-add" type="button" disabled={!canAdd} onClick={onAdd}>
                + Добавить правило
              </button>
              <span className={incomplete ? 'is-visible' : ''}>Сначала заполните текущее правило</span>
            </div>
          </section>

          <p className="client-local-rules-note">
            Можно вставить полный URL: Harbor сохранит только домен. Путь и параметры HTTPS недоступны для маршрутизации.
          </p>
          <InlineError error={error} context="routing" />
          <InlineProgress operations={operations} context="routing" />
          <div className="client-local-rules-actions">
            <button type="button" onClick={onClose}>Отмена</button>
          </div>
        </form>
      </div>
    </aside>
  );
}

function DurationPart({ name, children }) {
  return (
    <span
      className={`client-duration-part client-duration-${name}${name.endsWith('-value') ? ' is-value' : ' is-label'}`}
    >
      {children}
    </span>
  );
}

function AnimatedSeconds({ value, padded = true }) {
  return String(value).padStart(padded ? 2 : 1, '0').split('').map((digit, index) => (
    <span className="client-duration-second-digit" key={`${index}-${digit}`}>{digit}</span>
  ));
}

function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, blocked, onSetGatewayAuto }) {
  const [modeAnimating, setModeAnimating] = useState(false);
  const [arrowTurns, setArrowTurns] = useState(gatewayDirect ? 0.5 : 0);
  const stopModeAnimationRef = useRef(false);
  const previousGatewayDirectRef = useRef(gatewayDirect);
  const product = isGateway ? 'Gateway' : 'Connect';
  const switchable = !isGateway && gatewayAvailable;
  const label = gatewayDirect
    ? 'Игнорировать Harbor Gateway и использовать локальный VPN'
    : 'Использовать обнаруженный Harbor Gateway';

  useLayoutEffect(() => {
    if (previousGatewayDirectRef.current === gatewayDirect) return;
    previousGatewayDirectRef.current = gatewayDirect;
    setArrowTurns((turns) => turns + 0.5);
  }, [gatewayDirect]);

  function startModeAnimation() {
    stopModeAnimationRef.current = false;
    setModeAnimating(true);
  }

  function finishModeAnimation() {
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setModeAnimating(false);
      return;
    }
    stopModeAnimationRef.current = true;
  }

  const content = <div className="harbor-brand-content">
    <svg viewBox="0 0 32 32" aria-hidden="true">
      <circle cx="16" cy="6" r="3" />
      <path d="M16 9v15M10 14h12" />
      <path className="harbor-anchor-left" d="M16 28C11 28 8.4 25.2 6.3 22v-3.3M3.8 21.4l2.5-2.7 2.5 2.7" />
      <path className="harbor-anchor-right" d="M16 28C21 28 23.6 25.2 25.7 22v-3.3M23.2 21.4l2.5-2.7 2.5 2.7" />
    </svg>
    <span className="harbor-brand-name">
      <strong>Harbor</strong>
      {switchable ? <span className="harbor-mode-control">
        <span className="harbor-mode-stack" aria-hidden="true">
          <em className="harbor-mode-connect">Connect</em>
          <em className="harbor-mode-gateway"><span>Gateway</span></em>
        </span>
        <svg
          className="harbor-mode-swap"
          viewBox="0 0 18 18"
          aria-hidden="true"
          style={{ '--harbor-arrow-turn': `${arrowTurns}turn` }}
        >
          <g className="is-connect"><path d="M3 6h10m-3-3 3 3-3 3" /></g>
          <g className="is-gateway"><path d="M15 12H5m3 3-3-3 3-3" /></g>
        </svg>
        <span id="harbor-mode-tooltip" className="harbor-mode-tooltip" role="tooltip">
          <strong>{gatewayDirect ? 'Harbor Gateway активен' : 'Harbor Gateway доступен'}</strong>
          <span>{gatewayDirect
            ? 'Трафик идёт через Gateway в этой сети. Нажмите, чтобы использовать локальный VPN.'
            : 'Сейчас используется локальный VPN. Нажмите, чтобы направить трафик через Gateway.'}</span>
        </span>
      </span> : <em>{product}</em>}
    </span>
  </div>;

  return (
    <div className={`harbor-brand is-${product.toLowerCase()}${switchable ? ` is-switchable${gatewayDirect ? ' is-gateway-active' : ''}` : ''}`}>
      {switchable ? <button
        className={`harbor-brand-control${modeAnimating ? ' is-mode-animating' : ''}`}
        type="button"
        aria-label={label}
        aria-describedby="harbor-mode-tooltip"
        aria-pressed={gatewayDirect}
        disabled={blocked}
        onPointerEnter={startModeAnimation}
        onPointerLeave={finishModeAnimation}
        onFocus={startModeAnimation}
        onBlur={finishModeAnimation}
        onAnimationIteration={(event) => {
          if (event.animationName === 'harbor-mode-float-front' && stopModeAnimationRef.current) {
            stopModeAnimationRef.current = false;
            setModeAnimating(false);
          }
        }}
        onClick={() => onSetGatewayAuto(!gatewayDirect)}
      >
        {content}
      </button> : <div aria-label={`Harbor ${product}`}>{content}</div>}
    </div>
  );
}

export function ClientOverviewPage({
  state,
  versionInfo,
  operations = {},
  error,
  subscriptionUrl,
  setSubscriptionUrl,
  servers,
  pendingServerId,
  setPendingServerId,
  onFetchSubscription,
  onRefreshSubscription,
  onForgetSubscription,
  onApply,
  onRestart,
  onStop,
  onSetGatewayAuto,
  onSaveRouteRules,
  onDismissError,
}) {
  const isGateway = state?.mode === 'gateway';
  const gatewayDirect = !isGateway && state?.gatewayAuto?.mode === 'gateway-direct';
  const gatewayAvailable = !isGateway && Boolean(state?.gatewayAuto?.available);
  const connected = Boolean(state?.singboxRunning);
  const hasSubscription = Boolean(state?.hasSubscription);
  const selectedServerId = pendingServerId || state?.selection?.desiredServerId || '';
  const showPower = hasSubscription && Boolean(selectedServerId);
  const canStart = Boolean(selectedServerId || state?.configExists);
  const [now, setNow] = useState(Date.now());
  const [durationMode, setDurationMode] = useState(() => {
    try {
      return localStorage.getItem(DURATION_MODE_STORAGE_KEY) === 'words' ? 'words' : 'digital';
    } catch {
      return 'digital';
    }
  });
  const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
  const [showIntro, setShowIntro] = useState(!hasSubscription);
  const [subscriptionContentReady, setSubscriptionContentReady] = useState(hasSubscription);
  const [pings, setPings] = useState({});
  const [accessTab, setAccessTab] = useState('gateway');
  const [copyFeedback, setCopyFeedback] = useState(null);
  const [refreshingInfo, setRefreshingInfo] = useState(false);
  const [usageUpdated, setUsageUpdated] = useState(false);
  const [serverRevealVersion, setServerRevealVersion] = useState(0);
  const [serversLeaving, setServersLeaving] = useState(false);
  const [instructionsOpen, setInstructionsOpen] = useState(false);
  const [localRulesOpen, setLocalRulesOpen] = useState(false);
  const [localRulesDraft, setLocalRulesDraft] = useState([]);
  const [localRulesRevision, setLocalRulesRevision] = useState(state?.route?.localRulesRevision || 0);
  const [confirmingLocalRulesClose, setConfirmingLocalRulesClose] = useState(false);
  const [confirmingDelete, setConfirmingDelete] = useState(false);
  const [openInstructionId, setOpenInstructionId] = useState('');
  const subscriptionInputRef = useRef(null);
  const subscriptionRef = useRef(null);
  const copyTimerRef = useRef(null);
  const instructionsPanelRef = useRef(null);
  const instructionsToggleRef = useRef(null);
  const instructionsCloseRef = useRef(null);
  const localRulesPanelRef = useRef(null);
  const localRulesToggleRef = useRef(null);
  const localRulesCloseRef = useRef(null);
  const localRulesBaselineRef = useRef('[]');
  const previousHasSubscriptionRef = useRef(hasSubscription);
  const serverKey = servers.map((server) => server.id).join('|');
  const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
  const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
  const usage = subscriptionUsage(state?.userInfo);
  const duration = connectionDurationParts(state?.singboxStartedAt, now);
  const connectionTitle = connected
    ? gatewayDirect ? 'Gateway подключён' : 'VPN включён'
    : 'Подключение выключено';
  const [displayedUsed, setDisplayedUsed] = useState(usage.used);
  const hasUsage = Boolean(
    state?.userInfo && ['upload', 'download', 'total', 'expire'].some((key) => key in state.userInfo),
  );
  const instructions = instructionBlocks({
    isGateway,
    host: gatewayAddress,
    port: state?.proxyPort || (isGateway ? 8080 : 8082),
  });
  const [instructionsIntro, ...instructionGuides] = instructions;
  const openInstruction = instructionGuides.find((block) => block.id === openInstructionId);
  const orderedInstructionGuides = openInstruction
    ? [openInstruction, ...instructionGuides.filter((block) => block.id !== openInstructionId)]
    : instructionGuides;
  const normalizedSubscriptionUrl = subscriptionUrl.trim();
  const subscriptionValidationStatus = !normalizedSubscriptionUrl
    ? 'idle'
    : isSubscriptionUrlValid(normalizedSubscriptionUrl) ? 'valid' : 'invalid';
  const subscriptionWaiting = hasSubscription && !subscriptionContentReady;
  const connectionBlocked = operationBlocked(operations, 'connection');
  const serverApplyBlocked = operationBlocked(operations, 'serverApply');
  const subscriptionImportBlocked = operationBlocked(operations, 'subscriptionImport');
  const subscriptionRefreshBlocked = operationBlocked(operations, 'subscriptionRefresh');
  const subscriptionDeleteBlocked = operationBlocked(operations, 'subscriptionDelete');
  const gatewayAutoBlocked = operationBlocked(operations, 'gatewayAuto');
  const routeRulesBlocked = operationBlocked(operations, 'routeRules');
  const localRulesDirty = localRulesSignature(localRulesDraft) !== localRulesBaselineRef.current;
  const pendingLocalRulesCount = (state?.route?.localRules || []).filter((rule) => (
    rule.enabled && !(state?.route?.activeLocalRules || []).some((active) => localRuleKey(active) === localRuleKey(rule))
  )).length;

  useEffect(() => {
    setNow(Date.now());
    if (!connected || !state?.singboxStartedAt) return undefined;

    const timer = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(timer);
  }, [connected, state?.singboxStartedAt]);

  useEffect(() => {
    if (!servers.length) {
      setPings({});
      return undefined;
    }

    let cancelled = false;
    setPings(Object.fromEntries(servers.map((server) => [server.id, { checking: true }])));
    api.servers.pingAll()
      .then((data) => {
        if (cancelled) return;
        setPings(Object.fromEntries((data.results || []).map((ping) => [
          ping.id || ping.tag,
          ping,
        ])));
      })
      .catch(() => {
        if (!cancelled) {
          setPings(Object.fromEntries(servers.map((server) => [server.id, { ok: false }])));
        }
      });

    return () => { cancelled = true; };
  }, [serverKey]);

  useEffect(() => {
    if (editingSubscription) subscriptionInputRef.current?.focus();
  }, [editingSubscription]);

  useEffect(() => {
    if (!showIntro) return undefined;
    const timer = setTimeout(() => setShowIntro(false), 1200);
    return () => clearTimeout(timer);
  }, [showIntro]);

  useEffect(() => {
    const previouslyHadSubscription = previousHasSubscriptionRef.current;
    previousHasSubscriptionRef.current = hasSubscription;

    if (!hasSubscription) {
      setSubscriptionContentReady(false);
      return undefined;
    }
    if (previouslyHadSubscription) {
      setSubscriptionContentReady(true);
      return undefined;
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setSubscriptionContentReady(true);
      return undefined;
    }

    const timer = setTimeout(() => setSubscriptionContentReady(true), SUBSCRIPTION_REVEAL_DELAY_MS);
    return () => clearTimeout(timer);
  }, [hasSubscription]);

  useEffect(() => {
    if (!hasSubscription) {
      setEditingSubscription(true);
      setInstructionsOpen(false);
      setLocalRulesOpen(false);
    }
  }, [hasSubscription]);

  useEffect(() => {
    if (!editingSubscription || !state?.hasSubscription || subscriptionUrl) return undefined;
    const timer = setTimeout(() => setEditingSubscription(false), 5000);
    return () => clearTimeout(timer);
  }, [editingSubscription, state?.hasSubscription, subscriptionUrl]);

  useEffect(() => {
    if (!editingSubscription || !hasSubscription) return undefined;
    const closeOnOutsideClick = (event) => {
      if (subscriptionRef.current?.contains(event.target)) return;
      setSubscriptionUrl('');
      setEditingSubscription(false);
    };
    document.addEventListener('pointerdown', closeOnOutsideClick);
    return () => document.removeEventListener('pointerdown', closeOnOutsideClick);
  }, [editingSubscription, hasSubscription, setSubscriptionUrl]);

  useEffect(() => {
    if (!state?.hasSubscription) return undefined;
    onRefreshSubscription();
    return undefined;
  }, [state?.hasSubscription]);

  useEffect(() => {
    const from = displayedUsed;
    const to = usage.used;
    if (from === to) return undefined;
    const startedAt = performance.now();
    let frame;
    const tick = (now) => {
      const progress = Math.min(1, (now - startedAt) / 900);
      const eased = 1 - Math.pow(1 - progress, 4);
      setDisplayedUsed(from + (to - from) * eased);
      if (progress < 1) frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [usage.used]);

  useEffect(() => () => clearTimeout(copyTimerRef.current), []);

  useEffect(() => {
    if (!instructionsOpen) return undefined;
    const frame = requestAnimationFrame(() => instructionsCloseRef.current?.focus());
    const closeOnEscape = (event) => {
      if (event.key === 'Escape') setInstructionsOpen(false);
    };
    document.addEventListener('keydown', closeOnEscape);
    return () => {
      cancelAnimationFrame(frame);
      document.removeEventListener('keydown', closeOnEscape);
      requestAnimationFrame(() => {
        if (instructionsPanelRef.current?.contains(document.activeElement)) instructionsToggleRef.current?.focus();
      });
    };
  }, [instructionsOpen]);

  useEffect(() => {
    if (!localRulesOpen) return undefined;
    const frame = requestAnimationFrame(() => localRulesCloseRef.current?.focus());
    return () => {
      cancelAnimationFrame(frame);
      requestAnimationFrame(() => {
        if (localRulesPanelRef.current?.contains(document.activeElement)) localRulesToggleRef.current?.focus();
      });
    };
  }, [localRulesOpen]);

  useEffect(() => {
    if (!localRulesOpen) return undefined;
    const closeLocalRules = (event) => {
      if (event.type === 'keydown') {
        if (event.key !== 'Escape' || event.defaultPrevented) return;
      } else {
        if (localRulesPanelRef.current?.contains(event.target)) return;
        if (localRulesToggleRef.current?.contains(event.target)) return;
      }
      requestCloseLocalRules();
    };
    document.addEventListener('pointerdown', closeLocalRules);
    document.addEventListener('keydown', closeLocalRules);
    return () => {
      document.removeEventListener('pointerdown', closeLocalRules);
      document.removeEventListener('keydown', closeLocalRules);
    };
  }, [localRulesOpen, localRulesDirty]);

  useEffect(() => {
    if (!localRulesOpen || !localRulesDirty) return undefined;
    const warnBeforeUnload = (event) => {
      event.preventDefault();
      event.returnValue = '';
    };
    window.addEventListener('beforeunload', warnBeforeUnload);
    return () => window.removeEventListener('beforeunload', warnBeforeUnload);
  }, [localRulesOpen, localRulesDirty]);

  useEffect(() => {
    if (!instructionsOpen) return undefined;
    const closeInstructionsOutside = (event) => {
      if (instructionsPanelRef.current?.contains(event.target)) return;
      if (instructionsToggleRef.current?.contains(event.target)) return;
      setInstructionsOpen(false);
    };
    document.addEventListener('pointerdown', closeInstructionsOutside);
    return () => document.removeEventListener('pointerdown', closeInstructionsOutside);
  }, [instructionsOpen]);

  async function toggleConnection() {
    const action = connectionAction({ connected, selectedServerId, configExists: state?.configExists });
    if (action?.type === 'stop') return onStop();
    if (action?.type === 'apply') return onApply(action.serverId);
    if (action?.type === 'restart') return onRestart();
  }

  function selectServer(serverId) {
    setPendingServerId(serverId);
    if (connected && serverId) onApply(serverId);
  }

  async function submitSubscription(event) {
    event.preventDefault();
    if (subscriptionValidationStatus !== 'valid') return;
    if (!await onFetchSubscription()) return;
    setSubscriptionUrl('');
    setEditingSubscription(false);
  }

  async function copyProxy(kind) {
    const value = kind === 'gateway' ? gatewayAddress : proxyUrls[kind];
    clearTimeout(copyTimerRef.current);
    try {
      await copyText(value);
      setCopyFeedback({ kind, failed: false });
    } catch {
      setCopyFeedback({ kind, failed: true });
    }
    copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800);
  }

  function navigateAccessTabs(event, current) {
    if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
    event.preventDefault();
    const next = accessTabForKey(ACCESS_TABS.map(([tab]) => tab), current, event.key);
    setAccessTab(next);
    requestAnimationFrame(() => document.getElementById(`client-access-tab-${next}`)?.focus());
  }

  async function refreshSubscription() {
    const startedAt = performance.now();
    setRefreshingInfo(true);
    try {
      if (!await onRefreshSubscription()) return;
      setUsageUpdated(false);
      requestAnimationFrame(() => setUsageUpdated(true));
      setTimeout(() => setUsageUpdated(false), 900);
      setServersLeaving(true);
      await new Promise((resolve) => setTimeout(resolve, 420 + Math.max(0, servers.length - 1) * 90));
      setServerRevealVersion((version) => version + 1);
      setServersLeaving(false);
    } finally {
      const elapsed = performance.now() - startedAt;
      const completeCyclesAt = Math.max(900, Math.ceil(elapsed / 900) * 900);
      await new Promise((resolve) => setTimeout(resolve, completeCyclesAt - elapsed));
      setRefreshingInfo(false);
    }
  }

  async function forgetSubscription() {
    if (!await onForgetSubscription()) return;
    setConfirmingDelete(false);
  }

  function toggleInstruction(id) {
    const update = () => flushSync(() => {
      setOpenInstructionId((current) => current === id ? '' : id);
    });

    if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) {
      update();
      return;
    }

    document.startViewTransition(update);
  }

  function toggleDurationMode() {
    setDurationMode((mode) => {
      const nextMode = mode === 'digital' ? 'words' : 'digital';
      try {
        localStorage.setItem(DURATION_MODE_STORAGE_KEY, nextMode);
      } catch {
        // The visual preference still works for this session.
      }
      return nextMode;
    });
  }

  function openLocalRules() {
    const rules = state?.route?.localRules || [];
    setInstructionsOpen(false);
    localRulesBaselineRef.current = localRulesSignature(rules);
    setLocalRulesDraft(rules.map(createLocalRuleDraft));
    setLocalRulesRevision(state?.route?.localRulesRevision || 0);
    setConfirmingLocalRulesClose(false);
    onDismissError();
    setLocalRulesOpen(true);
  }

  function changeLocalRule(index, field, value) {
    setLocalRulesDraft((rules) => rules.map((rule, ruleIndex) => (
      ruleIndex === index ? { ...rule, [field]: value } : rule
    )));
  }

  async function saveLocalRules(event) {
    event.preventDefault();
    const rules = localRuleValues(localRulesDraft);
    const result = await onSaveRouteRules(rules, localRulesRevision);
    if (!result) return;
    localRulesBaselineRef.current = JSON.stringify(rules);
    setLocalRulesRevision(result.state.route.localRulesRevision);
    setConfirmingLocalRulesClose(false);
    if (!result.state.route.localRulesPendingRestart) setLocalRulesOpen(false);
  }

  function requestCloseLocalRules() {
    if (localRulesDirty) {
      setConfirmingLocalRulesClose(true);
      return false;
    }
    setLocalRulesOpen(false);
    return true;
  }

  function discardLocalRules() {
    setConfirmingLocalRulesClose(false);
    setLocalRulesOpen(false);
  }

  function removeLocalRule(ruleKey) {
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setLocalRulesDraft((rules) => rules.filter((rule) => rule._key !== ruleKey));
      return;
    }
    setLocalRulesDraft((rules) => rules.map((rule) => (
      rule._key === ruleKey ? { ...rule, removing: true } : rule
    )));
  }

  function finishRemoveLocalRule(ruleKey) {
    const update = () => flushSync(() => {
      setLocalRulesDraft((rules) => rules.filter((rule) => rule._key !== ruleKey));
    });
    if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) {
      update();
      return;
    }
    document.startViewTransition(update);
  }

  return (
    <div
      className={`client-shell${hasSubscription ? '' : ' is-first-run'}${showIntro && !hasSubscription ? ' is-intro' : ''}`}
    >
      <VersionDisplay isGateway={isGateway} versionInfo={versionInfo} />
      <div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
        {copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
      </div>
      <HarborBrand
        isGateway={isGateway}
        gatewayAvailable={gatewayAvailable}
        gatewayDirect={gatewayDirect}
        blocked={gatewayAutoBlocked}
        onSetGatewayAuto={onSetGatewayAuto}
      />
      {hasSubscription && subscriptionContentReady && <button
        ref={instructionsToggleRef}
        className={`client-instructions-toggle${instructionsOpen ? ' is-open' : ''}`}
        type="button"
        aria-expanded={instructionsOpen}
        aria-controls="client-instructions"
        aria-label={instructionsOpen ? 'Закрыть инструкции' : 'Как использовать'}
        onClick={() => {
          if (localRulesOpen && !requestCloseLocalRules()) return;
          setInstructionsOpen((open) => !open);
        }}
      >
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <path d="m3 5 7 7-7 7" />
          <path d="m7 5 7 7-7 7" />
          <path d="m11 5 7 7-7 7" />
        </svg>
        <span>Как использовать</span>
      </button>}
      {hasSubscription && subscriptionContentReady && <button
        ref={localRulesToggleRef}
        className={`client-local-rules-toggle${localRulesOpen ? ' is-open' : ''}${state?.route?.localRulesPendingRestart ? ' has-pending' : ''}`}
        type="button"
        disabled={gatewayDirect}
        aria-expanded={localRulesOpen}
        aria-controls="client-local-rules"
        aria-label={gatewayDirect
          ? 'Сейчас работают правила Harbor Gateway'
          : localRulesOpen ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
        onClick={() => localRulesOpen ? requestCloseLocalRules() : openLocalRules()}
      >
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <path d="M4 7h9M17 7h3M4 17h3M11 17h9" />
          <circle cx="15" cy="7" r="2" />
          <circle cx="9" cy="17" r="2" />
        </svg>
        <span>{gatewayDirect
          ? 'Сейчас работают правила Gateway'
          : state?.route?.localRulesPendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}</span>
      </button>}
      <main className={`client-panel${showPower ? '' : ' is-setup'}`}>
        {showPower && (
        <section className="client-power-section" aria-labelledby="connection-title">
          <button
            className="client-power"
            type="button"
            role="switch"
            aria-checked={connected}
            aria-label={connected ? 'Остановить Harbor Connect' : 'Запустить Harbor Connect'}
            disabled={connectionBlocked || (!connected && !canStart)}
            onClick={toggleConnection}
          >
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path d="M12 2v10M5.6 5.6a9 9 0 1 0 12.8 0" />
            </svg>
          </button>
          <div className={`client-route-rules-pending${pendingLocalRulesCount ? ' is-visible' : ''}`} role="status" aria-live="polite">
            {pendingLocalRulesCount > 0 && (
              <>
                <span>{pendingLocalRulesCount} {pendingLocalRulesCount === 1 ? 'правило не применено' : 'правила не применены'}</span>
                <button type="button" disabled={connectionBlocked} onClick={onRestart}>Перезапустить VPN</button>
              </>
            )}
          </div>
          <div className="client-state-copy" aria-live="polite">
            <h2 id="connection-title" className="client-connection-title" aria-label={connectionTitle}>
              <span className={!connected ? 'is-active' : ''} aria-hidden="true">Подключение выключено</span>
              <span className={connected && !gatewayDirect ? 'is-active' : ''} aria-hidden="true">VPN включён</span>
              <span className={connected && gatewayDirect ? 'is-active' : ''} aria-hidden="true">Gateway подключён</span>
            </h2>
            <div className="client-state-detail">
              {connected ? (
                <button
                  className={`client-duration-toggle client-tooltip-anchor${durationMode === 'words' ? ' is-words' : ''}`}
                  type="button"
                  key="duration"
                  aria-label={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
                  onClick={toggleDurationMode}
                >
                  <span className="client-duration-stack">
                    <time
                      className={`client-duration${durationMode === 'digital' ? ' is-active' : ''}`}
                      aria-hidden={durationMode !== 'digital'}
                    >
                      <DurationPart name="hours-value">{String(duration.totalHours).padStart(2, '0')}</DurationPart>
                      :<DurationPart name="minutes-value">{String(duration.minutes.value).padStart(2, '0')}</DurationPart>
                      :<DurationPart name="seconds-value"><AnimatedSeconds value={duration.seconds.value} /></DurationPart>
                    </time>
                    <time
                      className={`client-duration${durationMode === 'words' ? ' is-active' : ''}`}
                      aria-hidden={durationMode !== 'words'}
                    >
                    {[
                      ['days', duration.days],
                      ['hours', duration.hours],
                      ['minutes', duration.minutes],
                      ['seconds', duration.seconds],
                    ]
                      .filter(([name, part]) => part.value || name === 'seconds')
                      .map(([name, part]) => (
                        <span className="client-duration-unit" data-unit={name} key={name}>
                          <DurationPart name={`${name}-value`}>{name === 'seconds'
                            ? <AnimatedSeconds value={part.value} padded={false} />
                            : part.value}</DurationPart>{' '}
                          <DurationPart name={`${name}-label`}>{part.label}</DurationPart>
                        </span>
                      ))}
                    </time>
                  </span>
                  <CloudTooltip>{durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}</CloudTooltip>
                </button>
              ) : (
                <p key="hint">
                  {canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}
                </p>
              )}
            </div>
          </div>

          <section className={`client-proxies${isGateway ? ' has-tabs' : ''}`} aria-label={isGateway ? 'Gateway и Gateway Proxy' : 'Локальный прокси'}>
            {isGateway && (
              <div className="client-access-tabs" role="tablist" aria-label="Способ подключения">
                {ACCESS_TABS.map(([tab, label]) => (
                  <button
                    id={`client-access-tab-${tab}`}
                    className={`client-access-tab${accessTab === tab ? ' is-active' : ''}`}
                    type="button"
                    role="tab"
                    key={tab}
                    aria-selected={accessTab === tab}
                    aria-controls={`client-access-panel-${tab}`}
                    tabIndex={accessTab === tab ? 0 : -1}
                    onClick={() => setAccessTab(tab)}
                    onKeyDown={(event) => navigateAccessTabs(event, tab)}
                  >
                    {label}
                  </button>
                ))}
              </div>
            )}
            {isGateway && (
              <div
                id="client-access-panel-gateway"
                className="client-access-point"
                role="tabpanel"
                aria-labelledby="client-access-tab-gateway"
                hidden={accessTab !== 'gateway'}
              >
                <strong className="client-proxy-address">{gatewayAddress}</strong>
                <button
                  className={`client-copy-button${copyFeedback?.kind === 'gateway' ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
                  type="button"
                  aria-label={`Скопировать Gateway: ${gatewayAddress}`}
                  onClick={() => copyProxy('gateway')}
                >
                  <span className="client-copy-label">КОПИРОВАТЬ</span>
                  {copyFeedback?.kind === 'gateway' && <span className="client-copy-feedback" aria-hidden="true">{copyFeedback.failed ? 'ОШИБКА' : 'ГОТОВО'}</span>}
                </button>
              </div>
            )}
            <div
              id={isGateway ? 'client-access-panel-proxy' : undefined}
              className="client-access-point"
              role={isGateway ? 'tabpanel' : undefined}
              aria-labelledby={isGateway ? 'client-access-tab-proxy' : undefined}
              hidden={isGateway && accessTab !== 'proxy'}
            >
              {!isGateway && (
                <span className={`client-proxy-label${gatewayDirect ? ' is-gateway' : ''}`}>
                  <span className={!gatewayDirect ? 'is-active' : ''}>Локальный VPN</span>
                  <span className={gatewayDirect ? 'is-active' : ''}>
                    Через <a href={`http://${state.gatewayAuto.address}:3456`}>Harbor Gateway</a> · {state.gatewayAuto.address}
                  </span>
                </span>
              )}
              <strong className="client-proxy-address">
                {proxyUrls.http.replace(/^https?:\/\//, '')}
              </strong>
              <div className="client-proxy-actions">
              {[
                ['socks5', 'SOCKS5'],
                ['http', 'HTTP'],
              ].map(([kind, label]) => (
                <button
                  className={`client-copy-button${copyFeedback?.kind === kind ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
                  type="button"
                  key={kind}
                  aria-label={`Скопировать ${label}: ${proxyUrls[kind]}`}
                  onClick={() => copyProxy(kind)}
                >
                  <span className="client-copy-label">{label}</span>
                  {copyFeedback?.kind === kind && <span className="client-copy-feedback" aria-hidden="true">{copyFeedback.failed ? 'ОШИБКА' : 'ГОТОВО'}</span>}
                </button>
              ))}
              </div>
            </div>
          </section>
          <InlineError error={error} context="connection" />
          <InlineProgress operations={operations} context="connection" />
        </section>
        )}

        <div
          className={`client-form${subscriptionWaiting ? ' is-waiting' : ''}`}
          aria-hidden={subscriptionWaiting}
          aria-disabled={gatewayDirect}
          inert={subscriptionWaiting || gatewayDirect ? true : undefined}
        >
          <div className="client-form-content">
          <div
            ref={subscriptionRef}
            className={`client-subscription ${editingSubscription ? 'is-editing' : ''}${editingSubscription && state?.hasSubscription && !subscriptionUrl ? ' is-timing-out' : ''}`}
          >
            <div
              className="client-subscription-summary"
              aria-hidden={editingSubscription}
              inert={editingSubscription ? true : undefined}
            >
              <div className="client-subscription-heading">
                <span className="client-subscription-label">Ваша подписка</span>
                <span className="client-icon-tooltip client-tooltip-anchor">
                  <button
                    className="client-subscription-refresh"
                    type="button"
                    aria-label="Обновить подписку"
                    disabled={refreshingInfo || subscriptionRefreshBlocked}
                    onClick={refreshSubscription}
                  >
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                      <path d="M21 12a9 9 0 0 0-15.2-6.5L3 8m0-5v5h5M3 12a9 9 0 0 0 15.2 6.5L21 16m0 5v-5h-5" />
                    </svg>
                  </button>
                  <CloudTooltip>Обновить подписку</CloudTooltip>
                </span>
                <span className="client-icon-tooltip client-tooltip-anchor">
                  <button
                    className="client-subscription-delete"
                    type="button"
                    aria-label="Удалить подписку"
                    disabled={subscriptionDeleteBlocked}
                    onClick={() => setConfirmingDelete(true)}
                  >
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                      <path className="client-trash-lid" d="M4 7h16M9 7V4h6v3" />
                      <path d="m6 7 1 13h10l1-13M10 11v5M14 11v5" />
                    </svg>
                  </button>
                  <CloudTooltip>Удалить подписку</CloudTooltip>
                </span>
              </div>
              <button
                className="client-subscription-domain-button"
                type="button"
                tabIndex={editingSubscription ? -1 : 0}
                onClick={() => setEditingSubscription(true)}
              >
                <strong>{subscriptionDomain(state?.subscriptionHost)}</strong>
              </button>
            </div>

            <form
              className={`client-subscription-edit is-${subscriptionValidationStatus}`}
              autoComplete="off"
              aria-hidden={!editingSubscription}
              inert={!editingSubscription ? true : undefined}
              onSubmit={submitSubscription}
            >
              <input
                ref={subscriptionInputRef}
                id="subscription-url"
                type="url"
                inputMode="url"
                autoComplete="off"
                tabIndex={editingSubscription ? 0 : -1}
                aria-label="Ссылка подписки"
                placeholder="Вставьте ссылку подписки"
                className={subscriptionUrl ? 'has-value' : ''}
                value={subscriptionUrl}
                onChange={(event) => setSubscriptionUrl(event.target.value)}
                onKeyDown={(event) => {
                  if (event.key === 'Escape' && state?.hasSubscription) {
                    setSubscriptionUrl('');
                    setEditingSubscription(false);
                  }
                }}
              />
              {subscriptionUrl && (
                <span className="client-subscription-domain">
                  {subscriptionDomain(subscriptionUrl)}
                </span>
              )}
              {normalizedSubscriptionUrl && (
                <button
                  className="client-subscription-submit"
                  type="submit"
                  aria-label={subscriptionValidationStatus === 'valid'
                    ? 'Сохранить подписку'
                    : 'Ссылка подписки не распознана'}
                  disabled={subscriptionImportBlocked || subscriptionValidationStatus !== 'valid'}
                >
                  {subscriptionValidationStatus === 'valid'
                    ? '✓'
                    : '×'}
                </button>
              )}
            </form>
            <InlineError error={error} context="subscription" />
            <InlineProgress operations={operations} context="subscription" />
          </div>

          {hasSubscription && subscriptionContentReady && hasUsage && (
            <section className={`client-usage${usageUpdated ? ' is-updated' : ''}`} aria-label="Статистика подписки">
              <span>Использовано</span>
              <strong>
                {formatBytes(displayedUsed)}
                <small> / {usage.total ? formatBytes(usage.total) : 'без лимита'}</small>
              </strong>
              {usage.percent !== null && (
                <div
                  className="client-usage-bar"
                  role="progressbar"
                  aria-label="Использованный трафик"
                  aria-valuemin="0"
                  aria-valuemax="100"
                  aria-valuenow={Math.round(usage.percent)}
                >
                  <i style={{ width: `${usage.percent}%` }} />
                </div>
              )}
              <div className="client-usage-details">
                {usage.expiresAt && !Number.isNaN(usage.expiresAt.getTime()) && (
                  <span>
                    до {usage.expiresAt.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' })}
                    {' · '}{subscriptionDaysLeft(usage.expiresAt)}
                  </span>
                )}
              </div>
            </section>
          )}

          {hasSubscription && subscriptionContentReady && (
          <section className="client-servers" aria-label="Выберите сервер">
            {!showPower && <span className="client-server-prompt">Выберите сервер</span>}
            <div
              className={`client-server-grid${serversLeaving ? ' is-leaving' : ''}`}
              key={`${serverKey}:${serverRevealVersion}`}
            >
              {servers.map((server, index) => {
                const ping = pings[server.id];
                const selected = server.id === selectedServerId;
                const pingText = ping?.checking
                  ? 'Проверка…'
                  : ping?.ok ? `${ping.latency} ms` : 'Недоступен';
                const pingClass = ping?.ok
                  ? ping.latency < 100 ? 'good' : ping.latency < 250 ? 'medium' : 'slow'
                  : '';

                return (
                  <button
                    className={`client-server ${selected ? 'is-selected' : ''}`}
                    type="button"
                    key={server.id}
                    disabled={serverApplyBlocked}
                    aria-pressed={selected}
                    aria-label={`${server.label}, ${server.host}:${server.port}, ${pingText}`}
                    style={{ '--server-index': index }}
                    onClick={() => selectServer(server.id)}
                  >
                    <strong>{server.label}</strong>
                    <small className={pingClass}>{pingText}</small>
                  </button>
                );
              })}
            </div>
          </section>
          )}
          </div>
        </div>
      </main>

      {hasSubscription && subscriptionContentReady && <aside
        ref={instructionsPanelRef}
        id="client-instructions"
        className={`client-instructions${instructionsOpen ? ' is-open' : ''}`}
        aria-labelledby="instructions-title"
        aria-hidden={!instructionsOpen}
        inert={!instructionsOpen ? true : undefined}
      >
        <div className="client-instructions-sheet">
          <button
            ref={instructionsCloseRef}
            className="client-drawer-close"
            type="button"
            aria-label="Закрыть инструкции"
            onClick={() => setInstructionsOpen(false)}
          >×</button>
          <header className="client-instructions-header">
            <span>Подключение</span>
            <h2 id="instructions-title">Как использовать {isGateway ? 'Gateway' : 'прокси'}</h2>
            <div className="client-instructions-intro">
              {instructionsIntro.paragraphs.map((paragraph) => <p key={paragraph}>{paragraph}</p>)}
            </div>
          </header>

          <div className="client-instruction-list">
            {orderedInstructionGuides.map((block) => (
              <InstructionBlock
                block={block}
                key={block.id}
                open={block.id === openInstructionId}
                onToggle={() => toggleInstruction(block.id)}
              />
            ))}
          </div>
        </div>
      </aside>}

      {hasSubscription && subscriptionContentReady && <LocalRulesPanel
        open={localRulesOpen}
        rules={localRulesDraft}
        savedRules={state?.route?.localRules || []}
        activeRules={state?.route?.activeLocalRules || []}
        blocked={routeRulesBlocked || localRulesDraft.some((rule) => rule.removing)}
        dirty={localRulesDirty}
        restartPending={state?.route?.localRulesPendingRestart === true}
        error={error}
        operations={operations}
        panelRef={localRulesPanelRef}
        closeRef={localRulesCloseRef}
        onAdd={() => setLocalRulesDraft((rules) => [
          ...rules,
          createLocalRuleDraft({ type: 'domain', value: '', enabled: true }),
        ])}
        onChange={changeLocalRule}
        onRemove={(index) => removeLocalRule(localRulesDraft[index]._key)}
        onRemoveComplete={finishRemoveLocalRule}
        onClose={requestCloseLocalRules}
        onSave={saveLocalRules}
      />}
      <ConfirmationPopup
        open={confirmingLocalRulesClose}
        id="discard-local-rules"
        title="Есть несохранённые настройки"
        description="Закрыть редактор и потерять изменения?"
        cancelLabel="Остаться"
        confirmLabel="Закрыть без сохранения"
        onCancel={() => setConfirmingLocalRulesClose(false)}
        onConfirm={discardLocalRules}
      />
      <ConfirmationPopup
        open={confirmingDelete}
        id="delete-subscription"
        kicker="Необратимое действие"
        title="Удалить подписку?"
        description="Harbor остановит VPN и удалит сохранённую подписку. Приложения с локальным прокси потеряют соединение до добавления новой подписки."
        cancelLabel="Отмена"
        confirmLabel="Удалить"
        busy={subscriptionDeleteBlocked}
        onCancel={() => setConfirmingDelete(false)}
        onConfirm={forgetSubscription}
      />
    </div>
  );
}
