import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'

import { externalizeMedia } from './media-storage.js'
import { DATA_DIR, DB_PATH, SQL_WASM_PATH } from './paths.js'

type PersistedAppState = {
  templates: Array<{
    id: string
    name: string
    image: unknown
    config: unknown
    updatedAt: string
  }>
  activeTemplateId: string
  classes: Array<{
    id: string
      name: string
      students: Array<{
        id: string
        name: string
        photo?: unknown
        photoFileName?: string
        idPhoto?: unknown
        idPhotoFileName?: string
        idPhotoCrop?: unknown
      }>
      idPhotoConfig?: unknown
      createdAt: string
    }>
  activeClassId: string
  certificates: Array<{
    id: string
    classId: string
    className: string
    studentId: string
    studentName: string
    photo: unknown
    template: PersistedAppState['templates'][number]
    asset: {
      fileName: string
    }
    jpgUrl?: string
    updatedAt: string
  }>
}

type SqlJsStatement = {
  run(values?: unknown[]): void
  free(): void
}

type SqlJsDatabase = {
  run(sql: string): void
  exec(sql: string): Array<{ columns: string[]; values: unknown[][] }>
  prepare(sql: string): SqlJsStatement
  export(): Uint8Array
}

type SqlJsStatic = {
  Database: new (data?: Uint8Array | Buffer) => SqlJsDatabase
}

let databasePromise: Promise<SqlJsDatabase> | undefined

const EMPTY_STATE: PersistedAppState = {
  templates: [],
  activeTemplateId: '',
  classes: [],
  activeClassId: '',
  certificates: []
}

function asString(value: unknown) {
  return typeof value === 'string' ? value : ''
}

function asNumber(value: unknown) {
  return typeof value === 'number' ? value : 0
}

function json<T>(value: string, fallback: T): T {
  try {
    return JSON.parse(value) as T
  } catch {
    return fallback
  }
}

function rows<T extends Record<string, unknown>>(database: SqlJsDatabase, sql: string): T[] {
  const result = database.exec(sql)[0]

  if (!result) {
    return []
  }

  return result.values.map((valueRow) =>
    Object.fromEntries(result.columns.map((column, index) => [column, valueRow[index]]))
  ) as T[]
}

function scalar(database: SqlJsDatabase, sql: string) {
  return database.exec(sql)[0]?.values[0]?.[0]
}

function persist(database: SqlJsDatabase) {
  writeFileSync(DB_PATH, Buffer.from(database.export()))
}

async function db() {
  if (!databasePromise) {
    databasePromise = (async () => {
      mkdirSync(DATA_DIR, { recursive: true })
      const runtimeRequire = createRequire(import.meta.url)
      const initSqlJs = runtimeRequire('sql.js') as (options?: {
        locateFile?: (file: string) => string
      }) => Promise<SqlJsStatic>
      const SQL = await initSqlJs({
        locateFile: (file) => path.join(SQL_WASM_PATH, file)
      })
      const database = existsSync(DB_PATH)
        ? new SQL.Database(readFileSync(DB_PATH))
        : new SQL.Database()

      database.run(`
        CREATE TABLE IF NOT EXISTS app_state (
          id INTEGER PRIMARY KEY CHECK (id = 1),
          json TEXT NOT NULL,
          updated_at TEXT NOT NULL
        );
      `)
      database.run(`
        CREATE TABLE IF NOT EXISTS templates (
          id TEXT PRIMARY KEY,
          name TEXT NOT NULL,
          image_json TEXT NOT NULL,
          config_json TEXT NOT NULL,
          updated_at TEXT NOT NULL
        );
      `)
      database.run(`
        CREATE TABLE IF NOT EXISTS classes (
          id TEXT PRIMARY KEY,
          name TEXT NOT NULL,
          id_photo_config_json TEXT,
          created_at TEXT NOT NULL
        );
      `)
      database.run(`
        CREATE TABLE IF NOT EXISTS students (
          id TEXT PRIMARY KEY,
          class_id TEXT NOT NULL,
          name TEXT NOT NULL,
          photo_json TEXT,
          photo_file_name TEXT,
          id_photo_json TEXT,
          id_photo_file_name TEXT,
          id_photo_crop_json TEXT,
          sort_order INTEGER NOT NULL,
          FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE
        );
      `)
      const classColumns = rows<{ name: string }>(database, 'PRAGMA table_info(classes)').map((column) => asString(column.name))
      const studentColumns = rows<{ name: string }>(database, 'PRAGMA table_info(students)').map((column) => asString(column.name))

      if (!classColumns.includes('id_photo_config_json')) {
        database.run('ALTER TABLE classes ADD COLUMN id_photo_config_json TEXT')
      }

      if (!studentColumns.includes('id_photo_json')) {
        database.run('ALTER TABLE students ADD COLUMN id_photo_json TEXT')
      }

      if (!studentColumns.includes('id_photo_file_name')) {
        database.run('ALTER TABLE students ADD COLUMN id_photo_file_name TEXT')
      }

      if (!studentColumns.includes('id_photo_crop_json')) {
        database.run('ALTER TABLE students ADD COLUMN id_photo_crop_json TEXT')
      }
      database.run(`
        CREATE TABLE IF NOT EXISTS generated_certificates (
          id TEXT PRIMARY KEY,
          class_id TEXT NOT NULL,
          student_id TEXT NOT NULL,
          template_id TEXT,
          class_name TEXT NOT NULL,
          student_name TEXT NOT NULL,
          asset_file_name TEXT NOT NULL,
          jpg_url TEXT,
          updated_at TEXT NOT NULL,
          FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE,
          FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
          FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE SET NULL
        );
      `)

      persist(database)
      return database
    })()
  }

  return databasePromise
}

