<template>
  <div class="overview-page">
      <h1>Обзор</h1>
      <p class="dashboard-sac-version">{{ appName }} v.{{ appVersion }}</p>

      <p v-if="error" class="error">{{ error }}</p>

      <p v-else-if="loading">Загрузка…</p>

      <template v-else-if="data">

    <div class="dashboard-cards">

      <div class="dash-card">

        <div class="dash-value">{{ data.events_last_24h }}</div>

        <div class="dash-label">Событий за 24 ч</div>

        <RouterLink to="/events">Все события →</RouterLink>

      </div>

      <div class="dash-card">

        <div class="dash-value">{{ data.problems_open }}</div>

        <div class="dash-label">Открытых проблем</div>

        <RouterLink :to="{ path: '/problems', query: { status: 'open' } }">Проблемы →</RouterLink>

      </div>

      <div class="dash-card">

        <div class="dash-value">{{ data.problems_opened_24h }}</div>

        <div class="dash-label">Новых проблем за 24 ч</div>

        <RouterLink :to="{ path: '/problems', query: { created_within_hours: '24' } }">Новые за 24 ч →</RouterLink>

      </div>

      <div class="dash-card">

        <div class="dash-value">{{ data.problems_resolved_24h }}</div>

        <div class="dash-label">Закрыто за 24 ч</div>

        <RouterLink :to="{ path: '/problems', query: { resolved_within_hours: '24' } }">Закрытые за 24 ч →</RouterLink>

      </div>

      <div class="dash-card">

        <div class="dash-value">{{ data.hosts_total }}</div>

        <div class="dash-label">Хостов</div>

        <RouterLink to="/hosts">Хосты →</RouterLink>

      </div>

      <div class="dash-card" :class="{ 'dash-warn': data.hosts_stale > 0 }">

        <div class="dash-value">{{ data.hosts_stale }}</div>

        <div class="dash-label">Устаревший heartbeat</div>

        <RouterLink :to="{ path: '/hosts', query: { agent_status: 'stale' } }">Stale-хосты →</RouterLink>

      </div>

      <div class="dash-card">

        <div class="dash-value">{{ data.heartbeats_24h }}</div>

        <div class="dash-label">Heartbeat за 24 ч</div>

        <RouterLink :to="{ path: '/events', query: { type: 'agent.heartbeat' } }">События →</RouterLink>

      </div>

      <div class="dash-card">

        <div class="dash-value">{{ data.daily_reports_24h }}</div>

        <div class="dash-label">Отчёты за 24 ч</div>

        <RouterLink to="/reports">Отчёты →</RouterLink>

      </div>

    </div>



    <div class="dashboard-panels">

      <section class="dash-panel">

        <h2>Топ хостов (24 ч)</h2>

        <div v-if="data.top_hosts.length" class="dash-table-scroll">
          <table class="dash-table">

          <thead>

            <tr>

              <th>Хост</th>

              <th>Событий</th>

            </tr>

          </thead>

          <tbody>

            <tr v-for="h in data.top_hosts" :key="h.host_id">

              <td>

                <RouterLink :to="{ path: '/events', query: { hostname: h.hostname } }">

                  {{ h.hostname }}

                </RouterLink>

              </td>

              <td>{{ h.count }}</td>

            </tr>

          </tbody>

          </table>
        </div>

        <p v-else class="muted">Нет событий за 24 ч</p>

      </section>



      <section class="dash-panel">

        <h2>Топ типов (24 ч)</h2>

        <div v-if="data.top_event_types.length" class="dash-table-scroll">
          <table class="dash-table">

          <thead>

            <tr>

              <th>Type</th>

              <th>Событий</th>

            </tr>

          </thead>

          <tbody>

            <tr v-for="t in data.top_event_types" :key="t.type">

              <td>

                <RouterLink :to="{ path: '/events', query: { type: t.type } }">

                  <code>{{ t.type }}</code>

                </RouterLink>

              </td>

              <td>{{ t.count }}</td>

            </tr>

          </tbody>

          </table>
        </div>

        <p v-else class="muted">Нет событий за 24 ч</p>

      </section>

    </div>



    <h2>Severity за 24 ч</h2>

    <ul class="severity-list">

      <li v-for="(count, sev) in data.severity_24h" :key="sev">

        <RouterLink :to="{ path: '/events', query: { severity: sev } }">

          <span :class="'sev-' + sev">{{ sev }}</span>: {{ count }}

        </RouterLink>

      </li>

      <li v-if="!Object.keys(data.severity_24h).length">Нет событий</li>

    </ul>



    <h2>Последние события</h2>

    <p class="muted dash-recent-hint">

      Обновляется при поступлении событий (live, ~5 с). Время — момент приёма SAC.

    </p>

    <table>

      <thead>

        <tr>

          <th>ID</th>

          <th>Получено</th>

          <th>Хост</th>

          <th>Имя сервера</th>

          <th>Пользователь</th>

          <th>Версия агента</th>

          <th>Severity</th>

          <th>Title</th>

          <th>Действия</th>

        </tr>

      </thead>

      <tbody>

        <tr v-for="e in data.recent_events" :key="e.id">

          <td>

            <RouterLink :to="`/events/${e.id}`">{{ e.id }}</RouterLink>

          </td>

          <td>{{ formatDt(e.received_at) }}</td>

          <td>

            <RouterLink :to="{ path: '/events', query: { hostname: e.hostname } }">

              {{ e.hostname }}

            </RouterLink>

          </td>

          <td>{{ formatServerName(e.display_name) }}</td>

          <td>{{ e.actor_user || "—" }}</td>

          <td>{{ e.product_version || "—" }}</td>

          <td :class="'sev-' + e.severity">{{ e.severity }}</td>

          <td>{{ e.title }}</td>

          <td class="dash-actions">

            <button
              v-if="e.rdg_flap"
              type="button"
              class="secondary dash-qwinsta-btn"
              :disabled="qwinstaLoadingId === e.id"
              @click="runQwinsta(e)"
            >
              qwinsta
            </button>

          </td>

        </tr>

      </tbody>

    </table>

    <div v-if="qwinstaModal.open" class="modal-backdrop" @click.self="closeQwinstaModal">
      <div class="modal-card qwinsta-modal" role="dialog" aria-modal="true">
        <h3>qwinsta — событие #{{ qwinstaModal.eventId }}</h3>
        <p v-if="qwinstaModal.error" class="error">{{ qwinstaModal.error }}</p>
        <p v-else-if="qwinstaModal.loading">Ожидание ответа агента…</p>
        <template v-else>
          <pre v-if="qwinstaModal.stdout" class="qwinsta-raw">{{ qwinstaModal.stdout }}</pre>
          <p v-else class="muted">Нет вывода</p>
          <table v-if="qwinstaModal.sessions.length" class="dash-table">
            <thead>
              <tr>
                <th>SESSION</th>
                <th>USER</th>
                <th>ID</th>
                <th>STATE</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="s in qwinstaModal.sessions" :key="s.id">
                <td>{{ s.sessionName }}</td>
                <td>{{ s.userName }}</td>
                <td>{{ s.id }}</td>
                <td>{{ s.state }}</td>
                <td>
                  <button
                    type="button"
                    class="danger"
                    :disabled="qwinstaModal.logoffId === s.id"
                    @click="runLogoff(s.id)"
                  >
                    logoff
                  </button>
                </td>
              </tr>
            </tbody>
          </table>
        </template>
        <div class="modal-actions">
          <button type="button" class="secondary" @click="closeQwinstaModal">Закрыть</button>
        </div>
      </div>
    </div>

    <div class="filters" style="margin-top: 1rem">

      <button type="button" class="secondary" @click="load">Обновить всё</button>

      <span v-if="live" class="live-badge">live</span>

    </div>

      </template>
  </div>
