import { HttpStatusCode } from '@peertube/peertube-models'
import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, EventAuditView } from '@server/helpers/audit-logger.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { createHash } from 'crypto'
import {
  apiRateLimiter,
  asyncMiddleware,
  authenticate,
  paginationValidator,
  setDefaultPagination
} from '@server/middlewares/index.js'
import { openapiOperationDoc } from '@server/middlewares/doc.js'
import {
  createEventValidator,
  updateEventValidator,
  deleteEventValidator,
  updateEventStateValidator,
  getEventValidator
} from '@server/middlewares/validators/events.js'
import {
  createEventRegistrationValidator,
  deleteEventRegistrationValidator,
  getEventRegistrationValidator,
  listEventRegistrationsValidator,
  updateEventRegistrationValidator
} from '@server/middlewares/validators/event-registrations.js'
import { EventRegistrationModel } from '@server/models/video/event-registration.js'
import { EventModel, EventState } from '@server/models/video/event.js'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { createReqFiles } from '@server/helpers/express-utils.js'
import { MIMETYPES, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
import { CONFIG } from '@server/initializers/config.js'
import { processImageFromWorker } from '@server/lib/worker/parent-process.js'
import { join } from 'path'
import { ensureDir, remove } from 'fs-extra'
import { readFile } from 'fs/promises'
import { generateImageFilename } from '@server/helpers/image-utils.js'

const auditLogger = auditLoggerFactory('events')
const eventsRouter = express.Router()
const SPECIAL_THUMBNAILS_TMP_DIR = join(CONFIG.STORAGE.TMP_DIR, 'special-thumbnails')

const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)

eventsRouter.use(apiRateLimiter)

// List public events
eventsRouter.get(
  '/',
  openapiOperationDoc({ operationId: 'getEvents' }),
  paginationValidator,
  setDefaultPagination,
  asyncMiddleware(listPublicEvents)
)

eventsRouter.get(
  '/registrations',
  openapiOperationDoc({ operationId: 'getAllEventRegistrations' }),
  authenticate,
  paginationValidator,
  setDefaultPagination,
  asyncMiddleware(listAllEventRegistrations)
)

// Get event by ID (public)
eventsRouter.get(
  '/:id',
  openapiOperationDoc({ operationId: 'getEvent' }),
  getEventValidator,
  asyncMiddleware(getEvent)
)

eventsRouter.get(
  '/:eventId/registrations',
  openapiOperationDoc({ operationId: 'getEventRegistrations' }),
  authenticate,
  paginationValidator,
  setDefaultPagination,
  listEventRegistrationsValidator,
  asyncMiddleware(listEventRegistrations)
)

eventsRouter.get(
  '/:eventId/registrations/:registrationId',
  openapiOperationDoc({ operationId: 'getEventRegistration' }),
  authenticate,
  getEventRegistrationValidator,
  asyncMiddleware(getEventRegistration)
)

eventsRouter.post(
  '/:eventId/registrations',
  openapiOperationDoc({ operationId: 'createEventRegistration' }),
  createEventRegistrationValidator,
  asyncMiddleware(createEventRegistration)
)

eventsRouter.put(
  '/:eventId/registrations/:registrationId',
  openapiOperationDoc({ operationId: 'updateEventRegistration' }),
  authenticate,
  updateEventRegistrationValidator,
  asyncMiddleware(updateEventRegistration)
)

eventsRouter.delete(
  '/:eventId/registrations/:registrationId',
  openapiOperationDoc({ operationId: 'deleteEventRegistration' }),
  authenticate,
  deleteEventRegistrationValidator,
  asyncMiddleware(deleteEventRegistration)
)

// Create event (admin/channel owner only)
eventsRouter.post(
  '/:channelId/events',
  openapiOperationDoc({ operationId: 'createEvent' }),
  authenticate,
  reqThumbnailFile,
  createEventValidator,
  asyncMiddleware(createEvent)
)

// Update event (admin/channel owner only)
eventsRouter.put(
  '/:channelId/events/:eventId',
  openapiOperationDoc({ operationId: 'updateEvent' }),
  authenticate,
  reqThumbnailFile,
  updateEventValidator,
  asyncMiddleware(updateEvent)
)

// Delete event (admin/channel owner only)
eventsRouter.delete(
  '/:channelId/events/:eventId',
  openapiOperationDoc({ operationId: 'deleteEvent' }),
  authenticate,
  deleteEventValidator,
  asyncMiddleware(deleteEvent)
)

// Change event state (admin/channel owner only)
eventsRouter.patch(
  '/:channelId/events/:eventId/state',
  openapiOperationDoc({ operationId: 'updateEventState' }),
  authenticate,
  updateEventStateValidator,
  asyncMiddleware(updateEventState)
)

// List channel events (admin/channel owner only)
eventsRouter.get(
  '/:channelId/events',
  openapiOperationDoc({ operationId: 'getChannelEvents' }),
  authenticate,
  asyncMiddleware(listChannelEvents)
)

