<script setup lang="ts">
import type { BackendError, FileSystem, Library, PrivacyLevel } from '~/types'
import type { VueUploadItem } from 'vue-upload-component'
import type { paths } from '~/generated/types'

import { computed, ref, reactive, watch, nextTick } from 'vue'
import { useEventListener, useIntervalFn } from '@vueuse/core'
import { humanSize, truncate } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { sortBy } from 'lodash-es'
import { useStore } from '~/store'

import axios from 'axios'

import LibraryFilesTable from '~/views/content/libraries/FilesTable.vue'
import FileUploadWidget from './FileUploadWidget.vue'
import FsBrowser from './FsBrowser.vue'
import FsLogs from './FsLogs.vue'

import useWebSocketHandler from '~/composables/useWebSocketHandler'
import updateQueryString from '~/composables/updateQueryString'
import useErrorHandler from '~/composables/useErrorHandler'
import useSharedLabels from '~/composables/locale/useSharedLabels'

import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
import Slider from '~/components/ui/Slider.vue'
import Section from '~/components/ui/Section.vue'
import Pill from '~/components/ui/Pill.vue'
import Layout from '~/components/ui/Layout.vue'
import Table from '~/components/ui/Table.vue'

interface Events {
  (e: 'uploads-finished', delta: number):void
}

interface Props {
  defaultImportReference?: string
}

const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), {
  defaultImportReference: ''
})

const { t } = useI18n()
const store = useStore()

const upload = ref()
const currentTab = ref('uploads')
const supportedExtensions = computed(() => store.state.ui.supportedExtensions)

const labels = computed(() => ({
  tooltips: {
    denied: t('components.library.FileUpload.tooltip.denied'),
    server: t('components.library.FileUpload.tooltip.size'),
    network: t('components.library.FileUpload.tooltip.network'),
    timeout: t('components.library.FileUpload.tooltip.timeout'),
    retry: t('components.library.FileUpload.tooltip.retry'),
    extension: t(
      'components.library.FileUpload.tooltip.extension',
      { extensions: supportedExtensions.value.join(', ') }
    )
  } as Record<string, string>
}))

const uploads = reactive({
  pending: 0,
  finished: 0,
  skipped: 0,
  errored: 0,
  objects: {} as Record<string, any>
})

// Select corresponding user-library when slider changes

const sharedLabels = useSharedLabels()

const options = {
  me: sharedLabels.fields.privacy_level.choices.me,
  instance: sharedLabels.fields.privacy_level.choices.instance,
  everyone: sharedLabels.fields.privacy_level.choices.everyone
} as const satisfies Record<PrivacyLevel, string>

const privacyLevel = defineModel<keyof typeof options>({ required: true })

const library = ref<Library>()

// New implementation with `useClient`:

// watch(privacyLevel, (newValue) =>
//   get({
//       privacy_level: newValue,
//       scope: 'me'
//   })
//   .then((data) =>
//     library.value = data?.results.find(({name}) => name === privacyLevel.value)
//   ),
//   { immediate: true }
// )

// Old implementation:

watch(privacyLevel, async (newValue) => {
  try {
    const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
      params: {
        privacy_level: privacyLevel.value,
        scope: 'me'
      }
    })

    library.value = response.data.results.find(({ name }) => name === privacyLevel.value)
  } catch (error) {
    useErrorHandler(error as Error)
  }
}, { immediate: true })

//
// File counts
//
const files = ref([] as VueUploadItem[])
const processedFilesCount = computed(() => uploads.skipped + uploads.errored + uploads.finished)
const uploadedFilesCount = computed(() => files.value.filter(file => file.success).length)
const retryableFiles = computed(() => files.value.filter(file => file.error))
const erroredFilesCount = computed(() => retryableFiles.value.length)
const processableFiles = computed(() => uploads.pending
  + uploads.skipped
  + uploads.errored
  + uploads.finished
  + uploadedFilesCount.value
)

