import { type Job } from 'bullmq'
import { prisma } from '@/lib/prisma'
import { LOCATION_IMAGE_RATIO, PROP_IMAGE_RATIO } from '@/lib/constants'
import { type TaskJobData } from '@/lib/task/types'
import {
  assertTaskActive,
  getUserModels,
  resolveImageSourceFromGeneration,
  toSignedUrlIfCos,
  uploadImageSourceToCos,
} from '../utils'
import {
  normalizeReferenceImagesForGeneration,
} from '@/lib/media/outbound-image'
import {
  type LocationAvailableSlot,
  stringifyLocationAvailableSlots,
} from '@/lib/location-available-slots'
import {
  AnyObj,
  parseImageUrls,
} from './image-task-handler-shared'
import { encodeImageUrls } from '@/lib/contracts/image-urls-contract'
import { PRIMARY_APPEARANCE_INDEX } from '@/lib/constants'
import { createScopedLogger } from '@/lib/logging/core'
import {
  buildCharacterDescriptionFields,
  generateModifiedAssetDescription,
  readIndexedDescription,
} from './modify-description-sync'

const logger = createScopedLogger({ module: 'worker.asset-hub-modify' })

interface GlobalCharacterAppearanceRecord {
  id: string
  appearanceIndex: number
  changeReason: string | null
  description: string | null
  descriptions: string | null
  imageUrl: string | null
  imageUrls: string | null
  selectedIndex: number | null
  previousDescription: string | null
  previousDescriptions: string | null
}

interface GlobalCharacterRecord {
  id: string
  name: string
  appearances: GlobalCharacterAppearanceRecord[]
}

interface GlobalLocationImageRecord {
  id: string
  imageIndex: number
  description: string | null
  availableSlots?: string | null
  imageUrl: string | null
  previousDescription: string | null
}

interface GlobalLocationRecord {
  id: string
  name: string
  images: GlobalLocationImageRecord[]
}

interface AssetHubModifyDb {
  globalCharacter: {
    findFirst(args: Record<string, unknown>): Promise<GlobalCharacterRecord | null>
  }
  globalCharacterAppearance: {
    update(args: Record<string, unknown>): Promise<unknown>
  }
  globalLocation: {
    findFirst(args: Record<string, unknown>): Promise<GlobalLocationRecord | null>
  }
  globalLocationImage: {
    update(args: Record<string, unknown>): Promise<unknown>
  }
}

function readModifyInstruction(value: unknown): string {
  return typeof value === 'string' ? value.trim() : ''
}

