import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';
import {
  Activity,
  AlertCircle,
  Box,
  CheckCircle2,
  Clipboard,
  Cpu,
  Download,
  ExternalLink,
  HardDrive,
  Loader2,
  PackageCheck,
  Play,
  PlugZap,
  RefreshCcw,
  RotateCcw,
  Search,
  Server,
  Settings,
  ShieldCheck,
  Trash2,
  WifiOff,
  X,
  XCircle
} from 'lucide-react';
import {
  DEFAULT_AGENT_BASE_URL,
  DEFAULT_PACKAGE_BASE_URL,
  fetchAgentHealth,
  fetchAgentSystemInfo,
  fetchApplicationDetail,
  fetchApplicationManifest,
  fetchInstalledApps,
  fetchLatestAgentPackage,
  fetchPackageApps,
  fetchTaskComponents,
  fetchTaskLogs,
  fetchTaskStatus,
  getAppOpenUrl,
  joinUrl,
  normalizeUrl,
  queueInstall,
  queueRemove,
  queueUpdate
} from './services/api.js';
import './styles.css';

const SETTINGS_KEY = 'robot-installer-client-settings';
const TERMINAL_TASK_STATUSES = new Set(['success', 'failed', 'cancelled']);
const TASK_STATUS_TONES = {
  queued: 'info',
  running: 'info',
  success: 'success',
  failed: 'danger',
  cancelled: 'warning'
};
const TASK_ACTION_LABELS = {
  install: 'Installing',
  update: 'Updating',
  remove: 'Removing'
};
const AGENT_VERSION_COLLATOR = new Intl.Collator('en', {
  numeric: true,
  sensitivity: 'base'
});

function readSettings() {
  try {
    const parsed = JSON.parse(window.localStorage.getItem(SETTINGS_KEY) || '{}');
    return {
      packageBaseUrl: normalizeUrl(parsed.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL),
      agentBaseUrl: normalizeUrl(parsed.agentBaseUrl || DEFAULT_AGENT_BASE_URL)
    };
  } catch {
    return {
      packageBaseUrl: DEFAULT_PACKAGE_BASE_URL,
      agentBaseUrl: DEFAULT_AGENT_BASE_URL
    };
  }
}

function saveSettings(settings) {
  window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}

function getErrorMessage(error) {
  return error instanceof Error ? error.message : String(error || 'Có lỗi xảy ra');
}

function copyTextFallback(text) {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.setAttribute('readonly', '');
  textarea.style.position = 'fixed';
  textarea.style.top = '-1000px';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);
  textarea.focus();
  textarea.select();
  textarea.setSelectionRange(0, textarea.value.length);

  try {
    return document.execCommand('copy');
  } finally {
    document.body.removeChild(textarea);
  }
}

function isTaskActive(task) {
  return Boolean(task?.taskId && !TERMINAL_TASK_STATUSES.has(task.status));
}

function clampProgress(value) {
  return Math.max(0, Math.min(100, Number(value) || 0));
}

function statusBadgeClass(status) {
  const tone = TASK_STATUS_TONES[status] || 'info';
  return `badge badge-${tone}`;
}