function runPrepared(
  database: SqlJsDatabase,
  sql: string,
  run: (statement: SqlJsStatement) => void
) {
  const statement = database.prepare(sql)

  try {
    run(statement)
  } finally {
    statement.free()
  }
}

function hasStructuredState(database: SqlJsDatabase) {
  const [{ count = 0 } = { count: 0 }] = rows<{ count: number }>(
    database,
    `
      SELECT
        (SELECT COUNT(*) FROM templates) +
        (SELECT COUNT(*) FROM classes) +
        (SELECT COUNT(*) FROM students) +
        (SELECT COUNT(*) FROM generated_certificates) AS count
    `
  )

  return count > 0
}

export async function loadAppState(): Promise<PersistedAppState> {
  const database = await db()
  const value = scalar(database, 'SELECT json FROM app_state WHERE id = 1 LIMIT 1')
  const savedState = json(asString(value), EMPTY_STATE)

  if (!hasStructuredState(database)) {
    if (
      savedState.templates?.length ||
      savedState.classes?.length ||
      savedState.certificates?.length
    ) {
      await saveAppState(savedState)
      return loadStructuredState(database, savedState)
    }

    return {
      ...EMPTY_STATE,
      activeTemplateId: savedState.activeTemplateId ?? '',
      activeClassId: savedState.activeClassId ?? ''
    }
  }

  const structuredState = loadStructuredState(database, savedState)
  const normalizedState = externalizeMedia(structuredState) as PersistedAppState

  if (JSON.stringify(normalizedState) !== JSON.stringify(structuredState)) {
    await saveAppState(normalizedState)
  }

  return normalizedState
}

export async function saveAppState(state: PersistedAppState): Promise<void> {
  const database = await db()
  const previousAppStateBytes = asNumber(
    scalar(database, 'SELECT length(json) FROM app_state WHERE id = 1 LIMIT 1')
  )
  const normalizedState = externalizeMedia(state) as PersistedAppState
  const updatedAt = new Date().toISOString()
  const activeState = {
    activeTemplateId: normalizedState.activeTemplateId,
    activeClassId: normalizedState.activeClassId
  }

  try {
    database.run('BEGIN TRANSACTION')
    database.run('DELETE FROM generated_certificates')
    database.run('DELETE FROM students')
    database.run('DELETE FROM classes')
    database.run('DELETE FROM templates')

    runPrepared(database, 'INSERT INTO templates (id, name, image_json, config_json, updated_at) VALUES (?, ?, ?, ?, ?)', (statement) => {
      for (const template of normalizedState.templates) {
        statement.run([
          template.id,
          template.name,
          JSON.stringify(template.image),
          JSON.stringify(template.config),
          template.updatedAt
        ])
      }
    })

    runPrepared(database, 'INSERT INTO classes (id, name, id_photo_config_json, created_at) VALUES (?, ?, ?, ?)', (statement) => {
      for (const classItem of normalizedState.classes) {
        statement.run([
          classItem.id,
          classItem.name,
          classItem.idPhotoConfig ? JSON.stringify(classItem.idPhotoConfig) : null,
          classItem.createdAt
        ])
      }
    })

    runPrepared(
      database,
      'INSERT INTO students (id, class_id, name, photo_json, photo_file_name, id_photo_json, id_photo_file_name, id_photo_crop_json, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
      (statement) => {
        for (const classItem of normalizedState.classes) {
          classItem.students.forEach((student, index) => {
            statement.run([
              student.id,
              classItem.id,
              student.name,
              student.photo ? JSON.stringify(student.photo) : null,
              student.photoFileName ?? null,
              student.idPhoto ? JSON.stringify(student.idPhoto) : null,
              student.idPhotoFileName ?? null,
              student.idPhotoCrop ? JSON.stringify(student.idPhotoCrop) : null,
              index
            ])
          })
        }
      }
    )

    runPrepared(
      database,
      'INSERT INTO generated_certificates (id, class_id, student_id, template_id, class_name, student_name, asset_file_name, jpg_url, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
      (statement) => {
        for (const certificate of normalizedState.certificates) {
          statement.run([
            certificate.id,
            certificate.classId,
            certificate.studentId,
            certificate.template.id,
            certificate.className,
            certificate.studentName,
            certificate.asset.fileName,
            certificate.jpgUrl ?? null,
            certificate.updatedAt
          ])
        }
      }
    )

    runPrepared(
      database,
      'INSERT OR REPLACE INTO app_state (id, json, updated_at) VALUES (1, ?, ?)',
      (statement) => {
        statement.run([JSON.stringify(activeState), updatedAt])
      }
    )

    database.run('COMMIT')
  } catch (error) {
    database.run('ROLLBACK')
    throw error
  }

  if (previousAppStateBytes > 1_000_000) {
    database.run('VACUUM')
  }

  persist(database)
}

