import config from '../knexfile.cjs';
import knexConfig from 'knex';
import assert from 'assert';
import { attachPaginate } from 'knex-paginate';

export const knex = knexConfig(config.development);

// run the PERF_TRICKS to configure sqlite3 when thing start, really need to make this
// a configuration and only do it with sqlite3, but for now just get this done
if(config.development.client === "sqlite3") {
  const PERF_TRICKS = [
    "pragma journal_mode = WAL", // use a WAL journal to not block writers/readers
    "pragma synchronous = normal", // set concurrency to normal now that we have WAL
    "pragma temp_store = memory", // use RAM to make the temporary indices
    // "pragma mmap_size = 30000000000", // use 30GB of mmap to store DB, not useful in multi-process settings
    // "pragma page_size = 32768", // improve performance only if you store large stuff
     "pragma vacuum",  // give it a vacuum, this could impact startup time significantly
    "pragma optimize", // optimize it, but should be done periodically
  ];

  for(let sql of PERF_TRICKS) {
    await knex.raw(sql);
  }
}

attachPaginate();

export const SCHEMA = {};

const load_schema = async () => {
  const raw = await knex("sqlite_schema").where({"type": "table"}).select(['type', 'name']);
  for(let table of raw) {
    table._columns = await knex(table.name).columnInfo();
    SCHEMA[table.name] = table;
  }
}

await load_schema();

/*
 In some cases (like the generic admin) you need to get validation
 rules but you don't have a specific class to work with.  This function
 is called by Model.validation and you can call it directly to get rules
 for a database table.

 + `name string` - the table name.
 + `rules Object` - default rules with empty "" for the rules you want filled in
 + `all boolean` - set this to true if you want everything
 + `no_id boolean` - defaults to true, set false if you also want the id
 + `return Object` - the resulting rules to use with Validator
 */
export const validation = (name, rules, all=false, no_id=true) => {
  assert(rules, "rules parameter is required and will be modified");
  const schema = SCHEMA[name];
  assert(schema, `There is no schema for table named ${name}. Did you forget to migrate:latest?`);

  for(let [key, opts] of Object.entries(schema._columns)) {
    // most of the time you don't want the id
    if(no_id && key === "id") continue;

    if(all || rules[key] === "") {
      let required = opts.nullable || opts.defaultValue ? "nullable|" : "required|";

      switch(opts.type) {
        case "varchar": // fallthrough
        case "text":
          // some databases have an email type but we'll also look for ones named email
          if(key === "email") {
            rules[key] = required + "email";
          } else {
            rules[key] = required + "string";
          }

          if(opts.maxLength) {
            rules[key] += `|min:0|max:${opts.maxLength}`;
          }
          break;
        case "boolean":
          rules[key] = required + "boolean";
          break;
        case "integer":
        case "float":
          rules[key] = required + "numeric";
          break;
        case "date":
          // BUG: validator doesn't do proper date formatting yet
          // rules[key] = required + "date";
          break;
        case "email":
          // not all databases have this
          rules[key] = required + "email";
          break;
        default:
          rules[key] = required;
      }
    }
  }

  return rules;
}

export class Model {
  constructor(attr) {
    assert(attr, "Must give attributes.");
    Object.assign(this, attr);
  }

  static from(attr, also_remove=undefined) {
    return new this(this.clean(attr, also_remove));
  }

  get schema() {
    return this.constructor.schema;
  }

  static clean(attr, also_remove=undefined) {
    assert(attr, "Must give attributes to clean.");

    let clean_entries = Object.entries(attr)
      .filter(([k,v]) => k in this.schema);

    if(also_remove) also_remove.forEach(k => delete clean_entries[k]);

    return Object.fromEntries(clean_entries);
  }

  get table_name() {
    return this.constructor.table_name;
  }

  /**
   * Returns an object of basic rules meant for lib/api.js:validate
   * based on what's in the database.  It's meant to be an easy to
   * pass in starter which you can augment.  It expects a set of rules
   * with keys you want configured. Any key that's set to an empty string ""
   * will be filled in with a minimum rule to match the database schema.
   *
   * It's designed to be called once at the top of an api/ handler to get
   * a basic set of rules.  You could also run it to print out the rules then
   * simply write the rules directly where you need them.
   *
   * @param rules {Object} - rules specifier
   */
  static validation(rules) {
    return validation(this.table_name, rules);
  }