async function listPublicEvents (req: express.Request, res: express.Response) {
  const state = req.query.state as string | undefined
  const states = state ? state.split(',').filter(s => s) : undefined
  const events = await EventModel.listPublicEvents(undefined, states)
  const count = events.length

  return res.json(getFormattedObjects(events, count))
}

async function getEvent (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event

  return res.json(event.toFormattedJSON())
}

async function listEventRegistrations (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event

  if (!isEventOwnerOrAdmin(res, event)) {
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this event'
    })
  }

  const { count, rows } = await EventRegistrationModel.listForApi({
    eventId: event.id,
    start: req.query.start as number,
    count: req.query.count as number
  })

  return res.json(getFormattedObjects(rows, count))
}

async function listAllEventRegistrations (req: express.Request, res: express.Response) {
  const user = res.locals.oauth.token.User

  // Only admin allowed (or extend if needed)
  if (user.role !== 0) {
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'Only admin can view all event registrations'
    })
  }

  const { count, rows } = await EventRegistrationModel.findAndCountAll({
    include: [
      {
        model: EventModel,
        attributes: ['id', 'title', 'startTime', 'endTime', 'thumbnailFilename']
      }
    ],
    offset: req.query.start as number,
    limit: req.query.count as number,
    order: [['createdAt', 'DESC']]
  })

  const data = rows.map((row: any) => {
    const json = row.toJSON()

    return {
      ...json,
      event: json.Event
        ? {
            id: json.Event.id,
            title: json.Event.title,
            startTime: json.Event.startTime,
            endTime: json.Event.endTime,
            thumbnailFilename: json.Event.thumbnailFilename
          }
        : null
    }
  })

  return res.json({
    total: count,
    data
  })
}

async function getEventRegistration (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event
  const registration = (res.locals as any).eventRegistration

  if (!isEventOwnerOrAdmin(res, event)) {
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this event'
    })
  }

  return res.json(registration.toFormattedJSON())
}

async function listChannelEvents (req: express.Request, res: express.Response) {
  const { channelId } = req.params
  const channel = await VideoChannelModel.scope([ 'WITH_ACCOUNT' ]).findByPk(channelId)

  if (!channel) {
    return res.fail({
      status: HttpStatusCode.NOT_FOUND_404,
      message: 'Channel not found'
    })
  }

  // Check if user is channel owner or admin
  const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
  if (!isChannelOwner && res.locals.oauth.token.User.role !== 0) { // 0 = admin
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this channel'
    })
  }

  const events = await EventModel.listByChannelId(parseInt(channelId))
  const count = events.length

  return res.json(getFormattedObjects(events, count))
}

async function createEvent (req: express.Request, res: express.Response) {
  const { channelId } = req.params
  const { title, description, startTime, endTime, state, liveUrl } = req.body
  const channel = (res.locals as any).channel
  const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined
  const thumbnailFile = files?.thumbnailfile?.[0]

  // Check if user is channel owner or admin
  const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
  if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this channel'
    })
  }

  const event = await sequelizeTypescript.transaction(async t => {
    let thumbnail: { filename: string, data: Buffer, size: number, mimeType: string, etag: string } | null = null

    if (thumbnailFile) {
      thumbnail = await saveEventThumbnailToTemporaryFile(thumbnailFile.path)
    }

    const created = await EventModel.create(
      {
        title,
        description: description || null,
        startTime: new Date(startTime),
        endTime: new Date(endTime),
        state: state || EventState.ACTIVE,
        liveUrl: liveUrl || null,
        channelId: parseInt(channelId),
        thumbnailFilename: thumbnail?.filename || null,
        thumbnailData: thumbnail?.data || null,
        thumbnailMimeType: thumbnail?.mimeType || null,
        thumbnailSize: thumbnail?.size || null,
        thumbnailEtag: thumbnail?.etag || null
      },
      { transaction: t }
    )

    return created
  })

  auditLogger.create(getAuditIdFromRes(res), new EventAuditView(event))

  return res.status(HttpStatusCode.CREATED_201).json(event.toFormattedJSON())
}

async function updateEvent (req: express.Request, res: express.Response) {
  const { title, description, startTime, endTime, state, liveUrl } = req.body
  const event = (res.locals as any).event
  const channel = (res.locals as any).channel
  const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined
  const thumbnailFile = files?.thumbnailfile?.[0]
  const oldEventAuditView = new EventAuditView(event.toFormattedJSON())

  // Check if user is channel owner or admin
  const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
  if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this channel'
    })
  }

  const updatedEvent = await sequelizeTypescript.transaction(async t => {
    let thumbnailFilename = event.thumbnailFilename

    if (thumbnailFile) {
      const thumbnail = await saveEventThumbnailToTemporaryFile(thumbnailFile.path)
      thumbnailFilename = thumbnail.filename

      Object.assign(event, {
        thumbnailData: thumbnail.data,
        thumbnailMimeType: thumbnail.mimeType,
        thumbnailSize: thumbnail.size,
        thumbnailEtag: thumbnail.etag
      })
    }

    await event.update(
      {
        title: title || event.title,
        description: description !== undefined ? description : event.description,
        startTime: startTime ? new Date(startTime) : event.startTime,
        endTime: endTime ? new Date(endTime) : event.endTime,
        state: state || event.state,
        liveUrl: liveUrl !== undefined ? liveUrl : event.liveUrl,
        thumbnailFilename,
        thumbnailData: event.thumbnailData,
        thumbnailMimeType: event.thumbnailMimeType,
        thumbnailSize: event.thumbnailSize,
        thumbnailEtag: event.thumbnailEtag
      },
      { transaction: t }
    )

    return event
  })

  auditLogger.update(getAuditIdFromRes(res), new EventAuditView(updatedEvent.toFormattedJSON()), oldEventAuditView)

  return res.json(updatedEvent.toFormattedJSON())
}

