import { randomUUID } from 'crypto'
import { access, mkdir, readFile, unlink, writeFile } from 'fs/promises'
import { basename, isAbsolute, join } from 'path'
import 'reflect-metadata'

import { app, shell } from 'electron'
import JSZip from 'jszip'
import type { Repository } from 'typeorm'

import { ClassEntitySchema, type ClassEntity } from '../entities/ClassEntity'
import { ReportEntitySchema, type ReportEntity } from '../entities/ReportEntity'
import {
  StudentProfileEntitySchema,
  type StudentProfileEntity
} from '../entities/StudentProfileEntity'
import { TemplateEntitySchema, type TemplateEntity } from '../entities/TemplateEntity'
import type { GenerateReportsInput, ReportFormat, ReportItem } from '../types/report'
import { getAppDataSource } from './databaseService'

const REPORT_FOLDER_NAME = 'reports'
const TEMPLATE_FOLDER_NAME = 'templates'
const CONTENT_TYPES_PATH = '[Content_Types].xml'

type ShapeLocation = {
  relsPath: string
  shapeXml: string
  embedRelId?: string
}

type ReportStudentData = {
  id: string
  classId: string
  className: string
  name: string
  englishName: string
  sex: string
  gender: string
  birthday: string
  zodiac: string
  friends: string
  friend: string
  hobbies: string
  hobby: string
  favoriteGames: string
  game: string
  favoriteFoods: string
  food: string
  traits: string
  comment: string
  comments: string
  teacherName: string
  teacher_name: string
}

function getReportStoragePath(): string {
  return join(app.getPath('userData'), REPORT_FOLDER_NAME)
}

function resolveTemplateFilePath(filePath: string): string {
  if (!isAbsolute(filePath)) {
    return join(app.getPath('userData'), filePath)
  }

  return join(app.getPath('userData'), TEMPLATE_FOLDER_NAME, basename(filePath))
}

async function getReportRepository(): Promise<Repository<ReportEntity>> {
  const source = await getAppDataSource()
  return source.getRepository<ReportEntity>(ReportEntitySchema)
}

function normalizeReportFormat(format: string): ReportFormat {
  if (format === 'word' || format === 'pdf' || format === 'excel') {
    return format
  }

  return 'ppt'
}

function mapReportEntity(entity: ReportEntity): ReportItem {
  return {
    id: entity.id,
    title: entity.title,
    classId: entity.classId,
    className: entity.className,
    studentId: entity.studentId,
    studentName: entity.studentName,
    format: normalizeReportFormat(entity.format),
    originalFileName: entity.originalFileName,
    filePath: entity.filePath,
    fileExtension: entity.fileExtension,
    templateId: entity.templateId,
    createdAt: entity.createdAt,
    updatedAt: entity.updatedAt
  }
}

function escapeXml(value: string): string {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function sanitizeFileName(value: string): string {
  return (
    [...value]
      .filter((character) => !'<>:"/\\|?*'.includes(character) && character.charCodeAt(0) >= 32)
      .join('')
      .trim() || '未命名'
  )
}

function getFormatFromExtension(extension: string): ReportFormat {
  if (['.doc', '.docx'].includes(extension)) {
    return 'word'
  }

  if (['.xls', '.xlsx'].includes(extension)) {
    return 'excel'
  }

  if (extension === '.pdf') {
    return 'pdf'
  }

  return 'ppt'
}

function getStudentData(student: StudentProfileEntity): ReportStudentData {
  const friends = parseJsonList(student.friends).join('、') || ' '
  const hobbies = parseJsonList(student.hobbies).join('、') || ' '
  const favoriteGames = parseJsonList(student.favoriteGames).join('、') || ' '
  const favoriteFoods = parseJsonList(student.favoriteFoods).join('、') || ' '

  return {
    id: student.id,
    classId: student.classId,
    className: student.className,
    name: student.name || '未填写',
    englishName: student.englishName || ' ',
    sex: student.gender || '男',
    gender: student.gender || '男',
    birthday: student.birthday || ' ',
    zodiac: student.zodiac || ' ',
    friends,
    friend: friends,
    hobbies,
    hobby: hobbies,
    favoriteGames,
    game: favoriteGames,
    favoriteFoods,
    food: favoriteFoods,
    traits: student.traits || ' ',
    comment: student.comment || '暂无评语',
    comments: student.comment || '暂无评语',
    teacherName: '',
    teacher_name: ''
  }
}

function parseTemplateFieldMappings(value: string): Record<string, string> {
  try {
    const parsed = JSON.parse(value)

    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
      return {}
    }

    return Object.fromEntries(
      Object.entries(parsed).filter(
        (entry): entry is [string, string] =>
          typeof entry[0] === 'string' && typeof entry[1] === 'string'
      )
    )
  } catch {
    return {}
  }
}

