import 'reflect-metadata'

import { randomUUID } from 'crypto'
import { Brackets, type Repository } from 'typeorm'
import { BrowserWindow } from 'electron'

import {
  StudentProfileEntitySchema,
  type StudentProfileEntity
} from '../entities/StudentProfileEntity'
import { ClassEntitySchema, type ClassEntity } from '../entities/ClassEntity'
import type {
  ChildProfile,
  ClassProfile,
  CommentGenerationProgress,
  GenerateStudentCommentsInput,
  ListStudentProfilesInput,
  LoadStudentDataInput
} from '../types/student'
import type { ClassTypeConfig } from '../types/settings'
import { getAppDataSource } from './databaseService'
import { isDataUrlImage, readImageAsDataUrl, storeImageValue } from './imageStorageService'
import { getChatCompletionsUrl, logLargeModelRequest } from './modelService'
import { DEFAULT_CLASS_TYPE_CONFIGS, loadSettings } from './settingsService'

type CommentGenerationController = {
  batchId: string
  paused: boolean
  stopped: boolean
  lastProgress: CommentGenerationProgress
  resumeWaiters: Array<() => void>
}

const commentGenerationControllers = new Map<string, CommentGenerationController>()
const CHINESE_ZODIAC_BY_BRANCH: Record<string, string> = {
  子: '鼠',
  丑: '牛',
  寅: '虎',
  卯: '兔',
  辰: '龙',
  巳: '蛇',
  午: '马',
  未: '羊',
  申: '猴',
  酉: '鸡',
  戌: '狗',
  亥: '猪'
}
const GREGORIAN_ZODIACS = ['猴', '鸡', '狗', '猪', '鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊']
const chineseYearFormatter = new Intl.DateTimeFormat('zh-CN-u-ca-chinese', {
  year: 'numeric',
  timeZone: 'UTC'
})

function parseJsonList(value: string): string[] {
  try {
    const parsed = JSON.parse(value)
    return Array.isArray(parsed) ? parsed : []
  } catch {
    return []
  }
}

function stringifyList(items: string[]): string {
  return JSON.stringify(items)
}

function normalizeClassType(type: string): string {
  return type.trim() || 'cheap'
}

function createUtcDate(year: number, month: number, day: number): Date | null {
  const date = new Date(Date.UTC(year, month - 1, day))

  if (
    date.getUTCFullYear() !== year ||
    date.getUTCMonth() !== month - 1 ||
    date.getUTCDate() !== day
  ) {
    return null
  }

  return date
}

function parseBirthdayDate(value: string): Date | null {
  const trimmedValue = value.trim()

  if (!trimmedValue) {
    return null
  }

  if (/^\d{5}(?:\.\d+)?$/.test(trimmedValue)) {
    const excelSerialDate = Number(trimmedValue)

    if (Number.isFinite(excelSerialDate)) {
      const excelEpoch = Date.UTC(1899, 11, 30)
      return new Date(excelEpoch + Math.floor(excelSerialDate) * 24 * 60 * 60 * 1000)
    }
  }

  const compactMatch = trimmedValue.match(/^(\d{4})(\d{2})(\d{2})$/)

  if (compactMatch) {
    return createUtcDate(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3]))
  }

  const datePartsMatch = trimmedValue.match(/^(\d{4})\D+(\d{1,2})\D+(\d{1,2})/)

  if (datePartsMatch) {
    return createUtcDate(
      Number(datePartsMatch[1]),
      Number(datePartsMatch[2]),
      Number(datePartsMatch[3])
    )
  }

  const parsedTime = Date.parse(trimmedValue)

  if (!Number.isNaN(parsedTime)) {
    const parsedDate = new Date(parsedTime)
    return createUtcDate(parsedDate.getFullYear(), parsedDate.getMonth() + 1, parsedDate.getDate())
  }

  return null
}

function getChineseZodiac(date: Date): string {
  const chineseYear = chineseYearFormatter.format(date)
  const branch = chineseYear.match(/[子丑寅卯辰巳午未申酉戌亥]/)?.[0]

  if (branch && CHINESE_ZODIAC_BY_BRANCH[branch]) {
    return CHINESE_ZODIAC_BY_BRANCH[branch]
  }

  return GREGORIAN_ZODIACS[date.getUTCFullYear() % 12]
}

function normalizeProfileMatchValue(value: string): string {
  return String(value ?? '')
    .trim()
    .replace(/\s+/g, '')
    .toLowerCase()
}