//
// Uploading
//
const importReference = ref(props.defaultImportReference || new Date().toISOString())
history.replaceState(history.state, '', updateQueryString(location.href, 'import', importReference.value))
const uploadData = computed(() => ({
  library: library.value?.uuid,
  import_reference: importReference
}))

watch(() => uploads.finished, (newValue, oldValue) => {
  if (newValue > oldValue) {
    emit('uploads-finished', newValue - oldValue)
  }
})

//
// Upload status
//
const fetchStatus = async () => {
  for (const status of Object.keys(uploads)) {
    if (status === 'objects') continue

    try {
      const response = await axios.get('uploads/', {
        params: {
          import_reference: importReference.value,
          import_status: status,
          page_size: 1,
          library: library.value?.uuid
        }
      })

      uploads[status as keyof typeof uploads] = response.data.count
    } catch (error) {
      useErrorHandler(error as Error)
    }
  }
}

fetchStatus()

const needsRefresh = ref(false)
useWebSocketHandler('import.status_updated', async (event) => {
  if (event.upload.import_reference !== importReference.value) {
    return
  }

  // TODO (wvffle): Why?
  await nextTick()

  if (event.new_status === 'errored') {
    for (const file of files.value) {
      if (file.response?.uuid === event.upload.uuid) {
        file.error = event.new_status
        break
      }
    }
  }

  uploads[event.old_status] -= 1
  uploads[event.new_status] += 1
  uploads.objects[event.upload.uuid] = event.upload
  needsRefresh.value = true
})

//
// Files
//
const sortedFiles = computed(() => {
  const filesToSort = files.value

  return [
    ...sortBy(filesToSort.filter(file => file.errored), ['name']),
    ...sortBy(filesToSort.filter(file => !file.errored && !file.success), ['name']),
    ...sortBy(filesToSort.filter(file => file.success), ['name'])
  ]
})

const hasActiveUploads = computed(() => files.value.some(file => file.active))

//
// Quota status
//
const quotaStatus = ref()

const uploadedSize = computed(() => {
  let uploaded = 0

  for (const file of files.value) {
    if (!file.error) {
      uploaded += (file.size ?? 0) * +(file.progress ?? 0) / 100
    }
  }

  return uploaded
})

const remainingSpace = computed(() => Math.max(
  (quotaStatus.value?.remaining ?? 0) - uploadedSize.value / 1e6,
  0
))

watch(remainingSpace, space => {
  if (space <= 0) {
    upload.value.active = false
  }
})

const isLoadingQuota = ref(false)
const fetchQuota = async () => {
  isLoadingQuota.value = true

  try {
    const response = await axios.get('users/me/')
    quotaStatus.value = response.data.quota_status
  } catch (error) {
    useErrorHandler(error as Error)
  }

  isLoadingQuota.value = false
}

fetchQuota()

//
// Filesystem
//
const fsPath = ref([])
const fsStatus = ref({
  import: {
    status: 'pending'
  }
} as FileSystem)
watch(fsPath, () => fetchFilesystem(true))

const { pause, resume } = useIntervalFn(() => {
  fetchFilesystem(false)
}, 5000, { immediate: false })

const isLoadingFs = ref(false)
const fetchFilesystem = async (updateLoading: boolean) => {
  if (updateLoading) isLoadingFs.value = true
  pause()

  try {
    const response = await axios.get('libraries/fs-import', { params: { path: fsPath.value.join('/') } })
    fsStatus.value = response.data
  } catch (error) {
    useErrorHandler(error as Error)
  }

  if (updateLoading) isLoadingFs.value = false
  if (store.state.auth.availablePermissions.library) resume()
}

if (store.state.auth.availablePermissions.library) {
  fetchFilesystem(true)
}

const fsErrors = ref([] as string[])
const importFs = async () => {
  isLoadingFs.value = true

  try {
    const response = await axios.post('libraries/fs-import', {
      path: fsPath.value.join('/'),
      library: library.value?.uuid,
      import_reference: importReference.value
    })

    fsStatus.value = response.data
  } catch (error) {
    fsErrors.value = (error as BackendError).backendErrors
  }

  isLoadingFs.value = false
}

