<template>
  <div>
    <div
      class="ml-3 mr-3 align-center"
      style="display: flex; flex-direction: row-reverse"
    >
      <div style="height: 40px" />
      <header-menu
        :headers-full="headersFull"
        :initial-selected-headers="selectedHeadersTemp"
        :default-headers="headersFull.filter((h) => h.defaultHeader === true)"
        @submit="submitHeaderForm"
      />
    </div>
    <v-data-table-server
      v-model:sort-by="sortBy"
      v-model:items-per-page="itemsPerPage"
      v-model:page="currentPage"
      :headers="headers"
      :items="tasks"
      item-value="uniqueId"
      :items-length="totalItems"
      :items-per-page-options="[10, 25, 50, 100]"
      :loading="loading"
      show-expand
      @update:expanded="handleItemExpanded"
      @update:options="handleOptionsChange"
    >
      <template #expanded-row="{ columns, item }">
        <td :colspan="columns.length" class="pa-4">
          <v-card flat>
            <v-card-title class="subtitle-1 pb-4">
              <v-row no-gutters align="center">
                <v-col cols="4">
                  <span class="font-weight-bold">Task #{{ item.id }}</span>
                  <v-chip
                    v-if="item.module_name || item.task_name"
                    size="small"
                    class="ml-2"
                    color="primary"
                    variant="outlined"
                  >
                    {{ item.module_name || item.task_name }}
                  </v-chip>
                </v-col>
                <v-col
                  v-if="
                    (expandedTasks[item.uniqueId].options &&
                      Object.keys(expandedTasks[item.uniqueId].options).length >
                        0) ||
                    (expandedTasks[item.uniqueId].module_options &&
                      Object.keys(expandedTasks[item.uniqueId].module_options)
                        .length > 0)
                  "
                  cols="6"
                >
                  <div class="d-flex flex-wrap gap-2">
                    <span class="font-weight-bold ml-2 mr-2 subtitle-1"
                      >Options:</span
                    >
                    <v-chip
                      v-for="(value, key) in expandedTasks[item.uniqueId]
                        .options || expandedTasks[item.uniqueId].module_options"
                      :key="key"
                      size="small"
                      label
                      class="mr-1 mb-1"
                    >
                      <span class="subtitle-2 font-weight-bold mr-1"
                        >{{ key }}:</span
                      >
                      <span class="subtitle-2">{{ value }}</span>
                    </v-chip>
                  </div>
                </v-col>
                <v-col class="text-right">
                  <v-switch
                    v-model="expandedTasks[item.uniqueId].backgroundColor"
                    color="primary"
                    false-value="white"
                    true-value="black"
                    label="Dark Mode Output"
                    hide-details
                    class="mt-0 pt-0 d-inline-block"
                    @update:model-value="updateTaskBackgroundColor(item)"
                  />
                </v-col>
              </v-row>
            </v-card-title>
            <v-divider />

            <v-card-text>
              <v-row dense>
                <v-col cols="12">
                  <div class="mb-4">
                    <div class="d-flex align-center mb-1">
                      <span class="subtitle-2 font-weight-bold"
                        >Task Input</span
                      >
                      <v-spacer />
                      <tooltip-button
                        :icon="item.expandedInput ? 'fa-minus' : 'fa-plus'"
                        :text="
                          item.expandedInput ? 'Show Less' : 'See Full Input'
                        "
                        x-small
                        @click="toggleSeeFullInput(item)"
                      />
                    </div>
                    <div
                      :class="
                        'mono rounded pa-2 ' +
                        (expandedTasks[item.uniqueId].backgroundColor ===
                        'white'
                          ? 'font-black'
                          : 'font-white')
                      "
                      :style="
                        'background-color: ' +
                        expandedTasks[item.uniqueId].backgroundColor +
                        ';'
                      "
                    >
                      {{
                        addBlankLines(
                          item.expandedInput
                            ? expandedTasks[item.uniqueId].full_input
                            : item.input,
                        )
                      }}
                    </div>
                  </div>

                  <div v-if="hasOutput(item)">
                    <div class="d-flex align-center mb-1">
                      <span class="subtitle-2 font-weight-bold"
                        >Task Output</span
                      >
                      <v-spacer />
                      <v-btn
                        v-if="
                          item.downloads.length > 0 &&
                          item.downloads.some((d) =>
                            d.filename.match(/[^/]+(jpg|jpeg|png|gif)$/),
                          )
                        "
                        variant="text"
                        size="x-small"
                        @click="getImagesForTask(item)"
                      >
                        View Images
                      </v-btn>
                    </div>

                    <div
                      v-if="
                        item.downloads.length > 0 &&
                        item.downloads.some((d) =>
                          d.filename.match(/[^/]+(jpg|jpeg|png|gif)$/),
                        )
                      "
                    >
                      <div class="d-flex flex-wrap gap-2 mb-2">
                        <v-img
                          v-for="download in item.downloads"
                          :key="download.id"
                          :src="imageData(item, download)"
                          :alt="download.filename"
                          :max-width="400"
                          class="bg-grey-lighten-2 rounded mb-2"
                          contain
                        >
                          <template #placeholder>
                            <v-row
                              class="fill-height ma-0"
                              align="center"
                              justify="center"
                            >
                              <v-progress-circular
                                indeterminate
                                color="grey-lighten-5"
                              />
                            </v-row>
                          </template>
                        </v-img>
                      </div>
                    </div>

                    <div
                      :class="
                        'mono rounded pa-2 ' +
                        (expandedTasks[item.uniqueId].backgroundColor ===
                        'white'
                          ? 'font-black'
                          : 'font-white')
                      "
                      :style="
                        'background-color: ' +
                        expandedTasks[item.uniqueId].backgroundColor +
                        ';'
                      "
                    >
                      <div
                        v-if="expandedTasks[item.uniqueId].htmlOutput"
                        v-html="expandedTasks[item.uniqueId].htmlOutput"
                      />
                      <div v-else>
                        {{ addBlankLines(item.output) }}
                      </div>
                    </div>
                  </div>
                </v-col>
              </v-row>
            </v-card-text>
          </v-card>
        </td>
      </template>
      <template #item.status="{ item }">
        <v-icon v-if="item.status === 'started'" color="blue" size="small">
          fa-check-square
        </v-icon>
        <v-icon
          v-else-if="item.status === 'queued'"
          color="orange"
          size="small"
        >
          fa-clock
        </v-icon>
        <v-icon
          v-else-if="item.status === 'completed'"
          color="green"
          size="small"
        >
          fa-check-circle
        </v-icon>
        <v-icon v-else-if="item.status === 'error'" color="red" size="small">
          fa-times-circle
        </v-icon>
        <v-icon
          v-else-if="item.status === 'continuous'"
          color="purple"
          size="small"
        >
          fa-infinity
        </v-icon>
      </template>
      <template v-if="!plugin" #item.plugin_id="{ item }">
        <router-link
          style="color: inherit"
          :to="{ name: 'pluginEdit', params: { id: item.plugin_id } }"
        >
          {{ item.plugin_id }}
        </router-link>
      </template>
      <template #item.input="{ item }">
        <span>{{ truncateMessage(item.input) }}</span>
      </template>
      <template #item.updated_at="{ item }">
        <date-time-display :timestamp="item.updated_at" />
      </template>
      <template #item.tags="{ item }">
        <tag-viewer
          :tags="item.tags"
          @update-tag="updateTag(item, ...arguments)"
          @delete-tag="deleteTag(item, ...arguments)"
          @new-tag="addTag(item, ...arguments)"
        />
      </template>
      <template #item.actions="{ item }">
        <v-menu>
          <template #activator="{ props: activatorProps }">
            <v-btn variant="text" icon size="x-small" v-bind="activatorProps">
              <v-icon>fa-ellipsis-v</v-icon>
            </v-btn>
          </template>
          <v-list class="ml-2 mr-2">
            <v-list-subheader>Execution</v-list-subheader>
            <v-list-item
              v-show="supportsRerun(item)"
              key="rerunTask"
              link
              @click="rerunTask(item)"
            >
              <v-list-item-title>
                <v-icon size="small">fa-redo</v-icon>
                Rerun Task
              </v-list-item-title>
            </v-list-item>
            <v-divider />
            <v-list-subheader>Input</v-list-subheader>
            <v-list-item key="clipboardInput" link @click="copyInput(item)">
              <v-list-item-title>
                <v-icon size="small">fa-paperclip</v-icon>
                Copy to Clipboard
              </v-list-item-title>
            </v-list-item>
            <v-list-item key="downloadInput" link @click="downloadInput(item)">
              <v-list-item-title>
                <v-icon size="small">fa-download</v-icon>
                Download
              </v-list-item-title>
            </v-list-item>
            <v-divider v-if="hasOutput(item)" />
            <v-list-subheader v-if="hasOutput(item)">Output</v-list-subheader>
            <v-list-item
              v-if="hasOutput(item)"
              key="clipboardOutput"
              link
              @click="copyOutput(item)"
            >
              <v-list-item-title>
                <v-icon size="small">fa-paperclip</v-icon>
                Copy to Clipboard
              </v-list-item-title>
            </v-list-item>
            <v-list-item
              v-if="hasOutput(item)"
              key="downloadOutput"
              link
              @click="downloadOutput(item)"
            >
              <v-list-item-title>
                <v-icon size="small">fa-download</v-icon>
                Download
              </v-list-item-title>
            </v-list-item>
            <v-divider v-if="item.downloads.length > 0" />
            <v-list-subheader v-if="item.downloads.length > 0"
              >Files</v-list-subheader
            >
            <v-list-item
              v-for="download in item.downloads"
              :key="'download-' + download.id"
              link
              @click="downloadFile(download)"
            >
              <v-list-item-title title="Download file">
                <v-icon size="small">fa-download</v-icon>
                {{ download.filename }}
              </v-list-item-title>
            </v-list-item>
          </v-list>
        </v-menu>
      </template>
    </v-data-table-server>
  </div>
