import { knex, Model } from './ormish.js';
import bcrypt from 'bcryptjs';
import {v4 as uuid} from "uuid";
import logging from "./logging.js";
import assert from "assert";
import crypto from "crypto";

const log = logging.create("lib/models.js");

export const RESET_CODE_SIZE = 8; // how big to make the unique email reset code
export const UNSUB_CODE_SIZE = 16; // how big to make the unique email unsubscribe code

export class User extends Model.from_table('user') {

  static async auth(username, password) {
    try {
      const uname = username.toLowerCase();
      const res = await knex('user').where({email: uname}).select();

      if(res === undefined) {
        log.error(res, "Received undefined but knex claims only Array returns.");
        return undefined;
      } else if(res.length > 1) {
        log.error(res, `Multiple accounts under the same email ${uname}`);
        return undefined;
      } else if(res.length === 0) {
        // no user found
        return undefined;
      } else {
        // user found, check password
        const user = res[0];
        const good = bcrypt.compareSync(password, user.password);
        return good ? user : undefined;
      }
    } catch(error) {
      log.error(error);
      return undefined;
    }
  }

  /**
   * Performs all the cleanup and checks needed for a registration. It will
   * ensure that the password and password_repeat are the same, lowercase the email,
   * clean out unwanted attrbiutes with User.clean, generate required keys, etc.
   *
   * @param {Object} attr - attributes usually from a web form
   * @return {Object} undefined or the new user on success
   */
  static async register(attr) {
    let user = undefined;

    if(attr.password != attr.password_repeat) {
      return undefined;
    } else {
      user = User.clean(attr);
    }

    // TODO: force emails to lowercase here rather than in the database?
    user.email = user.email.toLowerCase();

    // validate here?
    let exists = await User.first({email: user.email});

    if(exists) {
      log.error("User exists", user.email);
      return undefined;
    } else {
      // BUG: accepting unfiltered user input
      user.password = User.encrypt_password(user.password);
      user.unsubkey = User.random_hex(UNSUB_CODE_SIZE);

      let res = await knex('user').insert(user);
      user.id = res[0];

      return res.length == 0 ? undefined : user;
    }
  }

  static random_hex(size) {
    assert(size, `random_hex size can't be falsy ${ size }`);
    return crypto.randomBytes(size).toString("hex");
  }

  static encrypt_password(password) {
    assert(password, `Password cannot be falsy: ${ password }`);
    let salt = bcrypt.genSaltSync(10);
    return bcrypt.hashSync(password, salt);
  }

  async emails(setting) {
    // don't change it if it's already set this way
    if(this.unsubscribe !== setting) {
      return await User.update({id: this.id}, {
        unsubscribe: setting, unsubscribed_on: Date.now()
      });
    } else {
      return 1;
    }
  }

  /* Users have many Payments, but Payment has only one User. */
  async payments() {
    return await this.has_many(Payment, { user_id: this.id });
  }
}


export class Payment extends Model.from_table('payment') {
  /* Payment has only one User, but User has many Payments. */
  get user() {
    return this.user_id !== undefined ? this.has_one(User, { id: this.user_id }) : undefined;
  }

  static fake_payment() {
    return Payment.insert({
      system: 'fake',
      status: 'complete',
      internal_id: Payment.gen_internal_id(),
      sys_primary_id: Payment.gen_internal_id(),
      sys_secondary_id: Payment.gen_internal_id(),
      sys_created_on: new Date()
    });
  }


  static gen_internal_id() {
    return uuid();
  }

  static async paid(user) {
    assert(user !== undefined, "Invalid user given to Payment.paid");
    assert(user.id !== undefined, "User object given has an user.id === undefined.");

    // ignore refunded payments
    const payment = await Payment.first(builder => {
      builder
        .whereNotIn("status", ["refunded", "failed", "pending"])
        .where({user_id: user.id})
    }, ["user_id", "system", "status", "status_reason"]);

    // it's paid if there's a payment and it is complete
    const paid = payment !== undefined && payment.status === "complete";

    // this now returns paid and also paid is false with more info about why
    return [paid, payment];
  }
}

export class Media extends Model.from_table("media") {
}


export class Site extends Model.from_table("site") {
  static async get(key) {
    const row = await knex(this.table_name).first().where({key});
    return  row !== undefined ? JSON.parse(row.value) : undefined;
  }

  static async set(key, value) {
    const json_value = JSON.stringify(value);
    return await Site.upsert({key, value: json_value}, "key");
  }

  // TODO: figure out how to get sqlite3 to do a return of the value
  static async increment(key, count) {
    return await knex(this.table_name).where({key}).increment("value", count);
  }

  static async decrement(key, count) {
    return await knex(this.table_name).where({key}).decrement("value", count);
  }
}

export class Livestream extends Model.from_table("livestream") {
  media() {
    return Media.first({id: this.media_id});
  }

  static add_viewers(id) {
    return knex(this.table_name).where({id}).increment('viewer_count', 1);
  }
}

export class Product extends Model.from_table('product') {
}
