const express = require("express")
const router = express.Router()
const Errors = require("../lib/errors.js")
const auth = require("../lib/authorize.js")
const axios = require("axios")
const { User, Session, Theme, Friend, Attachment } = require("../models")
const cryptoRandomString = require("crypto-random-string")
const { Op } = require("sequelize")
const speakeasy = require("speakeasy")
const argon2 = require("argon2")
const UAParser = require("ua-parser-js")
const fs = require("fs")
const path = require("path")
const semver = require("semver")
const multer = require("multer")
const FileType = require("file-type")
const rateLimit = require("express-rate-limit")
const Mailgen = require("mailgen")
const nodemailer = require("nodemailer")

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "usercontent/")
  },
  filename: function (req, file, cb) {
    cb(
      null,
      cryptoRandomString({ length: 32 }) + path.extname(file.originalname)
    )
  }
})

const limiter = rateLimit({
  windowMs: 5 * 60 * 1000,
  max: 2,
  message: Errors.rateLimit,
  standardHeaders: true,
  legacyHeaders: false,
  skipFailedRequests: true,
  keyGenerator: (req, res) => req.user?.id || req.ip
})

const mailLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 1,
  message: Errors.rateLimit,
  standardHeaders: true,
  legacyHeaders: false,
  skipFailedRequests: true,
  keyGenerator: (req, res) => req.user?.id || req.ip
})

const whitelist = [
  "image/png",
  "image/jpeg",
  "image/jpg",
  "image/webp",
  "image/gif"
]

const upload = multer({
  storage: storage,
  limits: { fileSize: 2 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    if (!whitelist.includes(file.mimetype)) {
      return cb(new Error("Only images are allowed"))
    }

    cb(null, true)
  }
})

router.post("/verify/resend", auth, mailLimiter, async (req, res, next) => {
  try {
    if (!req.app.locals.config.emailVerification) {
      throw Errors.invalidParameter("Email verification is disabled")
    }
    if (req.user.emailVerified) {
      throw Errors.invalidParameter("Email is already verified")
    }
    const token = "COLUBRINA-VERIFY-" + cryptoRandomString({ length: 64 })
    await req.user.update({
      emailToken: token
    })
    const mailGenerator = new Mailgen({
      theme: "default",
      product: {
        name: req.app.locals.config.siteName,
        link: req.app.locals.config.corsHostname
      }
    })
    const email = {
      body: {
        name: req.user.username,
        intro: `${req.app.locals.config.siteName} Account Verification`,
        action: {
          instructions: `You are receiving this email because you registered on ${req.app.locals.config.siteName}, please use the link below to verify your account.`,
          button: {
            color: "#1A97FF",
            text: "Account Verification",
            link: req.app.locals.config.corsHostname + "/email/confirm/" + token
          }
        },
        outro: "If you did not register, please disregard this email."
      }
    }
    const emailBody = mailGenerator.generate(email)

    const emailText = mailGenerator.generatePlaintext(email)
    const transporter = nodemailer.createTransport({
      host: req.app.locals.config.emailSMTPHost,
      port: req.app.locals.config.emailSMTPPort,
      secure: req.app.locals.config.emailSMTPSecure,
      auth: {
        user: req.app.locals.config.emailSMTPUser,
        pass: req.app.locals.config.emailSMTPPassword
      }
    })
    let info = await transporter.sendMail({
      from: req.app.locals.config.emailSMTPFrom,
      to: req.user.email,
      subject: "Email Verification - " + req.app.locals.config.siteName,
      text: emailText,
      html: emailBody
    })
    if (info) {
      res.json({ success: true })
    } else {
      throw Errors.mailFail
    }
  } catch (e) {
    next(e)
  }
})

router.post("/verify/confirm/:token", async (req, res, next) => {
  try {
    if (!req.app.locals.config.emailVerification) {
      throw Errors.invalidParameter("Email verification is disabled")
    }
    if (!req.params.token) {
      throw Errors.invalidToken
    }
    const user = await User.findOne({
      where: {
        emailToken: req.params.token,
        emailVerified: false
      }
    })
    if (user) {
      await user.update({
        emailVerified: true,
        emailToken: null
      })
      res.json({ success: true })
    } else {
      throw Errors.invalidToken
    }
  } catch (e) {
    next(e)
  }
})

