<script setup lang="ts">
import type { BackendError, Channel, Upload, Track, Album } from '~/types'
import type { VueUploadItem } from 'vue-upload-component'

import { computed, ref, reactive, watchEffect, watch, onMounted } from 'vue'
import { whenever } from '@vueuse/core'
import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { useModal } from '~/ui/composables/useModal.ts'

import axios from 'axios'
import { type paths, type operations, type components } from '~/generated/types.ts'

import UploadMetadataForm from '~/components/channels/UploadMetadataForm.vue'
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
import LicenseSelect from '~/components/channels/LicenseSelect.vue'
import AlbumSelect from '~/components/channels/AlbumSelect.vue'
import AlbumModal from '~/components/channels/AlbumModal.vue'

import useErrorHandler from '~/composables/useErrorHandler'

import Layout from '~/components/ui/Layout.vue'
import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue'

interface Events {
  (e: 'status', status: UploadStatus): void
}

interface Props {
  channel: Channel | null,
  filter: 'podcast' | 'music'
}

interface QuotaStatus {
  remaining: number
}

interface UploadStatus {
  totalSize: number
  totalFiles: number
  progress: number
  speed: number
  quotaStatus: QuotaStatus
  uploadedSize: number
  canSubmit: boolean
}

interface UploadedFile extends VueUploadItem {
  _fileObj?: VueUploadItem
  removed: boolean
  metadata: Metadata
}

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

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

const errors = ref([] as string[])

const values = reactive({
  channel: props.channel?.uuid ?? null,
  license: null,
  album: null
})

const files = ref([] as VueUploadItem[])

//
// Channels
//
const availableChannels = ref<Channel[]>([])

/*
          availableChannels>1?  :=1     :=0
          |                               |       |
          v                               v       v
props   select a channel           |       create empty channel
|         |                                     null
v         v                                       |
          channelDropdownId                 v
          |
          v
selectedChannel
|
v
as a model to Album
|
v
albums

*/

// In the channel dropdown, we can select a value
//

const channelDropdownId = ref<Channel['artist']['id'] | null>(null)
const isLoading = ref(false)

const selectedChannel = computed(() =>
  props.channel
    ? props.channel
    : availableChannels.value.length === 0
      ? (createEmptyChannel(), null)
      : availableChannels.value.length === 1
        ? availableChannels.value[0]
        : availableChannels.value.find(({ artist }) => artist.id === channelDropdownId.value)
)

const emptyChannelCreateRequest:components['schemas']['ChannelCreateRequest'] = {
  name: store.state.auth.fullUsername,
  username: store.state.auth.username,
  description: null,
  tags: [],
  content_category: 'music'
}

const createEmptyChannel = async () => {
  try {
    const response = await axios.post(
      'channels/',
      (emptyChannelCreateRequest satisfies operations['create_channel_2']['requestBody']['content']['application/json'])
    )
    console.log('Created Channel: ', response.data)
  } catch (error) {
    errors.value = (error as BackendError).backendErrors
    console.log('Error:', error)
  }
}

const fetchChannels = async () => {
  isLoading.value = true
  try {
    const response = await axios.get('channels/', { params: { scope: 'me' /* Ask Pablo: which param to filter for `music` | `podcast`? */ } })
    availableChannels.value = response.data.results
  } catch (error) {
    errors.value = (error as BackendError).backendErrors
  }

  isLoading.value = false
}

// Albums
const albumSelection = ref<{channel: Channel, albumId: Album['id'] | '', albums: Album[]}>

watch(selectedChannel, (channel) =>
  albumSelection.value
      = {
      channel,
      albumId: '',
      albums: []
    }
)

const channelChange = async (channelId) => {
  selectedChannel.value = channelId
  await fetchAlbums(channelId)
}

// Quota and space
//
const quotaStatus = ref()
const fetchQuota = async () => {
  try {
    const response = await axios.get('users/me/')
    quotaStatus.value = response.data.quota_status as QuotaStatus
  } catch (error) {
    errors.value = (error as BackendError).backendErrors
  }
}

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

  for (const file of uploadedFiles.value) {
    if (file._fileObj && !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
))

//
// Draft uploads
//
const includeDraftUploads = ref()
const draftUploads = ref([] as Upload[])
whenever(() => values.channel !== null, async () => {
  files.value = []
  draftUploads.value = []

  try {
    const response = await axios.get('uploads', {
      params: { import_status: 'draft', channel: values.channel }
    })

    draftUploads.value = response.data.results as Upload[]
    for (const upload of response.data.results as Upload[]) {
      // @ts-expect-error TODO (wvffle): Resolve type errors when API client is done
      uploadImportData[upload.uuid] = upload.import_metadata ?? {}
    }
  } catch (error) {
    errors.value = (error as BackendError).backendErrors
  }
}, { immediate: true })

