import { capitalize } from 'es-toolkit'
import { computed, ref, shallowRef, toValue, watch } from 'vue'
import type { MaybeRefOrGetter, Ref } from 'vue'

import { t } from '@/i18n'
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
import {
  filterItemByBaseModels,
  filterItemByOwnership
} from '@/platform/assets/utils/assetFilterUtils'
import {
  getAssetBaseModels,
  getAssetDisplayFilename,
  getAssetDisplayName,
  getAssetFilename,
  getAssetUrlFilename
} from '@/platform/assets/utils/assetMetadataUtils'
import type {
  FilterOption,
  OwnershipOption
} from '@/platform/assets/types/filterTypes'
import type { FormDropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import type { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { resolveOutputAssetItems } from '@/platform/assets/utils/outputAssetUtil'
import type { IAssetsProvider } from '@/platform/assets/composables/media/IAssetsProvider'
import type { AssetKind } from '@/types/widgetTypes'
import { getMediaTypeFromFilename } from '@/utils/formatUtil'

function getDisplayLabel(
  value: string,
  getOptionLabel?: ((value?: string | null) => string) | undefined
): string {
  if (!getOptionLabel) return value

  try {
    return getOptionLabel(value) || value
  } catch (e) {
    console.warn('Failed to map value:', e)
    return value
  }
}

function assetKindToMediaType(kind: AssetKind): string {
  return kind === 'mesh' ? '3D' : kind
}

function getMediaUrl(
  filename: string,
  type: 'input' | 'output',
  assetKind: AssetKind | undefined
): string {
  if (!['image', 'video', 'audio'].includes(assetKind ?? '')) return ''
  const params = new URLSearchParams({ filename, type })
  appendCloudResParam(params, filename)
  return `/api/view?${params}`
}

interface UseWidgetSelectItemsOptions {
  values: MaybeRefOrGetter<unknown[] | undefined>
  getOptionLabel: MaybeRefOrGetter<
    ((value?: string | null) => string) | undefined
  >
  modelValue: Ref<string | undefined>
  assetKind: MaybeRefOrGetter<AssetKind | undefined>
  outputMediaAssets: IAssetsProvider
  assetData: ReturnType<typeof useAssetWidgetData> | null
  isAssetMode: MaybeRefOrGetter<boolean | undefined>
}

export function useWidgetSelectItems(options: UseWidgetSelectItemsOptions) {
  const { modelValue, outputMediaAssets, assetData } = options

  const missingMediaStore = useMissingMediaStore()
  const missingMediaValues = computed<ReadonlySet<string>>(
    () =>
      new Set(
        missingMediaStore.missingMediaCandidates?.map((c) => c.name) ?? []
      )
  )

  const filterSelected = ref('all')
  const filterOptions = computed<FilterOption[]>(() => {
    const isAsset = toValue(options.isAssetMode)
    if (isAsset) {
      const categoryName = assetData?.category.value ?? 'All'
      return [{ name: capitalize(categoryName), value: 'all' }]
    }
    return [
      { name: t('g.all'), value: 'all' },
      { name: t('sideToolbar.labels.imported'), value: 'inputs' },
      { name: t('sideToolbar.labels.generated'), value: 'outputs' }
    ]
  })

  const ownershipSelected = ref<OwnershipOption>('all')
  const showOwnershipFilter = computed(() => !!toValue(options.isAssetMode))

  const { ownershipOptions, availableBaseModels } = useAssetFilterOptions(
    () => assetData?.assets.value ?? []
  )

  const baseModelSelected = ref<Set<string>>(new Set())
  const showBaseModelFilter = computed(() => !!toValue(options.isAssetMode))
  const baseModelOptions = computed<FilterOption[]>(() => {
    if (!toValue(options.isAssetMode) || !assetData) return []
    return availableBaseModels.value
  })

  const resolvedByJobId = shallowRef(new Map<string, AssetItem[]>())

  watch(
    () => outputMediaAssets.media.value,
    (assets, _, onCleanup) => {
      let cancelled = false
      onCleanup(() => {
        cancelled = true
      })

      const seenJobIds = new Set<string>()
      const jobsToResolve: Array<{
        jobId: string
        meta: ReturnType<typeof getOutputAssetMetadata>
        createdAt?: string
      }> = []

      for (const asset of assets) {
        // Hash-keyed assets are leaf rows from the cloud `/assets` API and
        // already carry their own URL-resolvable filename. Expanding them via
        // resolveOutputAssetItems would synthesize sibling AssetItems without
        // a hash and reintroduce the FE-227 hash→name fallback bug.
        if (asset.hash) continue

        const meta = getOutputAssetMetadata(asset.user_metadata)
        if (!meta) continue

        const outputCount = meta.outputCount ?? meta.allOutputs?.length ?? 0
        if (
          outputCount <= 1 ||
          resolvedByJobId.value.has(meta.jobId) ||
          seenJobIds.has(meta.jobId)
        )
          continue

        seenJobIds.add(meta.jobId)
        jobsToResolve.push({
          jobId: meta.jobId,
          meta,
          createdAt: asset.created_at
        })
      }

      if (jobsToResolve.length === 0) return

      void Promise.all(
        jobsToResolve.map(({ jobId, meta, createdAt }) =>
          resolveOutputAssetItems(meta!, { createdAt })
            .then((resolved) => ({ jobId, resolved }))
            .catch((error) => {
              console.warn('Failed to resolve multi-output job', jobId, error)
              return { jobId, resolved: [] as AssetItem[] }
            })
        )
      ).then((results) => {
        if (cancelled) return

        const next = new Map(resolvedByJobId.value)
        let changed = false
        for (const { jobId, resolved } of results) {
          if (!resolved.length) continue
          next.set(jobId, resolved)
          changed = true
        }
        if (changed) resolvedByJobId.value = next
      })
    },
    { immediate: true }
  )

  const inputItems = computed<FormDropdownItem[]>(() => {
    const values = toValue(options.values) || []
    if (!Array.isArray(values)) return []

    const labelFn = toValue(options.getOptionLabel)
    const kind = toValue(options.assetKind)
    const missing = missingMediaValues.value
    return values
      .filter((value) => !missing.has(String(value)))
      .map((value, index) => ({
        id: `input-${index}`,
        preview_url: getMediaUrl(String(value), 'input', kind),
        name: String(value),
        label: getDisplayLabel(String(value), labelFn)
      }))
  })

  const outputItems = computed<FormDropdownItem[]>(() => {
    const kind = toValue(options.assetKind)
    if (!['image', 'video', 'audio', 'mesh'].includes(kind ?? '')) return []

    const targetMediaType = assetKindToMediaType(kind!)
    const seen = new Set<string>()
    const items: FormDropdownItem[] = []
    const labelFn = toValue(options.getOptionLabel)

    const assets = outputMediaAssets.media.value.flatMap((asset) => {
      const meta = getOutputAssetMetadata(asset.user_metadata)
      const resolved = meta ? resolvedByJobId.value.get(meta.jobId) : undefined
      return resolved ?? [asset]
    })

    const missing = missingMediaValues.value
    for (const asset of assets) {
      if (getMediaTypeFromFilename(asset.name) !== targetMediaType) continue
      if (seen.has(asset.id)) continue
      seen.add(asset.id)
      const filenameForUrl = getAssetUrlFilename(asset)
      const subfolder =
        kind === 'mesh'
          ? getOutputAssetMetadata(asset.user_metadata)?.subfolder
          : undefined
      const pathWithSubfolder = subfolder
        ? `${subfolder}/${filenameForUrl}`
        : filenameForUrl
      const annotatedPath = `${pathWithSubfolder} [output]`
      if (missing.has(annotatedPath)) continue
      const displayLabel = `${getAssetDisplayFilename(asset)} [output]`
      items.push({
        id: `output-${asset.id}`,
        preview_url:
          kind === 'mesh'
            ? ''
            : asset.preview_url || getMediaUrl(filenameForUrl, 'output', kind),
        name: annotatedPath,
        label: getDisplayLabel(displayLabel, labelFn)
      })
    }

    return items
  })

  const missingValueItem = computed<FormDropdownItem | undefined>(() => {
    const currentValue = modelValue.value
    if (!currentValue) return undefined
    const labelFn = toValue(options.getOptionLabel)
    const kind = toValue(options.assetKind)

    if (missingMediaValues.value.has(currentValue)) return undefined

    if (toValue(options.isAssetMode) && assetData) {
      const existsInAssets = assetData.assets.value.some(
        (asset) => getAssetFilename(asset) === currentValue
      )
      if (existsInAssets) return undefined

      return {
        id: `missing-${currentValue}`,
        preview_url: '',
        name: currentValue,
        label: getDisplayLabel(currentValue, labelFn)
      }
    }

    const existsInInputs = inputItems.value.some(
      (item) => item.name === currentValue
    )
    const existsInOutputs = outputItems.value.some(
      (item) => item.name === currentValue
    )

    if (existsInInputs || existsInOutputs) return undefined

    const isOutput = currentValue.endsWith(' [output]')
    const strippedValue = isOutput
      ? currentValue.replace(' [output]', '')
      : currentValue

    return {
      id: `missing-${currentValue}`,
      preview_url: getMediaUrl(
        strippedValue,
        isOutput ? 'output' : 'input',
        kind
      ),
      name: currentValue,
      label: getDisplayLabel(currentValue, labelFn)
    }
  })

  const assetItems = computed<FormDropdownItem[]>(() => {
    if (!toValue(options.isAssetMode) || !assetData) return []
    return assetData.assets.value.map((asset) => ({
      id: asset.id,
      name: getAssetFilename(asset),
      label: getAssetDisplayName(asset),
      preview_url: asset.preview_url,
      is_immutable: asset.is_immutable,
      base_models: getAssetBaseModels(asset)
    }))
  })

  const filteredAssetItems = computed<FormDropdownItem[]>(() =>
    filterItemByBaseModels(
      filterItemByOwnership(assetItems.value, ownershipSelected.value),
      baseModelSelected.value
    )
  )

  const allItems = computed<FormDropdownItem[]>(() => {
    if (toValue(options.isAssetMode) && assetData) {
      return filteredAssetItems.value
    }
    return [
      ...(missingValueItem.value ? [missingValueItem.value] : []),
      ...inputItems.value,
      ...outputItems.value
    ]
  })

  const dropdownItems = computed<FormDropdownItem[]>(() => {
    if (toValue(options.isAssetMode)) {
      return allItems.value
    }

    switch (filterSelected.value) {
      case 'inputs':
        return inputItems.value
      case 'outputs':
        return outputItems.value
      case 'all':
      default:
        return allItems.value
    }
  })

  const displayItems = computed<FormDropdownItem[]>(() => {
    if (toValue(options.isAssetMode) && assetData && missingValueItem.value) {
      return [missingValueItem.value, ...filteredAssetItems.value]
    }
    return dropdownItems.value
  })

  const selectedSet = computed<Set<string>>(() => {
    const currentValue = modelValue.value
    if (currentValue === undefined) return new Set()

    const item = displayItems.value.find((item) => item.name === currentValue)
    return item ? new Set([item.id]) : new Set()
  })

  return {
    dropdownItems,
    displayItems,
    filterSelected,
    filterOptions,
    ownershipSelected,
    showOwnershipFilter,
    ownershipOptions,
    baseModelSelected,
    showBaseModelFilter,
    baseModelOptions,
    selectedSet
  }
}