function getProfileMatchKeys(
  profile: Pick<ChildProfile, 'id' | 'name' | 'englishName' | 'birthday'>
): string[] {
  const keys = new Set<string>()
  const id = normalizeProfileMatchValue(profile.id)
  const name = normalizeProfileMatchValue(profile.name)
  const englishName = normalizeProfileMatchValue(profile.englishName)
  const birthday = normalizeProfileMatchValue(profile.birthday)

  if (id) {
    keys.add(`id:${id}`)
  }

  if (name && birthday) {
    keys.add(`name-birthday:${name}|${birthday}`)
  }

  if (englishName && birthday) {
    keys.add(`english-birthday:${englishName}|${birthday}`)
  }

  if (name) {
    keys.add(`name:${name}`)
  }

  if (englishName) {
    keys.add(`english:${englishName}`)
  }

  return [...keys]
}

function mergeImportedProfileIntoExisting(
  existingProfile: StudentProfileEntity,
  importedProfile: ChildProfile,
  classItem: ClassProfile
): ChildProfile {
  return {
    id: existingProfile.id,
    classId: classItem.id,
    className: classItem.name,
    name: importedProfile.name,
    englishName: importedProfile.englishName,
    gender: importedProfile.gender,
    birthday: importedProfile.birthday,
    zodiac: importedProfile.zodiac,
    friends: importedProfile.friends,
    hobbies: importedProfile.hobbies,
    favoriteGames: importedProfile.favoriteGames,
    favoriteFoods: importedProfile.favoriteFoods,
    traits: importedProfile.traits,
    comment: existingProfile.comment ?? '',
    commentGeneratedAt: existingProfile.commentGeneratedAt ?? '',
    reportGenerated: Boolean(existingProfile.reportGenerated),
    meImage: existingProfile.meImage ?? '',
    workImage1: existingProfile.workImage1 ?? '',
    workImage2: existingProfile.workImage2 ?? '',
    importedAt: importedProfile.importedAt
  }
}

function parseTeacherNames(value: string): string[] {
  try {
    const parsed = JSON.parse(value)

    if (Array.isArray(parsed)) {
      return parsed
        .filter((item): item is string => typeof item === 'string')
        .map((item) => item.trim())
        .filter(Boolean)
    }
  } catch {
    // Older data may contain a single teacher name instead of a JSON array.
  }

  return value.trim() ? [value.trim()] : []
}

function stringifyTeacherNames(teacherNames: string[]): string {
  return JSON.stringify(
    teacherNames.map((teacherName) => teacherName.trim()).filter((teacherName) => teacherName)
  )
}

async function normalizeStoredClassImages(entity: ClassEntity): Promise<ClassEntity> {
  if (!isDataUrlImage(entity.familyPhoto)) {
    return entity
  }

  return {
    ...entity,
    familyPhoto: await storeImageValue('classes', entity.id, 'familyPhoto', entity.familyPhoto)
  }
}

async function hydrateClassImages(entity: ClassEntity): Promise<ClassEntity> {
  if (!entity.familyPhoto) {
    return entity
  }

  return {
    ...entity,
    familyPhoto: await readImageAsDataUrl(entity.familyPhoto)
  }
}

async function normalizeStoredStudentImages(
  entity: StudentProfileEntity
): Promise<StudentProfileEntity> {
  const [meImage, workImage1, workImage2] = await Promise.all([
    storeImageValue('students', entity.id, 'meImage', entity.meImage),
    storeImageValue('students', entity.id, 'workImage1', entity.workImage1),
    storeImageValue('students', entity.id, 'workImage2', entity.workImage2)
  ])

  return {
    ...entity,
    meImage,
    workImage1,
    workImage2
  }
}

async function hydrateStudentImages(entity: StudentProfileEntity): Promise<StudentProfileEntity> {
  const [meImage, workImage1, workImage2] = await Promise.all([
    readImageAsDataUrl(entity.meImage),
    readImageAsDataUrl(entity.workImage1),
    readImageAsDataUrl(entity.workImage2)
  ])

  return {
    ...entity,
    meImage,
    workImage1,
    workImage2
  }
}

function mapClassEntity(entity: ClassEntity): ClassProfile {
  return {
    id: entity.id,
    name: entity.name,
    type: normalizeClassType(entity.type),
    teacherNames: parseTeacherNames(entity.teacherNames || entity.teacherName || ''),
    familyPhoto: entity.familyPhoto || undefined
  }
}

