<script setup>

import { ref, reactive, computed, onMounted, watch, nextTick, useTemplateRef } from 'vue';
import { Button, TextInput, MultiSelect, Popover, FormGroup, DateTimeInput } from '@cloudron/pankow';
import { useDebouncedRef, prettyLongDate } from '@cloudron/pankow/utils';
import AppsModel from '../models/AppsModel.js';
import { eventlogDetails, eventlogSource } from '../utils.js';

const props = defineProps({
  fetchPage: { type: Function, required: true },
  availableActions: { type: Array, default: () => [] },
  app: { type: Object, default: null },
  showToolbar: { type: Boolean, default: true },
});

const appsModel = AppsModel.create();

const apps = ref([]);
const eventlogs = ref([]);
const refreshBusy = ref(false);
const page = ref(1);
const perPage = ref(100);
const eventlogContainer = useTemplateRef('eventlogContainer');
const actions = reactive([]);

const highlight = useDebouncedRef('', 300);
const currentMatchPosition = ref(-1);
const searching = ref(false);
const SEARCH_LOOKAHEAD_PAGES = 5;

const filterFrom = ref('');
const filterTo = ref('');
const dateFilterPopover = useTemplateRef('dateFilterPopover');
const dateFilterButton = useTemplateRef('dateFilterButton');

function getApp(id) {
  return apps.value.find(a => a.id === id);
}

function processEvent(e) {
  const app = props.app || (e.data?.appId ? getApp(e.data.appId) : null);
  return {
    id: Symbol(),
    raw: e,
    details: eventlogDetails(e, app, props.app?.id || ''),
    source: eventlogSource(e, app),
  };
}

function isMatch(eventlog, term) {
  if (!term) return false;
  const t = term.toLowerCase();
  if (eventlog.source.toLowerCase().includes(t)) return true;
  if (eventlog.details.replace(/<[^>]+>/g, '').toLowerCase().includes(t)) return true;
  if (JSON.stringify(eventlog.raw.data).toLowerCase().includes(t)) return true;
  if (eventlog.raw.action.toLowerCase().includes(t)) return true;
  return false;
}

const matchIndices = computed(() => {
  if (!highlight.value) return [];
  return eventlogs.value.reduce((acc, e, i) => {
    if (isMatch(e, highlight.value)) acc.push(i);
    return acc;
  }, []);
});

function scrollToIndex(idx) {
  const el = eventlogContainer.value?.querySelector(`[data-index="${idx}"]`);
  if (el) el.scrollIntoView({ behavior: 'instant', block: 'center' });
}

function goToPrevMatch() {
  if (currentMatchPosition.value > 0) {
    currentMatchPosition.value--;
    scrollToIndex(matchIndices.value[currentMatchPosition.value]);
  }
}

async function goToNextMatch() {
  if (!highlight.value || searching.value) return;

  if (currentMatchPosition.value + 1 < matchIndices.value.length) {
    currentMatchPosition.value++;
    scrollToIndex(matchIndices.value[currentMatchPosition.value]);
    return;
  }

  searching.value = true;
  let endOfLog = false;
  for (let i = 0; i < SEARCH_LOOKAHEAD_PAGES; i++) {
    const prevLength = eventlogs.value.length;
    await fetchMore();
    if (eventlogs.value.length === prevLength) { endOfLog = true; break; }

    if (currentMatchPosition.value + 1 < matchIndices.value.length) {
      currentMatchPosition.value++;
      scrollToIndex(matchIndices.value[currentMatchPosition.value]);
      searching.value = false;
      return;
    }
  }
  searching.value = false;
  if (endOfLog) window.pankow.notify({ text: `No more matches for "${highlight.value}".`, timeout: 3000 });
  else window.pankow.notify({ text: `No match found for "${highlight.value}" in ${eventlogs.value.length} entries. Click next to keep searching.`, timeout: 3000 });
}

function buildFilter() {
  const filter = {};
  if (actions.length) filter.actions = actions.join(',');
  if (filterFrom.value) filter.from = new Date(filterFrom.value + 'T00:00:00').toISOString();
  if (filterTo.value) filter.to = new Date(filterTo.value + 'T23:59:59.999').toISOString();
  return filter;
}

async function onRefresh() {
  highlight.value = '';
  refreshBusy.value = true;
  page.value = 1;

  const [error, result] = await props.fetchPage(buildFilter(), page.value, perPage.value);
  if (error) return console.error(error);

  eventlogs.value = result.map(processEvent);
  refreshBusy.value = false;
}

async function fetchMore() {
  page.value++;

  const [error, result] = await props.fetchPage(buildFilter(), page.value, perPage.value);
  if (error) return console.error(error);

  eventlogs.value = eventlogs.value.concat(result.map(processEvent));
}

async function onScroll(event) {
  if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) await fetchMore();
}

function onOpenDateFilter(event) {
  dateFilterPopover.value.open(event, dateFilterButton.value.$el || dateFilterButton.value);
}

