import { Arg, Ctx, Mutation, Resolver, Directive } from 'type-graphql'
import { EntityManager, In } from 'typeorm'
import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
import { Domain, getDataSource, getRedirectSubdomainPath, getRepository } from '@things-factory/shell'

import { thumbnail } from '../../controllers/thumbnail'
import { ProcessGroup } from '../process-group/process-group'
import { Process } from './process'
import { ProcessHistory } from './process-history'
import { ProcessPatch, NewProcess } from './process-type'

async function parseJSONFile(uploadedFile: FileUpload): Promise<any> {
  var { createReadStream } = await uploadedFile

  return new Promise((resolve, reject) => {
    const chunks: Uint8Array[] = []

    createReadStream()
      .on('data', (chunk: Uint8Array) => {
        chunks.push(chunk)
      })
      .on('end', () => {
        try {
          const fileContents = Buffer.concat(chunks).toString('utf-8')
          const jsonData = JSON.parse(fileContents)
          resolve(jsonData)
        } catch (error) {
          reject(error)
        }
      })
      .on('error', (error: Error) => {
        reject(error)
      })
  })
}

@Resolver(Process)
export class ProcessMutation {
  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => Process, { description: 'To create new Process' })
  async createProcess(@Arg('process') process: NewProcess, @Ctx() context: ResolverContext): Promise<Process> {
    const { domain, user, notify, tx } = context.state
    const repository = tx.getRepository(Process)
    const groupRepository = tx.getRepository(ProcessGroup)

    const oldProcess: Process = await repository.findOneBy({
      name: process.name,
      domain: { id: domain.id }
    })

    if (oldProcess) {
      throw new Error(context.t('error.process name is already taken', { name: process.name }))
    }

    const newProcess: Process = {
      ...process
    }

    newProcess.thumbnail ||=
      'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* empty thumbnail */

    if (process.groupId) {
      newProcess.group = await groupRepository.findOneBy({
        id: process.groupId
      })
    }

    const created = await repository.save({
      domain,
      ...newProcess,
      state: 'draft',
      creator: user,
      updater: user
    })

    notify &&
      notify({
        mode: 'in-app',
        title: `Process '${created.name}' created`,
        body: `Process '${created.name}' created by ${user.name}\n${created.description}`,
        url: getRedirectSubdomainPath(context, domain.subdomain, `/process-viewer/${created.id}`)
      })

    return created
  }

  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => Process, { description: 'To clone a Process from existing Process' })
  async cloneProcess(
    @Arg('id') id: string,
    @Arg('patch') patch: ProcessPatch,
    @Arg('targetSubdomain') targetSubdomain: string,
    @Arg('targetGroupId', { nullable: true }) targetGroupId: string,
    @Ctx() context: ResolverContext
  ): Promise<Process> {
    const { domain, user, notify, tx } = context.state
    const { t } = context
    const repository = tx.getRepository(Process)

    const process = await repository.findOneBy({ domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id })

    if (!patch.name || (patch.name == process.name && targetSubdomain == domain.subdomain)) {
      throw t('error.name must be unique from the original process', { name: patch.name })
    }

    if (targetSubdomain != domain.subdomain) {
      if ((await repository.count({ where: { domain: { subdomain: targetSubdomain }, name: patch.name } })) > 0) {
        throw t('error.name must be unique from the original process', { name: patch.name })
      }
    }

    var targetDomain = domain
    if (targetDomain && domain.subdomain != targetSubdomain) {
      targetDomain = await tx.getRepository(Domain).findOneBy({ subdomain: targetSubdomain })
      if (!targetDomain) {
        throw `given subdomain(${targetSubdomain}) not found`
      }
    }

    var targetProcessGroup = null
    if (targetGroupId) {
      targetProcessGroup = await tx
        .getRepository(ProcessGroup)
        .findOneBy({ domain: { id: targetDomain.id }, id: targetGroupId })
      if (!targetProcessGroup) {
        throw `given group(${targetGroupId}) in domain(${targetSubdomain}) not found`
      }
    }

    const { id: excluded, ...clone } = process

    const cloned = await repository.save({
      domain: targetDomain,
      ...clone,
      ...patch,
      group: targetProcessGroup,
      version: 0,
      state: 'draft',
      updater: user,
      creator: user
    })

    notify &&
      notify({
        mode: 'in-app',
        title: `Process '${cloned.name}' cloned`,
        body: `Process '${cloned.name}' cloned by ${user.name}\n${cloned.description}`,
        image: getRedirectSubdomainPath(context, targetSubdomain, `/thumbnail/${cloned.id}`),
        url: getRedirectSubdomainPath(context, targetSubdomain, `/process-viewer/${cloned.id}`)
      })

    return cloned
  }

  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => Process, { description: 'To modify Process information' })
  async updateProcess(
    @Arg('id') id: string,
    @Arg('patch') patch: ProcessPatch,
    @Ctx() context: ResolverContext
  ): Promise<Process> {
    const { domain, user, notify, tx } = context.state
    const repository = tx.getRepository(Process)

    const process = await repository.findOne({
      where: { domain: { id: domain.id }, id },
      relations: ['creator']
    })

    if (patch.model) {
      const thumbnailPromise = thumbnail({
        model: patch.model,
        context
      })

      try {
        const thumbnailBase64 = await Promise.race([
          thumbnailPromise,
          new Promise((_, reject) => setTimeout(() => reject(new Error('5 seconds timeout')), 5000))
        ])

        patch.thumbnail = 'data:image/png;base64,' + thumbnailBase64.toString('base64')
      } catch (e) {
        console.warn(`Failed to get thumbnail for '${process.name}' in first 5 seconds`)
        // 5초 안에 썸네일이 생성되지 않았으므로 모델만 저장합니다.
        patch.thumbnail =
          'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* empty thumbnail */

        Promise.race([
          thumbnailPromise,
          new Promise((_, reject) => setTimeout(() => reject(new Error('Thumbnail extended timeout')), 5000))
        ])
          .then(async thumbnailBase64 => {
            /* use new resource manager */
            await getDataSource().transaction(async (tx: EntityManager) => {
              await tx.getRepository(Process).save({
                id: updated.id,
                thumbnail: 'data:image/png;base64,' + thumbnailBase64.toString('base64')
              })
            })
          })
          .catch(error => {
            console.error(`Failed to save thumbnail for '${process.name}' even after extended time:`, error)
          })
      }
    }

    const { groupId, ...patched } = patch

    if (groupId !== undefined) {
      const groupRepository = tx.getRepository(ProcessGroup)
      process.group = groupId
        ? (await groupRepository.findOneBy({
            domain: { id: domain.id },
            id: groupId
          })) || null
        : null
    }

    const updated = await repository.save({
      domain,
      ...process,
      ...patched,
      state: 'draft',
      updater: user
    })

    notify &&
      notify({
        mode: 'in-app',
        title: `Process '${updated.name}' updated`,
        body: `Process '${updated.name}' updated by ${user.name}\n${updated.description}`,
        image: getRedirectSubdomainPath(context, domain.subdomain, `/thumbnail/${updated.id}`),
        url: getRedirectSubdomainPath(context, domain.subdomain, `/process-viewer/${updated.id}`)
      })

    return updated
  }

  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => Process, { description: 'To release a Process' })
  async releaseProcess(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Process> {
    const { domain, user, notify, tx } = context.state
    const repository = tx.getRepository(Process)

    const process = await repository.findOne({
      where: { domain: { id: domain.id }, id },
      relations: ['creator']
    })

    if (!process) {
      throw `Process given id(${id}) is not found`
    }

    if (process.state == 'released') {
      throw `Process given id(${id}) is already released`
    }

    const updated = await repository.save({
      domain,
      ...process,
      version: (process.version || 0) + 1,
      state: 'released',
      updater: user
    })

    notify &&
      notify({
        mode: 'in-app',
        title: `Process '${updated.name}' released`,
        body: `Process '${updated.name}' released by ${user.name}\n${updated.description}`,
        image: getRedirectSubdomainPath(context, domain.subdomain, `/thumbnail/${updated.id}`),
        url: getRedirectSubdomainPath(context, domain.subdomain, `/process-viewer/${updated.id}`)
      })

    return updated
  }

  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => Process, { description: 'To revert Process version' })
  async revertProcessVersion(
    @Arg('id') id: string,
    @Arg('version') version: number,
    @Ctx() context: ResolverContext
  ): Promise<Process> {
    const { domain, user, notify, tx } = context.state
    const repository = tx.getRepository(Process)

    const process = await repository.findOne({
      where: { domain: { id: domain.id }, id },
      relations: ['creator']
    })

    if (!process) {
      throw `Process with id(${id}) is not found`
    }

    const historyRepository = tx.getRepository(ProcessHistory)

    const processHistory = await historyRepository.findOne({
      where: { domain: { id: domain.id }, originalId: id, version },
      order: { version: 'DESC' }
    })

    if (!processHistory) {
      throw `Process with id:version(${id}:${version}) is not found`
    }

    const updated = await repository.save({
      domain,
      ...process,
      model: processHistory.model,
      thumbnail: processHistory.thumbnail,
      state: 'draft',
      updater: user
    })

    notify &&
      notify({
        mode: 'in-app',
        title: `Process '${updated.name}' updated`,
        body: `Process '${updated.name}' updated by ${user.name}\n${updated.description}`,
        image: getRedirectSubdomainPath(context, domain.subdomain, `/thumbnail/${updated.id}`),
        url: getRedirectSubdomainPath(context, domain.subdomain, `/process-viewer/${updated.id}`)
      })

    return updated
  }

  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => Boolean, { description: 'To delete Process' })
  async deleteProcess(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
    const { domain, user, notify, tx } = context.state
    const repository = tx.getRepository(Process)
    const process = await repository.findOneBy({ domain: { id: domain.id }, id })

    const deleted = await repository.softDelete(id)

    notify &&
      notify({
        mode: 'in-app',
        title: `Process '${process.name}' deleted`,
        body: `Process '${process.name}' deleted by ${user.name}\n${process.description}`
      })

    return true
  }

  @Directive('@transaction')
  @Directive('@privilege(category: "process", privilege: "mutation", domainOwnerGranted: true)')
  @Mutation(returns => [Process], { description: 'To import some Processs' })
  async importProcesss(
    @Arg('groupId') groupId: string,
    @Arg('files', () => [GraphQLUpload]) files: FileUpload[],
    @Arg('overwrite') overwrite: boolean,
    @Ctx() context: ResolverContext
  ): Promise<Process[]> {
    const { domain, user, notify, tx } = context.state
    const groupRepository = tx.getRepository(ProcessGroup)
    const processRepository = tx.getRepository(Process)
    const group = await groupRepository.findOneBy({ domain: { id: domain.id }, id: groupId })

    if (!group) {
      throw `ProcessGroup with id(${groupId}) is not found`
    }

    const processs = []

    for (const file of files) {
      const { id, name, description, model, thumbnail } = await parseJSONFile(file)

      var sameNameProcess = await processRepository.findOneBy({ domain: { id: domain.id }, name })
      var sameIdProcess = await processRepository.findOneBy({ id })

      if (overwrite) {
        var process = {} as any

        if (sameIdProcess) {
          if (overwrite && sameIdProcess.domainId != domain.id) {
            throw `Process with id(${id}) is already taken in another domain`
          }

          process = {
            ...sameIdProcess,
            name
          }

          if (sameNameProcess && sameIdProcess.id != sameNameProcess.id) {
            /* 이름 충돌 회피 */
            process.name = `${process.name}(${Date.now()})`
          }
        } else {
          process = {
            id,
            name: sameNameProcess ? `${name}(${Date.now()})` : name,
            version: 0,
            creator: user
          }
        }
      } else {
        process = {
          name,
          version: 0,
          creator: user
        }

        /* ID가 없으면, 사용해도 됨 */
        if (!sameIdProcess) {
          process.id = id
        }

        /* 이름 충돌 회피 */
        if (sameNameProcess) {
          process.name = `${process.name}(${Date.now()})`
        }
      }

      processs.push(
        await processRepository.save({
          ...process,
          domain,
          description,
          model: typeof model != 'string' ? JSON.stringify(model) : model,
          thumbnail,
          group,
          state: 'draft',
          updater: user
        })
      )
    }

    notify &&
      notify({
        mode: 'in-app',
        title: `${processs.length} Process(s) are imported`,
        body: `${processs.length} Process(s) are imported into group ${group.name} by ${user.name}`
      })

    return processs
  }
}