//
// Uploading files
//
const upload = ref()
const beforeFileUpload = (newFile: VueUploadItem) => {
  if (!newFile) return
  if (remainingSpace.value < (newFile.size ?? Infinity) / 1e6) {
    newFile.error = 'denied'
  } else {
    newFile.active = true
  }
}

const baseImportMetadata = computed(() => ({
  channel: values.channel,
  import_status: 'draft',
  import_metadata: { license: values.license, album: values.album }
}))

//
// Uploaded files
//
const removed = reactive(new Set<string>())
const uploadedFiles = computed(() => {
  const uploadedFiles = files.value.map(file => {
    const data = {
      ...file,
      _fileObj: file,
      removed: false,
      metadata: {}
    } as UploadedFile

    if (file.response?.uuid) {
      const uuid = file.response.uuid as string
      data.metadata = uploadImportData[uuid] ?? uploadData[uuid]?.import_metadata ?? {}
      data.removed = removed.has(uuid)
    }

    return data
  })

  if (includeDraftUploads.value) {
    // We have two different objects: draft uploads (so already uploaded in a previous)
    // session, and files uploaded in the current session
    // so we ensure we have a similar structure for both.
    uploadedFiles.unshift(...draftUploads.value.map(upload => ({
      id: upload.uuid,
      response: upload,
      __filename: null,
      size: upload.size,
      progress: '100.00',
      name: upload.source?.replace('upload://', '') ?? '',
      active: false,
      removed: removed.has(upload.uuid),
      metadata: uploadImportData[upload.uuid] ?? audioMetadata[upload.uuid] ?? upload.import_metadata ?? {}
    } as UploadedFile)))
  }

  return uploadedFiles.filter(file => !file.removed) as UploadedFile[]
})

const uploadedFilesById = computed(() => uploadedFiles.value.reduce((acc: Record<string, VueUploadItem>, file) => {
  acc[file.response?.uuid] = file
  return acc
}, {}))

//
// Metadata
//
type Metadata = Pick<Track, 'title' | 'position' | 'tags'> & { cover: string | null, description: string }
const uploadImportData = reactive({} as Record<string, Metadata>)
const audioMetadata = reactive({} as Record<string, Record<string, string>>)
const uploadData = reactive({} as Record<string, { import_metadata: Metadata }>)
const patchUpload = async (id: string, data: Record<string, Metadata>) => {
  const response = await axios.patch(`uploads/${id}/`, data)
  uploadData[id] = response.data
  uploadImportData[id] = response.data.import_metadata
}

const fetchAudioMetadata = async (uuid: string) => {
  delete audioMetadata[uuid]

  const response = await axios.get(`uploads/${uuid}/audio-file-metadata/`)
  audioMetadata[uuid] = response.data

  const uploadedFile = uploadedFilesById.value[uuid]
  if (uploadedFile.response?.import_metadata.title === uploadedFile._fileObj?.name.replace(/\.[^/.]+$/, '') && response.data.title) {
    // Replace existing title deduced from file by the one in audio file metadata, if any
    uploadImportData[uuid].title = response.data.title
  }

  for (const key of ['title', 'position', 'tags'] as const) {
    if (uploadImportData[uuid][key] === undefined) {
      // uploadImportData[uuid][key] = response.data[key] as never
    }
  }

  if (uploadImportData[uuid].description === undefined) {
    uploadImportData[uuid].description = (response.data.description ?? {}).text
  }

  await patchUpload(uuid, { import_metadata: uploadImportData[uuid] })
}

watchEffect(async () => {
  for (const file of files.value) {
    if (file.response?.uuid && audioMetadata[file.response.uuid] === undefined) {
      uploadData[file.response.uuid] = file.response as { import_metadata: Metadata }
      uploadImportData[file.response.uuid] = file.response.import_metadata
      fetchAudioMetadata(file.response.uuid)
    }
  }
})

//
// Select upload
//
const selectedUploadId = ref()
const selectedUpload = computed(() => {
  if (!selectedUploadId.value) return null

  const selected = uploadedFiles.value.find(file => file.response?.uuid === selectedUploadId.value)
  if (!selected) return null

  return {
    ...(selected.response ?? {}),
    _fileObj: selected._fileObj
  } as Upload & { _fileObj?: VueUploadItem }
})

//
// Actions
//
const remove = async (file: VueUploadItem) => {
  if (file.response?.uuid) {
    removed.add(file.response.uuid)
    try {
      await axios.delete(`uploads/${file.response.uuid}/`)
    } catch (error) {
      useErrorHandler(error as Error)
    }
  } else {
    upload.value.remove(file)
  }
}

const retry = async (file: VueUploadItem) => {
  upload.value.update(file, { error: '', progress: '0.00' })
  upload.value.active = true
}

//
// Init
//
fetchChannels()
fetchQuota()

