/*
  __WARNING__: Early documentation. This should be treated as notes to
  remind you of how to use the `api/` handlers with this, rather than
  a full guide.

  This is a convenience library that helps with normalizing JSON API
  requests and responses with [Express.js](https://expressjs.com). The
  `API` class contains useful functions for redirecting, building replies,
  sending back errors, authenticating users, and validating forms. It is
  primarily used in writing `api/` handlers.

  ## How `api/` Handlers Run

  There's a command in `commands/api.js` that runs everything, and contains a lot
  of tricks for Express.js that you should study.  You run this with the `bando.js`
  command runner like this:

  ```shell
  node bando.js api
  ```

  This command then imports all of the files in `api/` recursively, as well as
  the `socket/` handlers, and sets them up to handle requests.  Requests are mapped
  by URL to `api/` by taking the `.js` off the file and using that as the URL. So
  `api/running.js` becomes `/api/running`.

  ## Writing `api/` Handlers

  The code in `api/` is mapped to URLs almost directly, so to create a new
  URL just make a file in that directory.  For example, if you want `/api/running`
  as a check if your server is running, then create the file `api/running.js`.

  The easiest way to make this file is to use the `node bando.js djent` command like this:

  ```shell
  node bando.js djent --template static/djenterator/api.js --output api/running.js
  ```

  This will use the template in `static/djenterator/api.js` to craft a starter file
  for your `api/running.js`.  If you want to change the output you can access
  [http://127.0.0.1:5001/admin/#/djenterator/](http://127.0.0.1:5001/admin/#/djenterator/)
  and visually choose the template to change, and edit the `.json` variables file it uses.

  You should study this template output for all of the things you might need, although most
  of it you'll strip out in this little tutorial.

  ## Handling Requests

  To handle a `GET`, `POST`, or other request you simply create an exported function that
  matches the lowercase version of the name:

  ```javascript
  export const get = async (req, res) => {
    // do you thing here
  }
  ```

  In the example file `api/running.js` you just created with `bando.js djent`
  you should see this function.  Next you'll strip out everything that has
  nothing to do with replying with a status.

  ## Using `class API`

  You create the `API` class using the Express `request` and `response`
  objects in your `api/` handler, then use the methods `.reply()`, `.redirect()`,
  or `.error()` to report status of the HTTP request.  Let's strip down
  your generated `api/running.js` file to the minimum necessary:

  ```javascript
  import logging from '../lib/logging.js';
  import { API } from '../lib/api.js';

  const log = logging.create(import.meta.url);

  export const get = async (req, res) => {
    const api = new API(req, res);

    try {
      api.reply(200, { message: "OK"});
    } catch (error) {
      log.error(error);
      api.error(500, error.message || "Internal Server Error");
    }
  }
  ```

  Here you can see I am only leaving logging, `API`, and returning simple replies
  with `api.reply(200, {message: "OK"))` or a more complex error reply if that fails.

  ## Authenticating Requests

  Authentication for a handler is very simple and mostly done for you:

  ```javascript
  // add this after the above get method
  get.authenticated = true;
  ```

  You "tag" any handlers you want protected with authentication by setting the `authenticated`
  variable on them.  The `commands/api.js` runner then knows to demand authentication before
  running this handler.  When it is run you can then use `API.user` to get at the authenticated
  user.

  ## Form Validation

  Validating forms in a server is very important, as you can't trust that any
  browser actually performed the validation.  The `API` class uses the wonderful
  [Laravel validation rules](https://laravel.com/docs/9.x/validation) via the
  very nice [Validation](https://www.npmjs.com/package/Validator) module by
  [jfstn](https://github.com/jfstn/Validator).  These rules are also the same
  as returned by the `lib/ormish.js:Model.validation` method.  That means you
  can:

  1. Get your Model from `lib/models.js`.
  2. Call `TheModel.validation(rules)` with empty '' for any rules you want filled in based on the database schema.
  3. Pass those rules to `API.validate()` to check inputs match the Model's rules.
  4. Return a `API.validation_error()` when they fail.

  Here's an example taken from the `api/register.js` handler, but stipped down to
  the essentials so it only validates a user form then returns the cleaned up form:

  ```javascript
    import { User, Site, } from "../lib/models.js";

    const rules = User.validation({
      email: '',
      full_name: '',
      initials: 'required|alpha|max:3',
      password_repeat: 'required|same:password',
      password: '',
      tos_agree: 'required|boolean|accepted',
    });

    export const post = async (req, res) => {
      const api = new API(req, res);
      const form = api.validate(rules);

      if(!form._valid) {
        return api.validation_error(res, form);
      } else {
        api.clean_form(form, rules);
        api.reply(200, {clean_user: form});
      }
    }
  ```

  ## A Good Validation Pattern

  Validating forms and input is ver important, but you want to do it in a way that
  doesn't interfere with a user's typing, yet still uses the client-side browser to
  speed up the validation feedback.  There's also the problem that _all_ form validation
  systems seem to need the validation rules in both the backend and frontend of the
  application.

  I've found the best pattern to handle all of these is to "validate on first submit, return
  the rules for later submits."  It works like this:

  1. On the first view of the form do _not_ provide any validation feedback. This removes the annoying noise of fields claiming inputs are wrong when they're simply being worked on.  For example, as someone types their email it's annoying to flash it right/wrong until they're done.
  2. Always validate based on the rules, and even better based on the database schema.
  3. When this first validation fails, return a validation failure _and_ return the rules for the browser to use for later attempts.
  4. The browser then has the rules, shows that there was an error, and can then provide immediate fast feedback to the user and block submits until its correct.
  5. This also places the rules in only one place: the backend, where they're absolutely required.



  ## Client Side Validation

  The validation process I described needs one more component on the client side, which is
  demonstrated in the `client/pages/Registration.svelte` page:

  ```javascript
  const register = async () => {
    let [status, data] = await api.post("/api/register", form);

    if(status === 200) {
      $user.new_registration = true;
      push('/');
    } else {
      form = Object.assign(form, data);
    }
  }
  ```

  The `api.post("/api/register", form)` then uses `API.validation_error` to return an
  error response which contains the errors _and_ validation rules that the browser
  should use.  Remember, the `api.post()` above is from `client/api.js:post` while
  the `API.validation_error` is from this file.

  ___BUG___: Currently `client/pages/Registration.svelte` doesn't demonstrate the other part
  of using `client/api.js:validate` to do more validations in the browser.

  ## Going Further

  Once you get this working that's mostly all there is to writing an `api/` handler in
  The Bandolier.  You'll then want to learn how `lib/ormish.js` and `lib/models.js` works,
  and how to create new UIs in `client/` or `admin/`.  On the client side you'll want to
  study the `client/api.js` which is the counterpart to this module, and look at the
  `client/components/FormField.svelte` plus `client/pages/Registration.svelte` for other
  examples.
 */