// TODO (wvffle): Maybe use AbortController?
const cancelFsScan = async () => {
  try {
    await axios.delete('libraries/fs-import')
    fetchFilesystem(false)
  } catch (error) {
    useErrorHandler(error as Error)
  }
}

const inputFile = (newFile: VueUploadItem) => {
  if (!newFile) return

  if (remainingSpace.value < (newFile.size ?? Infinity) / 1e6) {
    newFile.error = 'denied'
  } else {
    newFile.active = true
  }
}

// NOTE: For some weird reason typescript thinks that xhr field is not compatible with the same type
const retry = (files: Omit<VueUploadItem, 'xhr'>[]) => {
  for (const file of files) {
    upload.value.update(file, { error: '', progress: '0.00' })
  }

  upload.value.active = true
}

//
// Before unload
//
useEventListener(window, 'beforeunload', (event) => {
  if (!hasActiveUploads.value) return null
  event.preventDefault()
  return (event.returnValue = t('components.library.FileUpload.message.listener'))
})

// collapse section
const section = ref(false)
</script>

<template>
  <div :class="{loading: isLoadingQuota}">
    <div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']">
      <div class="label">
        {{ t('components.library.FileUpload.label.remainingSpace') }}
      </div>
      <div class="value">
        {{ humanSize(remainingSpace * 1000 * 1000) }}
      </div>
    </div>
  </div>
  <Slider
    v-model="privacyLevel"
    :options="options"
    :label="t('components.manage.library.UploadsTable.label.visibility')"
  />

  <Alert blue>
    <p>
      {{ t('components.library.FileUpload.message.local.message') }}
    </p>
    <ul>
      <li v-if="library?.privacy_level != 'me'">
        {{ t('components.library.FileUpload.message.local.copyright') }}
      </li>
      <li>
        {{ t('components.library.FileUpload.message.local.tag') }}&nbsp;
        <a
          href="http://picard.musicbrainz.org/"
          target="_blank"
        >{{ t('components.library.FileUpload.link.picard') }}</a>
      </li>
      <li>
        {{ t('components.library.FileUpload.message.local.format') }}
      </li>
    </ul>
  </Alert>

  <file-upload-widget
    ref="upload"
    v-model="files"
    :data="uploadData"
    @input-file="inputFile"
  >
    <Button
      primary
      icon="bi bi-upload"
    >
      {{ t('components.library.FileUpload.label.uploadWidget') }}
    </Button>
    <p>
      {{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
    </p>
  </file-upload-widget>

  <!-- Show how many files are uploading and processing -->

  <Layout
    v-if="files.length > 0"
    flex
  >
    <Layout
      flex
      gap-8
    >
      <label>{{ t('components.library.FileUpload.link.uploading') }}</label>
      <Pill
        v-bind="{
          'green': erroredFilesCount === 0,
          'red': erroredFilesCount > 0,
          'yellow': files.length > uploadedFilesCount + erroredFilesCount
        }"
      >
        {{ uploadedFilesCount + erroredFilesCount }} / {{ files.length }}
      </Pill>
    </Layout>
    <Layout
      flex
      gap-8
    >
      <label>{{ t('components.library.FileUpload.link.processing') }}</label>
      <Pill>
        {{ processedFilesCount }} / {{ processableFiles }}
      </Pill>
    </Layout>
  </Layout>

  <Alert
    v-if="fsErrors.length > 0"
    red
  >
    <h3 class="header">
      {{ t('components.library.FileUpload.header.failure') }}
    </h3>
    <ul class="list">
      <li
        v-for="(error, key) in fsErrors"
        :key="key"
      >
        {{ error }}
      </li>
    </ul>
  </Alert>

  <!-- Show list of processed files -->

  <library-files-table
    :needs-refresh="needsRefresh"
    ordering-config-name="library.detail.upload"
    :filters="{import_reference: importReference}"
    :custom-objects="Object.values(uploads.objects)"
    @fetch-start="needsRefresh = false"
  />

  <!-- Edit the metadata of uploaded files -->

  <Table
    :class="$style.table"
    v-if="files.length > 0"
    :grid-template-columns="['1fr', 'auto', 'auto', 'auto']"
  >
    <template #header>
      <b class="ten wide">
        {{ t('components.library.FileUpload.table.upload.header.filename') }}
      </b>
      <b>
        {{ t('components.library.FileUpload.table.upload.header.size') }}
      </b>
      <b>
        {{ t('components.library.FileUpload.table.upload.header.status') }}
      </b>
      <b>
        {{ t('components.library.FileUpload.table.upload.header.actions') }}
      </b>
    </template>

    <!-- Retry row -->
    <template v-if="retryableFiles.length > 1">
      <b> </b>
      <b />
      <b />
      <b>
        <Button
          auto
          primary
          @click.prevent="retry(retryableFiles)"
        >
          {{ t('components.library.FileUpload.button.retry') }}
        </Button>
      </b>
    </template>

    <!-- Rows for each file -->
    <template
      v-for="file in sortedFiles"
      :key="file.id"
    >
      <b :title="file.name">
        {{ truncate(file.name ?? '', 60) }}
      </b>
      <b>{{ humanSize(file.size ?? 0) }}</b>
      <b>
        <span
          v-if="typeof file.error === 'string' && file.error"
          class="ui tooltip"
          :data-tooltip="labels.tooltips[file.error]"
        >
          <span class="ui danger icon label">
            <i class="bi bi-question-circle-fill" /> {{ file.error }}
          </span>
        </span>
        <span
          v-else-if="file.success"
          class="ui success label"
        >
          <span key="1">
            {{ t('components.library.FileUpload.table.upload.status.uploaded') }}
          </span>
        </span>
        <span
          v-else-if="file.active"
          class="ui warning label"
        >
          <span key="2">
            {{ t('components.library.FileUpload.table.upload.status.uploading') }}
          </span>

          {{ t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
        </span>
        <span
          v-else
          class="ui label"
        >
          <span key="3">
            {{ t('components.library.FileUpload.table.upload.status.pending') }}
          </span>
        </span>
      </b>
      <b>
        <template v-if="file.error">
          <Button
            v-if="retryableFiles.includes(file)"
            square
            secondary
            :title="labels.tooltips.retry"
            icon="bi-arrow-clockwise"
            @click.prevent="retry([file])"
          />
        </template>
        <template v-else-if="!file.success">
          <Button
            square-small
            destructive
            icon="bi-trash-fill"
            @click.prevent="upload.remove(file)"
          />
        </template>
      </b>
    </template>
  </Table>

  <!-- Progressive disclosure: Import from server -->

  <Section
    :h2="t('components.library.FileUpload.header.server')"
    align-left
    no-items
    v-bind="
      section
        ? { collapse: () => { section = false } }
        : { expand: () => { section = true } }
    "
  >
    <div style="grid-column: 1 / -1">
      <fs-browser
        v-model="fsPath"
        :loading="isLoadingFs"
        :data="fsStatus"
        @import="importFs"
      />
      <template v-if="fsStatus && fsStatus.import">
        <h3 class="ui header">
          {{ t('components.library.FileUpload.header.status') }}
        </h3>
        <p v-if="fsStatus.import.reference !== importReference">
          {{ t('components.library.FileUpload.description.previousImport') }}
        </p>
        <p v-else>
          {{ t('components.library.FileUpload.description.import') }}
        </p>

        <Button
          v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'"
          secondary
          @click="cancelFsScan"
        >
          {{ t('components.library.FileUpload.button.cancel') }}
        </Button>
        <fs-logs :data="fsStatus.import" />
      </template>
    </div>
  </Section>
</template>

<style module lang="scss">
  .file-uploads {
    padding: 32px;
    border-radius: var(--fw-border-radius);
    border: 2px dashed var(--border-color);
  }
  .table {
    b:not(:first-child) { margin-left: 12px; }
  }
</style>