function mapChildEntity(
  entity: StudentProfileEntity,
  options: { includeImages?: boolean } = {}
): ChildProfile {
  return {
    id: entity.id,
    classId: entity.classId,
    className: entity.className,
    name: entity.name,
    englishName: entity.englishName,
    gender: entity.gender,
    birthday: entity.birthday,
    zodiac: entity.zodiac,
    friends: parseJsonList(entity.friends),
    hobbies: parseJsonList(entity.hobbies),
    favoriteGames: parseJsonList(entity.favoriteGames),
    favoriteFoods: parseJsonList(entity.favoriteFoods),
    traits: entity.traits,
    comment: entity.comment ?? '',
    commentGeneratedAt: entity.commentGeneratedAt ?? '',
    reportGenerated: Boolean(entity.reportGenerated),
    meImage: options.includeImages === false ? '' : (entity.meImage ?? ''),
    workImage1: options.includeImages === false ? '' : (entity.workImage1 ?? ''),
    workImage2: options.includeImages === false ? '' : (entity.workImage2 ?? ''),
    importedAt: entity.importedAt
  }
}

async function mapClassProfile(profile: ClassProfile): Promise<ClassEntity> {
  return {
    id: profile.id,
    name: profile.name,
    type: profile.type,
    teacherName: (profile.teacherNames ?? []).join(' ') || null,
    teacherNames: stringifyTeacherNames(profile.teacherNames ?? []),
    familyPhoto:
      (await storeImageValue('classes', profile.id, 'familyPhoto', profile.familyPhoto)) || null
  }
}

async function mapChildProfile(
  profile: ChildProfile,
  classItem?: ClassProfile
): Promise<StudentProfileEntity> {
  return normalizeStoredStudentImages({
    id: profile.id,
    classId: classItem?.id ?? profile.classId,
    className: classItem?.name ?? profile.className,
    name: profile.name,
    englishName: profile.englishName,
    gender: profile.gender,
    birthday: profile.birthday,
    zodiac: profile.zodiac,
    friends: stringifyList(profile.friends),
    hobbies: stringifyList(profile.hobbies),
    favoriteGames: stringifyList(profile.favoriteGames),
    favoriteFoods: stringifyList(profile.favoriteFoods),
    traits: profile.traits,
    comment: profile.comment ?? '',
    commentGeneratedAt: profile.commentGeneratedAt ?? '',
    reportGenerated: profile.reportGenerated ?? false,
    meImage: profile.meImage ?? '',
    workImage1: profile.workImage1 ?? '',
    workImage2: profile.workImage2 ?? '',
    importedAt: profile.importedAt
  })
}

function getClassTypeConfig(
  classType: string,
  classTypeConfigs: ClassTypeConfig[]
): ClassTypeConfig {
  return (
    classTypeConfigs.find((config) => config.id === classType) ??
    DEFAULT_CLASS_TYPE_CONFIGS.find((config) => config.id === classType) ?? {
      id: classType,
      label: classType || '便宜班',
      courseContent: ''
    }
  )
}

function getGivenName(name: string): string {
  const trimmedName = name.trim()

  if (trimmedName.length <= 2) {
    return trimmedName || '宝贝'
  }

  return [...trimmedName].slice(1).join('')
}

function buildStudentCommentPrompt(profile: ChildProfile, courseContent: string): string {
  return [
    '请为这名幼儿生成一段学期末成长评语。',
    '严格要求：',
    `1. 第一句必须是“${getGivenName(profile.name || profile.englishName)}宝贝：你好，${courseContent}”。`,
    '2. 正文保持一段完整段落，不要换行。',
    '3. 只能依据下方资料描写表现，不要编造具体课程名称、课堂活动、比赛、绘本、故事创编、阅读课等资料中没有的信息。',
    '4. 语气温暖、具体、积极，适合幼儿园成长报告；结尾委婉提出一个期望并送上祝福。',
    '5. 字数控制在 150 到 250 字。',
    '',
    `姓名：${profile.name || '未填写'}`,
    `英文名：${profile.englishName || '未填写'}`,
    `班级：${profile.className || '未分班'}`,
    `课程内容：${courseContent || '未配置'}`,
    `性别：${profile.gender || '未填写'}`,
    `生日：${profile.birthday || '未填写'}`,
    `属相：${profile.zodiac || '未填写'}`,
    `好朋友：${profile.friends.length > 0 ? profile.friends.join('、') : '未填写'}`,
    `爱好：${profile.hobbies.length > 0 ? profile.hobbies.join('、') : '未填写'}`,
    `喜欢的游戏：${profile.favoriteGames.length > 0 ? profile.favoriteGames.join('、') : '未填写'}`,
    `喜欢吃的食物：${profile.favoriteFoods.length > 0 ? profile.favoriteFoods.join('、') : '未填写'}`,
    `表现特征：${profile.traits || '未填写'}`
  ].join('\n')
}