export async function handleAssetHubModifyTask(job: Job<TaskJobData>) {
  const payload = (job.data.payload || {}) as AnyObj
  const userId = job.data.userId
  const db = prisma as unknown as AssetHubModifyDb
  const userModels = await getUserModels(userId)
  const editModel = userModels.editModel
  if (!editModel) throw new Error('User edit model not configured')

  const generationOptions = payload.generationOptions as Record<string, unknown> | undefined
  const resolution = typeof generationOptions?.resolution === 'string'
    ? generationOptions.resolution
    : undefined
  const modifyInstruction = readModifyInstruction(payload.modifyPrompt)

  if (payload.type === 'character') {
    const character = await db.globalCharacter.findFirst({
      where: { id: payload.id, userId },
      include: { appearances: true },
    })
    if (!character) throw new Error('Global character not found')

    const appearanceIndex = Number(payload.appearanceIndex ?? PRIMARY_APPEARANCE_INDEX)
    const appearance = character.appearances.find((appearanceItem) => appearanceItem.appearanceIndex === appearanceIndex)
    if (!appearance) throw new Error('Global character appearance not found')

    const imageUrls = parseImageUrls(appearance.imageUrls, 'globalCharacterAppearance.imageUrls')
    const targetImageIndex = Number(payload.imageIndex ?? appearance.selectedIndex ?? 0)
    const currentKey = imageUrls[targetImageIndex] || appearance.imageUrl
    const currentUrl = toSignedUrlIfCos(currentKey, 3600)
    if (!currentUrl) throw new Error('No global character image to modify')

    const extraReferenceInputs: string[] = []
    if (Array.isArray(payload.extraImageUrls)) {
      for (const url of payload.extraImageUrls) {
        if (typeof url === 'string' && url.trim().length > 0) {
          extraReferenceInputs.push(url.trim())
        }
      }
    }
    const normalizedExtras = await normalizeReferenceImagesForGeneration(extraReferenceInputs)
    const referenceImages = Array.from(new Set([currentUrl, ...normalizedExtras]))
    const currentDescription = readIndexedDescription({
      descriptions: appearance.descriptions,
      fallbackDescription: appearance.description,
      index: targetImageIndex,
    })

    const prompt = `请根据以下指令修改图片，保持人物核心特征一致：\n${modifyInstruction}`
    const source = await resolveImageSourceFromGeneration(job, {
      userId,
      modelId: editModel,
      prompt,
      options: {
        referenceImages,
        aspectRatio: '3:2',
        ...(resolution ? { resolution } : {}),
      },
    })

    const imageKey = await uploadImageSourceToCos(source, 'global-character-modify', appearance.id)

    while (imageUrls.length <= targetImageIndex) imageUrls.push('')
    imageUrls[targetImageIndex] = imageKey

    const selectedIndex = appearance.selectedIndex
    const shouldUpdateMain = selectedIndex === targetImageIndex || selectedIndex === null || imageUrls.length === 1

    let descriptionFields: { description: string; descriptions: string } | null = null
    if (currentDescription && modifyInstruction && userModels.analysisModel) {
      try {
        const nextDescription = await generateModifiedAssetDescription({
          userId,
          model: userModels.analysisModel,
          locale: job.data.locale,
          type: 'character',
          currentDescription,
          modifyInstruction,
          referenceImages: normalizedExtras,
        })
        descriptionFields = buildCharacterDescriptionFields({
          descriptions: appearance.descriptions,
          fallbackDescription: appearance.description,
          index: targetImageIndex,
          nextDescription: nextDescription.prompt,
        })
      } catch (err) {
        logger.warn({ message: '资产库角色描述同步失败', details: { error: String(err) } })
      }
    }

    await assertTaskActive(job, 'persist_global_character_modify')
    await db.globalCharacterAppearance.update({
      where: { id: appearance.id },
      data: {
        previousImageUrl: appearance.imageUrl || null,
        previousImageUrls: appearance.imageUrls,
        previousDescription: appearance.description || null,
        previousDescriptions: appearance.descriptions ?? null,
        imageUrls: encodeImageUrls(imageUrls),
        imageUrl: shouldUpdateMain ? imageKey : appearance.imageUrl,
        ...(descriptionFields || {}),
      },
    })

    return { type: payload.type, appearanceId: appearance.id, imageUrl: imageKey }
  }

  if (payload.type === 'location' || payload.type === 'prop') {
    const location = await db.globalLocation.findFirst({
      where: { id: payload.id, userId },
      include: { images: true },
    })
    if (!location) throw new Error('Global location not found')

    const targetImageIndex = Number(payload.imageIndex ?? 0)
    const locationImage = location.images.find((imageItem) => imageItem.imageIndex === targetImageIndex)
    if (!locationImage?.imageUrl) throw new Error('Global location image not found')

    const currentUrl = toSignedUrlIfCos(locationImage.imageUrl, 3600)
    if (!currentUrl) throw new Error('No global location image to modify')

    const extraReferenceInputs: string[] = []
    if (Array.isArray(payload.extraImageUrls)) {
      for (const url of payload.extraImageUrls) {
        if (typeof url === 'string' && url.trim().length > 0) {
          extraReferenceInputs.push(url.trim())
        }
      }
    }
    const normalizedExtras = await normalizeReferenceImagesForGeneration(extraReferenceInputs)
    const referenceImages = Array.from(new Set([currentUrl, ...normalizedExtras]))

    const isProp = payload.type === 'prop'
    const prompt = isProp
      ? `请根据以下指令修改道具图片，保持道具主体、结构和关键材质一致：\n${modifyInstruction}`
      : `请根据以下指令修改场景图片，保持整体风格一致：\n${modifyInstruction}`
    const aspectRatio = isProp ? PROP_IMAGE_RATIO : LOCATION_IMAGE_RATIO
    const source = await resolveImageSourceFromGeneration(job, {
      userId,
      modelId: editModel,
      prompt,
      options: {
        referenceImages,
        aspectRatio,
        ...(resolution ? { resolution } : {}),
      },
    })

    const imageKey = await uploadImageSourceToCos(source, isProp ? 'global-prop-modify' : 'global-location-modify', locationImage.id)

    let extractedDescription: {
      prompt: string
      availableSlots: LocationAvailableSlot[]
    } | null = null
    if (locationImage.description && modifyInstruction && userModels.analysisModel) {
      try {
        extractedDescription = await generateModifiedAssetDescription({
          userId,
          model: userModels.analysisModel,
          locale: job.data.locale,
          type: isProp ? 'prop' : 'location',
          currentDescription: locationImage.description,
          modifyInstruction,
          referenceImages: normalizedExtras,
          locationName: location.name,
          propName: isProp ? location.name : undefined,
        })
      } catch (err) {
        logger.warn({ message: isProp ? '资产库道具描述同步失败' : '资产库场景描述同步失败', details: { error: String(err) } })
      }
    }

    await assertTaskActive(job, isProp ? 'persist_global_prop_modify' : 'persist_global_location_modify')
    await db.globalLocationImage.update({
      where: { id: locationImage.id },
      data: {
        previousImageUrl: locationImage.imageUrl,
        previousDescription: locationImage.description || null,
        imageUrl: imageKey,
        ...(extractedDescription ? {
          description: extractedDescription.prompt,
          availableSlots: stringifyLocationAvailableSlots(extractedDescription.availableSlots),
        } : {}),
      },
    })

    return { type: payload.type, locationImageId: locationImage.id, imageUrl: imageKey }
  }

  throw new Error(`Unsupported asset-hub modify type: ${String(payload.type)}`)
}