function normalizeFieldKey(value: string): string {
  return value.replace(/[_\s-]+(.)?/g, (_, next: string | undefined) =>
    next ? next.toUpperCase() : ''
  )
}

function getMappedValue(
  fieldKey: string,
  studentData: ReportStudentData,
  classItem: ClassEntity
): string {
  if (fieldKey === 'class' || fieldKey === 'className') {
    return classItem.name
  }

  const directValue = studentData[fieldKey as keyof ReportStudentData]

  if (typeof directValue === 'string') {
    return directValue
  }

  const normalizedValue = studentData[normalizeFieldKey(fieldKey) as keyof ReportStudentData]

  return typeof normalizedValue === 'string' ? normalizedValue : ' '
}

function buildTextReplacements(
  template: TemplateEntity,
  studentData: ReportStudentData,
  classItem: ClassEntity
): Record<string, string> {
  const defaultReplacements: Record<string, string> = {
    name: studentData.name,
    class: classItem.name,
    comments: studentData.comments,
    teacher_name: studentData.teacher_name,
    english_name: studentData.englishName,
    sex: studentData.sex,
    birthday: studentData.birthday,
    zodiac: studentData.zodiac,
    friend: studentData.friend,
    hobby: studentData.hobby,
    game: studentData.game,
    food: studentData.food
  }
  const fieldMappings = parseTemplateFieldMappings(template.fieldMappings)

  for (const [placeholder, fieldKey] of Object.entries(fieldMappings)) {
    if (fieldKey === 'classImage' || fieldKey === 'familyPhoto') {
      continue
    }

    defaultReplacements[placeholder] = getMappedValue(fieldKey, studentData, classItem)
  }

  return defaultReplacements
}

function buildImageReplacements(template: TemplateEntity): Record<string, string> {
  const imageReplacements: Record<string, string> = {
    class_image: 'familyPhoto'
  }
  const fieldMappings = parseTemplateFieldMappings(template.fieldMappings)

  for (const [placeholder, fieldKey] of Object.entries(fieldMappings)) {
    if (fieldKey === 'classImage' || fieldKey === 'familyPhoto') {
      imageReplacements[placeholder] = 'familyPhoto'
    }
  }

  return imageReplacements
}

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

function getSlidePaths(zip: JSZip): string[] {
  return Object.keys(zip.files)
    .filter((path) => /^ppt\/slides\/slide\d+\.xml$/.test(path))
    .sort((first, second) => {
      const firstNumber = Number(first.match(/slide(\d+)\.xml$/)?.[1] ?? 0)
      const secondNumber = Number(second.match(/slide(\d+)\.xml$/)?.[1] ?? 0)

      return firstNumber - secondNumber
    })
}

function getRelsPath(slidePath: string): string {
  return slidePath.replace('ppt/slides/', 'ppt/slides/_rels/') + '.rels'
}

function replaceShapeText(slideXml: string, shapeName: string, text: string): string {
  return slideXml.replace(/<p:sp[\s\S]*?<\/p:sp>/g, (shapeXml: string) => {
    if (!hasShapeName(shapeXml, shapeName)) {
      return shapeXml
    }

    const escapedText = escapeXml(text)
    const textMatches = [...shapeXml.matchAll(/<a:t>([\s\S]*?)<\/a:t>/g)]

    if (textMatches.length === 0) {
      return shapeXml
    }

    let replacedFirstText = false
    return shapeXml.replace(/<a:t>[\s\S]*?<\/a:t>/g, () => {
      if (replacedFirstText) {
        return '<a:t></a:t>'
      }

      replacedFirstText = true
      return `<a:t>${escapedText}</a:t>`
    })
  })
}

function hasShapeName(shapeXml: string, shapeName: string): boolean {
  return new RegExp(`<p:cNvPr[^>]*name="${escapeRegExp(shapeName)}"[^>]*>`).test(shapeXml)
}

function replaceInlineTextPlaceholder(slideXml: string, placeholder: string, text: string): string {
  const placeholderPattern = new RegExp(`\\{\\{\\s*${escapeRegExp(placeholder)}\\s*\\}\\}`, 'g')

  return slideXml.replace(/<a:t>([\s\S]*?)<\/a:t>/g, (textNode, value: string) => {
    placeholderPattern.lastIndex = 0

    if (!placeholderPattern.test(value)) {
      return textNode
    }

    placeholderPattern.lastIndex = 0
    return `<a:t>${value.replace(placeholderPattern, escapeXml(text))}</a:t>`
  })
}