router.post("/login", async (req, res, next) => {
  async function checkPassword(password, hash) {
    try {
      return await argon2.verify(hash, password)
    } catch {
      console.log("Error")
      return false
    }
  }
  async function generateSession(user) {
    try {
      const ua = UAParser(req.headers["user-agent"])
      let ip = {}
      await axios
        .get("http://ip-api.com/json/ " + req.ip)
        .then((res) => {
          ip = res.data
        })
        .catch(() => {})
      const session = await Session.create({
        userId: user.id,
        session: "COLUBRINA-" + cryptoRandomString({ length: 128 }),
        expiredAt: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 30),
        other: {
          ip: req.ip,
          location: ip.country
            ? `${ip.city} - ${ip.regionName} - ${ip.country}`
            : null,
          isp: ip.isp,
          asn: ip.as,
          browserString: ua.browser.name + " v" + ua.browser.major,
          osString: ua.os.name + " " + ua.os.version,
          browser: ua.browser.name,
          browserVersion: ua.browser.version,
          browserVersionMajor: ua.browser.major,
          os: ua.os.name,
          osVersion: ua.os.version
        }
      })
      res.json({
        session: session.session,
        success: true,
        userId: user.id
      })
    } catch (e) {
      console.log(e)
      return false
    }
  }
  try {
    const user = await User.findOne({
      where: {
        username: req.body.username || ""
      }
    })
    if (user) {
      if (user.banned) throw Errors.banned
      if (await checkPassword(req.body.password, user.password)) {
        if (user.totpEnabled) {
          const verified = speakeasy.totp.verify({
            secret: user.totp,
            encoding: "base32",
            token: req.body.totp || ""
          })
          if (verified) {
            await generateSession(user)
          } else {
            throw Errors.invalidTotp
          }
        } else {
          await generateSession(user)
        }
      } else {
        throw Errors.invalidCredentials
      }
    } else {
      throw Errors.invalidCredentials
    }
  } catch (err) {
    next(err)
  }
})

router.post("/register", limiter, async (req, res, next) => {
  async function generatePassword(password) {
    try {
      return await argon2.hash(password)
    } catch {
      console.log("Error")
      return false
    }
  }
  async function generateSession(user) {
    try {
      const ua = UAParser(req.headers["user-agent"])
      let ip = {}
      await axios
        .get("http://ip-api.com/json/ " + req.ip)
        .then((res) => {
          ip = res.data
        })
        .catch(() => {})
      const session = await Session.create({
        userId: user.id,
        session: "COLUBRINA-" + cryptoRandomString({ length: 128 }),
        expiredAt: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 30),
        other: {
          ip: req.ip,
          location: ip.country
            ? `${ip.city} - ${ip.regionName} - ${ip.country}`
            : null,
          isp: ip.isp,
          asn: ip.as,
          browserString: ua.browser.name + " v" + ua.browser.major,
          osString: ua.os.name + " " + ua.os.version,
          browser: ua.browser.name,
          browserVersion: ua.browser.version,
          browserVersionMajor: ua.browser.major,
          os: ua.os.name,
          osVersion: ua.os.version
        }
      })
      res.json({
        session: session.session,
        success: true,
        userId: user.id
      })
    } catch (e) {
      console.log(e)
      return false
    }
  }
  try {
    if (!req.app.locals.config.allowRegistrations) {
      throw Errors.registrationsDisabled
    }
    if (req.body.password.length < 8) {
      throw Errors.invalidPassword
    }
    const user = await User.create({
      username: req.body.username,
      password: await generatePassword(req.body.password),
      theme: "dark",
      themeId: 1,
      guidedWizard: true,
      name: req.body.username,
      admin: false,
      email: req.body.email,
      font: "Inter",
      status: "offline",
      storedStatus: "online",
      experiments: [],
      avatar: null
    })
    if (user) {
      await generateSession(user)
    } else {
      throw Errors.unknown
    }
  } catch (err) {
    next(err)
  }
})

router.get("/", auth, (req, res, next) => {
  try {
    res.json(req.user)
  } catch {}
})

router.get("/sessions", auth, async (req, res, next) => {
  try {
    const sessions = await Session.findAll({
      where: {
        userId: req.user.id
      },
      attributes: {
        exclude: ["session"]
      }
    })
    res.json(sessions)
  } catch (e) {
    next(e)
  }
})