function formatTaskTime(value) {
  if (!value) return '--';
  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) return value;
  return parsed.toLocaleTimeString([], {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
}

function compareAgentVersions(currentVersion, latestVersion) {
  const current = String(currentVersion || '').trim();
  const latest = String(latestVersion || '').trim();
  if (!current && !latest) return 0;
  if (!current) return -1;
  if (!latest) return 1;
  return AGENT_VERSION_COLLATOR.compare(current, latest);
}

const CLIENT_OS_LABELS = {
  android: 'Android',
  linux: 'Linux',
  macos: 'macOS',
  unknown: 'unknown OS',
  windows: 'Windows'
};

function detectClientOs() {
  if (typeof navigator === 'undefined') return 'unknown';

  const ua = String(navigator.userAgent || '').toLowerCase();
  const platform = [
    navigator.userAgentData?.platform,
    navigator.platform
  ].filter(Boolean).join(' ').toLowerCase();

  if (ua.includes('android') || platform.includes('android')) return 'android';
  if (platform.includes('linux') || ua.includes('linux')) return 'linux';
  if (platform.includes('win') || ua.includes('windows')) return 'windows';
  if (platform.includes('mac') || ua.includes('mac os')) return 'macos';
  return 'unknown';
}

function getClientOsLabel(os) {
  return CLIENT_OS_LABELS[os] || CLIENT_OS_LABELS.unknown;
}

function parseUrlLike(value) {
  const text = normalizeUrl(value);
  if (!text) return null;

  const urlText = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(text)
    ? text
    : `http://${text}`;

  try {
    return new URL(urlText);
  } catch {
    return null;
  }
}

function isLoopbackHostname(hostname) {
  const host = String(hostname || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
  return Boolean(
    host === 'localhost'
    || host.endsWith('.localhost')
    || host === '::1'
    || host === '0.0.0.0'
    || /^127(?:\.\d{1,3}){0,3}$/.test(host)
  );
}

function isLoopbackAgentEndpoint(agentBaseUrl) {
  const parsed = parseUrlLike(agentBaseUrl);
  return !parsed?.hostname || isLoopbackHostname(parsed.hostname);
}

function resolveTargetOpenUrl(openUrl, agentBaseUrl, isRemoteAgentEndpoint) {
  if (!isRemoteAgentEndpoint) return openUrl;

  const parsedOpenUrl = parseUrlLike(openUrl);
  const parsedAgentUrl = parseUrlLike(agentBaseUrl);
  if (!parsedOpenUrl || !parsedAgentUrl || !isLoopbackHostname(parsedOpenUrl.hostname)) {
    return openUrl;
  }

  parsedOpenUrl.hostname = parsedAgentUrl.hostname;
  return parsedOpenUrl.toString();
}

function App() {
  const [settings, setSettings] = useState(readSettings);
  const [draftSettings, setDraftSettings] = useState(settings);
  const [apps, setApps] = useState([]);
  const [installedApps, setInstalledApps] = useState([]);
  const [latestAgentPackage, setLatestAgentPackage] = useState(null);
  const [agentHealth, setAgentHealth] = useState(null);
  const [systemInfo, setSystemInfo] = useState(null);
  const [packageStatus, setPackageStatus] = useState({ state: 'idle', message: '' });
  const [agentStatus, setAgentStatus] = useState({ state: 'idle', message: '' });
  const [selectedAppId, setSelectedAppId] = useState('');
  const [selectedDetail, setSelectedDetail] = useState(null);
  const [selectedManifest, setSelectedManifest] = useState(null);
  const [detailStatus, setDetailStatus] = useState({ state: 'idle', message: '' });
  const [query, setQuery] = useState('');
  const [filter, setFilter] = useState('all');
  const [toast, setToast] = useState(null);
  const [busyAction, setBusyAction] = useState('');
  const [activeTask, setActiveTask] = useState(null);
  const [endpointDialogOpen, setEndpointDialogOpen] = useState(false);
  const [agentDialogOpen, setAgentDialogOpen] = useState(false);

  const packageBaseUrl = settings.packageBaseUrl;
  const agentBaseUrl = settings.agentBaseUrl;
  const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`;
  const agentCommand = latestAgentPackage?.installCommand || installCommand;
  const clientOs = useMemo(() => detectClientOs(), []);
  const isClientLinux = clientOs === 'linux';
  const isClientWindows = clientOs === 'windows';
  const isLocalAgentEndpoint = useMemo(() => isLoopbackAgentEndpoint(agentBaseUrl), [agentBaseUrl]);
  const isRemoteAgentEndpoint = !isLocalAgentEndpoint;
  const canUseAgentEndpoint = isRemoteAgentEndpoint || isClientLinux;
  const canManageApps = canUseAgentEndpoint;
  const canShowAgentCommand = isClientLinux || isClientWindows || isRemoteAgentEndpoint;
  const clientOsLabel = getClientOsLabel(clientOs);
  const agentTargetLabel = isRemoteAgentEndpoint ? 'Remote Ubuntu Agent' : 'Local Agent';
  const agentTargetMachine = isRemoteAgentEndpoint ? 'remote Ubuntu server' : 'local machine';

  const installedByAppId = useMemo(() => {
    return new Map(installedApps.map((app) => [app.appId, app]));
  }, [installedApps]);

  const mergedApps = useMemo(() => {
    return apps.map((app) => {
      const installed = installedByAppId.get(app.appId) || installedByAppId.get(app.appCode);
      const isInstalled = Boolean(installed);
      const canUpdate = Boolean(isInstalled && installed.version && installed.version !== app.version);

      return {
        ...app,
        installed,
        localStatus: canUpdate ? 'update' : (isInstalled ? 'installed' : 'available'),
        canUpdate
      };
    });
  }, [apps, installedByAppId]);

  const filteredApps = useMemo(() => {
    const needle = query.trim().toLowerCase();
    return mergedApps.filter((app) => {
      const matchesQuery = !needle || [
        app.appId,
        app.appCode,
        app.appName,
        app.version,
        app.status
      ].join(' ').toLowerCase().includes(needle);

      if (!matchesQuery) return false;
      if (filter === 'installed') return app.localStatus === 'installed' || app.localStatus === 'update';
      if (filter === 'updates') return app.localStatus === 'update';
      if (filter === 'available') return app.localStatus === 'available';
      return true;
    });
  }, [filter, mergedApps, query]);

  const selectedApp = useMemo(() => {
    return mergedApps.find((app) => app.appId === selectedAppId) || mergedApps[0] || null;
  }, [mergedApps, selectedAppId]);

  const agentNeedsUpdate = useMemo(() => {
    return Boolean(
      agentHealth?.agentVersion
      && latestAgentPackage?.version
      && compareAgentVersions(agentHealth.agentVersion, latestAgentPackage.version) < 0
    );
  }, [agentHealth?.agentVersion, latestAgentPackage?.version]);

  const notify = useCallback((type, message) => {
    setToast({ id: Date.now(), type, message });
  }, []);

  const refreshPackage = useCallback(async () => {
    setPackageStatus({ state: 'loading', message: 'Đang tải app từ package server' });
    try {
      const [nextApps, nextAgentPackage] = await Promise.all([
        fetchPackageApps(packageBaseUrl),
        fetchLatestAgentPackage(packageBaseUrl).catch(() => null)
      ]);
      setApps(nextApps);
      setLatestAgentPackage(nextAgentPackage);
      const agentNote = nextAgentPackage?.version ? ` · Agent ${nextAgentPackage.version}` : '';
      setPackageStatus({ state: 'success', message: `${nextApps.length} app released${agentNote}` });
      return nextApps;
    } catch (error) {
      setPackageStatus({ state: 'danger', message: getErrorMessage(error) });
      setApps([]);
      setLatestAgentPackage(null);
      return [];
    }
  }, [packageBaseUrl]);

  const refreshAgent = useCallback(async () => {
    if (!canUseAgentEndpoint) {
      setAgentHealth(null);
      setSystemInfo(null);
      setInstalledApps([]);
      setAgentStatus({
        state: 'warning',
        message: isClientWindows
          ? 'Local endpoint points to this Windows machine. Use a remote Ubuntu Agent endpoint.'
          : `Local endpoint requires Linux. Current client is ${clientOsLabel}.`
      });
      return false;
    }

    setAgentStatus({ state: 'loading', message: `Checking ${agentTargetLabel}` });
    try {
      const health = await fetchAgentHealth(agentBaseUrl);
      setAgentHealth(health);
      setAgentStatus({ state: 'success', message: `${health.hostname || agentTargetLabel} online` });

      const [info, installed] = await Promise.all([
        fetchAgentSystemInfo(agentBaseUrl).catch(() => null),
        fetchInstalledApps(agentBaseUrl)
      ]);
      setSystemInfo(info);
      setInstalledApps(installed);
      return true;
    } catch (error) {
      setAgentHealth(null);
      setSystemInfo(null);
      setInstalledApps([]);
      setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
      return false;
    }
  }, [agentBaseUrl, agentTargetLabel, canUseAgentEndpoint, clientOsLabel, isClientWindows]);

  const refreshAll = useCallback(async () => {
    await Promise.all([refreshPackage(), refreshAgent()]);
  }, [refreshAgent, refreshPackage]);

  const loadSelectedDetail = useCallback(async (app) => {
    if (!app) {
      setSelectedDetail(null);
      setSelectedManifest(null);
      return;
    }

    setDetailStatus({ state: 'loading', message: 'Đang tải manifest' });
    try {
      const [detail, manifest] = await Promise.all([
        fetchApplicationDetail(packageBaseUrl, app.appId).catch(() => null),
        fetchApplicationManifest(packageBaseUrl, app.appId, app.version).catch(() => null)
      ]);
      setSelectedDetail(detail);
      setSelectedManifest(manifest);
      setDetailStatus({ state: 'success', message: manifest ? 'Manifest sẵn sàng' : 'Đã tải app detail' });
    } catch (error) {
      setSelectedDetail(null);
      setSelectedManifest(null);
      setDetailStatus({ state: 'danger', message: getErrorMessage(error) });
    }
  }, [packageBaseUrl]);

  const loadTaskSnapshot = useCallback(async (taskId) => {
    try {
      const [nextTask, logs, components] = await Promise.all([
        fetchTaskStatus(agentBaseUrl, taskId),
        fetchTaskLogs(agentBaseUrl, taskId).catch(() => []),
        fetchTaskComponents(agentBaseUrl, taskId).catch(() => [])
      ]);
      const snapshot = {
        ...nextTask,
        logs,
        components,
        pollError: ''
      };

      setActiveTask((current) => {
        if (!current || current.taskId !== taskId) return current;
        return { ...current, ...snapshot };
      });

      if (TERMINAL_TASK_STATUSES.has(snapshot.status)) {
        await refreshAgent();
      }
      return snapshot;
    } catch (error) {
      const message = getErrorMessage(error);
      setActiveTask((current) => {
        if (!current || current.taskId !== taskId) return current;
        return { ...current, pollError: message };
      });
      return null;
    }
  }, [agentBaseUrl, refreshAgent]);

  const startTask = useCallback((queuedTask, action, app) => {
    const queuedAt = new Date().toISOString();
    setActiveTask({
      taskId: queuedTask.taskId,
      action,
      appId: app.appId,
      appName: app.appName,
      status: queuedTask.status || 'queued',
      progress: 0,
      currentStep: 'queued',
      logs: [
        {
          time: queuedAt,
          level: 'info',
          message: `Task ${queuedTask.taskId} queued`
        }
      ],
      components: [],
      pollError: '',
      queuedAt
    });
  }, []);

  const startPreflightFailedTask = useCallback((action, app, message) => {
    const failedAt = new Date().toISOString();
    const taskId = `preflight_${Date.now()}`;

    setActiveTask({
      taskId,
      action,
      appId: app.appId,
      appName: app.appName,
      status: 'failed',
      progress: 5,
      currentStep: 'package preflight failed',
      errorMessage: message,
      logs: [
        {
          time: failedAt,
          level: 'error',
          message
        }
      ],
      components: [],
      pollError: '',
      localOnly: true,
      queuedAt: failedAt,
      finishedAt: failedAt
    });
  }, []);

  const runAppAction = useCallback(async (action, app) => {
    if (!canManageApps) {
      notify('warning', 'Local Agent endpoint can install only from a Linux browser machine. Use a remote Ubuntu Agent endpoint.');
      return;
    }

    if (!agentHealth) {
      notify('warning', `${agentTargetLabel} is offline. Check the endpoint and press Retry.`);
      return;
    }

    if (action === 'remove' && !window.confirm(`Remove ${app.appName} from ${agentTargetMachine}?`)) {
      return;
    }

    const key = `${action}:${app.appId}`;
    setBusyAction(key);
    try {
      let queuedTask;
      if (action === 'install') {
        const manifest = await fetchApplicationManifest(packageBaseUrl, app.appId, app.version);
        setSelectedManifest(manifest);
        setDetailStatus({ state: 'success', message: 'Manifest ready' });
        queuedTask = await queueInstall(agentBaseUrl, app);
      } else if (action === 'update') {
        const manifest = await fetchApplicationManifest(packageBaseUrl, app.appId, app.version);
        setSelectedManifest(manifest);
        setDetailStatus({ state: 'success', message: 'Manifest ready' });
        queuedTask = await queueUpdate(agentBaseUrl, app, app.installed);
      } else {
        queuedTask = await queueRemove(agentBaseUrl, app);
      }

      startTask(queuedTask, action, app);
      notify('success', `Đã queue task ${queuedTask.taskId}`);
    } catch (error) {
      const message = getErrorMessage(error);
      if (action === 'install' || action === 'update') {
        setSelectedManifest(null);
        setDetailStatus({ state: 'danger', message });
        startPreflightFailedTask(action, app, message);
      }
      notify('failure', message);
    } finally {
      setBusyAction('');
    }
  }, [agentBaseUrl, agentHealth, agentTargetLabel, agentTargetMachine, canManageApps, notify, packageBaseUrl, startPreflightFailedTask, startTask]);

  const openEndpointDialog = useCallback(() => {
    setDraftSettings(settings);
    setEndpointDialogOpen(true);
  }, [settings]);

  const closeEndpointDialog = useCallback(() => {
    setDraftSettings(settings);
    setEndpointDialogOpen(false);
  }, [settings]);

  const applySettings = useCallback(() => {
    const nextSettings = {
      packageBaseUrl: normalizeUrl(draftSettings.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL),
      agentBaseUrl: normalizeUrl(draftSettings.agentBaseUrl || DEFAULT_AGENT_BASE_URL)
    };
    setSettings(nextSettings);
    setDraftSettings(nextSettings);
    saveSettings(nextSettings);
    setEndpointDialogOpen(false);
    notify('info', 'Đã cập nhật endpoint test');
  }, [draftSettings, notify]);

  const copyInstallCommand = useCallback(async () => {
    if (!canShowAgentCommand) {
      notify('warning', 'Agent command is hidden for this client OS.');
      return;
    }

    if (!navigator.clipboard?.writeText) {
      if (copyTextFallback(agentCommand)) {
        notify('success', agentNeedsUpdate ? 'Da copy lenh update Agent' : 'Da copy lenh cai Agent');
        return;
      }

      window.prompt('Copy Agent command', agentCommand);
      notify('warning', 'Browser dang chan copy tu dong. Hay copy lenh trong popup.');
      return;
    }

    try {
      await navigator.clipboard.writeText(agentCommand);
      notify('success', agentNeedsUpdate ? 'Đã copy lệnh update Agent' : 'Đã copy lệnh cài Agent');
    } catch {
      notify('warning', 'Không thể copy tự động trong browser này');
    }
  }, [agentCommand, agentNeedsUpdate, canShowAgentCommand, notify]);

  useEffect(() => {
    refreshAll();
  }, [refreshAll]);

  useEffect(() => {
    if (!selectedApp) {
      setSelectedDetail(null);
      setSelectedManifest(null);
      return;
    }
    setSelectedAppId(selectedApp.appId);
    loadSelectedDetail(selectedApp);
  }, [loadSelectedDetail, selectedApp?.appId]);

  useEffect(() => {
    if (!activeTask?.taskId || TERMINAL_TASK_STATUSES.has(activeTask.status)) return undefined;
    let disposed = false;
    let terminalNotified = false;

    async function poll() {
      const nextTask = await loadTaskSnapshot(activeTask.taskId);
      if (disposed || !nextTask) return;
      if (TERMINAL_TASK_STATUSES.has(nextTask.status)) {
        window.clearInterval(timer);
        if (!terminalNotified) {
          terminalNotified = true;
          if (nextTask.status === 'success') {
            notify('success', `${activeTask.appName || 'Task'} completed`);
          } else if (nextTask.status === 'failed') {
            notify('failure', nextTask.errorMessage || `Task ${nextTask.taskId} failed`);
          } else {
            notify('warning', `Task ${nextTask.taskId} ${nextTask.status}`);
          }
        }
      }
    }

    poll();
    const timer = window.setInterval(poll, 1200);

    return () => {
      disposed = true;
      window.clearInterval(timer);
    };
  }, [activeTask?.action, activeTask?.appName, activeTask?.status, activeTask?.taskId, loadTaskSnapshot, notify]);

  useEffect(() => {
    if (!toast) return undefined;
    const timer = window.setTimeout(() => setToast(null), 3200);
    return () => window.clearTimeout(timer);
  }, [toast]);

  useEffect(() => {
    if (!endpointDialogOpen && !agentDialogOpen) return undefined;

    function onKeyDown(event) {
      if (event.key === 'Escape') {
        if (endpointDialogOpen) closeEndpointDialog();
        if (agentDialogOpen) setAgentDialogOpen(false);
      }
    }

    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [agentDialogOpen, closeEndpointDialog, endpointDialogOpen]);

  const agentStatusTitle = !canUseAgentEndpoint
    ? (isClientWindows ? 'Remote endpoint needed' : 'Linux client required')
    : (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline'));
  const agentStatusDetail = !canUseAgentEndpoint
    ? (isClientWindows ? 'Change endpoint to Ubuntu server IP' : `${clientOsLabel} detected`)
    : (agentHealth ? `${agentHealth.hostname || agentTargetLabel} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : agentBaseUrl);
  const agentStatusTone = !canUseAgentEndpoint
    ? 'warning'
    : (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : (agentStatus.state === 'loading' ? 'info' : 'danger')));
  const topbarAgentState = !canUseAgentEndpoint
    ? 'Use Ubuntu Agent endpoint'
    : (agentHealth ? `Ready on ${agentTargetMachine}` : `Waiting for ${agentTargetLabel}`);

  return (
    <div className="app-shell">
      <aside className="sidebar">
        <div className="brand-block">
          <div className="brand-mark">
            <PackageCheck size={20} aria-hidden="true" />
          </div>
          <div className="brand-copy">
            <strong>Robot Installer</strong>
            <span>Web Client</span>
          </div>
        </div>

        <div className="nav-section">
          <div className="nav-label">Runtime</div>
          <StatusBox
            icon={agentHealth ? PlugZap : WifiOff}
            title={agentStatusTitle}
            detail={agentStatusDetail}
            tone={agentStatusTone}
          />
          <StatusBox
            icon={Server}
            title="Package API"
            detail={packageStatus.message || packageBaseUrl}
            tone={packageStatus.state === 'success' ? 'success' : packageStatus.state}
          />

          <div className="nav-label">Endpoint</div>
          <div className="endpoint-summary" aria-label="Current endpoints">
            <div className="endpoint-summary-row">
              <span>Package server</span>
              <strong title={packageBaseUrl}>{packageBaseUrl}</strong>
            </div>
            <div className="endpoint-summary-row">
              <span>Agent endpoint</span>
              <strong title={agentBaseUrl}>{agentBaseUrl}</strong>
            </div>
          </div>
          <button className="btn btn-secondary full" type="button" onClick={openEndpointDialog}>
            <Settings size={15} aria-hidden="true" />
            Endpoint settings
          </button>
        </div>
      </aside>

      <main className="main-shell">
        <header className="topbar">
          <div className="topbar-title">
            <span>robot.installer</span>
            <strong>{topbarAgentState}</strong>
          </div>
          <div className="topbar-actions">
            <AgentStatusButton
              detail={agentStatusDetail}
              onClick={() => setAgentDialogOpen(true)}
              title={agentStatusTitle}
              tone={agentStatusTone}
            />
            <a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
              <ExternalLink size={17} aria-hidden="true" />
            </a>
            <button className="btn btn-secondary" type="button" onClick={refreshAll}>
              <RefreshCcw size={15} aria-hidden="true" />
              Retry
            </button>
          </div>
        </header>

        <section className="page">
          {!canUseAgentEndpoint && (
            <ClientOsNotice
              agentCommand={agentCommand}
              canShowCommand={canShowAgentCommand}
              isWindows={isClientWindows}
              onCopyCommand={copyInstallCommand}
              osLabel={clientOsLabel}
            />
          )}

          {canUseAgentEndpoint && !agentHealth && (
            <div className="offline-banner">
              <AlertCircle size={19} aria-hidden="true" />
              <div>
                <strong>{agentTargetLabel} is offline</strong>
                <code>{agentCommand}</code>
              </div>
              <button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
                <Clipboard size={15} aria-hidden="true" />
                Copy
              </button>
            </div>
          )}

          {canUseAgentEndpoint && agentNeedsUpdate && (
            <div className="offline-banner agent-update-banner">
              <AlertCircle size={19} aria-hidden="true" />
              <div>
                <strong>Agent {latestAgentPackage.version} is ready</strong>
                <code>{agentCommand}</code>
              </div>
              <button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
                <Clipboard size={15} aria-hidden="true" />
                Copy
              </button>
            </div>
          )}

          <div className="workbench-grid">
            <section className="table-panel">
              <div className="page-filters inline">
                <label className="filter-field wide">
                  <span>Search</span>
                  <div className="input-with-icon">
                    <Search size={15} aria-hidden="true" />
                    <input
                      value={query}
                      onChange={(event) => setQuery(event.target.value)}
                      placeholder="App code, name, version..."
                    />
                  </div>
                </label>
                <label className="filter-field">
                  <span>Status</span>
                  <select value={filter} onChange={(event) => setFilter(event.target.value)}>
                    <option value="all">All</option>
                    <option value="available">Available</option>
                    <option value="installed">Installed</option>
                    <option value="updates">Updates</option>
                  </select>
                </label>
              </div>

              <div className="table-wrap">
                <table>
                  <thead>
                    <tr>
                      <th>Application</th>
                      <th>Released</th>
                      <th>Local</th>
                      <th>Packages</th>
                      <th className="action-col">Actions</th>
                    </tr>
                  </thead>
                  <tbody>
                    {packageStatus.state === 'loading' && (
                      <tr>
                        <td colSpan="5" className="table-empty">Đang tải danh sách app...</td>
                      </tr>
                    )}
                    {packageStatus.state === 'danger' && (
                      <tr>
                        <td colSpan="5" className="table-empty danger-text">{packageStatus.message}</td>
                      </tr>
                    )}
                    {packageStatus.state !== 'loading' && packageStatus.state !== 'danger' && filteredApps.length === 0 && (
                      <tr>
                        <td colSpan="5" className="table-empty">Chưa có app phù hợp bộ lọc.</td>
                      </tr>
                    )}
                    {filteredApps.map((app) => {
                      const rowTask = activeTask?.appId === app.appId ? activeTask : null;
                      const rowTaskBusy = isTaskActive(rowTask);
                      const installBusy = busyAction === `install:${app.appId}` || (rowTaskBusy && rowTask.action === 'install');
                      const updateBusy = busyAction === `update:${app.appId}` || (rowTaskBusy && rowTask.action === 'update');
                      const removeBusy = busyAction === `remove:${app.appId}` || (rowTaskBusy && rowTask.action === 'remove');
                      const appOpenUrl = app.installed
                        ? getAppOpenUrl({ ...app, openUrl: app.openUrl || app.installed.openUrl })
                        : '';
                      const openUrl = resolveTargetOpenUrl(appOpenUrl, agentBaseUrl, isRemoteAgentEndpoint);

                      return (
                        <tr
                          key={app.appId}
                          className={selectedApp?.appId === app.appId ? 'selected-row' : ''}
                          onClick={() => setSelectedAppId(app.appId)}
                        >
                          <td>
                            <button className="table-title as-button" type="button" onClick={() => setSelectedAppId(app.appId)}>
                              {app.appName}
                            </button>
                            <span className="table-subtitle">{app.appCode || app.appId}</span>
                          </td>
                          <td>
                            <span className="badge badge-info">{app.version}</span>
                          </td>
                          <td>
                            <LocalStatus app={app} task={rowTaskBusy ? rowTask : null} />
                          </td>
                          <td>{app.packageCount || 0}</td>
                          <td className="action-col">
                            <div className="action-group">
                              {canManageApps && !app.installed && (
                                <button
                                  className="btn btn-primary compact"
                                  type="button"
                                  disabled={!agentHealth || rowTaskBusy || installBusy}
                                  onClick={(event) => {
                                    event.stopPropagation();
                                    runAppAction('install', app);
                                  }}
                                >
                                  {installBusy ? <Loader2 className="spin" size={14} aria-hidden="true" /> : <Play size={14} aria-hidden="true" />}
                                  Install
                                </button>
                              )}
                              {canManageApps && app.installed && app.canUpdate && (
                                <button
                                  className="btn btn-warning compact"
                                  type="button"
                                  disabled={!agentHealth || rowTaskBusy || updateBusy}
                                  onClick={(event) => {
                                    event.stopPropagation();
                                    runAppAction('update', app);
                                  }}
                                >
                                  {updateBusy ? <Loader2 className="spin" size={14} aria-hidden="true" /> : <RotateCcw size={14} aria-hidden="true" />}
                                  Update
                                </button>
                              )}
                              {canManageApps && app.installed && openUrl && (
                                <a
                                  className="btn btn-secondary compact"
                                  href={openUrl}
                                  target="_blank"
                                  rel="noreferrer"
                                  title={`Open ${app.appName}`}
                                  onClick={(event) => event.stopPropagation()}
                                >
                                  <ExternalLink size={14} aria-hidden="true" />
                                  Open App
                                </a>
                              )}
                              {canManageApps && app.installed && (
                                <button
                                  className="icon-button danger"
                                  type="button"
                                  title="Remove"
                                  disabled={!agentHealth || rowTaskBusy || removeBusy}
                                  onClick={(event) => {
                                    event.stopPropagation();
                                    runAppAction('remove', app);
                                  }}
                                >
                                  {removeBusy ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Trash2 size={16} aria-hidden="true" />}
                                </button>
                              )}
                            </div>
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
              <div className="page-pager">
                <span>{filteredApps.length} / {mergedApps.length} apps</span>
                <span>{packageBaseUrl}</span>
              </div>
            </section>

            <aside className="side-stack">
              {activeTask && (
                <TaskPanel
                  task={activeTask}
                  onClear={() => setActiveTask(null)}
                  onRefresh={() => loadTaskSnapshot(activeTask.taskId)}
                />
              )}
              <AppDetailPanel
                app={selectedApp}
                detail={selectedDetail}
                manifest={selectedManifest}
                status={detailStatus}
                packageBaseUrl={packageBaseUrl}
              />
            </aside>
          </div>
        </section>
      </main>

      {toast && <Toast toast={toast} />}
      {agentDialogOpen && (
        <AgentDialog
          endpoint={agentBaseUrl}
          health={agentHealth}
          latestAgentPackage={latestAgentPackage}
          needsUpdate={agentNeedsUpdate}
          onClose={() => setAgentDialogOpen(false)}
          onCopyUpdate={copyInstallCommand}
          showAgentActions={canShowAgentCommand}
          status={agentStatus}
          systemInfo={systemInfo}
          title={agentTargetLabel}
        />
      )}
      {endpointDialogOpen && (
        <EndpointDialog
          draftSettings={draftSettings}
          onApply={applySettings}
          onCancel={closeEndpointDialog}
          onChange={setDraftSettings}
        />
      )}
    </div>
  );
}

function AgentStatusButton({ title, detail, tone, onClick }) {
  const Icon = tone === 'success'
    ? CheckCircle2
    : tone === 'danger'
      ? XCircle
      : tone === 'warning'
        ? AlertCircle
        : Loader2;

  return (
    <button className={`agent-status-button tone-${tone || 'info'}`} type="button" onClick={onClick}>
      <span className="agent-status-icon">
        <Icon className={tone === 'info' ? 'spin' : ''} size={16} aria-hidden="true" />
      </span>
      <span className="agent-status-copy">
        <strong>{title}</strong>
        <small>{detail}</small>
      </span>
    </button>
  );
}

function AgentDialog(props) {
  return (
    <div className="dialog-backdrop">
      <section
        aria-labelledby="agent-dialog-title"
        aria-modal="true"
        className="dialog-panel agent-dialog-panel"
        role="dialog"
      >
        <AgentPanel {...props} />
      </section>
    </div>
  );
}

function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) {
  return (
    <div className="dialog-backdrop">
      <section
        aria-labelledby="endpoint-dialog-title"
        aria-modal="true"
        className="dialog-panel"
        role="dialog"
      >
        <form
          onSubmit={(event) => {
            event.preventDefault();
            onApply();
          }}
        >
          <div className="dialog-header">
            <div>
              <h2 id="endpoint-dialog-title">Endpoint settings</h2>
              <p>Changes only apply after you press Apply.</p>
            </div>
            <button className="icon-button subtle" type="button" title="Close" onClick={onCancel}>
              <X size={16} aria-hidden="true" />
            </button>
          </div>

          <div className="dialog-body">
            <label className="settings-field">
              <span>Package server</span>
              <input
                autoFocus
                value={draftSettings.packageBaseUrl}
                onChange={(event) => onChange((current) => ({
                  ...current,
                  packageBaseUrl: event.target.value
                }))}
              />
            </label>
            <label className="settings-field">
              <span>Agent endpoint</span>
              <input
                value={draftSettings.agentBaseUrl}
                onChange={(event) => onChange((current) => ({
                  ...current,
                  agentBaseUrl: event.target.value
                }))}
              />
            </label>
          </div>

          <div className="dialog-actions">
            <button className="btn btn-secondary" type="button" onClick={onCancel}>
              Cancel
            </button>
            <button className="btn btn-primary" type="submit">
              <Settings size={15} aria-hidden="true" />
              Apply endpoints
            </button>
          </div>
        </form>
      </section>
    </div>
  );
}

function StatusBox({ icon: Icon, title, detail, tone }) {
  return (
    <div className={`status-box tone-${tone || 'muted'}`}>
      <span className="status-icon"><Icon size={16} aria-hidden="true" /></span>
      <div>
        <strong>{title}</strong>
        <span>{detail}</span>
      </div>
    </div>
  );
}

function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand, osLabel }) {
  return (
    <div className={`offline-banner client-os-banner ${canShowCommand ? 'command-visible' : ''}`}>
      <AlertCircle size={19} aria-hidden="true" />
      <div>
        <strong>{isWindows ? 'Remote Ubuntu endpoint needed' : 'Local Agent requires Linux'}</strong>
        <p>
          {isWindows
            ? 'The endpoint is localhost, so it points at this Windows machine. Change it to http://<ubuntu-server-ip>:5010 or run the command below over Ubuntu SSH.'
            : `Current client is ${osLabel}. Use a remote Ubuntu Agent endpoint or open the Web Client from Linux.`}
        </p>
        {canShowCommand && <code>{agentCommand}</code>}
      </div>
      {canShowCommand && (
        <button className="btn btn-secondary" type="button" onClick={onCopyCommand}>
          <Clipboard size={15} aria-hidden="true" />
          Copy
        </button>
      )}
    </div>
  );
}

function LocalStatus({ app, task }) {
  if (isTaskActive(task)) {
    return (
      <span className="status-inline">
        <span className={statusBadgeClass(task.status)}>
          {TASK_ACTION_LABELS[task.action] || task.status}
        </span>
        <small>{clampProgress(task.progress)}%</small>
      </span>
    );
  }

  if (app.localStatus === 'update') {
    return (
      <span className="status-inline">
        <span className="badge badge-warning">Update</span>
        <small>{app.installed.version}</small>
      </span>
    );
  }

  if (app.localStatus === 'installed') {
    return (
      <span className="status-inline">
        <span className="badge badge-success">Installed</span>
        <small>{app.installed.version}</small>
      </span>
    );
  }

  return <span className="badge badge-muted">Available</span>;
}

function TaskPanel({ task, onClear, onRefresh }) {
  const progress = clampProgress(task.progress);
  const statusTone = TASK_STATUS_TONES[task.status] || 'info';
  const components = task.components || [];
  const logs = task.logs || [];
  const visibleLogs = logs.slice(-6);
  const canClear = TERMINAL_TASK_STATUSES.has(task.status);
  const canRefresh = !task.localOnly;

  return (
    <section className="panel task-panel">
      <div className="panel-header">
        <div>
          <h2>Task monitor</h2>
          <p>{task.taskId}</p>
        </div>
        <div className="panel-actions">
          <span className={statusBadgeClass(task.status)}>{task.status || 'queued'}</span>
          <button className="icon-button subtle" type="button" title="Refresh task" onClick={onRefresh} disabled={!canRefresh}>
            <RefreshCcw size={16} aria-hidden="true" />
          </button>
          {canClear && (
            <button className="icon-button subtle" type="button" title="Clear task" onClick={onClear}>
              <XCircle size={16} aria-hidden="true" />
            </button>
          )}
        </div>
      </div>

      <div className="task-progress" aria-label={`Task progress ${progress}%`}>
        <div className={`task-progress-bar tone-${statusTone}`} style={{ width: `${progress}%` }} />
      </div>

      <dl className="detail-list compact-list">
        <div>
          <dt>Action</dt>
          <dd>{task.action || task.type || '-'}</dd>
        </div>
        <div>
          <dt>App</dt>
          <dd>{task.appName || task.appId || '-'}</dd>
        </div>
        <div>
          <dt>Step</dt>
          <dd>{task.currentStep || '-'}</dd>
        </div>
        {(task.errorMessage || task.pollError) && (
          <div>
            <dt>Error</dt>
            <dd className="danger-text">{task.errorMessage || task.pollError}</dd>
          </div>
        )}
      </dl>

      <div className="component-list task-components">
        <div className="component-list-title">
          <Activity size={15} aria-hidden="true" />
          Components
        </div>
        {components.map((component) => (
          <div className="component-item task-component-item" key={component.componentId}>
            <div>
              <strong>{component.componentId}</strong>
              <span>{component.currentStep || component.type || '-'}</span>
            </div>
            <span className={statusBadgeClass(component.status)}>{component.progress}%</span>
          </div>
        ))}
        {!components.length && (
          <div className="table-empty compact-empty">Waiting for component details...</div>
        )}
      </div>

      <div className="task-log-list">
        {visibleLogs.map((log, index) => (
          <div className="task-log-line" key={`${log.time}-${log.level}-${index}`}>
            <span>{formatTaskTime(log.time)}</span>
            <strong>{log.level}</strong>
            <p>{log.message}</p>
          </div>
        ))}
        {!visibleLogs.length && (
          <div className="table-empty compact-empty">Waiting for task logs...</div>
        )}
      </div>
    </section>
  );
}

function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPackage, needsUpdate, onClose, onCopyUpdate, showAgentActions }) {
  const statusTone = needsUpdate
    ? 'warning'
    : health
      ? 'success'
      : status.state === 'loading'
        ? 'info'
        : status.state === 'warning'
          ? 'warning'
          : 'danger';
  const statusLabel = needsUpdate
    ? 'Update available'
    : health
      ? 'Online'
      : status.state === 'loading'
        ? 'Checking'
        : status.state === 'warning'
          ? 'Warning'
          : 'Offline';

  return (
    <section className="agent-detail">
      <div className="panel-header">
        <div>
          <h2 id="agent-dialog-title">{title || 'Agent'}</h2>
          <p>{status.message || endpoint || '127.0.0.1:5010'}</p>
        </div>
        <div className="panel-actions">
          {needsUpdate ? <AlertCircle className="panel-state warning" size={20} aria-hidden="true" /> : health ? <CheckCircle2 className="panel-state success" size={20} aria-hidden="true" /> : <XCircle className="panel-state danger" size={20} aria-hidden="true" />}
          <button className="icon-button subtle" type="button" title="Close" onClick={onClose}>
            <X size={16} aria-hidden="true" />
          </button>
        </div>
      </div>
      <dl className="detail-list compact-list">
        <div>
          <dt>Status</dt>
          <dd><span className={`badge badge-${statusTone}`}>{statusLabel}</span></dd>
        </div>
        <div>
          <dt>Endpoint</dt>
          <dd>{endpoint || '127.0.0.1:5010'}</dd>
        </div>
        <div>
          <dt>Version</dt>
          <dd>
            <span className={needsUpdate ? 'status-inline' : ''}>
              {health?.agentVersion || '-'}
              {needsUpdate && <span className="badge badge-warning">Update</span>}
            </span>
          </dd>
        </div>
        <div>
          <dt>Latest</dt>
          <dd>{latestAgentPackage?.version || '-'}</dd>
        </div>
        <div>
          <dt>Host</dt>
          <dd>{health?.hostname || '-'}</dd>
        </div>
        <div>
          <dt>OS</dt>
          <dd>{systemInfo?.os || health?.os || '-'}</dd>
        </div>
        <div>
          <dt>Arch</dt>
          <dd>{systemInfo?.architecture || health?.architecture || '-'}</dd>
        </div>
      </dl>
      <div className="agent-metrics">
        <span><Cpu size={14} aria-hidden="true" /> {systemInfo?.kernel || 'kernel -'}</span>
        <span><HardDrive size={14} aria-hidden="true" /> {systemInfo?.diskFree || 'disk -'}</span>
      </div>
      {showAgentActions && (
        <div className="agent-update-action">
          <button className={`btn ${needsUpdate ? 'btn-warning' : 'btn-secondary'}`} type="button" onClick={onCopyUpdate}>
            <Clipboard size={15} aria-hidden="true" />
            {needsUpdate ? 'Copy update command' : 'Copy Agent command'}
          </button>
          {needsUpdate && latestAgentPackage?.downloadUrl && (
            <a className="btn btn-secondary" href={latestAgentPackage.downloadUrl} target="_blank" rel="noreferrer">
              <Download size={15} aria-hidden="true" />
              Latest .deb
            </a>
          )}
        </div>
      )}
    </section>
  );
}

