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

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

import { TemplateEntitySchema, type TemplateEntity } from '../entities/TemplateEntity'
import type {
  TemplateFieldMapping,
  TemplateInput,
  TemplateItem,
  TemplatePlaceholder,
  TemplateSemester,
  TemplateType
} from '../types/template'
import { getAppDataSource } from './databaseService'

const TEMPLATE_FOLDER_NAME = 'templates'
const SUPPORTED_TEMPLATE_EXTENSIONS = ['.ppt', '.pptx', '.doc', '.docx', '.xls', '.xlsx']
const BUILT_IN_PPT_PLACEHOLDERS = new Set([
  'name',
  'class',
  'comments',
  'teacher_name',
  'english_name',
  'sex',
  'birthday',
  'zodiac',
  'friend',
  'hobby',
  'game',
  'food',
  'class_image'
])

function getTemplateStoragePath(): string {
  return join(app.getPath('userData'), TEMPLATE_FOLDER_NAME)
}

function getStoredTemplateFilePath(id: string, extension: string): string {
  return join(TEMPLATE_FOLDER_NAME, `${id}${extension}`)
}

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

  return join(getTemplateStoragePath(), basename(filePath))
}

async function getTemplateRepository(): Promise<Repository<TemplateEntity>> {
  const source = await getAppDataSource()
  return source.getRepository<TemplateEntity>(TemplateEntitySchema)
}

function normalizeTemplateType(type: string): TemplateType {
  return type === 'landscape' ? 'landscape' : 'portrait'
}

function normalizeTemplateSemester(semester: string): TemplateSemester {
  return semester === 'second' ? 'second' : 'first'
}

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

    if (!Array.isArray(parsed)) {
      return []
    }

    return parsed.filter(
      (item): item is TemplatePlaceholder =>
        item && typeof item.name === 'string' && (item.kind === 'text' || item.kind === 'image')
    )
  } catch {
    return []
  }
}

function parseStoredFieldMappings(value: string): TemplateFieldMapping {
  try {
    const parsed = JSON.parse(value)

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

function mapTemplateEntity(entity: TemplateEntity): TemplateItem {
  return {
    id: entity.id,
    name: entity.name,
    grade: entity.grade,
    semester: normalizeTemplateSemester(entity.semester),
    type: normalizeTemplateType(entity.type),
    originalFileName: entity.originalFileName,
    filePath: resolveTemplateFilePath(entity.filePath),
    fileExtension: entity.fileExtension,
    placeholders: parseStoredPlaceholders(entity.placeholders),
    fieldMappings: parseStoredFieldMappings(entity.fieldMappings),
    createdAt: entity.createdAt,
    updatedAt: entity.updatedAt
  }
}

function ensureSupportedTemplateFile(filePath: string): string {
  const extension = extname(filePath).toLowerCase()

  if (!SUPPORTED_TEMPLATE_EXTENSIONS.includes(extension)) {
    throw new Error('仅支持 PPT、Word、Excel 模板文件')
  }

  return extension
}

async function removeFileIfExists(filePath: string): Promise<void> {
  try {
    await unlink(filePath)
  } catch {
    // Removing an old copied template is best-effort; metadata remains the source of truth.
  }
}

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 decodeXml(value: string): string {
  return value
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&')
}

function isPictureShape(slideXml: string, shapeNameIndex: number): boolean {
  return (
    slideXml.lastIndexOf('<p:pic', shapeNameIndex) >
    slideXml.lastIndexOf('</p:pic>', shapeNameIndex)
  )
}

function upsertPlaceholder(
  placeholdersByName: Map<string, TemplatePlaceholder>,
  name: string,
  kind: TemplatePlaceholder['kind']
): void {
  const trimmedName = decodeXml(name).trim()

  if (!trimmedName) {
    return
  }

  const existing = placeholdersByName.get(trimmedName)

  if (existing?.kind === 'image') {
    return
  }

  placeholdersByName.set(trimmedName, { name: trimmedName, kind })
}

async function parsePptxPlaceholders(filePath: string): Promise<TemplatePlaceholder[]> {
  const templateBuffer = await readFile(filePath)
  const zip = await JSZip.loadAsync(templateBuffer)
  const placeholdersByName = new Map<string, TemplatePlaceholder>()

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

    if (!slideFile) {
      continue
    }

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

    for (const shapeMatch of slideXml.matchAll(/<p:cNvPr[^>]*name="([^"]+)"/g)) {
      const rawName = shapeMatch[1]
      const name = decodeXml(rawName)

      if (!BUILT_IN_PPT_PLACEHOLDERS.has(name) && !/^field[:_-]/i.test(name)) {
        continue
      }

      upsertPlaceholder(
        placeholdersByName,
        name.replace(/^field[:_-]/i, ''),
        isPictureShape(slideXml, shapeMatch.index ?? 0) ? 'image' : 'text'
      )
    }

    for (const placeholderMatch of slideXml.matchAll(/\{\{\s*([^{}\s][^{}]*?)\s*\}\}/g)) {
      upsertPlaceholder(placeholdersByName, placeholderMatch[1], 'text')
    }
  }

  return [...placeholdersByName.values()].sort((first, second) =>
    first.name.localeCompare(second.name, 'zh-CN')
  )
}