async function replaceTextPlaceholders(
  zip: JSZip,
  replacements: Record<string, string>
): Promise<void> {
  for (const slidePath of getSlidePaths(zip)) {
    const slideFile = zip.file(slidePath)

    if (!slideFile) {
      continue
    }

    let slideXml = await slideFile.async('text')

    for (const [shapeName, text] of Object.entries(replacements)) {
      slideXml = replaceShapeText(slideXml, shapeName, text)
      slideXml = replaceInlineTextPlaceholder(slideXml, shapeName, text)
    }

    zip.file(slidePath, slideXml)
  }
}

function findPictureShape(slideXml: string, shapeName: string): ShapeLocation['shapeXml'] | null {
  const pictureShapes = slideXml.match(/<p:pic[\s\S]*?<\/p:pic>/g) ?? []

  return pictureShapes.find((shapeXml) => hasShapeName(shapeXml, shapeName)) ?? null
}

function getEmbedRelId(shapeXml: string): string | undefined {
  return shapeXml.match(/r:embed="([^"]+)"/)?.[1]
}

function findShapeLocation(
  shapeName: string,
  slideXmlByPath: Map<string, string>
): ShapeLocation | null {
  for (const [slidePath, slideXml] of slideXmlByPath.entries()) {
    const shapeXml = findPictureShape(slideXml, shapeName)

    if (!shapeXml) {
      continue
    }

    return {
      relsPath: getRelsPath(slidePath),
      shapeXml,
      embedRelId: getEmbedRelId(shapeXml)
    }
  }

  return null
}

async function loadSlideXmlByPath(zip: JSZip): Promise<Map<string, string>> {
  const slideXmlByPath = new Map<string, string>()

  for (const slidePath of getSlidePaths(zip)) {
    const slideFile = zip.file(slidePath)

    if (slideFile) {
      slideXmlByPath.set(slidePath, await slideFile.async('text'))
    }
  }

  return slideXmlByPath
}

async function replacePicturePlaceholder(
  zip: JSZip,
  slideXmlByPath: Map<string, string>,
  shapeName: string,
  imageDataUrl?: string
): Promise<void> {
  if (!imageDataUrl) {
    return
  }

  const location = findShapeLocation(shapeName, slideXmlByPath)

  if (!location?.embedRelId) {
    return
  }

  const relsFile = zip.file(location.relsPath)

  if (!relsFile) {
    return
  }

  const relsXml = await relsFile.async('text')
  const relationshipPattern = new RegExp(
    `(<Relationship[^>]*Id="${escapeRegExp(location.embedRelId)}"[^>]*Target=")([^"]+)("[^>]*/>)`
  )
  const target = relsXml.match(relationshipPattern)?.[2]

  if (!target) {
    return
  }

  const mediaPath = `ppt/${target.replace(/^\.\.\//, '')}`
  const imageMatch = imageDataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/)

  if (!imageMatch) {
    return
  }

  const mimeType = imageMatch[1]
  const extension = mimeType.includes('png') ? 'png' : mimeType.includes('bmp') ? 'bmp' : 'jpg'
  const targetMediaPath = mediaPath.replace(/\.[^.]+$/, `.${extension}`)

  zip.file(targetMediaPath, Buffer.from(imageMatch[2], 'base64'))

  if (targetMediaPath !== mediaPath) {
    zip.remove(mediaPath)
    zip.file(
      location.relsPath,
      relsXml.replace(relationshipPattern, `$1${target.replace(/\.[^.]+$/, `.${extension}`)}$3`)
    )
    await ensureImageContentType(zip, extension, mimeType)
  }
}

async function ensureImageContentType(
  zip: JSZip,
  extension: string,
  mimeType: string
): Promise<void> {
  const contentTypesFile = zip.file(CONTENT_TYPES_PATH)

  if (!contentTypesFile) {
    return
  }

  const contentTypesXml = await contentTypesFile.async('text')

  if (contentTypesXml.includes(`Extension="${extension}"`)) {
    return
  }

  zip.file(
    CONTENT_TYPES_PATH,
    contentTypesXml.replace(
      '</Types>',
      `<Default Extension="${extension}" ContentType="${mimeType}"/></Types>`
    )
  )
}