function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
  const packages = detail?.packages || [];
  const components = manifest?.components || [];

  return (
    <section className="panel app-detail-panel">
      <div className="panel-header">
        <div>
          <h2>{app?.appName || 'App detail'}</h2>
          <p>{app?.appCode || app?.appId || status.message || packageBaseUrl}</p>
        </div>
        {status.state === 'loading' ? <Loader2 className="spin panel-state" size={20} aria-hidden="true" /> : <Box className="panel-state" size={20} aria-hidden="true" />}
      </div>

      {app ? (
        <>
          <dl className="detail-list compact-list">
            <div>
              <dt>Released</dt>
              <dd>{app.version}</dd>
            </div>
            <div>
              <dt>Status</dt>
              <dd><span className="badge badge-success">{app.status || 'Released'}</span></dd>
            </div>
            <div>
              <dt>Local</dt>
              <dd>{app.installed ? `${app.installed.version} · ${app.installed.status || 'installed'}` : 'Not installed'}</dd>
            </div>
          </dl>

          <div className="component-list">
            <div className="component-list-title">
              <ShieldCheck size={15} aria-hidden="true" />
              Components
            </div>
            {status.state === 'danger' && (
              <div className="table-empty compact-empty danger-text">{status.message}</div>
            )}
            {(components.length ? components : packages).map((item) => (
              <div className="component-item" key={item.componentId || item.id || item.packageId}>
                <div>
                  <strong>{item.componentId || item.code || item.name}</strong>
                  <span>{item.packageName || item.selectedVersion || item.type}</span>
                </div>
                <span className="badge badge-muted">{item.version || item.selectedVersion || item.type || 'pkg'}</span>
              </div>
            ))}
            {!components.length && !packages.length && (
              <div className="table-empty compact-empty">{status.message || 'Chưa có component.'}</div>
            )}
          </div>
        </>
      ) : (
        <div className="table-empty compact-empty">Chọn app để xem manifest.</div>
      )}
    </section>
  );
}

function Toast({ toast }) {
  const tone = toast.type === 'failure' ? 'danger' : toast.type;
  const Icon = tone === 'danger' ? AlertCircle : tone === 'success' ? CheckCircle2 : Activity;
  return (
    <div className={`toast tone-${tone || 'info'}`}>
      <Icon size={17} aria-hidden="true" />
      <span>{toast.message}</span>
    </div>
  );
}

createRoot(document.getElementById('root')).render(<App />);
