import 'regenerator-runtime/runtime'
import path from 'path'
import dotenv from 'dotenv'

dotenv.config({ path: path.join(__dirname, '../.env') })

import http from 'http'
import express from 'express'
import proxy from 'express-http-proxy'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import session from 'express-session'
import connectRedis from 'connect-redis'

import { Server as SocketIO } from 'socket.io'
import { createAdapter } from 'socket.io-redis'
import passportSocketIo from 'passport.socketio'

import morgan from 'morgan'

import logger, { morganStream } from './logger'

import passport from 'passport'
import mongoose from 'mongoose'
import UserModel from './models/user'

import { getMuseumDocument } from './museumStats'

import redisClient, {
  asyncHEXISTS,
  asyncHGET,
  asyncHSET,
  asyncHDEL,
  asyncHGETALL,
  asyncDEL,
} from './redis'

import { authRouter, initPassport } from './auth/passport'

const RedisStore = connectRedis(session)

const mongoHost = process.env.MONGODB_HOST ?? 'localhost'

mongoose.connect(
  `mongodb://${mongoHost}:27017`,
  {
    useCreateIndex: true,
    useNewUrlParser: true,
    useUnifiedTopology: true,
    user: process.env.MONGODB_USER,
    pass: process.env.MONGODB_PASSWORD,
    dbName: 'museum',
  },
  () => {
    logger.info('Connection to MongoDB has been established successfully.')
  },
)

const app = express()
app.set('trust proxy', 1)
const server = http.createServer(app)

asyncDEL('socket')
asyncDEL('transform')
const io = new SocketIO(server, { serveClient: false })

const pubClient = redisClient
const subClient = pubClient.duplicate()
io.adapter(
  createAdapter({
    pubClient,
    subClient,
  }),
)

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser(process.env.SESSION_SECRET))
app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
  }),
)

app.use(passport.initialize())
app.use(passport.session())
initPassport()
app.use('/auth', authRouter)

app.use(morgan('short', { stream: morganStream }))

async function onAuthorizeSuccess(data, accept) {
  if (data.user._id) {
    const userExists = await asyncHEXISTS('socket', String(data.user._id))

    if (userExists === 1) {
      logger.debug(
        `${data.user._id} (${data.user.email}) is already logged in. Kicking...`,
      )
      const socketId = await asyncHGET('socket', String(data.user._id))
      const socket = io.of('/').sockets.get(socketId)
      if (socket) {
        logger.debug(`Disconnecting ${socketId}`)
        socket.disconnect()
      }
    }
    logger.debug(
      `Successful connection to socket.io from ${data.user._id} (${data.user.email})`,
    )
    accept(null, true)
  }
}

function onAuthorizeFail(_, message, error, accept) {
  if (error) throw new Error(message)
  logger.debug('Client failed to connect to socket.io:', message)
  accept(null, false)
}

io.use(
  passportSocketIo.authorize({
    // @ts-ignore
    cookieParser: cookieParser,
    secret: process.env.SESSION_SECRET,
    store: new RedisStore({ client: redisClient }),
    success: onAuthorizeSuccess,
    fail: onAuthorizeFail,
  }),
)

io.on('connection', async (socket) => {
  await asyncHSET('socket', String(socket.request.user._id), String(socket.id))
  logger.debug(`Assigned ${socket.id} to ${socket.request.user._id}`)

  socket.on('transform', async (payload) => {
    await asyncHSET('transform', String(socket.id), JSON.stringify(payload))
  })

  socket.on('disconnect', async () => {
    logger.debug(`User with id ${socket.id} disconnected`)
    const user = await UserModel.findById(socket.request.user._id)

    if (user) {
      const transform = await asyncHGET('transform', String(socket.id))
      user.lastLocation = JSON.parse(transform)
      user.save()
    }

    await asyncHDEL('transform', String(socket.id))
    await asyncHDEL('socket', String(socket.request.user._id))
  })

  socket.on('register-visit', async () => {
    logger.debug(`Registering new new visit from: ${socket.request.user.name}`)
    const museumStats = await getMuseumDocument()
    museumStats.visitCount = museumStats.visitCount + 1

    const socketCount = io.of('/').sockets.size

    if (museumStats.maxSimultaneousVisitsCount < socketCount) {
      museumStats.maxSimultaneousVisitsCount = socketCount
    }
    museumStats.save()

    const user = await UserModel.findById(socket.request.user._id)
    user.visitCount = user.visitCount + 1
    user.save()
  })

  const user = await UserModel.findById(socket.request.user._id)
  if (user) {
    socket.emit('initial-transform', user.lastLocation)
  }
})

async function broadcastTransforms() {
  const transforms = await asyncHGETALL('transform')

  if (transforms) {
    let parsed = {}
    Object.entries(transforms).forEach(([key, value]) => {
      parsed = { ...parsed, [key]: { ...JSON.parse(value) } }
    })

    io.sockets.emit('broadcast-transforms', parsed)
  }

  setTimeout(broadcastTransforms, 30)
}

broadcastTransforms()

if (process.env.NODE_ENV !== 'production') {
  app.use('/', proxy('http://localhost:4000/'))
} else {
  const buildPath = path.resolve(__dirname, '../../client/build')
  const indexHtml = path.join(buildPath, 'index.html')

  app.use(express.static(buildPath))
  app.get('*', (_, res) => res.sendFile(indexHtml))
}

const port = process.env.PORT ?? 3000
server.listen(port, () => {
  logger.info(`Server ready on port ${port}`)
})