async function buildPptxReport(
  template: TemplateEntity,
  outputPath: string,
  student: StudentProfileEntity,
  classItem: ClassEntity
): Promise<void> {
  const templateBuffer = await readFile(resolveTemplateFilePath(template.filePath))
  const zip = await JSZip.loadAsync(templateBuffer)
  const studentData = getStudentData(student)

  await replaceTextPlaceholders(zip, buildTextReplacements(template, studentData, classItem))

  const slideXmlByPath = await loadSlideXmlByPath(zip)

  for (const [placeholder, fieldKey] of Object.entries(buildImageReplacements(template))) {
    if (fieldKey === 'familyPhoto') {
      await replacePicturePlaceholder(
        zip,
        slideXmlByPath,
        placeholder,
        classItem.familyPhoto ?? undefined
      )
    }
  }

  const outputBuffer = await zip.generateAsync({
    type: 'nodebuffer',
    compression: 'DEFLATE'
  })

  await writeFile(outputPath, outputBuffer)
}

async function removeFileIfExists(filePath: string): Promise<void> {
  try {
    await unlink(filePath)
  } catch {
    // Report files may have been moved or removed by the user.
  }
}

export async function loadReports(): Promise<ReportItem[]> {
  const repository = await getReportRepository()
  const reports = await repository.find({
    order: {
      updatedAt: 'DESC'
    }
  })

  return reports.map(mapReportEntity)
}

export async function generateReports(input: GenerateReportsInput): Promise<{
  reports: ReportItem[]
  skipped: Array<{ studentName: string; reason: string }>
}> {
  const source = await getAppDataSource()
  const templateRepository = source.getRepository<TemplateEntity>(TemplateEntitySchema)
  const classRepository = source.getRepository<ClassEntity>(ClassEntitySchema)
  const studentRepository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
  const reportRepository = source.getRepository<ReportEntity>(ReportEntitySchema)
  const template = await templateRepository.findOneBy({ id: input.templateId })

  if (!template) {
    throw new Error('请选择报告模板')
  }

  if (!['.ppt', '.pptx'].includes(template.fileExtension.toLowerCase())) {
    throw new Error('当前生成方式仅支持 PPT/PPTX 模板')
  }

  if (template.fileExtension.toLowerCase() !== '.pptx') {
    throw new Error('请使用 .pptx 模板生成报告，旧版 .ppt 暂不支持直接写入')
  }

  await access(resolveTemplateFilePath(template.filePath))

  const classItem = await classRepository.findOneBy({ id: input.classId })

  if (!classItem) {
    throw new Error('请选择班级')
  }

  const selectedIds = new Set(input.studentIds ?? [])
  const students = await studentRepository.find({
    where: {
      classId: input.classId
    },
    order: {
      name: 'ASC',
      id: 'ASC'
    }
  })
  const targetStudents =
    selectedIds.size > 0 ? students.filter((student) => selectedIds.has(student.id)) : students

  if (targetStudents.length === 0) {
    throw new Error('当前班级没有可生成的学生')
  }

  await mkdir(getReportStoragePath(), { recursive: true })

  const now = new Date().toISOString()
  const reports: ReportItem[] = []
  const skipped: Array<{ studentName: string; reason: string }> = []

  for (const student of targetStudents) {
    const studentName = student.name || student.englishName || '未命名学生'
    const title = `${classItem.name} ${studentName} 幼儿成长报告`
    const reportId = randomUUID()
    const fileName = `${sanitizeFileName(title)}${template.fileExtension}`
    const outputPath = join(getReportStoragePath(), `${reportId}${template.fileExtension}`)

    try {
      await buildPptxReport(template, outputPath, student, classItem)

      const entity: ReportEntity = {
        id: reportId,
        title,
        classId: classItem.id,
        className: classItem.name,
        studentId: student.id,
        studentName,
        format: getFormatFromExtension(template.fileExtension),
        originalFileName: fileName,
        filePath: outputPath,
        fileExtension: template.fileExtension,
        templateId: template.id,
        createdAt: now,
        updatedAt: now
      }

      await reportRepository.save(entity)
      reports.push(mapReportEntity(entity))
    } catch (error) {
      skipped.push({
        studentName,
        reason: error instanceof Error ? error.message : '生成失败'
      })
      await removeFileIfExists(outputPath)
    }
  }

  return { reports, skipped }
}

export async function openReport(filePath: string): Promise<void> {
  await access(filePath)

  const errorMessage = await shell.openPath(filePath)

  if (errorMessage) {
    throw new Error(errorMessage)
  }
}

export async function deleteReport(id: string): Promise<void> {
  const repository = await getReportRepository()
  const report = await repository.findOneBy({ id })

  if (!report) {
    return
  }

  await repository.delete({ id })
  await removeFileIfExists(report.filePath)
}