watch(actions, onRefresh);
watch(filterFrom, onRefresh);
watch(filterTo, onRefresh);
watch(highlight, async () => {
  if (matchIndices.value.length > 0) {
    currentMatchPosition.value = 0;
    await nextTick();
    scrollToIndex(matchIndices.value[0]);
  } else {
    currentMatchPosition.value = -1;
    if (highlight.value) goToNextMatch();
  }
});

onMounted(async () => {
  if (!props.app) {
    const [error, result] = await appsModel.list();
    if (error) console.error(error);
    else apps.value = result;
  }

  onRefresh();

  while (eventlogContainer.value && eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) {
    await fetchMore();
  }
});

function setHighlight(value) { highlight.value = value; }

defineExpose({ refresh: onRefresh, setHighlight });

</script>

<template>
  <div style="overflow: hidden; display: flex; flex-direction: column; height: 100%;">
    <div v-if="showToolbar" style="display: flex; align-items: center; gap: 5px; flex-wrap: wrap; padding-bottom: 10px; justify-content: flex-end;">
      <TextInput placeholder="Highlight..." v-model="highlight" @keydown.enter="goToNextMatch()"/>
      <Button tool plain :disabled="!highlight || currentMatchPosition <= 0 || searching" @click="goToPrevMatch()" icon="fa-solid fa-chevron-up" />
      <Button tool plain :disabled="!highlight || searching" :loading="searching" @click="goToNextMatch()" icon="fa-solid fa-chevron-down" />
      <Button tool secondary ref="dateFilterButton" @click="onOpenDateFilter($event)" :icon="(filterFrom || filterTo) ? 'fa-solid fa-calendar-check' : 'fa-solid fa-calendar'" />
      <MultiSelect v-if="availableActions.length" :search-threshold="10" v-model="actions" :options="availableActions" option-label="label" option-key="id" :selected-label="actions.length ? $t('main.multiselect.selected', { n: actions.length }) : $t('eventlog.filterAllEvents')"/>
      <Button tool secondary @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
    </div>
    <Popover ref="dateFilterPopover" width="300px">
      <div style="padding: 15px; display: flex; flex-direction: column; gap: 10px;">
        <FormGroup>
          <label>From</label>
          <DateTimeInput date-only v-model="filterFrom" :max="filterTo || undefined" />
        </FormGroup>
        <FormGroup>
          <label>To</label>
          <DateTimeInput date-only v-model="filterTo" :min="filterFrom || undefined" />
        </FormGroup>
      </div>
    </Popover>
    <div ref="eventlogContainer" class="section-body" style="overflow-y: auto; overflow-x: hidden; flex: 1;" @scroll="onScroll">
      <table class="eventlog-table">
        <thead>
          <tr>
            <th style="width: 160px;">{{ $t('eventlog.time') }}</th>
            <th style="width: 15%;">{{ $t('eventlog.source') }}</th>
            <th>{{ $t('eventlog.details') }}</th>
            <th style="width: 40px;"></th>
          </tr>
        </thead>
        <tbody>
          <template v-for="(eventlog, index) in eventlogs" :key="eventlog.id">
            <tr :data-index="index" :class="{ 'active': eventlog.isOpen, 'eventlog-match': highlight && isMatch(eventlog, highlight), 'eventlog-match-current': matchIndices[currentMatchPosition] === index }" @click="eventlog.isOpen = !eventlog.isOpen">
              <td style="white-space: nowrap;">{{ prettyLongDate(eventlog.raw.creationTime) }}</td>
              <td class="eventlog-source">{{ eventlog.source }}</td>
              <td style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" v-html="eventlog.details"></td>
              <td><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="app ? `/logs.html?appId=${app.id}&taskId=${eventlog.raw.data.taskId}` : `/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></td>
            </tr>
            <tr v-show="eventlog.isOpen">
              <td colspan="4" class="eventlog-details" @click.stop>
                <pre>{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
              </td>
            </tr>
          </template>
        </tbody>
      </table>
    </div>
  </div>
</template>

<style scoped>

.eventlog-table {
  width: 100%;
  border-spacing: 0;
  table-layout: fixed;
}

.eventlog-table th {
  text-align: left;
}

.eventlog-table th,
.eventlog-table td {
  padding: 6px;
}

.eventlog-table tbody tr.active,
.eventlog-table tbody tr:hover {
  background-color: var(--pankow-color-background-hover);
}

.eventlog-source {
  font-weight: var(--pankow-font-weight-bold);
}

.eventlog-details {
  background-color: color-mix(in oklab, var(--pankow-color-background-hover), black 5%);
  position: relative;
}

.eventlog-details pre {
  white-space: pre-wrap;
  font-size: 13px;
  padding-left: 10px;
  margin: 0;
}

.eventlog-table tbody tr.eventlog-match {
  background-color: rgba(255, 193, 7, 0.15);
}

.eventlog-table tbody tr.eventlog-match-current {
  background-color: rgba(255, 193, 7, 0.35);
}

</style>
