import {
  BadRequestException,
  Injectable,
  NotFoundException,
  ServiceUnavailableException,
  StreamableFile,
} from '@nestjs/common'
import { InjectConnection, InjectModel } from '@nestjs/mongoose'
import { FastifyReply, FastifyRequest } from 'fastify'
import { GridFSBucket, ObjectId } from 'mongodb'
import { Connection, Model, mongo } from 'mongoose'
import { Stream } from 'stream'
import { File } from './models/file.entity'

type Request = FastifyRequest
type Response = FastifyReply

@Injectable()
export class AppService {
  private readonly bucket: GridFSBucket

  constructor(
    @InjectModel('fs.files') private readonly fileModel: Model<File>,
    @InjectConnection() private readonly connection: Connection,
  ) {
    this.bucket = new mongo.GridFSBucket(this.connection.db)
  }

  async upload(request: Request): Promise<{ id: string }> {
    return new Promise((resolve, reject) => {
      try {
        request.multipart(
          (field, file: Stream, filename, encoding, mimetype) => {
            const id = new ObjectId()
            const uploadStream = this.bucket.openUploadStreamWithId(
              id,
              filename,
              {
                contentType: mimetype,
              },
            )

            file.on('end', () => {
              resolve({
                id: uploadStream.id.toString(),
              })
            })

            file.pipe(uploadStream)
          },
          (err) => {
            console.error(err)
            reject(new ServiceUnavailableException())
          },
        )
      } catch (e) {
        console.error(e)
        reject(new ServiceUnavailableException())
      }
    })
  }

  async download(
    id: string,
    request: Request,
    response: Response,
  ): Promise<StreamableFile> {
    try {
      if (!ObjectId.isValid(id)) {
        throw new BadRequestException(null, 'InvalidVideoId')
      }

      const oId = new ObjectId(id)
      const fileInfo = await this.fileModel.findOne({ _id: id }).exec()

      if (!fileInfo) {
        throw new NotFoundException(null, 'VideoNotFound')
      }

      if (request.headers.range) {
        const range = request.headers.range.substr(6).split('-')
        const start = parseInt(range[0], 10)
        const end = parseInt(range[1], 10) || null
        const readstream = this.bucket.openDownloadStream(oId, {
          start,
          end,
        })

        response.status(206)
        response.headers({
          'Accept-Ranges': 'bytes',
          'Content-Type': fileInfo.contentType,
          'Content-Range': `bytes ${start}-${end ? end : fileInfo.length - 1}/${
            fileInfo.length
          }`,
          'Content-Length': (end ? end : fileInfo.length) - start,
          'Content-Disposition': `attachment; filename="${fileInfo.filename}"`,
        })

        return new StreamableFile(readstream)
      } else {
        const readstream = this.bucket.openDownloadStream(oId)

        response.status(200)
        response.headers({
          'Accept-Range': 'bytes',
          'Content-Type': fileInfo.contentType,
          'Content-Length': fileInfo.length,
          'Content-Disposition': `attachment; filename="${fileInfo.filename}"`,
        })

        response.send(readstream)
      }
    } catch (e) {
      console.error(e)
      throw new ServiceUnavailableException()
    }
  }

  getList() {
    return this.fileModel.find().exec()
  }
}