function loadStructuredState(
  database: SqlJsDatabase,
  savedState: Partial<PersistedAppState> = EMPTY_STATE
): PersistedAppState {
  const templates = rows<{
    id: string
    name: string
    image_json: string
    config_json: string
    updated_at: string
  }>(database, 'SELECT id, name, image_json, config_json, updated_at FROM templates ORDER BY updated_at ASC').map(
    (template) => ({
      id: asString(template.id),
      name: asString(template.name),
      image: json(asString(template.image_json), EMPTY_STATE.templates[0]?.image),
      config: json(asString(template.config_json), EMPTY_STATE.templates[0]?.config),
      updatedAt: asString(template.updated_at)
    })
  )

  const studentsByClass = new Map<string, PersistedAppState['classes'][number]['students']>()
  rows<{
    id: string
    class_id: string
    name: string
    photo_json: string | null
    photo_file_name: string | null
    id_photo_json: string | null
    id_photo_file_name: string | null
    id_photo_crop_json: string | null
    sort_order: number
  }>(
    database,
    'SELECT id, class_id, name, photo_json, photo_file_name, id_photo_json, id_photo_file_name, id_photo_crop_json, sort_order FROM students ORDER BY class_id ASC, sort_order ASC'
  ).forEach((student) => {
    const classId = asString(student.class_id)
    const classStudents = studentsByClass.get(classId) ?? []
    const photoJson = asString(student.photo_json)
    const idPhotoJson = asString(student.id_photo_json)
    const idPhotoCropJson = asString(student.id_photo_crop_json)

    classStudents.push({
      id: asString(student.id),
      name: asString(student.name),
      photo: photoJson ? json(photoJson, undefined) : undefined,
      photoFileName: asString(student.photo_file_name) || undefined,
      idPhoto: idPhotoJson ? json(idPhotoJson, undefined) : undefined,
      idPhotoFileName: asString(student.id_photo_file_name) || undefined,
      idPhotoCrop: idPhotoCropJson ? json(idPhotoCropJson, undefined) : undefined
    })
    studentsByClass.set(classId, classStudents)
  })

  const classes = rows<{
    id: string
    name: string
    id_photo_config_json: string | null
    created_at: string
  }>(database, 'SELECT id, name, id_photo_config_json, created_at FROM classes ORDER BY created_at ASC').map((classItem) => ({
    id: asString(classItem.id),
    name: asString(classItem.name),
    students: studentsByClass.get(asString(classItem.id)) ?? [],
    idPhotoConfig: json(asString(classItem.id_photo_config_json), undefined),
    createdAt: asString(classItem.created_at)
  }))

  const templatesById = new Map(templates.map((template) => [template.id, template]))
  const classesById = new Map(classes.map((classItem) => [classItem.id, classItem]))
  const studentsById = new Map(classes.flatMap((classItem) => classItem.students.map((student) => [student.id, student])))

  const certificates = rows<{
    id: string
    class_id: string
    student_id: string
    template_id: string | null
    class_name: string
    student_name: string
    asset_file_name: string
    jpg_url: string | null
    updated_at: string
  }>(
    database,
    'SELECT id, class_id, student_id, template_id, class_name, student_name, asset_file_name, jpg_url, updated_at FROM generated_certificates ORDER BY updated_at ASC'
  ).flatMap((certificate) => {
    const classId = asString(certificate.class_id)
    const studentId = asString(certificate.student_id)
    const template = templatesById.get(asString(certificate.template_id)) ?? templates[0]
    const student = studentsById.get(studentId)
    const photo = student?.photo

    if (!template || !photo) {
      return []
    }

    return [
      {
        id: asString(certificate.id),
        classId,
        className: asString(certificate.class_name) || classesById.get(classId)?.name || '',
        studentId,
        studentName: asString(certificate.student_name) || student?.name || '',
        photo,
        template,
        asset: {
          fileName: asString(certificate.asset_file_name)
        },
        jpgUrl: asString(certificate.jpg_url) || undefined,
        updatedAt: asString(certificate.updated_at)
      }
    ]
  })

  return {
    templates,
    activeTemplateId: savedState.activeTemplateId ?? templates[0]?.id ?? '',
    classes,
    activeClassId: savedState.activeClassId ?? classes[0]?.id ?? '',
    certificates
  }
}
