import * as bcrypt from "bcrypt";
import * as r from "rethinkdb";
import { createModelSchema, primitive, serialize, update } from "serializr";

import { Database } from "./Database";

export interface IUser {
    id: string | undefined;
    username: string;
    name: string;
    passwordHash: string;
}

const HASH_ROUNDS = 10;

export class User implements IUser {
    static readonly tableName = "users";

    id: string | undefined;
    username: string = "";
    name: string = "";
    passwordHash: string = "";

    private db: Database;

    private get table() {
        return this.db.db.table(User.tableName);
    }

    constructor(db: Database, data?: Partial<IUser>) {
        this.db = db;
        if (data) {
            update(this, data);
        }
    }

    static async createTable(db: Database) {
        await db.db.tableCreate(User.tableName).run(db.conn);
        await db.db.table(User.tableName).indexCreate("username").run(db.conn);
    }

    static async loadAll(db: Database): Promise<User[]> {
        const cursor = await db.db.table(User.tableName)
            .run(db.conn);
        const users = await cursor.toArray();
        return users.map((data) => {
            return new User(db, data);
        });
    }

    static async load(db: Database, id: string): Promise<User | null> {
        const data = await db.db.table(User.tableName)
            .get(id)
            .run(db.conn);
        if (data == null) {
            return null;
        }
        return new User(db, data);
    }

    static async loadByUsername(db: Database, username: string): Promise<User | null> {
        const seq = await db.db.table(User.tableName)
            .filter(r.row("username").eq(username))
            .run(db.conn);
        const data = await seq.toArray();
        if (data.length === 0) {
            return null;
        }
        return new User(db, data[0]);
    }

    async create() {
        const data = serialize(this);
        delete data.id;
        await this.table
            .insert(data)
            .run(this.db.conn);
    }

    async createOrUpdate() {
        const data = serialize(this);
        delete data.id;
        const user = this.table.filter(r.row("username").eq(this.username));
        const usernameDoesNotExist = user.isEmpty();
        const a: r.WriteResult = await r.branch(usernameDoesNotExist,
            this.table.insert(data) as r.Expression<any>,
            user.nth(0).update(data) as r.Expression<any>)
            .run(this.db.conn);
        return a.inserted > 0;
    }

    async setPassword(newPassword: string): Promise<void> {
        this.passwordHash = await bcrypt.hash(newPassword, HASH_ROUNDS);
    }

    async comparePassword(password: string): Promise<boolean> {
        return bcrypt.compare(password, this.passwordHash);
    }

    toJSON(): any {
        const data = serialize(this);
        delete data.passwordHash;
        return data;
    }
}

createModelSchema(User, {
    id: primitive(),
    username: primitive(),
    name: primitive(),
    passwordHash: primitive(),
});