import crypto from "crypto";
import db from "../connections/sqlite";
import { ObjectId } from "mongodb";
import { IUserDB } from "../dbTypes";
import env from "../../enviroment/env";
import NotAuthorizedError from "../../utils/NotAuthorizedError";
import {
  encryptToken,
  decryptToken,
  getEncryptionKey,
  generateEncryptionKeyMaterial,
  hashPassword,
  comparePassword,
  signAuthTokens,
  signStreamVideoToken,
  signTempAuthToken,
  signEmailVerifyToken,
  signPasswordResetToken,
} from "../../utils/userCrypto";

type TokenRecord = { token: string; uuid?: string; time?: number };

class SqliteUserDoc {
  _id: string;
  email: string;
  password: string;
  tokens: TokenRecord[];
  tempTokens: TokenRecord[];
  privateKey?: string;
  publicKey?: string;
  emailVerified?: boolean;
  emailToken?: string;
  passwordResetToken?: string;
  passwordLastModified?: number;
  createdAt: Date;
  updatedAt: Date;

  private originalPasswordHash: string;

  constructor(row: any) {
    this._id = row.id;
    this.email = row.email;
    this.password = row.password;
    this.originalPasswordHash = row.password;
    this.privateKey = row.privateKey || undefined;
    this.publicKey = row.publicKey || undefined;
    this.emailVerified = !!row.emailVerified;
    this.emailToken = row.emailToken || undefined;
    this.passwordResetToken = row.passwordResetToken || undefined;
    this.passwordLastModified = row.passwordLastModified || undefined;
    this.createdAt = new Date(row.createdAt);
    this.updatedAt = new Date(row.updatedAt);

    const tokenRows = db
      .prepare("SELECT * FROM user_tokens WHERE user_id = ?")
      .all(this._id) as any[];

    this.tokens = tokenRows
      .filter((t) => !t.is_temp)
      .map((t) => ({ token: t.token, uuid: t.uuid, time: t.time }));
    this.tempTokens = tokenRows
      .filter((t) => t.is_temp)
      .map((t) => ({ token: t.token, uuid: t.uuid, time: t.time }));
  }

  getEncryptionKey = () => getEncryptionKey(this as any);

  encryptToken = (token: string, key: any, iv: any) =>
    encryptToken(token, key, iv);

  decryptToken = (encryptedToken: any, key: string, iv: any) =>
    decryptToken(encryptedToken, key, iv);

  private persistTokens = () => {
    db.prepare("DELETE FROM user_tokens WHERE user_id = ?").run(this._id);
    const insert = db.prepare(
      "INSERT INTO user_tokens (user_id, token, is_temp, uuid, time) VALUES (?, ?, ?, ?, ?)"
    );
    for (const t of this.tokens) {
      insert.run(this._id, t.token, 0, t.uuid ?? null, t.time ?? null);
    }
    for (const t of this.tempTokens) {
      insert.run(this._id, t.token, 1, t.uuid ?? null, t.time ?? null);
    }
  };

  generateAuthToken = async (uuid: string | undefined) => {
    const { accessToken, refreshToken, encryptedRefreshToken, time } =
      signAuthTokens(this as any);

    this.tokens.push({
      token: encryptedRefreshToken,
      uuid: uuid || "unknown",
      time,
    });
    this.persistTokens();

    return { accessToken, refreshToken };
  };

  generateAuthTokenStreamVideo = async (uuid: string | undefined) => {
    const { token, encryptedToken: enc, time } = signStreamVideoToken(
      this as any
    );

    this.tempTokens.push({ token: enc, uuid: uuid || "unknown", time });
    this.persistTokens();

    return token;
  };

  generateTempAuthToken = async () => {
    const { token, encryptedToken: enc } = signTempAuthToken(this as any);

    this.tempTokens.push({ token: enc });
    this.persistTokens();

    return token;
  };

  generateEncryptionKeys = async () => {
    const randomKey = crypto.randomBytes(32);
    const { privateKey, publicKey } = generateEncryptionKeyMaterial(
      this.password,
      randomKey
    );

    this.privateKey = privateKey;
    this.publicKey = publicKey;

    await this.save();
  };

  changeEncryptionKey = async (randomKey: Buffer) => {
    const { privateKey, publicKey } = generateEncryptionKeyMaterial(
      this.password,
      randomKey
    );

    this.privateKey = privateKey;
    this.publicKey = publicKey;

    await this.save();
  };

  generateEmailVerifyToken = async () => {
    const { token, encryptedToken: enc } = signEmailVerifyToken(this as any);
    this.emailToken = enc;
    await this.save();
    return token;
  };

  generatePasswordResetToken = async () => {
    const { token, encryptedToken: enc } = signPasswordResetToken(
      this as any
    );
    this.passwordResetToken = enc;
    await this.save();
    return token;
  };

  save = async () => {
    if (this.password !== this.originalPasswordHash) {
      this.password = await hashPassword(this.password);
      this.originalPasswordHash = this.password;
    }

    db.prepare(
      `UPDATE users SET email = ?, password = ?, privateKey = ?, publicKey = ?,
       emailVerified = ?, emailToken = ?, passwordResetToken = ?, passwordLastModified = ?,
       updatedAt = ? WHERE id = ?`
    ).run(
      this.email,
      this.password,
      this.privateKey ?? null,
      this.publicKey ?? null,
      this.emailVerified ? 1 : 0,
      this.emailToken ?? null,
      this.passwordResetToken ?? null,
      this.passwordLastModified ?? null,
      new Date().toISOString(),
      this._id
    );

    this.persistTokens();
  };

  toJSON = () => {
    const obj: any = {
      _id: this._id,
      email: this.email,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };

    if (env.emailVerification === "true") {
      obj.emailVerified = this.emailVerified || false;
    }

    return obj;
  };
}

const rowToUser = (row: any) => (row ? new SqliteUserDoc(row) : null);

class UserDB implements IUserDB {
  constructor() {}

  getUserInfo = async (userID: string) => {
    const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userID);
    return rowToUser(row);
  };

  findByEmail = async (email: string) => {
    const row = db.prepare("SELECT * FROM users WHERE email = ?").get(email);
    return rowToUser(row);
  };

  findByCreds = async (email: string, password: string) => {
    const row = db.prepare("SELECT * FROM users WHERE email = ?").get(email);

    if (!row) throw new NotAuthorizedError("User not found");

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

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

    return user;
  };

  createUser = async (email: string, password: string) => {
    const id = new ObjectId().toString();
    const now = new Date().toISOString();
    const hashedPassword = await hashPassword(password);

    db.prepare(
      `INSERT INTO users (id, email, password, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)`
    ).run(id, email, hashedPassword, now, now);

    const row = db.prepare("SELECT * FROM users WHERE id = ?").get(id);
    return rowToUser(row);
  };

  removeOldTokens = async (
    userID: string,
    uuid: string | undefined,
    minusTime: number,
    isTemp: boolean
  ) => {
    uuid = uuid ? uuid : "unknown";

    if (uuid === "unknown") return;

    db.prepare(
      "DELETE FROM user_tokens WHERE user_id = ? AND is_temp = ? AND uuid = ? AND time < ?"
    ).run(userID, isTemp ? 1 : 0, uuid, minusTime);
  };

  removeTempTokenByValue = async (userID: string, token: string) => {
    db.prepare(
      "DELETE FROM user_tokens WHERE user_id = ? AND is_temp = 1 AND token = ?"
    ).run(userID, token);
  };
}

export default UserDB;
