import jwt from 'jsonwebtoken'
import { ISocket, SocketEvent, SocketEventHandler } from './types'
import { IUser, User } from '../api/users/models'
import constants from './constants'
import services from './services'
import helpers from './helpers'

interface UserPayload {
  _id: string
  hash: string
}

async function authorize(socket: ISocket, next: (err?: Error | undefined) => void) {
  try {
    const token = socket.handshake.auth.token
    let payload: UserPayload
    try {
      payload = jwt.verify(token, process.env.JWT_SECRET!) as UserPayload
    } catch (error) {
      socket.data.error = { event: 'authorization-failed', data: {message: 'authorization failed'}} 
      return next()
    }
    const { _id, hash } = payload
    const user = await User.findById(_id)
    if (!user) {
      socket.data.error = { event: 'authorization-failed', data: {message: 'authorization failed'} }
      return next()
    }
    if (user.auth.password.slice(-10) !== hash) {
      socket.data.error = { event: 'authorization-failed', data: {message: 'authorization failed'} }
      return next()
    }
    const tokenHash = helpers.getHash(token)
    const device = user.auth.loggedinDevices.find(e => e.tokenHash === tokenHash)
    if(!device) {
      socket.data.error = { event: 'authorization-failed', data: {message: 'authorization failed'} }
      return next()
    }
    if (!user.auth.emailVerified) {
      socket.data.error = { event: 'verification-required', data: {message: 'Please verify your email first'} }
      return next()
    }
    if (user.terminated) {
      socket.data.error = { event: 'account-terminated', data: {message: 'Your account has been terminated due to a violation of our Terms and Conditions and in accordance with our Privacy Policy'} }
      return next()
    }
    socket.data.user = user
    socket.data.device = device
    return next()
  } catch (error) {
    console.error(error)
    socket.data.error = { event: 'server-error', data: {message: 'Server error'} }
    return next()
  }
}

async function onInit(socket: ISocket) {
  try {
    if (socket.data.error) {
      socket.emit('error', socket.data.error)
      console.log(`❌ socket error message sent`)
      console.dir(socket.data.error, {depth: null})
      return socket.disconnect(true)
    }
    const user = socket.data.user as IUser
    console.log(`✅ User connected: ${socket.id} (${user.username ?? user.email ?? user._id})`)
    constants.sockets[`${user._id}`] ??= []
    constants.sockets[`${user._id}`]!.push(socket)
    socket.on('disconnect', () => onDisconnect(socket))
    socket.on('error', (error) => console.error(error))
    socket.onAny((event, data) => onMessage(socket, data))
    if(!user.isManuallyOffline) {
      services.socketNotifyOnUserPresenceUpdated(user, 'online')
    }
    await User.updateOne(
      { _id: user._id },
      { $set: { isOnline: true, lastOnlineAt: new Date() } }
    )
  } catch (error) {
    console.log(error)
  }
}

function onMessage(socket: ISocket, payload: any) {
  try {
    const {event, data} = payload
    console.log(`💬 Socket received: ${event}`, payload)
    const handler = constants.socketHandlers[event as SocketEvent] as SocketEventHandler | undefined
    try {
      handler?.(socket, data)
    } catch (err) {
      console.log(err)
      services.socketErrorNotifyUser(socket, 'server-error', {message: 'server error'})
    }
  } catch (error) {
    console.error(error)
  }
}

async function onDisconnect(socket: ISocket) {
  try {
    const user = socket.data.user as IUser | undefined
    if (!user) {
      return console.log(`❌ User connection failed: ${socket.id}`)
    }
    let sockets = constants.sockets[`${user._id}`] || []
    sockets = sockets.filter(e => e.id !== socket.id)
    constants.sockets[`${user._id}`] = sockets
    if(!sockets.length) {
      delete constants.sockets[`${user._id}`] 
    }
    console.log(`❌ User disconnected: ${socket.id} (${user.username ?? user.email ?? user._id})`)
    await User.updateOne(
      { _id: user._id },
      {
        $set: { lastOnlineAt: new Date() },
        $unset: { isOnline: '' }
      }
    )
    const at = new Date()
    at.setMinutes(at.getMinutes() + constants.maxOnlineVisibilityInMinutes)
    await services.createScheduler('user-offline', at, {user: `${user._id}`})
  } catch (error) {
    console.log(error)
  }
}

const sockets = {
  authorize,
  onInit,
  onMessage,
  onDisconnect
}

export default sockets