</template>



<script setup lang="ts">

import { onMounted, onUnmounted, ref } from "vue";

import {
  apiFetch,
  fetchEventAction,
  getToken,
  postEventLogoff,
  postEventQwinsta,
  type AgentCommandResponse,
  type DashboardSummary,
  type EventSummary,
} from "../api";
import { useSacVersion } from "../composables/useSacVersion";
import { formatServerName } from "../utils/hostDisplay";

const { appName, appVersion } = useSacVersion();



const data = ref<DashboardSummary | null>(null);

const loading = ref(false);

const error = ref("");

const live = ref(false);

const lastKnownEventDbId = ref<number | null>(null);

let eventSource: EventSource | null = null;

let refreshingRecent = false;

let qwinstaPollTimer: ReturnType<typeof setInterval> | null = null;

const qwinstaLoadingId = ref<number | null>(null);

type QwinstaSessionRow = {
  sessionName: string;
  userName: string;
  id: number;
  state: string;
};

const qwinstaModal = ref({
  open: false,
  eventId: 0,
  actorUser: "",
  commandUuid: "",
  loading: false,
  error: "",
  stdout: "",
  sessions: [] as QwinstaSessionRow[],
  logoffId: null as number | null,
});

function parseQwinstaSessions(stdout: string, actorUser: string): QwinstaSessionRow[] {
  const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
  const rows: QwinstaSessionRow[] = [];
  for (const line of lines) {
    if (/^SESSION/i.test(line) || /^---/.test(line)) continue;
    const parts = line.split(/\s+/);
    if (parts.length < 4) continue;
    const sessionName = parts[0];
    const userName = parts[1];
    const id = Number.parseInt(parts[2], 10);
    const state = parts.slice(3).join(" ");
    if (!Number.isFinite(id)) continue;
    const norm = (s: string) => s.replace(/^B26\\/i, "").toLowerCase();
    const matchUser = actorUser && norm(userName).includes(norm(actorUser));
    if (actorUser && !matchUser) continue;
    rows.push({ sessionName, userName, id, state });
  }
  if (rows.length === 0 && stdout.trim()) {
    for (const line of lines) {
      if (/^SESSION/i.test(line) || /^---/.test(line)) continue;
      const parts = line.split(/\s+/);
      if (parts.length < 4) continue;
      const id = Number.parseInt(parts[2], 10);
      if (!Number.isFinite(id)) continue;
      rows.push({
        sessionName: parts[0],
        userName: parts[1],
        id,
        state: parts.slice(3).join(" "),
      });
    }
  }
  return rows;
}