import Validator from 'Validator';

/*
  Used internally to handle security decisions based on whether someone
  runs the app with `DANGER_ADMIN=1` environment variable set or not.
  Originally I was very loose in how this can be set, with sometimes `DANGER_ADMING=true`
  or `DANGER_ADMIN="1"`, but I've since normalized that it will _only_ work if
  `DANGER_ADMIN==="1"`.
 */
export const developer_admin = process.env.DANGER_ADMIN === "1";

/*
  The main class you use in `api/` handlers. Primarily used like this:

  ```javascript
    import { API } from '../lib/api.js';

    export const get = async (req, res) => {
      const api = new API(req, res);
    }
  ```

  It is a very thin layer over the regular Express.js request/response objects,
  but it's main purpose is to return consistent errors when validation fails.
  If you don't want to use this, then look at `validation_error` to see what
  you need to replicate for form validation.
 */
export class API {

  /*
    Constructor for the `API` class.  Takes the Express.js request/response
    objects.

    + `req Object` -- Express.js Request object.
    + `res Object` -- Express.js Response object.
   */
  constructor(req, res) {
    this.req = req;
    this.res = res;
  }

  /*
    Simply returns a status and JSON data to the browser.  This normalizes
    the type of the response to it's always JSON.  I use this with `return`
    even though it doesn't matter, mostly to make sure the control flow
    exits when I want.  This also helps `eslint` detect when I've missed a
    branch on the returns.  Ultimately Express.js doesn't care about the
    return so it's harmless.


    + `status number` -- The status code as an integer.
    + `data Object` -- The object to encode as JSON. Must be JSON.stringify() capable.
    + ___return___ undefined -- Whatever `res.status().json()` returns.
   */
  reply(status, data) {
    return this.res.status(status).json(data);
  }