async function deleteEvent (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event
  const channel = (res.locals as any).channel

  // Check if user is channel owner or admin
  const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
  if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this channel'
    })
  }

  await sequelizeTypescript.transaction(async t => {
    await event.destroy({ transaction: t })
  })

  auditLogger.delete(getAuditIdFromRes(res), new EventAuditView(event.toFormattedJSON()))

  return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

async function saveEventThumbnailToTemporaryFile (sourcePath: string) {
  const thumbnailFilename = generateImageFilename('.jpg')
  const destination = join(SPECIAL_THUMBNAILS_TMP_DIR, thumbnailFilename)

  await ensureDir(SPECIAL_THUMBNAILS_TMP_DIR)

  await processImageFromWorker({
    path: sourcePath,
    destination,
    newSize: THUMBNAILS_SIZE,
    keepOriginal: true
  })

  const data = await readFile(destination)
  await remove(destination).catch(() => undefined)

  return {
    filename: thumbnailFilename,
    data,
    size: data.byteLength,
    mimeType: 'image/jpeg',
    etag: createHash('sha256').update(data).digest('hex')
  }
}

async function updateEventState (req: express.Request, res: express.Response) {
  const { state } = req.body
  const event = (res.locals as any).event
  const channel = (res.locals as any).channel
  const oldEventAuditView = new EventAuditView(event.toFormattedJSON())

  // Check if user is channel owner or admin
  const isChannelOwner = channel.Account.userId === (res.locals as any).oauth.token.User.id
  if (!isChannelOwner && (res.locals as any).oauth.token.User.role !== 0) { // 0 = admin
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to manage this channel'
    })
  }

  await sequelizeTypescript.transaction(async t => {
    await event.update({ state }, { transaction: t })
  })

  auditLogger.update(getAuditIdFromRes(res), new EventAuditView(event.toFormattedJSON()), oldEventAuditView)

  return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

async function createEventRegistration (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event

  const registration = await sequelizeTypescript.transaction(async t => {
    return EventRegistrationModel.create({
      firstName: req.body.firstName,
      lastName: req.body.lastName,
      email: req.body.email,
      phone: req.body.phone,
      companyName: req.body.companyName,
      jobTitle: req.body.jobTitle,
      country: req.body.country,
      eventId: event.id
    }, {
      transaction: t
    })
  })

  return res.status(HttpStatusCode.CREATED_201).json(registration.toFormattedJSON())
}

async function updateEventRegistration (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event
  const registration = (res.locals as any).eventRegistration as EventRegistrationModel

  if (!canManageRegistration(res, event, registration)) {
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to update this registration'
    })
  }

  await sequelizeTypescript.transaction(async t => {
    await registration.update({
      firstName: req.body.firstName ?? registration.firstName,
      lastName: req.body.lastName ?? registration.lastName,
      email: req.body.email ?? registration.email,
      phone: req.body.phone ?? registration.phone,
      companyName: req.body.companyName ?? registration.companyName,
      jobTitle: req.body.jobTitle ?? registration.jobTitle,
      country: req.body.country ?? registration.country
    }, {
      transaction: t
    })
  })

  return res.json(registration.toFormattedJSON())
}

async function deleteEventRegistration (req: express.Request, res: express.Response) {
  const event = (res.locals as any).event
  const registration = (res.locals as any).eventRegistration as EventRegistrationModel

  if (!canManageRegistration(res, event, registration)) {
    return res.fail({
      status: HttpStatusCode.FORBIDDEN_403,
      message: 'User is not allowed to delete this registration'
    })
  }

  await sequelizeTypescript.transaction(async t => {
    await registration.destroy({ transaction: t })
  })

  return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

function isEventOwnerOrAdmin (res: express.Response, event: EventModel) {
  const user = res.locals.oauth.token.User
  const isEventOwner = event.Channel?.Account?.userId === user.id

  return isEventOwner || user.role === 0
}

function canManageRegistration (res: express.Response, event: EventModel, registration: EventRegistrationModel) {
  return isEventOwnerOrAdmin(res, event)
}

export { eventsRouter }