function stopQwinstaPoll() {
  if (qwinstaPollTimer) {
    clearInterval(qwinstaPollTimer);
    qwinstaPollTimer = null;
  }
}

function closeQwinstaModal() {
  stopQwinstaPoll();
  qwinstaModal.value.open = false;
}

function applyCommandResult(eventId: number, actorUser: string, cmd: AgentCommandResponse) {
  if (cmd.status === "pending") return false;
  qwinstaModal.value.loading = false;
  if (cmd.status === "failed") {
    qwinstaModal.value.error = cmd.result_stderr || cmd.result_stdout || "Команда завершилась с ошибкой";
    return true;
  }
  qwinstaModal.value.stdout = cmd.result_stdout || "";
  qwinstaModal.value.sessions = parseQwinstaSessions(qwinstaModal.value.stdout, actorUser);
  return true;
}

function pollQwinstaCommand(eventId: number, commandUuid: string, actorUser: string) {
  stopQwinstaPoll();
  qwinstaPollTimer = setInterval(async () => {
    try {
      const cmd = await fetchEventAction(eventId, commandUuid);
      if (applyCommandResult(eventId, actorUser, cmd)) {
        stopQwinstaPoll();
      }
    } catch (e) {
      qwinstaModal.value.loading = false;
      qwinstaModal.value.error = e instanceof Error ? e.message : "Ошибка опроса команды";
      stopQwinstaPoll();
    }
  }, 2000);
}

async function runQwinsta(event: EventSummary) {
  qwinstaLoadingId.value = event.id;
  qwinstaModal.value = {
    open: true,
    eventId: event.id,
    actorUser: event.actor_user || "",
    commandUuid: "",
    loading: true,
    error: "",
    stdout: "",
    sessions: [],
    logoffId: null,
  };
  try {
    const cmd = await postEventQwinsta(event.id);
    qwinstaModal.value.commandUuid = cmd.command_uuid;
    if (applyCommandResult(event.id, event.actor_user || "", cmd)) {
      return;
    }
    pollQwinstaCommand(event.id, cmd.command_uuid, event.actor_user || "");
  } catch (e) {
    qwinstaModal.value.loading = false;
    qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta";
  } finally {
    qwinstaLoadingId.value = null;
  }
}

async function runLogoff(sessionId: number) {
  const modal = qwinstaModal.value;
  if (!modal.eventId) return;
  if (!window.confirm(`Завершить сеанс ID ${sessionId}?`)) return;
  modal.logoffId = sessionId;
  modal.error = "";
  try {
    const cmd = await postEventLogoff(modal.eventId, sessionId);
    if (cmd.status === "pending") {
      const deadline = Date.now() + 60000;
      while (Date.now() < deadline) {
        await new Promise((r) => setTimeout(r, 2000));
        const polled = await fetchEventAction(modal.eventId, cmd.command_uuid);
        if (polled.status !== "pending") {
          if (polled.status === "failed") {
            modal.error = polled.result_stderr || "logoff failed";
          } else {
            modal.loading = true;
            modal.stdout = "";
            modal.sessions = [];
            const again = await postEventQwinsta(modal.eventId);
            pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser);
          }
          break;
        }
      }
    }
  } catch (e) {
    modal.error = e instanceof Error ? e.message : "logoff error";
  } finally {
    modal.logoffId = null;
  }
}