function ensureCommentIncludesCourseContent(comment: string, courseContent: string): string {
  const trimmedComment = comment.trim()
  const trimmedCourseContent = courseContent.trim()

  if (!trimmedCourseContent || trimmedComment.includes(trimmedCourseContent)) {
    return trimmedComment
  }

  const greetingMatch = trimmedComment.match(/^([^：:]{1,12}[：:])/)

  if (!greetingMatch) {
    return `${trimmedCourseContent}${trimmedComment}`
  }

  return `${greetingMatch[1]}你好，${trimmedCourseContent}${trimmedComment.slice(greetingMatch[0].length)}`
}

function getStudentDisplayName(profile: ChildProfile | StudentProfileEntity): string {
  return profile.name || profile.englishName || '未命名学生'
}

function sendCommentProgress(progress: CommentGenerationProgress): void {
  for (const window of BrowserWindow.getAllWindows()) {
    window.webContents.send('comments:generation-progress', progress)
  }
}

function updateCommentControllerProgress(
  controller: CommentGenerationController,
  progress: CommentGenerationProgress
): void {
  controller.lastProgress = progress
  sendCommentProgress(progress)
}

async function waitForCommentGenerationResume(
  controller: CommentGenerationController
): Promise<void> {
  if (!controller.paused || controller.stopped) {
    return
  }

  updateCommentControllerProgress(controller, {
    ...controller.lastProgress,
    status: 'paused',
    studentId: undefined,
    studentName: undefined
  })

  await new Promise<void>((resolve) => {
    controller.resumeWaiters.push(resolve)
  })
}

export function pauseCommentGeneration(batchId: string): boolean {
  const controller = commentGenerationControllers.get(batchId)

  if (!controller || controller.stopped) {
    return false
  }

  controller.paused = true
  updateCommentControllerProgress(controller, {
    ...controller.lastProgress,
    status: 'paused',
    studentId: undefined,
    studentName: undefined
  })
  return true
}

export function resumeCommentGeneration(batchId: string): boolean {
  const controller = commentGenerationControllers.get(batchId)

  if (!controller || controller.stopped) {
    return false
  }

  controller.paused = false
  const waiters = controller.resumeWaiters.splice(0)

  for (const resolve of waiters) {
    resolve()
  }

  updateCommentControllerProgress(controller, {
    ...controller.lastProgress,
    status: 'started',
    studentId: undefined,
    studentName: undefined
  })
  return true
}

export function stopCommentGeneration(batchId: string): boolean {
  const controller = commentGenerationControllers.get(batchId)

  if (!controller) {
    return false
  }

  controller.stopped = true
  controller.paused = false
  const waiters = controller.resumeWaiters.splice(0)

  for (const resolve of waiters) {
    resolve()
  }

  updateCommentControllerProgress(controller, {
    ...controller.lastProgress,
    status: 'stopped',
    studentId: undefined,
    studentName: undefined
  })
  return true
}

async function getRepositories(): Promise<{
  classRepository: Repository<ClassEntity>
  childRepository: Repository<StudentProfileEntity>
}> {
  const source = await getAppDataSource()

  return {
    classRepository: source.getRepository<ClassEntity>(ClassEntitySchema),
    childRepository: source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  }
}