</template>

<script>
import debounce from "lodash.debounce";
// eslint-disable-next-line import/no-named-default
import { default as AnsiUp } from "ansi_up";
import DateTimeDisplay from "@/components/DateTimeDisplay.vue";
import TooltipButton from "@/components/TooltipButton.vue";
import TagViewer from "@/components/TagViewer.vue";
import HeaderMenu from "@/components/HeaderMenu.vue";
import DownloadMixin from "@/mixins/download-stager";
import { useApplicationStore } from "@/stores/application-module";
import * as downloadApi from "@/api/download-api";
import * as pluginApi from "@/api/plugin-api";
import { copyToClipboard } from "@/utils/clipboard";

export default {
  name: "PluginTasksTable",
  components: {
    DateTimeDisplay,
    TagViewer,
    TooltipButton,
    HeaderMenu,
  },
  mixins: [DownloadMixin],
  inject: ["snack"],
  props: {
    plugin: {
      type: Object,
      required: false,
      default: null,
    },
    refreshTasks: {
      type: Boolean,
      default: false,
    },
    hideColumns: {
      type: Array,
      default: () => [],
    },
    selectedPlugins: {
      type: Array,
      default: () => [],
    },
    selectedUsers: {
      type: Array,
      default: () => [],
    },
    selectedTags: {
      type: Array,
      default: () => [],
    },
    search: {
      type: String,
      default: "",
    },
    noFilters: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      tasks: [],
      currentPage: 1,
      totalItems: 0,
      itemsPerPage: 10,
      loading: false,
      sortBy: [{ key: "updated_at", order: "desc" }],
      refreshInterval: null,
      expandedTasks: {},
      debouncedGetTasks: debounce(this.getTasks, 500),
      selectedHeadersTemp: [],
      headersFull: [
        {
          title: "Task ID",
          key: "id",
          sortable: true,
          defaultHeader: false,
          alwaysShow: false,
          align: "center",
          order: 1,
        },
        {
          title: "Status",
          key: "status",
          sortable: true,
          defaultHeader: true,
          order: 2,
        },
        {
          title: "Plugin",
          key: "plugin_id",
          sortable: true,
          defaultHeader: true,
          order: 3,
        },
        {
          title: "Task Input",
          key: "input",
          sortable: false,
          defaultHeader: false,
          order: 5,
        },
        {
          title: "User",
          key: "username",
          sortable: false,
          defaultHeader: true,
          order: 6,
        },
        {
          title: "Updated At",
          key: "updated_at",
          sortable: true,
          defaultHeader: true,
          order: 7,
        },
        {
          title: "Tags",
          key: "tags",
          sortable: false,
          width: 400,
          defaultHeader: true,
          order: 8,
        },
        {
          title: "Actions",
          key: "actions",
          sortable: false,
          defaultHeader: true,
          alwaysShow: true,
          order: 9,
        },
      ],
    };
  },
  computed: {
    applicationStore() {
      return useApplicationStore();
    },
    headers() {
      return this.headersFull
        .filter(
          (h) =>
            this.applicationStore.pluginTaskHeaders.findIndex(
              (h2) => h2.title === h.title,
            ) > -1,
        )
        .sort((a, b) => a.order - b.order);
    },
  },
  watch: {
    refreshTasks: {
      handler(newVal) {
        if (newVal) {
          if (this.debouncedGetTasks) {
            this.debouncedGetTasks();
          } else {
            this.getTasks();
          }
          this.refreshInterval = setInterval(() => {
            this.debouncedGetTasks();
          }, 8000);
        } else {
          clearInterval(this.refreshInterval);
        }
      },
      immediate: true,
    },
    currentPage() {
      this.debouncedGetTasks();
    },
    plugin() {
      this.debouncedGetTasks();
    },
    selectedPlugins() {
      this.debouncedGetTasks();
    },
    selectedUsers() {
      this.debouncedGetTasks();
    },
    selectedTags() {
      this.debouncedGetTasks();
    },
    search() {
      this.debouncedGetTasks();
    },
  },
  mounted() {
    this.debouncedGetTasks();
    if (
      this.applicationStore.pluginTaskHeaders.length === 0 ||
      !this.applicationStore.pluginTaskHeaders[0].title
    ) {
      this.applicationStore.pluginTaskHeaders = this.headersFull.filter(
        (h) => h.defaultHeader === true,
      );
    }
    this.selectedHeadersTemp = this.headersFull.filter((h) =>
      this.applicationStore.pluginTaskHeaders.some(
        (h2) => h2.title === h.title,
      ),
    );
  },
  beforeUnmount() {
    clearInterval(this.refreshInterval);
  },
  methods: {
    submitHeaderForm(val) {
      this.selectedHeadersTemp = val;
      this.applicationStore.pluginTaskHeaders = [...this.selectedHeadersTemp];
    },
    resetHeaders() {
      this.applicationStore.pluginTaskHeaders = this.headersFull.filter(
        (h) => h.defaultHeader === true,
      );
      this.selectedHeadersTemp = this.headersFull.filter((h) =>
        this.applicationStore.pluginTaskHeaders.some(
          (h2) => h2.title === h.title,
        ),
      );
    },
    // eslint-disable-next-line no-unused-vars
    // from https://github.com/xpl/ansicolor
    stripAnsi(text) {
      return text.replace(
        // eslint-disable-next-line no-control-regex
        /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g,
        "",
      ); // hope V8 caches the regexp
    },
    isAnsi(output) {
      return this.stripAnsi(output) !== output;
    },
    ansiToHtml(output) {
      return new AnsiUp().ansi_to_html(output);
    },
    addBlankLines(text) {
      return `\n${text}\n`;
    },
    deleteTag(task, tag) {
      pluginApi
        .deleteTag(task.plugin_id, task.id, tag.id)
        .then(() => {
          task.tags = task.tags.filter((t) => t.id !== tag.id);
          this.$emit("refresh-tags");
        })
        .catch((err) => this.snack.error(`Error: ${err}`));
    },
    updateTag(task, tag) {
      pluginApi
        .updateTag(task.plugin_id, task.id, tag)
        .then((t) => {
          const index = task.tags.findIndex((x) => x.id === t.id);
          task.tags.splice(index, 1, t);
          this.$emit("refresh-tags");
          this.snack.success("Tag updated");
        })
        .catch((err) => this.snack.error(`Error: ${err}`));
    },
    addTag(task, tag) {
      pluginApi
        .addTag(task.plugin_id, task.id, tag)
        .then((t) => {
          task.tags = [...task.tags, t];
          this.$emit("refresh-tags");
        })
        .catch((err) => this.snack.error(`Error: ${err}`));
    },
    truncateMessage(task) {
      if (task) {
        return task.length > 30 ? `${task.substr(0, 30)}...` : task;
      }
      return "";
    },
    updateTaskBackgroundColor(task) {
      if (task.backgroundColor === "black") {
        task.backgroundColor = "white";
      } else {
        task.backgroundColor = "black";
      }
      this.expandedTasks[task.uniqueId].backgroundColor = task.backgroundColor;

      // Trigger reactivity on the table
      this.tasks.splice(this.tasks.indexOf(task), 1, task);
    },
    downloadFile(download) {
      downloadApi.getDownload(download.id);
    },
    hasOutput(task) {
      return !!task.output;
    },
    async downloadInput(task) {
      if (task.input) {
        if (!this.expandedTasks[task.uniqueId]?.full_input) {
          const data = await pluginApi.getTask(task.plugin_id, task.id);
          this.expandedTasks[task.uniqueId] = {
            ...this.expandedTasks[task.uniqueId],
            ...data,
          };
        }

        this.downloadText(
          this.expandedTasks[task.uniqueId].full_input,
          `${task.uniqueId}-input.txt`,
        );
      }
    },
    downloadOutput(task) {
      if (task.output) {
        this.downloadText(task.output, `${task.uniqueId}-output.txt`);
      }
    },
    async copyInput(task) {
      if (task.input) {
        if (!this.expandedTasks[task.uniqueId]?.full_input) {
          const data = await pluginApi.getTask(task.plugin_id, task.id);
          this.expandedTasks[task.uniqueId] = {
            ...this.expandedTasks[task.uniqueId],
            ...data,
          };
        }

        await copyToClipboard(
          this.expandedTasks[task.uniqueId].full_input,
          this.snack,
        );
      }
    },
    async copyOutput(task) {
      if (task.output) {
        await copyToClipboard(task.output, this.snack);
      }
    },
    imageData(task, download) {
      const expandedDownloads = this.expandedTasks[task.uniqueId]?.downloads;
      if (expandedDownloads) {
        const found = expandedDownloads.find((d) => d.id === download.id);
        if (found) {
          return found.image;
        }
      }
      return null;
    },
    async getImagesForTask(task) {
      if (!this.expandedTasks[task.uniqueId].imagesRetrieved) {
        const data = await pluginApi.getTask(task.plugin_id, task.id);
        this.expandedTasks[task.uniqueId] = {
          ...this.expandedTasks[task.uniqueId],
          ...data,
          imagesRetrieved: true,
        };
      }

      for (let i = 0; i < task.downloads.length; i++) {
        const download = task.downloads[i];
        if (
          !this.expandedTasks[task.uniqueId].downloads[download.id]?.image &&
          download.filename.match(/[^/]+(jpg|jpeg|png|gif)$/)
        ) {
          // eslint-disable-next-line
          const url = await downloadApi.getDownloadAsUrl(download.id);
          this.expandedTasks[task.uniqueId].downloads[i].image = url;
        }
      }
      this.tasks.splice(this.tasks.indexOf(task), 1, task);
    },
    async toggleSeeFullInput(task) {
      if (!task.expandedInput) {
        const data = await pluginApi.getTask(task.plugin_id, task.id);
        this.expandedTasks[task.uniqueId] = {
          ...this.expandedTasks[task.uniqueId],
          ...data,
          expandedInput: true,
        };
        task.expandedInput = true;
      } else {
        this.expandedTasks[task.uniqueId].expandedInput = false;
        task.expandedInput = false;
      }

      // Trigger reactivity on the table
      this.tasks.splice(this.tasks.indexOf(task), 1, task);
    },
    async rerunTask(task) {
      try {
        const fullTask = await pluginApi.getTask(task.plugin_id, task.id);
        const options = fullTask.options || {};
        await pluginApi.executePlugin(fullTask.plugin_id, options);
        this.snack.info(`Plugin ${fullTask.plugin_id} rerun queued.`);
      } catch (err) {
        this.snack.error(`Error rerunning task: ${err}`);
      }
    },
    supportsRerun() {
      return true;
    },
    async handleItemExpanded(expandedIds) {
      const toLoad = expandedIds.filter(
        (uniqueId) =>
          this.expandedTasks[uniqueId] &&
          !this.expandedTasks[uniqueId].fullTaskLoaded,
      );

      await Promise.all(
        toLoad.map(async (uniqueId) => {
          const item = this.tasks.find((t) => t.uniqueId === uniqueId);
          if (!item) return;
          try {
            const data = await pluginApi.getTask(item.plugin_id, item.id);
            this.expandedTasks[item.uniqueId] = {
              ...this.expandedTasks[item.uniqueId],
              ...data,
              fullTaskLoaded: true,
            };
            this.tasks.splice(this.tasks.indexOf(item), 1, item);
          } catch (err) {
            this.snack.error(`Error loading task ${item.id}: ${err}`);
          }
        }),
      );
    },
    handleOptionsChange(value) {
      this.currentPage = value.page;
      this.itemsPerPage = value.itemsPerPage;
      this.sortBy = Array.isArray(value.sortBy) ? value.sortBy : [];
      this.debouncedGetTasks();
    },
    async getTasks() {
      if (
        !this.noFilters &&
        (this.selectedPlugins.length === 0 || this.selectedUsers.length === 0)
      ) {
        // seems weird to do this but it would be weirder to select all agents
        // when no agents are selected. Even though the api sees no agents as all agents.
        this.tasks = [];
        this.currentPage = 1;
        this.totalItems = 0;
        return;
      }
      this.loading = true;
      let plugins = null;
      if (this.selectedPlugins.length > 0) {
        plugins = this.selectedPlugins;
      }

      const sortEntry =
        this.sortBy.length > 0 ? this.sortBy[0] : { key: "id", order: "desc" };
      try {
        const response = await pluginApi.getTasks(plugins, {
          page: this.currentPage,
          limit: this.itemsPerPage,
          sortBy: sortEntry.key,
          sortOrder: sortEntry.order,
          users: this.selectedUsers,
          tags: this.selectedTags,
          search: this.search,
        });
        this.currentPage = response.page;
        this.totalItems = response.total;

        // iterate response.records and add expandedInput if it exists in expandedTasks
        // this ensures that the expandedInput doesn't get wiped away after a refresh
        this.tasks = response.records.map((task) => {
          task.uniqueId = `${task.plugin_id}-${task.id}`;

          if (!this.expandedTasks[task.uniqueId]) {
            this.expandedTasks[task.uniqueId] = {};
          }

          if (this.expandedTasks[task.uniqueId].expandedInput) {
            task.expandedInput = true;
          }

          this.expandedTasks[task.uniqueId].backgroundColor =
            this.expandedTasks[task.uniqueId].backgroundColor || "black";
          task.backgroundColor =
            this.expandedTasks[task.uniqueId].backgroundColor;

          if (this.isAnsi(task.output || "")) {
            this.expandedTasks[task.uniqueId].htmlOutput = this.ansiToHtml(
              task.output,
            );
          }

          return task;
        });
      } catch (err) {
        this.snack.error(`Failed to load tasks: ${err}`);
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>

<style>
.mono {
  white-space: pre-wrap;
  font:
    1.1em "Andale Mono",
    Consolas,
    "Courier New";
  font-weight: bold;
  line-height: 1.6em;
  text-align: left;
}

.font-white {
  color: white;
}

.font-black {
  color: black;
}
</style>