router.delete("/sessions/:id", auth, async (req, res, next) => {
  try {
    await Session.destroy({
      where: {
        id: req.params.id,
        userId: req.user.id
      }
    })
    res.sendStatus(204)
  } catch (e) {
    next(e)
  }
})

router.post(
  "/settings/avatar",
  auth,
  upload.single("avatar"),
  async (req, res, next) => {
    try {
      const io = req.app.get("io")
      if (req.file) {
        const meta = await FileType.fromFile(req.file.path)
        if (!whitelist.includes(meta.mime)) {
          throw Errors.invalidFileType
        }
        const attachment = await Attachment.create({
          userId: req.user.id,
          type: "avatar",
          attachment: req.file.filename,
          name: req.file.originalname,
          extension: meta.ext,
          size: req.file.size
        })
        await User.update(
          {
            avatar: attachment.attachment
          },
          {
            where: {
              id: req.user.id
            }
          }
        )
        res.json(attachment)
        const friends = await Friend.findAll({
          where: {
            userId: req.user.id
          }
        })
        io.to(req.user.id).emit("userSettings", {
          userId: req.user.id,
          avatar: attachment.attachment,
          status: req.body.status === "invisible" ? "offline" : req.body.status,
          storedStatus: req.body.status
        })
        friends.forEach((friend) => {
          io.to(friend.id).emit("userSettings", {
            userId: req.user.id,
            avatar: attachment.attachment,
            status:
              req.body.status === "invisible" ? "offline" : req.body.status,
            storedStatus: req.body.status
          })
        })
      } else {
        throw Errors.invalidParameter("avatar")
      }
    } catch (e) {
      next(e)
    }
  }
)
router.put("/settings/:type", auth, async (req, res, next) => {
  async function checkPasswordArgon2(password, hash) {
    try {
      return await argon2.verify(hash, password)
    } catch {
      console.log("Error")
      return false
    }
  }
  try {
    const io = req.app.get("io")
    if (req.params.type === "full") {
      await User.update(
        {
          theme: req.body.theme,
          guidedWizard: req.body.guidedWizard,
          font: req.body.font,
          experiments: req.body.experiments,
          username: req.body.username,
          email: req.body.email
        },
        {
          where: {
            id: req.user.id
          }
        }
      )
      res.sendStatus(204)
    } else if (req.params.type === "theme") {
      const theme = await Theme.findOne({
        where: {
          id: req.body.id,
          [Op.or]: [
            {
              userId: req.user.id
            },
            {
              public: true
            }
          ]
        }
      })
      if (theme) {
        await User.update(
          {
            themeId: theme.id,
            accentColor: req.body.accent
          },
          {
            where: {
              id: req.user.id
            }
          }
        )
        res.sendStatus(204)
      } else {
        throw Errors.invalidParameter("Theme", "Invalid theme specified")
      }
    } else if (req.params.type === "totp") {
      if (req.user.totpEnabled && req.body.code) {
        res.json({ enabled: true })
      } else if (!req.user.totpEnabled && req.body.password) {
        const match = await checkPassword(req.body.password)
        if (match) {
          const token = speakeasy.generateSecret({
            name: "Colubrina - " + req.user.username,
            issuer: "Colubrina"
          })
          await User.update(
            {
              totp: token.base32
            },
            {
              where: {
                id: req.user.id
              }
            }
          )
          res.json({ secret: token.base32, enabled: false })
        } else {
          throw Errors.invalidCredentials
        }
      } else {
        throw Errors.invalidParameter("Password or Code")
      }
    } else if (req.params.type === "password") {
      const user = await User.findOne({
        where: {
          id: req.user.id
        }
      })
      const match = await checkPasswordArgon2(req.body.current, user.password)
      if (match) {
        await user.update({
          password: await argon2.hash(req.body.new)
        })
        res.sendStatus(204)
      } else {
        throw Errors.invalidCredentials
      }
    } else if (req.params.type === "communications") {
      const user = await User.findOne({
        where: {
          id: req.user.id
        }
      })
      await user.update({
        privacy: {
          communications: {
            enabled: req.body.enabled,
            outsideTenant: req.body.outsideTenant,
            directMessages: req.body.directMessages,
            friendRequests: req.body.friendRequests
          }
        }
      })
      res.sendStatus(204)
    } else if (req.params.type === "status") {
      const user = await User.findOne({
        where: {
          id: req.user.id
        }
      })
      if (!["online", "away", "busy", "invisible"].includes(req.body.status)) {
        res.json({
          status: user.status,
          storedStatus: user.storedStatus
        })
      } else {
        await user.update({
          storedStatus: req.body.status,
          status: req.body.status === "invisible" ? "offline" : req.body.status
        })
        const friends = await Friend.findAll({
          where: {
            userId: user.id,
            status: "accepted"
          }
        })
        friends.forEach((friend) => {
          io.to(friend.friendId).emit("userStatus", {
            userId: user.id,
            status:
              req.body.status === "invisible" ? "offline" : req.body.status
          })
        })
        io.to(user.id).emit("userStatus", {
          userId: user.id,
          status: req.body.status === "invisible" ? "offline" : req.body.status
        })
        io.to(user.id).emit("userSettings", {
          userId: user.id,
          status: req.body.status === "invisible" ? "offline" : req.body.status,
          storedStatus: req.body.status
        })
        res.json({
          status: req.body.status === "invisible" ? "offline" : req.body.status,
          storedStatus: req.body.status
        })
      }
    } else {
      throw Errors.invalidParameter("Settings type", "Invalid settings type")
    }
  } catch (e) {
    console.log(e)
    next(e)
  }
})

