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

const SUBSCRIPTION_REVEAL_DELAY_MS = 1350;
const DURATION_MODE_STORAGE_KEY = 'harbor-duration-mode';

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, singBox, incompatible = false }) {
  const parsed = parseVersion(version);
  const values = parsed ? VERSION_PARTS.map(([key]) => parsed[key]) : ['–', '–', '–'];

  function description(key) {
    if (key === 'major') {
      return 'Общий уровень совместимости Harbor. При его изменении обновляются Mac и вся Gateway-инфраструктура.';
    }
    if (key === 'minor' && componentKey === 'macClient') {
      return 'Линия Mac-клиента. Gateway может менять minor без обязательного обновления Mac.';
    }
    if (key === 'minor') {
      return 'Линия Gateway. Gateway client и backend должны совпадать по major.minor.';
    }
    return `Совместимое исправление только компонента ${component}; hotfix может обновляться независимо.`;
  }

  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>
                {singBox && <small>Runtime: sing-box {singBox}</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}
        singBox={runtimeSingBox}
      />
    </aside>;
  }

  const backendVersion = versionInfo?.components?.gatewayBackend;
  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"
      componentKey="gatewayClient"
      version={HARBOR_VERSIONS.gatewayClient}
      incompatible={incompatible}
    />
    <VersionBadge
      code="B"
      component="Gateway backend"
      componentKey="gatewayBackend"
      version={backendVersion}
      singBox={runtimeSingBox}
      incompatible={incompatible}
    />
  </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', 'Применяем сервер…'],
  gatewayAuto: ['connection', 'Переключаем маршрут…'],
  subscriptionImport: ['subscription', 'Загружаем подписку…'],
  subscriptionRefresh: ['subscription', 'Обновляем подписку…'],
  subscriptionDelete: ['subscription', 'Удаляем подписку…'],
};

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>
  );
}

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 stopModeAnimationRef = useRef(false);
  const product = isGateway ? 'Gateway' : 'Connect';
  const switchable = !isGateway && gatewayAvailable;
  const label = gatewayDirect
    ? 'Игнорировать Harbor Gateway и использовать локальный VPN'
    : 'Использовать обнаруженный Harbor Gateway';

  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">Gateway</em>
        </span>
        <svg className="harbor-mode-swap" viewBox="0 0 18 18" aria-hidden="true">
          <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,
  pendingTag,
  setPendingTag,
  onFetchSubscription,
  onRefreshSubscription,
  onForgetSubscription,
  onApply,
  onRestart,
  onStop,
  onSetGatewayAuto,
}) {
  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 selectedTag = pendingTag || state?.selectedTag || '';
  const showPower = hasSubscription && Boolean(selectedTag);
  const canStart = Boolean(selectedTag || 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 [subscriptionValidation, setSubscriptionValidation] = useState({ url: '', status: 'idle' });
  const [validationAttempt, setValidationAttempt] = useState(0);
  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 [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 previousHasSubscriptionRef = useRef(hasSubscription);
  const validationRequests = useRef(null);
  if (!validationRequests.current) validationRequests.current = createLatestRequest();
  const serverKey = servers.map((server) => `${server.tag}:${server.server}:${server.server_port}`).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 = subscriptionValidation.url === normalizedSubscriptionUrl
    ? subscriptionValidation.status
    : normalizedSubscriptionUrl ? 'checking' : 'idle';
  const subscriptionError = subscriptionValidation.url === normalizedSubscriptionUrl
    ? subscriptionValidation.error
    : null;
  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');

  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.tag, { checking: true }])));
    api.servers.pingAll()
      .then((data) => {
        if (cancelled) return;
        setPings(Object.fromEntries((data.results || []).map((ping) => [
          String(ping.tag || '').trim(),
          ping,
        ])));
      })
      .catch(() => {
        if (!cancelled) {
          setPings(Object.fromEntries(servers.map((server) => [server.tag, { 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(() => {
    validationRequests.current.cancel();
    if (!normalizedSubscriptionUrl) {
      setSubscriptionValidation({ url: '', status: 'idle' });
      return undefined;
    }

    setSubscriptionValidation({ url: normalizedSubscriptionUrl, status: 'checking' });
    const timer = setTimeout(() => {
      validationRequests.current
        .run((signal) => api.subscription.validate(normalizedSubscriptionUrl, signal))
        .then((result) => {
          if (!result) return;
          setSubscriptionValidation({
            url: normalizedSubscriptionUrl,
            status: 'valid',
            error: null,
          });
        })
        .catch((validationError) => {
          setSubscriptionValidation({
            url: normalizedSubscriptionUrl,
            status: validationStatusForError(validationError),
            error: {
              context: 'subscription',
              message: validationError.message,
              correlationId: validationError.correlationId,
              retry: validationError.retryable
                ? () => {
                  setSubscriptionValidation({
                    url: normalizedSubscriptionUrl,
                    status: 'checking',
                    error: null,
                  });
                  setValidationAttempt((attempt) => attempt + 1);
                }
                : null,
            },
          });
        });
    }, 350);

    return () => {
      clearTimeout(timer);
      validationRequests.current.cancel();
    };
  }, [normalizedSubscriptionUrl, validationAttempt]);

  useEffect(() => {
    if (!hasSubscription) {
      setEditingSubscription(true);
      setInstructionsOpen(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 closeOnEscape = (event) => {
      if (event.key === 'Escape') setInstructionsOpen(false);
    };
    document.addEventListener('keydown', closeOnEscape);
    return () => document.removeEventListener('keydown', closeOnEscape);
  }, [instructionsOpen]);

  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, selectedTag, configExists: state?.configExists });
    if (action?.type === 'stop') return onStop();
    if (action?.type === 'apply') return onApply(action.selectedTag);
    if (action?.type === 'restart') return onRestart();
  }

  function selectServer(tag) {
    setPendingTag(tag);
    if (connected && tag) onApply(tag);
  }

  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);
    setCopyFeedback({ kind, failed: false });
    copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800);
    try {
      await copyText(value);
    } catch {
      setCopyFeedback({ kind, failed: true });
    }
  }

  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;
    });
  }

  return (
    <div className={`client-shell${hasSubscription ? '' : ' is-first-run'}${showIntro && !hasSubscription ? ' is-intro' : ''}`}>
      <VersionDisplay isGateway={isGateway} versionInfo={versionInfo} />
      <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={() => 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>}
      <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-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="Способ подключения">
                {[
                  ['gateway', 'Gateway'],
                  ['proxy', 'Gateway Proxy'],
                ].map(([tab, label]) => (
                  <button
                    className={`client-access-tab${accessTab === tab ? ' is-active' : ''}`}
                    type="button"
                    role="tab"
                    key={tab}
                    aria-selected={accessTab === tab}
                    onClick={() => setAccessTab(tab)}
                  >
                    {label}
                  </button>
                ))}
              </div>
            )}
            {(isGateway ? accessTab === 'gateway' : false) ? (
              <div className="client-access-point" role="tabpanel" key="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">{copyFeedback.failed ? 'Error' : 'Copied'}</span>}
                </button>
              </div>
            ) : (
            <div className="client-access-point" role={isGateway ? 'tabpanel' : undefined} key="proxy">
              {!isGateway && (
                <span className={`client-proxy-label${gatewayDirect ? ' is-gateway' : ''}`}>
                  {gatewayDirect
                    ? `Через Harbor Gateway · ${state.gatewayAuto.address}`
                    : 'Локальный VPN'}
                </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">{copyFeedback.failed ? 'Error' : 'Copied'}</span>}
                </button>
              ))}
              </div>
            </div>
            )}
          </section>
          <InlineError error={error} context="connection" />
          <InlineProgress operations={operations} context="connection" />
        </section>
        )}

        <div
          className={`client-form${subscriptionWaiting ? ' is-waiting' : ''}${confirmingDelete ? ' is-confirming-delete' : ''}`}
          aria-hidden={subscriptionWaiting}
          aria-disabled={gatewayDirect}
          inert={subscriptionWaiting || gatewayDirect ? true : undefined}
        >
          {confirmingDelete && <section
            className="client-delete-confirmation"
            role="group"
            aria-labelledby="delete-subscription-title"
            aria-describedby="delete-subscription-description"
          >
            <span>Необратимое действие</span>
            <h2 id="delete-subscription-title">Удалить подписку?</h2>
            <p id="delete-subscription-description">
              Harbor остановит VPN и удалит сохранённую подписку. Приложения, настроенные на локальный прокси, не смогут выходить в сеть до добавления новой подписки.
            </p>
            <div className="client-delete-actions">
              <button type="button" onClick={() => setConfirmingDelete(false)}>Отмена</button>
              <button className="is-danger" type="button" disabled={subscriptionDeleteBlocked} onClick={forgetSubscription}>Удалить</button>
            </div>
          </section>}
          <div className="client-form-content" inert={confirmingDelete ? true : undefined}>
          <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'
                    ? 'Сохранить подписку'
                    : subscriptionValidationStatus === 'invalid'
                      ? 'Ссылка подписки не распознана'
                      : subscriptionValidationStatus === 'unavailable'
                        ? 'Проверка подписки временно недоступна'
                        : 'Проверяем ссылку подписки'}
                  disabled={subscriptionImportBlocked || subscriptionValidationStatus !== 'valid'}
                >
                  {subscriptionValidationStatus === 'valid'
                    ? '✓'
                    : subscriptionValidationStatus === 'invalid'
                      ? '×'
                      : subscriptionValidationStatus === 'unavailable' ? '!' : '…'}
                </button>
              )}
            </form>
            <InlineError error={subscriptionError || 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.tag];
                const selected = server.tag === selectedTag;
                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.tag}
                    disabled={serverApplyBlocked}
                    aria-pressed={selected}
                    style={{ '--server-index': index }}
                    onClick={() => selectServer(server.tag)}
                  >
                    <strong>{server.tag}</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">
          <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>}

    </div>
  );
}