  async destroy() {
    assert(this.table_name !== undefined, "You must set class variable table_name.");

    await knex(this.table_name).
      where({id: this.id}).
      del();
  }

  async has_one(model, where, columns) {
    assert(where.id !== undefined, `where must at least have id for has_one ${model.table_name} you have ${JSON.stringify(where)}`);
    return await model.first(where, columns);
  }

  async has_many(model, where, columns) {
    return await model.all(where, columns);
  }

  async many_to_many(model, through_table) {
    // SECURITY: doing string interpolation which might allow injecting SQL
    let query = knex(model.table_name).where("id", "in",
      knex(through_table).select(`${model.table_name}_id as id`).where(`${this.table_name}_id`, "=", this.id));

    let rows = await query;

    let results = [];

    for(let r of rows) {
      results.push(new model(await model.clean(r)));
    }

    return results;
  }

  static async count(where, columns) {
    // the knex count api returns a DB specific result, so we need
    // to specify what we want, which is count:
    const spec = { count: columns || ['id']};
    let res = await knex(this.table_name).where(where).count(spec);
    if(res.length == 1) {
      // single result, just give the count
      return res[0].count;
    } else {
      console.warn("Your call to count in", this.table_name, "using where=", where, "columns: ", columns, "returned a weird result:", res);
      return res; // weird result let them deal with it
    }
  }

  static async insert(attr) {
    assert(this.table_name !== undefined, "You must set class variable table_name.");
    assert(attr, `You must give some attr to insert into ${this.table_name}`);

    let res = await knex(this.table_name).insert(attr);
    assert(res, `Failed to get an id from the insert for ${this.table_name}`);

    attr.id = res[0];

    return new this(attr);
  }

  /**
   * Implements an upsert (insert but update on conflict) for Postgres, MySQL, and SQLite3 only.
   *
   * @param { Object } attr - The attributes to insert or update.
   * @param { string } conflict_key - The key that can cause a conflict then update.
   * @param { boolean } merge - Defaults to true and will change the record. false will ignore and not update on conflict.
   * @return { number } - id or undefined
   */
  static async upsert(attr, conflict_key, merge=true) {
    assert(conflict_key !== undefined, `You forgot to set the conflict_key on upsert to table ${this.table_name}`);
    let result = undefined;

    // TODO: allow specifying returns for databases that support it
    if(merge) {
      result = await knex(this.table_name).insert(attr).onConflict(conflict_key).merge();
    } else {
      result = await knex(this.table_name).insert(attr).onConflict(conflict_key).ignore();
    }

    // returns the id of the row or undefined
    return result !== undefined ? result[0] : undefined;
  }

  static async update(where, what) {
    assert(where, "You must give a where options.");
    return knex(this.table_name).where(where).update(what);
  }

  static async first(where, columns) {
    assert(where, "You must give a where options.");
    let attr = undefined;

    if(columns) {
      attr = await knex(this.table_name).column(columns).first().where(where);
    } else {
      attr = await knex(this.table_name).first().where(where);
    }

    return attr !== undefined ? new this(attr) : attr;
  }

  static async delete(where) {
    assert(where, "You must give a where options.");
    return knex(this.table_name).where(where).del();
  }

  static async all(where, columns) {
    assert(where, "You must give a where options.");
    let results = [];

    if(columns) {
      results = await knex(this.table_name).column(columns).where(where).select();
    } else {
      results = await knex(this.table_name).where(where);
    }

    let final = results.map(r => new this(r));
    return final;
  }

  static from_table(table_name) {
    let m = class extends Model { };
    m.table_name = table_name;

    assert(SCHEMA, "schema is not loaded!");
    assert(SCHEMA[table_name], `table named ${table_name} not in SCHEMA: ${ Object.keys(SCHEMA) }`);

    m.schema = SCHEMA[table_name]._columns;
    return m;
  }

  static async exists(where) {
    let res = await knex(this.table_name).select('id').where(where).limit(1).first();
    return res ? res.id : false;
  }
}

export default { knex, SCHEMA, Model };