export async function loadStudentData(input: LoadStudentDataInput = {}): Promise<{
  classes: ClassProfile[]
  profiles: ChildProfile[]
}> {
  const includeProfiles = input.includeProfiles ?? true
  const includeImages = input.includeImages ?? true
  const includeClassImages = input.includeClassImages ?? includeImages
  const { classRepository, childRepository } = await getRepositories()
  const classes = await classRepository.find({
    order: {
      id: 'ASC'
    }
  })
  const profiles = includeProfiles
    ? await childRepository.find({
        order: {
          id: 'ASC'
        }
      })
    : []

  const normalizedClasses = await Promise.all(classes.map(normalizeStoredClassImages))
  const normalizedProfiles = includeImages
    ? await Promise.all(profiles.map(normalizeStoredStudentImages))
    : profiles
  const hydratedProfiles = includeImages
    ? await Promise.all(normalizedProfiles.map(hydrateStudentImages))
    : normalizedProfiles

  for (const normalizedClass of normalizedClasses) {
    const originalClass = classes.find((classItem) => classItem.id === normalizedClass.id)

    if (originalClass && normalizedClass.familyPhoto !== originalClass.familyPhoto) {
      await classRepository.save(normalizedClass)
    }
  }

  if (includeImages) {
    for (const normalizedProfile of normalizedProfiles) {
      const originalProfile = profiles.find((profile) => profile.id === normalizedProfile.id)

      if (
        originalProfile &&
        (normalizedProfile.meImage !== originalProfile.meImage ||
          normalizedProfile.workImage1 !== originalProfile.workImage1 ||
          normalizedProfile.workImage2 !== originalProfile.workImage2)
      ) {
        await childRepository.save(normalizedProfile)
      }
    }
  }

  const hydratedClasses = includeClassImages
    ? await Promise.all(normalizedClasses.map(hydrateClassImages))
    : normalizedClasses

  return {
    classes: hydratedClasses.map(mapClassEntity),
    profiles: hydratedProfiles.map((profile) => mapChildEntity(profile, { includeImages }))
  }
}

export async function listStudentProfiles(input: ListStudentProfilesInput): Promise<{
  profiles: ChildProfile[]
  total: number
  page: number
  pageSize: number
}> {
  const pageSize = Math.min(Math.max(Number(input.pageSize) || 10, 1), 100)
  const page = Math.max(Number(input.page) || 1, 1)
  const keyword = input.query?.trim()
  const source = await getAppDataSource()
  const queryBuilder = source
    .getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
    .createQueryBuilder('profile')
    .orderBy('profile.className', 'ASC')
    .addOrderBy('profile.name', 'ASC')
    .addOrderBy('profile.id', 'ASC')

  if (input.classId && input.classId !== 'all') {
    queryBuilder.andWhere('profile.classId = :classId', { classId: input.classId })
  }

  if (keyword) {
    queryBuilder.andWhere(
      new Brackets((builder) => {
        builder
          .where('profile.className LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.name LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.englishName LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.gender LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.birthday LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.zodiac LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.friends LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.hobbies LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.favoriteGames LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.favoriteFoods LIKE :keyword', { keyword: `%${keyword}%` })
          .orWhere('profile.traits LIKE :keyword', { keyword: `%${keyword}%` })
      })
    )
  }

  const [profiles, total] = await queryBuilder
    .skip((page - 1) * pageSize)
    .take(pageSize)
    .getManyAndCount()

  return {
    profiles: profiles.map((profile) => mapChildEntity(profile, { includeImages: false })),
    total,
    page,
    pageSize
  }
}

export async function getStudentProfile(profileId: string): Promise<ChildProfile> {
  const source = await getAppDataSource()
  const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const profile = await repository.findOneBy({ id: profileId })

  if (!profile) {
    throw new Error('学生信息不存在')
  }

  const normalizedProfile = await normalizeStoredStudentImages(profile)

  if (
    normalizedProfile.meImage !== profile.meImage ||
    normalizedProfile.workImage1 !== profile.workImage1 ||
    normalizedProfile.workImage2 !== profile.workImage2
  ) {
    await repository.save(normalizedProfile)
  }

  return mapChildEntity(await hydrateStudentImages(normalizedProfile))
}

export async function updateStudentProfile(profile: ChildProfile): Promise<ChildProfile> {
  const source = await getAppDataSource()
  const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const existingProfile = await repository.findOneBy({ id: profile.id })

  if (!existingProfile) {
    throw new Error('学生信息不存在')
  }

  const nextProfile: ChildProfile = {
    ...profile,
    comment: profile.comment ?? '',
    commentGeneratedAt: profile.commentGeneratedAt ?? existingProfile.commentGeneratedAt ?? ''
  }

  const storedProfile = await mapChildProfile(nextProfile)
  await repository.save(storedProfile)
  return mapChildEntity(await hydrateStudentImages(storedProfile))
}