router.post("/reset/send", mailLimiter, async (req, res, next) => {
  try {
    const user = await User.findOne({
      where: {
        email: req.body.email
      }
    })
    if (user) {
      const token = cryptoRandomString({ length: 64 })
      await user.update({
        passwordResetToken: token,
        passwordResetExpiry: Date.now() + 86400000
      })
      const mailGenerator = new Mailgen({
        theme: "default",
        product: {
          name: req.app.locals.config.siteName,
          link: req.app.locals.config.corsHostname
        }
      })
      const email = {
        body: {
          name: user.username,
          intro: `${req.app.locals.config.siteName} Password Reset`,
          action: {
            instructions: `You are receiving this email because you registered on ${req.app.locals.config.siteName}, please use the link below to reset your account's password.`,
            button: {
              color: "#1A97FF",
              text: "Password Reset",
              link: req.app.locals.config.corsHostname + "/reset/" + token
            }
          },
          outro: "If you did not request a password reset, ignore this email."
        }
      }
      const emailBody = mailGenerator.generate(email)

      const emailText = mailGenerator.generatePlaintext(email)
      const transporter = nodemailer.createTransport({
        host: req.app.locals.config.emailSMTPHost,
        port: req.app.locals.config.emailSMTPPort,
        secure: req.app.locals.config.emailSMTPSecure,
        auth: {
          user: req.app.locals.config.emailSMTPUser,
          pass: req.app.locals.config.emailSMTPPassword
        }
      })
      let info = await transporter.sendMail({
        from: req.app.locals.config.emailSMTPFrom,
        to: user.email,
        subject: "Password Reset - " + req.app.locals.config.siteName,
        text: emailText,
        html: emailBody
      })
      res.sendStatus(204)
    } else {
      throw Errors.customMessage(
        "Email not found, please type your email, not your username."
      )
    }
  } catch (e) {
    console.log(e)
    next(e)
  }
})

router.put("/reset", async (req, res, next) => {
  try {
    if (req.body.password && req.body.token) {
      const user = await User.findOne({
        where: {
          passwordResetToken: req.body.token,
          passwordResetExpiry: {
            [Op.gt]: Date.now()
          }
        }
      })
      if (!req.body.token) {
        throw Errors.invalidParameter("Token", "Invalid or expired token")
      }
      if (user) {
        await user.update({
          password: await argon2.hash(req.body.password),
          passwordResetToken: null,
          passwordResetExpiry: null
        })
        res.sendStatus(204)
      } else {
        throw Errors.invalidParameter("Token", "Invalid or expired token")
      }
    } else {
      throw Errors.invalidParameter("Password or Token")
    }
  } catch (e) {
    console.log(e)
    next(e)
  }
})

module.exports = router
