import mongoose, { Document } from "mongoose";
import validator from "validator";
import crypto from "crypto";
import jwt from "jsonwebtoken";
import env from "../enviroment/env";
import NotAuthorizedError from "../utils/NotAuthorizedError";
import {
  encryptToken as encryptTokenUtil,
  decryptToken as decryptTokenUtil,
  getEncryptionKey as getEncryptionKeyUtil,
  generateEncryptionKeyMaterial,
  hashPassword,
  comparePassword,
  signAuthTokens,
  signStreamVideoToken,
  signTempAuthToken,
  signEmailVerifyToken,
  signPasswordResetToken,
} from "../utils/userCrypto";

const userSchema = new mongoose.Schema(
  {
    email: {
      type: String,
      required: true,
      trim: true,
      unique: true,
      lowercase: true,
      validate(value: any): any {
        if (!validator.isEmail(value)) {
          throw new Error("Email is invalid");
        } else if (value.length > 320) {
          throw new Error("Email length must be less than 320 characters");
        } else if (value.length < 3) {
          throw new Error("Email length must be at least 3 characters");
        }
      },
    },
    password: {
      type: String,
      trim: true,
      required: true,
      validate(value: any): any {
        if (value.length < 6) {
          throw new Error("Password length must be at least 6 characters");
        } else if (value.length > 256) {
          throw new Error("Password length must be less than 256 characters");
        }
      },
    },
    tokens: [
      {
        token: {
          type: String,
          required: true,
        },
        uuid: {
          type: String,
          required: true,
        },
        time: {
          type: Number,
          required: true,
        },
      },
    ],
    tempTokens: [
      {
        token: {
          type: String,
          required: true,
        },
        uuid: {
          type: String,
          required: true,
        },
        time: {
          type: Number,
          required: true,
        },
      },
    ],
    privateKey: {
      type: String,
    },
    publicKey: {
      type: String,
    },
    emailVerified: {
      type: Boolean,
    },
    emailToken: {
      type: String,
    },
    passwordResetToken: {
      type: String,
    },
    passwordLastModified: Number,
  },
  {
    timestamps: true,
  }
);

export interface UserInterface extends Document {
  _id: mongoose.Types.ObjectId;
  email: string;
  password: string;
  tokens: any[];
  tempTokens: any[];
  privateKey?: string;
  publicKey?: string;
  token?: string;
  emailVerified?: boolean;
  emailToken?: string;
  passwordResetToken?: string;
  passwordLastModified?: number;

  getEncryptionKey: () => Buffer | undefined;
  generateTempAuthToken: () => Promise<any>;
  encryptToken: (tempToken: any, key: any, publicKey: any) => any;
  decryptToken: (encryptedToken: any, key: any, publicKey: any) => any;
  findByCreds: (email: string, password: string) => Promise<UserInterface>;
  generateAuthToken: (
    uuid: string | undefined
  ) => Promise<{ accessToken: string; refreshToken: string }>;
  generateAuthTokenStreamVideo: (uuid: string | undefined) => Promise<string>;
  generateEncryptionKeys: () => Promise<void>;
  changeEncryptionKey: (randomKey: Buffer) => Promise<void>;
  generateEmailVerifyToken: () => Promise<string>;
  generatePasswordResetToken: () => Promise<string>;
}

userSchema.pre("save", async function (this: any, next: any) {
  const user = this;

  if (user.isModified("password")) {
    user.password = await hashPassword(user.password);
  }

  next();
});

userSchema.statics.findByCreds = async (email: string, password: string) => {
  const user = await User.findOne({ email });

  if (!user) {
    throw new NotAuthorizedError("User not found");
  }

  const isMatch = await comparePassword(password, user.password);

  if (!isMatch) {
    throw new NotAuthorizedError("Incorrect password");
  }

  return user;
};

userSchema.methods.toJSON = function () {
  const user = this;

  const userObject = user.toObject();

  delete userObject.password;
  delete userObject.tokens;
  delete userObject.tempTokens;
  delete userObject.privateKey;
  delete userObject.publicKey;

  if (env.emailVerification !== "true") {
    delete userObject.emailVerified;
  } else {
    userObject.emailVerified = user.emailVerified || false;
  }

  return userObject;
};

userSchema.methods.generateAuthTokenStreamVideo = async function (
  uuid: string | undefined
) {
  const user = this;

  const { token, encryptedToken, time } = signStreamVideoToken(user);

  uuid = uuid ? uuid : "unknown";

  await User.updateOne(
    { _id: user._id },
    { $push: { tempTokens: { token: encryptedToken, uuid, time } } }
  );

  return token;
};

userSchema.methods.generateAuthToken = async function (
  uuid: string | undefined
) {
  const user = this;

  const { accessToken, refreshToken, encryptedRefreshToken, time } =
    signAuthTokens(user);

  uuid = uuid ? uuid : "unknown";

  await User.updateOne(
    { _id: user._id },
    {
      $push: {
        tokens: { token: encryptedRefreshToken, uuid, time },
      },
    }
  );

  return { accessToken, refreshToken };
};

userSchema.methods.encryptToken = function (
  token: string,
  key: string,
  iv: any
) {
  return encryptTokenUtil(token, key, iv);
};

userSchema.methods.decryptToken = function (
  encryptedToken: any,
  key: string,
  iv: any
) {
  return decryptTokenUtil(encryptedToken, key, iv);
};

userSchema.methods.generateEncryptionKeys = async function () {
  const user = this;
  const randomKey = crypto.randomBytes(32);

  const { privateKey, publicKey } = generateEncryptionKeyMaterial(
    user.password,
    randomKey
  );

  user.privateKey = privateKey;
  user.publicKey = publicKey;

  await user.save();
};

userSchema.methods.getEncryptionKey = function () {
  return getEncryptionKeyUtil(this);
};

userSchema.methods.changeEncryptionKey = async function (randomKey: Buffer) {
  const user = this;

  const { privateKey, publicKey } = generateEncryptionKeyMaterial(
    user.password,
    randomKey
  );

  user.privateKey = privateKey;
  user.publicKey = publicKey;

  await user.save();
};

userSchema.methods.generateTempAuthToken = async function () {
  const user = this as UserInterface;
  const { token, encryptedToken } = signTempAuthToken(user);

  user.tempTokens = user.tempTokens.concat({ token: encryptedToken });

  await user.save();
  return token;
};

userSchema.methods.generateEmailVerifyToken = async function () {
  const user = this as UserInterface;
  const { token, encryptedToken } = signEmailVerifyToken(user);

  user.emailToken = encryptedToken;

  await user.save();
  return token;
};

userSchema.methods.generatePasswordResetToken = async function () {
  const user = this as UserInterface;
  const { token, encryptedToken } = signPasswordResetToken(user);

  user.passwordResetToken = encryptedToken;

  await user.save();
  return token;
};

userSchema.methods.generateTempAuthTokenVideo = async function (
  cookie: string
) {
  const iv = crypto.randomBytes(16);

  const user = this;
  const token = jwt.sign(
    { _id: user._id.toString(), cookie, iv },
    env.passwordAccess!,
    { expiresIn: "5h" }
  );

  const encryptionKey = user.getEncryptionKey();
  const encryptedToken = user.encryptToken(token, encryptionKey, iv);

  user.tempTokens = user.tempTokens.concat({ token: encryptedToken });

  await user.save();
  return token;
};

const User = mongoose.model<UserInterface>("User", userSchema);

export default User;
module.exports = User;
