<template>
  <h1>Хосты</h1>
  <div class="filters">
    <label>
      Поиск (hostname / display name)
      <input v-model="search" type="search" placeholder="UNMS Kalina" @keyup.enter="applyFilters" />
    </label>
    <label>
      Статус агента
      <select v-model="agentStatusFilter" @change="applyFilters">
        <option value="">Все</option>
        <option value="online">online</option>
        <option value="stale">stale</option>
        <option value="unknown">unknown</option>
      </select>
    </label>
    <button type="button" class="secondary" @click="applyFilters">Найти</button>
  </div>
  <p v-if="error" class="error">{{ error }}</p>
  <p v-else-if="loading">Загрузка…</p>
  <template v-else>
    <div class="hosts-list-meta">
        <p class="hosts-list-total">
          Всего: {{ data?.total ?? 0 }}
          <span v-if="live" class="live-badge">live</span>
        </p>
        <aside
          class="agent-releases-plate"
          title="Версии из git-репозиториев агентов (version.txt, ветка main)"
        >
          <span class="agent-releases-plate__label">Последние версии агентов:</span>
          <span class="agent-releases-plate__item">
            Win: <code>{{ gitWinVersion }}</code>
          </span>
          <span class="agent-releases-plate__item">
            SSH: <code>{{ gitSshVersion }}</code>
          </span>
        </aside>
    </div>
    <table class="hosts-table">
      <thead>
        <tr>
          <th><button type="button" class="th-sort" @click="setSort('id')">ID {{ sortMark('id') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('display_name')">Имя {{ sortMark('display_name') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('hostname')">Hostname {{ sortMark('hostname') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('product_version')">Версия агента {{ sortMark('product_version') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('os_family')">OS {{ sortMark('os_family') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('ipv4')">IPv4 {{ sortMark('ipv4') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('agent_status')">Статус {{ sortMark('agent_status') }}</button></th>
          <th>Heartbeat</th>
          <th><button type="button" class="th-sort" @click="setSort('last_daily_report_at')">Отчёт {{ sortMark('last_daily_report_at') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('last_seen_at')">Last seen {{ sortMark('last_seen_at') }}</button></th>
          <th><button type="button" class="th-sort" @click="setSort('event_count')">Events {{ sortMark('event_count') }}</button></th>
          <th>Действия</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="h in sortedItems"
          :key="h.id"
          class="sac-host-row"
          tabindex="0"
          :title="`Карточка хоста ${h.hostname}`"
          @click="openHost(h.id)"
          @keydown.enter="openHost(h.id)"
        >
          <td>{{ h.id }}</td>
          <td>{{ h.display_name || h.hostname }}</td>
          <td>{{ h.hostname }}</td>
          <td>
            <span
              :class="{ 'agent-version-outdated': isVersionOutdated(h) }"
              :title="versionTitle(h)"
            >
              {{ h.product_version || "—" }}
            </span>
          </td>
          <td>{{ h.os_family }}</td>
          <td>{{ h.ipv4 || "—" }}</td>
          <td :class="'agent-' + h.agent_status">{{ agentLabel(h.agent_status) }}</td>
          <td>{{ h.last_heartbeat_at ? formatDt(h.last_heartbeat_at) : "—" }}</td>
          <td @click.stop>
            <RouterLink
              v-if="h.last_daily_report_at"
              :to="{ path: '/reports', query: { hostname: h.hostname } }"
            >
              {{ formatDt(h.last_daily_report_at) }}
            </RouterLink>
            <span v-else>—</span>
          </td>
          <td>{{ formatDt(h.last_seen_at) }}</td>
          <td>{{ h.event_count ?? 0 }}</td>
          <td class="actions" @click.stop>
            <div class="actions-inner">
              <button
                type="button"
                class="danger"
                :disabled="deletingId === h.id"
                title="Удалить хост и все его события"
                @click="confirmDelete(h)"
              >
                Удалить
              </button>
            </div>
          </td>
        </tr>
      </tbody>
    </table>
  </template>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
import {
  apiFetch,
  ApiError,
  fetchHost,
  fetchRecentEvents,
  type HostDetail,
  type HostListResponse,
  type HostSummary,
} from "../api";
import { bumpLatestAgentVersion, patchHostSummaryFromDetail } from "../utils/hostsLiveUpdate";
import { subscribeHostPatch } from "../utils/hostPatchBus";
import { isAgentVersionOutdated } from "../utils/agentVersion";

const router = useRouter();
const route = useRoute();

const data = ref<HostListResponse | null>(null);
const loading = ref(false);
const error = ref("");
const search = ref("");
const agentStatusFilter = ref("");
type SortKey =
  | "id"
  | "display_name"
  | "hostname"
  | "product_version"
  | "os_family"
  | "ipv4"
  | "agent_status"
  | "last_seen_at"
  | "event_count"
  | "last_daily_report_at";

const HOSTS_SORT_KEYS: SortKey[] = [
  "id",
  "display_name",
  "hostname",
  "product_version",
  "os_family",
  "ipv4",
  "agent_status",
  "last_seen_at",
  "event_count",
  "last_daily_report_at",
];

const HOSTS_SORT_STORAGE_KEY = "sac_hosts_sort";

function loadHostsSortPrefs(): { sortBy: SortKey; sortDir: "asc" | "desc" } {
  const fallback = { sortBy: "last_seen_at" as SortKey, sortDir: "desc" as const };
  try {
    const raw = localStorage.getItem(HOSTS_SORT_STORAGE_KEY);
    if (!raw) return fallback;
    const parsed = JSON.parse(raw) as { sortBy?: string; sortDir?: string };
    if (
      parsed.sortBy &&
      HOSTS_SORT_KEYS.includes(parsed.sortBy as SortKey) &&
      (parsed.sortDir === "asc" || parsed.sortDir === "desc")
    ) {
      return { sortBy: parsed.sortBy as SortKey, sortDir: parsed.sortDir };
    }
  } catch {
    /* ignore corrupt storage */
  }
  return fallback;
}

function saveHostsSortPrefs(sortBy: SortKey, sortDir: "asc" | "desc") {
  localStorage.setItem(HOSTS_SORT_STORAGE_KEY, JSON.stringify({ sortBy, sortDir }));
}

const initialSort = loadHostsSortPrefs();
const sortBy = ref<SortKey>(initialSort.sortBy);
const sortDir = ref<"asc" | "desc">(initialSort.sortDir);
const deletingId = ref<number | null>(null);
let patchingHostRow = false;
let unsubscribeHostPatch: (() => void) | null = null;

function applyHostPatchFromDetail(detail: HostDetail) {
  if (!data.value) return;
  const row = data.value.items.find((h) => h.id === detail.id);
  if (row) {
    patchHostSummaryFromDetail(row, detail);
    bumpLatestAgentVersion(data.value.latest_agent_versions, row.product, detail.product_version);
  }
}

const { live } = useSacLiveEventStream(() => {
  void patchHostRowFromLatestEvent();
});

function formatDt(iso: string) {
  return new Date(iso).toLocaleString("ru-RU");
}

function agentLabel(status: string) {
  if (status === "online") return "online";
  if (status === "stale") return "stale";
  return "unknown";
}

function isVersionOutdated(h: HostSummary): boolean {
  if (h.version_status === "outdated") return true;
  if (h.version_status === "ok") return false;
  const reference =
    data.value?.reference_agent_versions?.[h.product] ??
    data.value?.latest_agent_versions?.[h.product];
  return isAgentVersionOutdated(h.product_version, reference);
}

function versionTitle(h: HostSummary): string {
  const reference =
    data.value?.reference_agent_versions?.[h.product] ??
    data.value?.latest_agent_versions?.[h.product];
  if (!isVersionOutdated(h)) return "";
  if (!reference) return "Устаревшая версия агента";
  const fromSettings = data.value?.reference_agent_versions?.[h.product];
  if (fromSettings && fromSettings !== data.value?.latest_agent_versions?.[h.product]) {
    return `Устарела относительно эталона ${reference} (настройки SAC)`;
  }
  return `Устарела относительно эталона ${reference}`;
}

function openHost(id: number) {
  router.push(`/hosts/${id}`);
}

function agentStatusOrder(status: string) {
  if (status === "online") return 0;
  if (status === "stale") return 1;
  return 2;
}

function setSort(key: SortKey) {
  if (sortBy.value === key) {
    sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
  } else {
    sortBy.value = key;
    sortDir.value = key === "id" || key === "last_seen_at" || key === "event_count" ? "desc" : "asc";
  }
  saveHostsSortPrefs(sortBy.value, sortDir.value);
}

const gitWinVersion = computed(
  () => data.value?.git_latest_rdp_version?.trim() || "—",
);
const gitSshVersion = computed(
  () => data.value?.git_latest_ssh_version?.trim() || "—",
);

function sortMark(key: SortKey) {
  if (sortBy.value !== key) return "";
  return sortDir.value === "asc" ? "↑" : "↓";
}

function compareNullableStrings(a: string | null, b: string | null) {
  const av = (a ?? "").trim().toLowerCase();
  const bv = (b ?? "").trim().toLowerCase();
  return av.localeCompare(bv, "ru");
}

function compareNullableDates(a: string | null, b: string | null) {
  const av = a ? new Date(a).getTime() : 0;
  const bv = b ? new Date(b).getTime() : 0;
  return av - bv;
}

const sortedItems = computed(() => {
  const items = [...(data.value?.items ?? [])];
  const key = sortBy.value;
  const dir = sortDir.value === "asc" ? 1 : -1;
  items.sort((a: HostSummary, b: HostSummary) => {
    let cmp = 0;
    switch (key) {
      case "id":
        cmp = a.id - b.id;
        break;
      case "display_name":
        cmp = compareNullableStrings(a.display_name, b.display_name);
        break;
      case "hostname":
        cmp = compareNullableStrings(a.hostname, b.hostname);
        break;
      case "product_version":
        cmp = compareNullableStrings(a.product_version, b.product_version);
        break;
      case "os_family":
        cmp = compareNullableStrings(a.os_family, b.os_family);
        break;
      case "ipv4":
        cmp = compareNullableStrings(a.ipv4, b.ipv4);
        break;
      case "agent_status":
        cmp = agentStatusOrder(a.agent_status) - agentStatusOrder(b.agent_status);
        break;
      case "event_count":
        cmp = (a.event_count ?? 0) - (b.event_count ?? 0);
        break;
      case "last_daily_report_at":
        cmp = compareNullableDates(a.last_daily_report_at, b.last_daily_report_at);
        break;
      case "last_seen_at":
        cmp = compareNullableDates(a.last_seen_at, b.last_seen_at);
        break;
    }
    if (cmp === 0) {
      cmp = a.hostname.localeCompare(b.hostname, "ru");
    }
    return cmp * dir;
  });
  return items;
});

async function loadHosts(retry = 0) {
  loading.value = true;
  error.value = "";
  try {
    const q = search.value.trim();
    const params = new URLSearchParams({ page: "1", page_size: "100" });
    if (q) params.set("hostname", q);
    if (agentStatusFilter.value) params.set("agent_status", agentStatusFilter.value);
    data.value = await apiFetch<HostListResponse>(`/api/v1/hosts?${params}`);
  } catch (e) {
    const status = e instanceof ApiError ? e.status : 0;
    if (retry < 2 && (status === 502 || status === 503)) {
      await new Promise((resolve) => window.setTimeout(resolve, 2500));
      return loadHosts(retry + 1);
    }
    error.value = e instanceof Error ? e.message : "Ошибка";
  } finally {
    loading.value = false;
  }
}

function buildHostsQuery(): Record<string, string> {
  const q: Record<string, string> = {};
  if (search.value.trim()) q.hostname = search.value.trim();
  if (agentStatusFilter.value) q.agent_status = agentStatusFilter.value;
  return q;
}

function applyFilters() {
  void router.replace({ path: "/hosts", query: buildHostsQuery() });
  void loadHosts();
}

function syncFromRoute() {
  const hostname = route.query.hostname;
  if (typeof hostname === "string") search.value = hostname;
  const status = route.query.agent_status;
  agentStatusFilter.value = typeof status === "string" ? status : "";
}

async function patchHostRowFromLatestEvent() {
  if (!data.value?.items.length || patchingHostRow) return;
  patchingHostRow = true;
  try {
    const recent = await fetchRecentEvents(1);
    const ev = recent[0];
    if (!ev) return;
    const row = data.value.items.find((h) => h.id === ev.host_id);
    if (!row) return;
    const detail = await fetchHost(ev.host_id);
    patchHostSummaryFromDetail(row, detail);
  } catch {
    /* keep table on transient errors */
  } finally {
    patchingHostRow = false;
  }
}

interface HostDeleteResponse {
  status: string;
  host_id: number;
  hostname: string;
  deleted_events: number;
  deleted_problems: number;
}

async function confirmDelete(h: HostSummary) {
  const label = h.display_name || h.hostname;
  const n = h.event_count ?? 0;
  const msg =
    `Удалить хост «${label}» (${h.hostname})?\n\n` +
    `Будут удалены все события (${n}) и проблемы этого хоста. ` +
    `При новом ingest агент снова появится в списке.`;
  if (!window.confirm(msg)) {
    return;
  }
  deletingId.value = h.id;
  error.value = "";
  try {
    await apiFetch<HostDeleteResponse>(`/api/v1/hosts/${h.id}`, { method: "DELETE" });
    await loadHosts();
  } catch (e) {
    error.value = e instanceof Error ? e.message : "Не удалось удалить хост";
  } finally {
    deletingId.value = null;
  }
}

onMounted(() => {
  syncFromRoute();
  loadHosts();
  unsubscribeHostPatch = subscribeHostPatch(applyHostPatchFromDetail);
});

onUnmounted(() => {
  unsubscribeHostPatch?.();
  unsubscribeHostPatch = null;
});

watch(
  () => [route.query.hostname, route.query.agent_status] as const,
  () => {
    syncFromRoute();
    loadHosts();
  },
);
</script>

<style scoped>
.hosts-list-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.5rem 0.75rem;
  margin: 0.35rem 0 1rem;
}

.hosts-list-total {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  line-height: 1.5;
}

.agent-releases-plate {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.85rem;
  margin: -0.15rem 0 0;
  padding: 0.45rem 1rem 0.5rem 1.1rem;
  border-radius: 0 0.75rem 0.75rem 0.35rem;
  border: 1px solid #2d3a4d;
  border-left: 3px solid #58a6ff;
  background: linear-gradient(135deg, #1a2332 0%, #151d28 100%);
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.35),
    0 1px 0 rgba(88, 166, 255, 0.12) inset;
  font-size: 0.9rem;
  color: #9aa4b2;
  transform: translateY(-1px);
}

.agent-releases-plate__label {
  color: #c9d1d9;
  font-weight: 500;
}

.agent-releases-plate__item code {
  font-size: 0.88em;
  color: #79c0ff;
  background: rgba(88, 166, 255, 0.08);
  padding: 0.1rem 0.35rem;
  border-radius: 0.25rem;
}
</style>
