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

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 packageBaseUrl = settings.packageBaseUrl;
  const agentBaseUrl = settings.agentBaseUrl;
  const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`;
  const agentCommand = latestAgentPackage?.installCommand || installCommand;

  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 stats = useMemo(() => {
    return {
      available: apps.length,
      installed: installedApps.length,
      updates: mergedApps.filter((app) => app.canUpdate).length,
      components: selectedManifest?.components?.length || selectedDetail?.packages?.length || 0
    };
  }, [apps.length, installedApps.length, mergedApps, selectedDetail, selectedManifest]);

  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 () => {
    setAgentStatus({ state: 'loading', message: 'Đang kiểm tra Agent local' });
    try {
      const health = await fetchAgentHealth(agentBaseUrl);
      setAgentHealth(health);
      setAgentStatus({ state: 'success', message: `${health.hostname || 'Agent'} 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]);

  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 (!agentHealth) {
      notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
      return;
    }

    if (action === 'remove' && !window.confirm(`Remove ${app.appName} khỏi máy local?`)) {
      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, notify, packageBaseUrl, startPreflightFailedTask, startTask]);

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

  const copyInstallCommand = useCallback(async () => {
    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, 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]);

  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={agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline')}
            detail={agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010'}
            tone={agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger')}
          />
          <StatusBox
            icon={Server}
            title="Package API"
            detail={packageStatus.message || packageBaseUrl}
            tone={packageStatus.state === 'success' ? 'success' : packageStatus.state}
          />

          <div className="nav-label">Endpoint</div>
          <label className="settings-field">
            <span>Package server</span>
            <input
              value={draftSettings.packageBaseUrl}
              onChange={(event) => setDraftSettings((current) => ({
                ...current,
                packageBaseUrl: event.target.value
              }))}
            />
          </label>
          <label className="settings-field">
            <span>Local Agent</span>
            <input
              value={draftSettings.agentBaseUrl}
              onChange={(event) => setDraftSettings((current) => ({
                ...current,
                agentBaseUrl: event.target.value
              }))}
            />
          </label>
          <button className="btn btn-secondary full" type="button" onClick={applySettings}>
            <Settings size={15} aria-hidden="true" />
            Apply
          </button>
        </div>
      </aside>

      <main className="main-shell">
        <header className="topbar">
          <div className="topbar-title">
            <span>robot.installer</span>
            <strong>{agentHealth ? 'Ready for install' : 'Waiting for Agent'}</strong>
          </div>
          <div className="topbar-actions">
            <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">
          <div className="page-header">
            <div>
              <h1>Application catalog</h1>
              <p>Released apps từ package server và trạng thái cài đặt trên máy local.</p>
            </div>
            <div className="page-actions">
              <button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
                <Clipboard size={15} aria-hidden="true" />
                {agentNeedsUpdate ? 'Copy Agent update' : 'Copy Agent command'}
              </button>
              <a className="btn btn-primary" href={joinUrl(packageBaseUrl, '/install-agent.sh')} target="_blank" rel="noreferrer">
                <Download size={15} aria-hidden="true" />
                {agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
              </a>
            </div>
          </div>

          <div className="dashboard-stats">
            <MetricCard label="Released apps" value={stats.available} note={packageStatus.state === 'loading' ? 'loading' : 'from API'} />
            <MetricCard label="Installed local" value={stats.installed} note={agentHealth ? 'Agent SQLite' : 'Agent offline'} />
            <MetricCard label="Updates" value={stats.updates} note="version diff" tone={stats.updates ? 'warning' : 'success'} />
            <MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
          </div>

          {!agentHealth && (
            <div className="offline-banner">
              <AlertCircle size={19} aria-hidden="true" />
              <div>
                <strong>Local Installer Agent chưa online</strong>
                <code>{agentCommand}</code>
              </div>
              <button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
                <Clipboard size={15} aria-hidden="true" />
                Copy
              </button>
            </div>
          )}

          {agentNeedsUpdate && (
            <div className="offline-banner agent-update-banner">
              <AlertCircle size={19} aria-hidden="true" />
              <div>
                <strong>Agent {latestAgentPackage.version} đã sẵn sàng</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 openUrl = app.installed
                        ? getAppOpenUrl({ ...app, openUrl: app.openUrl || app.installed.openUrl })
                        : '';

                      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">
                              {!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>
                              )}
                              {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>
                              )}
                              {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>
                              )}
                              {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">
              <AgentPanel
                health={agentHealth}
                systemInfo={systemInfo}
                status={agentStatus}
                latestAgentPackage={latestAgentPackage}
                needsUpdate={agentNeedsUpdate}
                onCopyUpdate={copyInstallCommand}
              />
              {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} />}
    </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 MetricCard({ label, value, note, tone }) {
  return (
    <article className={`metric-card ${tone ? `tone-${tone}` : ''}`}>
      <span>{label}</span>
      <div>
        <strong>{value}</strong>
        <small>{note}</small>
      </div>
    </article>
  );
}

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, latestAgentPackage, needsUpdate, onCopyUpdate }) {
  return (
    <section className="panel">
      <div className="panel-header">
        <div>
          <h2>Local Agent</h2>
          <p>{status.message || '127.0.0.1:5010'}</p>
        </div>
        {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" />}
      </div>
      <dl className="detail-list compact-list">
        <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>
      {needsUpdate && (
        <div className="agent-update-action">
          <button className="btn btn-warning" type="button" onClick={onCopyUpdate}>
            <Clipboard size={15} aria-hidden="true" />
            Copy update command
          </button>
          {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).slice(0, 5).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 />);