function formatDt(iso: string) {

  return new Date(iso).toLocaleString("ru-RU");

}



function syncLastKnownFromRecent() {

  const top = data.value?.recent_events?.[0];

  lastKnownEventDbId.value = top?.id ?? null;

}



async function refreshRecentEvents() {

  if (!data.value || refreshingRecent) return;

  refreshingRecent = true;

  try {

    const events = await apiFetch<EventSummary[]>(

      "/api/v1/dashboards/recent-events?limit=8",

    );

    data.value.recent_events = events;

    syncLastKnownFromRecent();

  } catch {

    /* keep table on transient errors */

  } finally {

    refreshingRecent = false;

  }

}



async function load() {

  loading.value = true;

  error.value = "";

  try {

    data.value = await apiFetch<DashboardSummary>("/api/v1/dashboards/summary");

    syncLastKnownFromRecent();

  } catch (e) {

    error.value = e instanceof Error ? e.message : String(e);

  } finally {

    loading.value = false;

  }

}



function applyDashboardCounters(msg: {

  events_last_24h?: number;

  problems_open?: number;

  hosts_stale?: number;

  heartbeats_24h?: number;

  daily_reports_24h?: number;

  last_event_id?: number | null;

}) {

  if (!data.value) return;

  if (typeof msg.events_last_24h === "number") {

    data.value.events_last_24h = msg.events_last_24h;

  }

  if (typeof msg.problems_open === "number") {

    data.value.problems_open = msg.problems_open;

  }

  if (typeof msg.hosts_stale === "number") {

    data.value.hosts_stale = msg.hosts_stale;

  }

  if (typeof msg.heartbeats_24h === "number") {

    data.value.heartbeats_24h = msg.heartbeats_24h;

  }

  if (typeof msg.daily_reports_24h === "number") {

    data.value.daily_reports_24h = msg.daily_reports_24h;

  }



  const incoming = msg.last_event_id;

  if (incoming === undefined) return;

  const prev = lastKnownEventDbId.value;

  if (incoming === null) {

    if (prev !== null) void refreshRecentEvents();

    lastKnownEventDbId.value = null;

    return;

  }

  if (prev === null || incoming !== prev) {

    void refreshRecentEvents();

  }

  lastKnownEventDbId.value = incoming;

}



function connectLive() {

  const token = getToken();

  if (!token) return;

  eventSource?.close();

  const url = `/api/v1/stream/events?token=${encodeURIComponent(token)}`;

  eventSource = new EventSource(url);

  eventSource.onopen = () => {

    live.value = true;

  };

  eventSource.onmessage = (ev) => {

    try {

      const msg = JSON.parse(ev.data) as {

        type?: string;

        events_last_24h?: number;

        problems_open?: number;

        hosts_stale?: number;

        heartbeats_24h?: number;

        daily_reports_24h?: number;

        last_event_id?: number | null;

      };

      if (msg.type !== "dashboard" || !data.value) return;

      applyDashboardCounters(msg);

    } catch {

      /* ignore malformed SSE */

    }

  };

  eventSource.onerror = () => {

    live.value = false;

  };

}



onMounted(() => {

  load().then(() => connectLive());

});



onUnmounted(() => {

  stopQwinstaPoll();

  eventSource?.close();

  eventSource = null;

});

</script>



<style scoped>

.dash-table-scroll {

  max-height: 21rem;

  overflow-y: auto;

}

.dashboard-sac-version {
  margin: -0.35rem 0 1rem;
  color: #9aa4b2;
  font-size: 0.9rem;
}

.dash-recent-hint {

  margin: -0.25rem 0 0.75rem;

  font-size: 0.9rem;

}

.dash-actions {
  white-space: nowrap;
}

.dash-qwinsta-btn {
  font-size: 0.85rem;
  padding: 0.2rem 0.55rem;
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 1rem;
}

.modal-card {
  background: var(--card-bg, #1e2430);
  border: 1px solid var(--border, #3a4556);
  border-radius: 8px;
  max-width: 720px;
  width: 100%;
  max-height: 85vh;
  overflow: auto;
  padding: 1rem 1.25rem;
}

.qwinsta-raw {
  font-size: 0.8rem;
  max-height: 12rem;
  overflow: auto;
  background: #0d1117;
  padding: 0.75rem;
  border-radius: 4px;
}

.modal-actions {
  margin-top: 1rem;
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

</style>