export async function addClassZodiacs(classId: string): Promise<{
  profiles: ChildProfile[]
  updatedCount: number
  skipped: Array<{ studentName: string; reason: string }>
}> {
  const source = await getAppDataSource()
  const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const profiles = await repository.find({
    where: { classId },
    order: {
      name: 'ASC',
      id: 'ASC'
    }
  })

  if (profiles.length === 0) {
    throw new Error('当前班级没有幼儿数据')
  }

  const updatedProfiles: StudentProfileEntity[] = []
  const skipped: Array<{ studentName: string; reason: string }> = []

  for (const profile of profiles) {
    const studentName = getStudentDisplayName(profile)

    if (String(profile.zodiac ?? '').trim()) {
      skipped.push({ studentName, reason: '已填写属相' })
      continue
    }

    const birthdayDate = parseBirthdayDate(String(profile.birthday ?? ''))

    if (!birthdayDate) {
      skipped.push({ studentName, reason: '生日为空或格式无法识别' })
      continue
    }

    updatedProfiles.push({
      ...profile,
      zodiac: getChineseZodiac(birthdayDate)
    })
  }

  if (updatedProfiles.length > 0) {
    await repository.save(updatedProfiles)
  }

  return {
    profiles: [
      ...profiles.filter((profile) => !updatedProfiles.some((item) => item.id === profile.id)),
      ...updatedProfiles
    ]
      .sort((first, second) => {
        const nameCompare = first.name.localeCompare(second.name, 'zh-CN')
        return nameCompare || first.id.localeCompare(second.id)
      })
      .map((profile) => mapChildEntity(profile, { includeImages: false })),
    updatedCount: updatedProfiles.length,
    skipped
  }
}

async function generateStudentCommentForEntity(
  entity: StudentProfileEntity,
  repository: Repository<StudentProfileEntity>,
  classRepository: Repository<ClassEntity>,
  classTypeConfigs: ClassTypeConfig[]
): Promise<ChildProfile> {
  const profile = mapChildEntity(entity)
  const settings = await loadSettings()

  if (!settings.ok) {
    throw new Error(settings.message)
  }

  const modelConfig = settings.modelConfig

  if (!modelConfig?.baseUrl || !modelConfig.apiKey || !modelConfig.model) {
    throw new Error('请先在设置中配置模型接口、API Key 和模型')
  }

  const requestUrl = getChatCompletionsUrl(modelConfig.baseUrl)
  const classItem = await classRepository.findOneBy({ id: entity.classId })
  const classTypeConfig = getClassTypeConfig(classItem?.type ?? 'cheap', classTypeConfigs)
  const courseContent = classTypeConfig.courseContent
  const userPrompt = buildStudentCommentPrompt(profile, courseContent)

  logLargeModelRequest({
    label: '生成学生评语',
    url: requestUrl,
    apiKey: modelConfig.apiKey,
    model: modelConfig.model,
    prompt: userPrompt
  })

  const response = await fetch(requestUrl, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${modelConfig.apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: modelConfig.model,
      temperature: Number(modelConfig.temperature || 0.7),
      max_tokens: Number(modelConfig.maxTokens || 512),
      messages: [
        {
          role: 'system',
          content:
            modelConfig.systemPrompt ||
            '你是一名幼儿园老师，擅长撰写真诚、具体、积极的成长报告评语。'
        },
        {
          role: 'user',
          content: userPrompt
        }
      ]
    })
  })

  if (!response.ok) {
    throw new Error(`生成评语失败：HTTP ${response.status}`)
  }

  const result = (await response.json()) as {
    choices?: Array<{ message?: { content?: unknown } }>
  }
  const comment = result.choices?.[0]?.message?.content

  if (typeof comment !== 'string' || !comment.trim()) {
    throw new Error('模型没有返回可用评语')
  }

  const nextProfile: ChildProfile = {
    ...profile,
    comment: ensureCommentIncludesCourseContent(comment, courseContent),
    commentGeneratedAt: new Date().toLocaleString('zh-CN', { hour12: false })
  }

  const storedProfile = await mapChildProfile(nextProfile)
  await repository.save(storedProfile)
  return mapChildEntity(await hydrateStudentImages(storedProfile))
}

export async function generateStudentComment(profileId: string): Promise<ChildProfile> {
  const source = await getAppDataSource()
  const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const classRepository = source.getRepository<ClassEntity>(ClassEntitySchema)
  const entity = await repository.findOneBy({ id: profileId })

  if (!entity) {
    throw new Error('学生信息不存在')
  }

  const settings = await loadSettings()

  if (!settings.ok) {
    throw new Error(settings.message)
  }

  return generateStudentCommentForEntity(
    entity,
    repository,
    classRepository,
    settings.classTypeConfigs
  )
}