  /*
    Redirects the browser with status 301 by default. If you want a different
    status then change the second parameter (which defaults to 301).

    + `url string` -- The URL to redirect the browser to.
    + `status number (301)` -- The redirect status code to use, by default 301.
   */
  redirect(url, status=301) {
    return this.res.redirect(status, url);
  }

  /*
    Returns an error status to the browser.  The `err` parameter can either
    be a plain `string` or an Object.  Since this is an entirely JSON API
    system the function does this:

    1. If it's a string, it wraps it in `{message: err}`.
    2. If it's an object is sends it directly as JSON.

    + `status number` -- Status code for the error.
    + `err (string|Object)` -- Either a string message that will be wrapped, or a raw object.
   */
  error(status, err) {
    if(typeof err === 'string') {
      return this.res.status(status).send({message: err});
    } else {
      return this.res.status(status).json(err);
    }
  }

  /*
    A `get accessor` that returns whether the current user is authenticated
    or not.  Normally you would tag your handler as requiring authentication
    with `get.authenticated = true` but there's some instances when you need
    to do your own auth inside a handler.

    + ___return___ boolean -- whether the user is authenticated
   */
  get user_authenticated() {
    return this.req.user !== undefined;
  }

  /*
    Another `get accessor` that tells you if this user is authenicated _and_ an
    admin.  An admin user is defined by having the `User.admin` field of the `user`
    table in the database set to 1.

    + ___return___ boolean -- whether the user is authenticated and an admin.
   */
  get admin_authenticated() {
    return this.req.user !== undefined && this.req.user.admin === 1;
  }

  /*
    A `get accessor` that retuns the current user object if they're authenticated.

    + ___return___ User -- the currently authenticated user.
   */
  get user() {
    return this.req.user;
  }

  /*
    Validates the rules given using the Laravel rules system.  You can combine
    with with `Model.validation` to generate the rules based on the schema. You
    can use `extra` to pass in a callback to do extra validation. The `extra(form)`
    callback has access to the final result _after_ all validation has ran, so you
    can alter the results how you want.

    ___WARNING___: This documentation needs to be improved to show exactly how to use it.

    + `rules Object` -- Uses the validtion rules system from Laravel. See this module's docs at the top for more information.
    + `extra(form)` --- Callback that takes the validated form and does additional validation or cleaning.
   */
  validate(rules, extra) {
    const form = this.req.method === "GET" ? this.req.query : this.req.body;
    let validation = Validator.make(form, rules);

    // keep the rules as they're needed everywhere
    form._rules = rules;

    // BUG: validator has a bug that considers an empty
    // form valid even if there are required rules
    if(Object.getOwnPropertyNames(form).length === 0) {
      // add in a fake entry in the form
      form["_empty"] = null;
      // validate it
      form._valid = validation.passes();
      // then remove _empty to keep it clean
      delete form["_empty"];
    } else {
      form._valid = validation.passes();
    }

    form._errors = validation.getErrors();
    if(extra) extra(form);
    return form;
  }

  /*
    Returns a properly formatted validation error to the browser. This can then be used by
    the `client/components/FormField.svelte` to display validation errors, and since the
    rules are included in the response you can do further validation in the browser.

    + `res Object` -- The response object from Express.js.
    + `form Object` -- The failed form from `.validate()`.
   */
  validation_error(res, form) {
    return res.status(400).json(form);
  }

  /*
    Cleans a form you've received from the internet to have only fields
    matching `rules`.  You can also give an `extra` list of fields to
    keep.  You combine this with `Model.validation` to create a way to
    remove bad inputs from submitted forms before validation.

    + `form Object` -- The form to clean before validation.
    + `rules Object` -- The rules to base the cleaning on.
    + `extra Array` -- A list of additional fields to keep.
   */
  clean_form(form, rules, extras=[]) {
    // what I wouldn't give for set operations
    for(let [key, rule] of Object.entries(form)) {
      if(!(key in rules)) {
        delete form[key];
      }
    }

    for(let field of extras) {
      delete form[field];
    }
  }
}

/*
  BUG: This is a copy of the `defer()` in `client/helpers.js` but
  really both of these should be in another library.
 */
export const defer = () => {
  let res;
  let rej;

  let promise = new Promise((resolve, reject) => {
    res = resolve;
    rej = reject;
  });

  promise.resolve = res;
  promise.reject = rej;

  return promise;
}