watch(selectedUploadId, async (_, from) => {
  if (from) {
    await patchUpload(from, { import_metadata: uploadImportData[from] })
  }
})

//
// Status
//

watchEffect(() => {
  const uploaded = uploadedFiles.value
  const totalSize = uploaded.reduce(
    (acc, uploadedFile) => !uploadedFile.error
      ? acc + (uploadedFile.size ?? 0)
      : acc,
    0
  )

  const activeFile = files.value.find(file => file.active)

  emit('status', {
    totalSize,
    totalFiles: uploaded.length,
    progress: Math.floor(uploadedSize.value / totalSize * 100),
    speed: activeFile?.speed ?? 0,
    quotaStatus: quotaStatus.value,
    uploadedSize: uploadedSize.value,
    canSubmit: activeFile !== undefined && uploadedFiles.value.length > 0
  })
})

const labels = computed(() => ({
  editTitle: t('components.channels.UploadForm.button.edit')
}))

const publish = async () => {
  console.log('starting publish...')
  isLoading.value = true

  errors.value = []

  try {
    // Post list of uuids of uploadedFiles to axios action:publish

    /* { import_status: components["schemas"]["ImportStatusEnum"];
    audio_file: string;} */

    // const theUpdate : components['schemas']['PatchedUploadForOwnerRequest'] = {
    //   import_status: 'pending',
    // }

    // await axios.post('uploads/action/', {
    //   action: 'publish',
    //   objects: uploadedFiles.value.map((file) => file.response?.uuid)
    // } satisfies paths['/api/v2/uploads/action/']['post']['requestBody']['content']['application/json'],
    // {
    //   headers: { 'Authorization': `Bearer ${store.state.auth.oauth}` }
    // })

    console.log('Channels Store Before: ', store.state.channels)

    // Tell the store that the uploaded files are pending import
    store.commit('channels/publish', {
      uploads: uploadedFiles.value.map((file) => ({ ...file.response, import_status: 'pending' })),
      channel: selectedChannel.value
    })

    console.log('Channels Store After: ', store.state.channels)
  } catch (error) {
    // TODO: Use inferred error type instead of typecasting
    errors.value = (error as BackendError).backendErrors
    console.log('Error:', error)
  }

  isLoading.value = false

  console.log('...finished publish')
}

defineExpose({
  publish
})

// Api Calls

// Create a new channel
/*
"/api/v2/channels/": {
        parameters: {
            query?: never;
            header?: never;
            path?: never;
            cookie?: never;
        };
        get: operations["get_channels_2"]; //!
        put?: never;
        post: operations["create_channel_2"]; //!
        delete?: never;
        options?: never;
        head?: never;
        patch?: never;
        trace?: never;
    };

create_channel_2: {
        parameters: {
            query?: never;
            header?: never;
            path?: never;
            cookie?: never;
        };
        requestBody: {
            content: {
                "application/json": components["schemas"]["ChannelCreateRequest"];                      !
                "application/x-www-form-urlencoded": components["schemas"]["ChannelCreateRequest"];
                "multipart/form-data": components["schemas"]["ChannelCreateRequest"];
                "application/activity+json": components["schemas"]["ChannelCreateRequest"];
            };
        };
        responses: {
            201: {
                headers: {
                    [name: string]: unknown;
                };
                content: {
                    "application/json": components["schemas"]["ChannelCreate"];
                };
            };
        };
    };

ChannelCreateRequest: {
            cover?: string | null;
            name: string;                     !!!
            username: string;                     !!!
            description: components["schemas"]["ContentRequest"] | null;      null
            tags: string[];         []
            content_category: components["schemas"]["ContentCategoryEnum"];         'music'
            metadata?: {       //undefined
                [key: string]: unknown;
            };
        };
*/
</script>