export async function parseTemplatePlaceholders(filePath: string): Promise<TemplatePlaceholder[]> {
  await access(filePath)
  const extension = ensureSupportedTemplateFile(filePath)

  if (extension !== '.pptx') {
    return []
  }

  return parsePptxPlaceholders(filePath)
}

export async function selectTemplateFile(): Promise<{ filePath: string; fileName: string } | null> {
  const result = await dialog.showOpenDialog({
    title: '选择模板文件',
    buttonLabel: '选择模板',
    properties: ['openFile'],
    filters: [
      {
        name: 'Office 模板文件',
        extensions: ['ppt', 'pptx', 'doc', 'docx', 'xls', 'xlsx']
      }
    ]
  })

  if (result.canceled || result.filePaths.length === 0) {
    return null
  }

  const filePath = result.filePaths[0]
  ensureSupportedTemplateFile(filePath)

  return {
    filePath,
    fileName: basename(filePath)
  }
}

export async function loadTemplates(): Promise<TemplateItem[]> {
  const repository = await getTemplateRepository()
  const templates = await repository.find({
    order: {
      updatedAt: 'DESC'
    }
  })

  return templates.map(mapTemplateEntity)
}

export async function saveTemplate(input: TemplateInput): Promise<TemplateItem> {
  const name = input.name.trim()
  const grade = input.grade.trim()

  if (!name) {
    throw new Error('请输入模板名称')
  }

  if (!grade) {
    throw new Error('请输入适用年级')
  }

  await access(input.sourceFilePath)

  const repository = await getTemplateRepository()
  const existingTemplate = input.id ? await repository.findOneBy({ id: input.id }) : null
  const extension = ensureSupportedTemplateFile(input.sourceFilePath)
  const id = existingTemplate?.id ?? randomUUID()
  const now = new Date().toISOString()
  const nextStoredFilePath = getStoredTemplateFilePath(id, extension)
  const nextAbsoluteFilePath = resolveTemplateFilePath(nextStoredFilePath)
  const existingAbsoluteFilePath = existingTemplate
    ? resolveTemplateFilePath(existingTemplate.filePath)
    : null
  const shouldCopyFile = existingAbsoluteFilePath !== input.sourceFilePath

  if (shouldCopyFile) {
    await mkdir(getTemplateStoragePath(), { recursive: true })
    await copyFile(input.sourceFilePath, nextAbsoluteFilePath)

    if (existingAbsoluteFilePath && existingAbsoluteFilePath !== nextAbsoluteFilePath) {
      await removeFileIfExists(existingAbsoluteFilePath)
    }
  }

  const placeholders =
    extension === '.pptx'
      ? await parsePptxPlaceholders(nextAbsoluteFilePath)
      : existingTemplate
        ? parseStoredPlaceholders(existingTemplate.placeholders)
        : []
  const placeholderNames = new Set(placeholders.map((placeholder) => placeholder.name))
  const previousMappings = existingTemplate
    ? parseStoredFieldMappings(existingTemplate.fieldMappings)
    : {}
  const inputMappings = input.fieldMappings ?? previousMappings
  const fieldMappings = Object.fromEntries(
    Object.entries(inputMappings)
      .map(([placeholder, field]) => [placeholder.trim(), field.trim()])
      .filter(([placeholder, field]) => placeholderNames.has(placeholder) && field.length > 0)
  )

  const entity: TemplateEntity = {
    id,
    name,
    grade,
    semester: normalizeTemplateSemester(input.semester),
    type: normalizeTemplateType(input.type),
    originalFileName: basename(input.sourceFilePath),
    filePath: nextStoredFilePath,
    fileExtension: extension,
    placeholders: JSON.stringify(placeholders),
    fieldMappings: JSON.stringify(fieldMappings),
    createdAt: existingTemplate?.createdAt ?? now,
    updatedAt: now
  }

  await repository.save(entity)
  return mapTemplateEntity(entity)
}

export async function openTemplate(filePath: string): Promise<void> {
  const absoluteFilePath = resolveTemplateFilePath(filePath)
  await access(absoluteFilePath)

  const errorMessage = await shell.openPath(absoluteFilePath)

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

export async function deleteTemplate(id: string): Promise<void> {
  const repository = await getTemplateRepository()
  const template = await repository.findOneBy({ id })

  if (!template) {
    return
  }

  await repository.delete({ id })
  await removeFileIfExists(resolveTemplateFilePath(template.filePath))
}