export async function generateStudentComments(input: GenerateStudentCommentsInput): Promise<{
  profiles: ChildProfile[]
  skipped: Array<{ studentName: string; reason: string }>
  stopped?: boolean
}> {
  const source = await getAppDataSource()
  const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const classRepository = source.getRepository<ClassEntity>(ClassEntitySchema)
  const settings = await loadSettings()

  if (!settings.ok) {
    throw new Error(settings.message)
  }

  const selectedIds = new Set(input.profileIds ?? [])
  const allProfiles = await repository.find({
    where: input.classId ? { classId: input.classId } : undefined,
    order: {
      name: 'ASC',
      id: 'ASC'
    }
  })
  const targetProfiles =
    selectedIds.size > 0
      ? allProfiles.filter((profile) => selectedIds.has(profile.id))
      : allProfiles

  if (targetProfiles.length === 0) {
    throw new Error('没有可生成评语的学生')
  }

  const batchId = randomUUID()
  const className = targetProfiles[0]?.className
  const profiles: ChildProfile[] = []
  const skipped: Array<{ studentName: string; reason: string }> = []
  const controller: CommentGenerationController = {
    batchId,
    paused: false,
    stopped: false,
    lastProgress: {
      batchId,
      status: 'started',
      classId: input.classId,
      className,
      current: 0,
      total: targetProfiles.length
    },
    resumeWaiters: []
  }

  commentGenerationControllers.set(batchId, controller)

  function publishProgress(progress: CommentGenerationProgress): void {
    updateCommentControllerProgress(controller, progress)
  }

  publishProgress(controller.lastProgress)

  try {
    for (const [profileIndex, profile] of targetProfiles.entries()) {
      await waitForCommentGenerationResume(controller)

      if (controller.stopped) {
        break
      }

      const studentName = getStudentDisplayName(profile)

      try {
        publishProgress({
          batchId,
          status: 'student-started',
          classId: profile.classId,
          className: profile.className,
          studentId: profile.id,
          studentName,
          current: profileIndex,
          total: targetProfiles.length
        })

        const nextProfile = await generateStudentCommentForEntity(
          profile,
          repository,
          classRepository,
          settings.classTypeConfigs
        )
        profiles.push(nextProfile)

        publishProgress({
          batchId,
          status: 'student-finished',
          classId: profile.classId,
          className: profile.className,
          studentId: profile.id,
          studentName,
          current: profileIndex + 1,
          total: targetProfiles.length,
          profile: nextProfile
        })
      } catch (error) {
        const reason = error instanceof Error ? error.message : '生成失败'

        skipped.push({
          studentName,
          reason
        })
        publishProgress({
          batchId,
          status: 'student-failed',
          classId: profile.classId,
          className: profile.className,
          studentId: profile.id,
          studentName,
          current: profileIndex + 1,
          total: targetProfiles.length,
          error: reason
        })
      }
    }

    publishProgress({
      batchId,
      status: controller.stopped ? 'stopped' : 'finished',
      classId: input.classId,
      className,
      current: controller.lastProgress.current,
      total: targetProfiles.length
    })

    return { profiles, skipped, stopped: controller.stopped }
  } finally {
    commentGenerationControllers.delete(batchId)
  }
}

export async function saveClasses(classes: ClassProfile[]): Promise<void> {
  const source = await getAppDataSource()

  await source.transaction(async (manager) => {
    const classRepository = manager.getRepository<ClassEntity>(ClassEntitySchema)
    const childRepository = manager.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
    const nextClassIds = new Set(classes.map((classItem) => classItem.id))
    const existingClasses = await classRepository.find()

    for (const classItem of existingClasses) {
      if (!nextClassIds.has(classItem.id)) {
        await childRepository.delete({ classId: classItem.id })
        await classRepository.delete({ id: classItem.id })
      }
    }

    for (const classItem of classes) {
      await classRepository.save(await mapClassProfile(classItem))
      await childRepository.update({ classId: classItem.id }, { className: classItem.name })
    }
  })
}