<template>
  <Layout
    form
    gap-8
    :class="['ui', { loading: isLoading }, 'form component-file-upload']"
    @submit.stop.prevent
  >
    <!-- Error message -->
    <Alert
      v-if="errors.length > 0"
      red
    >
      <h4 class="header">
        {{ t('components.channels.UploadForm.header.error') }}
      </h4>
      <ul class="list">
        <li
          v-for="(error, key) in errors"
          :key="key"
        >
          {{ error }}
        </li>
      </ul>
    </Alert>
    <!-- Select Album and License -->

    <div :class="['ui', 'required', 'field']">
      <label
        v-if="availableChannels.length === 1"
        for="channel-dropdown"
      >
        {{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
      </label>
      <label
        v-else
        for="channel-dropdown"
      >
        {{ t('components.channels.UploadForm.label.channel') }}
      </label>
      <select
        v-if="availableChannels.length > 1"
        id="channel-dropdown"
        v-model="channelDropdownId"
        class="dropdown"
      >
        <option
          v-for="channel in availableChannels"
          :value="channel.artist.id"
        >
          {{ channel.artist.name }}
        </option>
      </select>
    </div>
    <album-select
      v-if="selectedChannel"
      v-model="albumSelection"
      :class="['ui', 'field']"
    />
    <div>
      <license-select
        v-model="values.license"
        :class="['ui', 'field']"
      />
      <div class="content">
        <p>
          <i class="copyright icon" />
          {{ t('components.channels.UploadForm.help.license') }}
        </p>
      </div>
    </div>

    <!-- Files to upload -->
    <template v-if="remainingSpace === 0">
      <Alert
        red
      >
        <i class="bi bi-exclamation-triangle" />
        {{ t('components.channels.UploadForm.warning.quota') }}
      </Alert>
    </template>
    <template v-else>
      <Alert
        v-if="draftUploads?.length > 0 && includeDraftUploads === undefined"
        blue
      >
        <p>
          <i class="bi bi-circle-clockwise" />
          {{ t('components.channels.UploadForm.message.pending') }}
        </p>
        <Button
          @click.stop.prevent="includeDraftUploads = false"
        >
          {{ t('components.channels.UploadForm.button.ignore') }}
        </Button>
        <Button
          @click.stop.prevent="includeDraftUploads = true"
        >
          {{ t('components.channels.UploadForm.button.resume') }}
        </Button>
      </Alert>
      <Alert
        v-if="uploadedFiles.length > 0"
        v-bind="{[ uploadedFiles.some(file=>file.error) ? 'red' : 'green' ]:true}"
      >
        <div
          v-for="file in uploadedFiles"
          :key="file.id"
          class="channel-file"
        >
          <div class="content">
            <Button
              v-if="file.response?.uuid"
              icon="bi-pencil-fill"
              class="ui basic icon button"
              :title="labels.editTitle"
              @click.stop.prevent="selectedUploadId = file.response?.uuid"
            />
            <div
              v-if="file.error"
              class="ui basic danger icon label"
              :title="file.error.toString()"
              @click.stop.prevent="selectedUploadId = file.response?.uuid"
            >
              <i class="bi bi-exclamation-triangle-fill" />
            </div>
            <Loader v-else-if="file.active && !file.response" />
          </div>
          <h4 class="ui header">
            <template v-if="file.metadata.title">
              {{ file.metadata.title }}
            </template>
            <template v-else>
              {{ file.name }}
            </template>
            <div class="sub header">
              <template v-if="file.response?.uuid">
                {{ humanSize(file.size ?? 0) }}
                <template v-if="file.response.duration">
                  <span class="middle middledot symbol" />
                  <human-duration :duration="file.response.duration" />
                </template>
              </template>
              <template v-else>
                <span v-if="file.active">
                  {{ t('components.channels.UploadForm.status.uploading') }}
                </span>
                <span v-else-if="file.error">
                  {{ t('components.channels.UploadForm.status.errored') }}
                </span>
                <span v-else>
                  {{ t('components.channels.UploadForm.status.pending') }}
                </span>
                <span class="middle middledot symbol" />
                {{ humanSize(file.size ?? 0) }}
                <span class="middle middledot symbol" />
                {{ parseFloat(file.progress ?? '0') }}
                <span class="percent symbol" />
              </template>
              <span class="middle middledot symbol" />
              <a @click.stop.prevent="remove(file)">
                {{ t('components.channels.UploadForm.button.remove') }}
              </a>
              <template v-if="file.error">
                <span class="middle middledot symbol" />
                <a @click.stop.prevent="retry(file)">
                  {{ t('components.channels.UploadForm.button.retry') }}
                </a>
              </template>
            </div>
          </h4>
        </div>
      </Alert>
    </template>
    <upload-metadata-form
      v-if="selectedUpload"
      v-model:values="uploadImportData[selectedUploadId]"
      :upload="selectedUpload"
    />
    <Alert
      blue
      class="ui message"
    >
      <Layout
        flex
        gap-8
      >
        <i class="bi bi-info-circle-fill" />
        {{ t('components.channels.UploadForm.description.extensions', {extensions: store.state.ui.supportedExtensions.join(', ')}) }}
      </Layout>
    </Alert>
    <file-upload-widget
      ref="upload"
      v-model="files"
      :class="['ui', 'button', 'channels']"
      :data="baseImportMetadata"
      @input-file="beforeFileUpload"
    >
      <div>
        <i class="bi bi-upload" />&nbsp;
        {{ t('components.channels.UploadForm.message.dragAndDrop') }}
      </div>
      <div class="ui very small divider" />
      <Button
        primary
        icon="bi-folder2-open"
      >
        {{ t('components.channels.UploadForm.label.openBrowser') }}
      </Button>
      <Spacer
        class="divider"
        :size="32"
      />
    </file-upload-widget>
  </Layout>
</template>