export async function migrateLocalStudentData(
  classes: ClassProfile[],
  profiles: ChildProfile[]
): Promise<void> {
  const source = await getAppDataSource()

  await source.transaction(async (manager) => {
    const classRepository = manager.getRepository<ClassEntity>(ClassEntitySchema)
    const childRepository = manager.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
    const classCount = await classRepository.count()
    const profileCount = await childRepository.count()

    if (classCount > 0 || profileCount > 0) {
      return
    }

    await classRepository.save(await Promise.all(classes.map(mapClassProfile)))
    await childRepository.save(
      await Promise.all(profiles.map((profile) => mapChildProfile(profile)))
    )
  })
}

export async function replaceProfilesForClass(
  classItem: ClassProfile,
  profiles: ChildProfile[]
): Promise<{
  profiles: ChildProfile[]
  insertedCount: number
  updatedCount: number
  skippedCount: number
}> {
  const source = await getAppDataSource()

  return source.transaction(async (manager) => {
    const childRepository = manager.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
    const existingProfiles = await childRepository.find({
      where: { classId: classItem.id },
      order: {
        name: 'ASC',
        id: 'ASC'
      }
    })

    if (existingProfiles.length === 0) {
      const storedProfiles = await Promise.all(
        profiles.map((profile) => mapChildProfile(profile, classItem))
      )

      if (storedProfiles.length > 0) {
        await childRepository.save(storedProfiles)
      }

      return {
        profiles: storedProfiles.map((profile) =>
          mapChildEntity(profile, { includeImages: false })
        ),
        insertedCount: storedProfiles.length,
        updatedCount: 0,
        skippedCount: 0
      }
    }

    const existingByMatchKey = new Map<string, StudentProfileEntity[]>()

    for (const existingProfile of existingProfiles) {
      for (const matchKey of getProfileMatchKeys(
        mapChildEntity(existingProfile, { includeImages: false })
      )) {
        existingByMatchKey.set(matchKey, [
          ...(existingByMatchKey.get(matchKey) ?? []),
          existingProfile
        ])
      }
    }

    const usedExistingIds = new Set<string>()
    const updatedProfiles: StudentProfileEntity[] = []
    let skippedCount = 0

    for (const profile of profiles) {
      const matchedProfile = getProfileMatchKeys(profile)
        .flatMap((matchKey) => existingByMatchKey.get(matchKey) ?? [])
        .find((existingProfile) => !usedExistingIds.has(existingProfile.id))

      if (!matchedProfile) {
        skippedCount += 1
        continue
      }

      usedExistingIds.add(matchedProfile.id)
      updatedProfiles.push(
        await mapChildProfile(mergeImportedProfileIntoExisting(matchedProfile, profile, classItem))
      )
    }

    if (updatedProfiles.length > 0) {
      await childRepository.save(updatedProfiles)
    }

    const updatedProfileById = new Map(updatedProfiles.map((profile) => [profile.id, profile]))
    const nextProfiles = existingProfiles.map(
      (profile) => updatedProfileById.get(profile.id) ?? profile
    )

    return {
      profiles: nextProfiles.map((profile) => mapChildEntity(profile, { includeImages: false })),
      insertedCount: 0,
      updatedCount: updatedProfiles.length,
      skippedCount
    }
  })
}

export async function deleteClassWithProfiles(classId: string): Promise<void> {
  const source = await getAppDataSource()

  await source.transaction(async (manager) => {
    await manager
      .getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
      .delete({ classId })
    await manager.getRepository<ClassEntity>(ClassEntitySchema).delete({ id: classId })
  })
}

export async function deleteStudentProfile(profileId: string): Promise<void> {
  const source = await getAppDataSource()
  const result = await source
    .getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
    .delete({ id: profileId })

  if (!result.affected) {
    throw new Error('学生信息不存在')
  }
}

export async function migrateStoredImagesToFiles(): Promise<void> {
  const source = await getAppDataSource()
  const classRepository = source.getRepository<ClassEntity>(ClassEntitySchema)
  const childRepository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const classes = await classRepository.find()
  const profiles = await childRepository.find()

  for (const classItem of classes) {
    const nextClassItem = await normalizeStoredClassImages(classItem)

    if (nextClassItem.familyPhoto !== classItem.familyPhoto) {
      await classRepository.save(nextClassItem)
    }
  }

  for (const profile of profiles) {
    const nextProfile = await normalizeStoredStudentImages(profile)

    if (
      nextProfile.meImage !== profile.meImage ||
      nextProfile.workImage1 !== profile.workImage1 ||
      nextProfile.workImage2 !== profile.workImage2
    ) {
      await childRepository.save(nextProfile)
    }
  }
}
