import { NextFunction, Request, Response } from 'express';

export interface StatusError {
  status: number;
  body: string;
}
export class Handler<P> {
  buildError: (err: any) => StatusError;
  prefix: string;
  token: string;
  constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, buildErr?: (err: any) => StatusError, prefix?: string, token?: string) {
    this.buildError = (buildErr ? buildErr : buildError);
    this.prefix = (prefix ? prefix : 'Bearer ');
    this.token = (token ? token : 'token');
    this.check = this.check.bind(this);
    this.require = this.require.bind(this);
  }
  check(): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      const data = req.headers['authorization'];
      if (data) {
        if (!data.startsWith(this.prefix)) {
          res.status(401).end(`Authorization must start with '${this.prefix.trim()}'`);
        } else {
          const token = data.substr(this.prefix.length);
          this.verify(token, this.secret).then(payload => {
            res.locals[this.token] = payload;
            next();
          }).catch(err => {
            const { status, body } = this.buildError(err);
            res.status(status).end(body);
          });
        }
      } else {
        next();
      }
    };
  }
  require(): (req: Request, res: Response, next: NextFunction) => void {
    return this.check();
  }
}
export const AuthorizationHandler = Handler;
export class AuthorizationChecker<P> {
  buildError: (err: any) => StatusError;
  prefix: string;
  token: string;
  constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, buildErr?: (err: any) => StatusError, prefix?: string, token?: string) {
    this.buildError = (buildErr ? buildErr : buildError);
    this.prefix = (prefix ? prefix : 'Bearer ');
    this.token = (token ? token : 'token');
    this.check = this.check.bind(this);
    this.require = this.require.bind(this);
  }
  check(): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      const data = req.headers['authorization'];
      if (data) {
        if (!data.startsWith(this.prefix)) {
          res.status(401).end(`Authorization must start with '${this.prefix.trim()}'`);
        } else {
          const token = data.substr(this.prefix.length);
          this.verify(token, this.secret).then(payload => {
            res.locals[this.token] = payload;
            next();
          }).catch(err => {
            const { status, body } = this.buildError(err);
            res.status(status).end(body);
          });
        }
      } else {
        res.status(401).end(`Require 'Authorization' in header`);
      }
    };
  }
  require(): (req: Request, res: Response, next: NextFunction) => void {
    return this.check();
  }
}
export function exist<T>(obj: T | T[], arr: T[]): boolean {
  if (Array.isArray(obj)) {
    for (const o of obj) {
      for (const v of arr) {
        // tslint:disable-next-line:triple-equals
        if (o == v) {
          return true;
        }
      }
    }
  } else {
    for (const v of arr) {
      // tslint:disable-next-line:triple-equals
      if (obj == v) {
        return true;
      }
    }
  }
  return false;
}
export class QuickChecker<T> {
  buildError: (err: any) => StatusError;
  key: string;
  token: string;
  constructor(key?: string, buildErr?: (err: any) => StatusError, token?: string) {
    this.buildError = (buildErr ? buildErr : buildError);
    this.key = (key ? key : 'userId');
    this.token = (token ? token : 'token');
    this.check = this.check.bind(this);
  }
  check(v: T[]): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      const payload: any = res.locals[this.token];
      if (!payload) {
        res.status(401).end('Payload cannot be undefined');
      } else {
        const obj = (payload as any)[this.key];
        if (!obj) {
          res.status(403).end('Payload must contain ' + this.key);
        } else {
          if (exist<T>(obj as any, v)) {
            next();
          } else {
            res.status(403).end('invalid ' + this.key);
          }
        }
      }
    };
  }
}
export class Checker<T, P> {
  buildError: (err: any) => StatusError;
  key: string;
  prefix: string;
  token: string;
  constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, key?: string, buildErr?: (err: any) => StatusError, prefix?: string, token?: string) {
    this.buildError = (buildErr ? buildErr : buildError);
    this.prefix = (prefix ? prefix : 'Bearer ');
    this.key = (key ? key : 'userId');
    this.token = (token ? token : 'token');
    this.check = this.check.bind(this);
  }
  check(v: T[]): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      const data = req.headers['authorization'];
      if (data) {
        if (!data.startsWith(this.prefix)) {
          res.status(401).end(`Authorization must start with '${this.prefix.trim()}'`);
        } else {
          const token = data.substr(this.prefix.length);
          this.verify(token, this.secret).then(payload => {
            if (payload === undefined) {
              res.status(401).end('Payload cannot be undefined');
            } else {
              res.locals[this.token] = payload;
              const obj = (payload as any)[this.key];
              if (!obj) {
                res.status(403).end('Payload must contain ' + this.key);
              } else {
                if (exist<T>(obj as any, v)) {
                  next();
                } else {
                  res.status(403).end('invalid ' + this.key);
                }
              }
            }
          }).catch(err => {
            const { status, body } = this.buildError(err);
            res.status(status).end(body);
          });
        }
      } else {
        res.status(401).end(`Require 'Authorization' in header`);
      }
    };
  }
}
export class MultiAuthorizer<T, P> {
  buildError: (err: any) => StatusError;
  user: string;
  key: string;
  prefix: string;
  exact: boolean;
  token: string;
  constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, public privilege: (userId: string, privilegeId: string) => Promise<number>, key?: string, buildErr?: (err: any) => StatusError, exact?: boolean, user?: string, prefix?: string, token?: string) {
    this.buildError = (buildErr ? buildErr : buildError);
    this.prefix = (prefix ? prefix : 'Bearer ');
    this.user = (user ? user : 'userId');
    this.key = (key ? key : 'userType');
    this.token = (token ? token : 'token');
    this.exact = (exact !== undefined ? exact : true);
    this.authorize = this.authorize.bind(this);
  }
  authorize(v: T[], privilege: string, action?: number): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      const data = req.headers['authorization'];
      if (data) {
        if (!data.startsWith(this.prefix)) {
          res.status(401).end(`Authorization must start with '${this.prefix.trim()}'`);
        } else {
          const token = data.substr(this.prefix.length);
          this.verify(token, this.secret).then(payload => {
            if (payload === undefined) {
              res.status(401).end('Payload cannot be undefined');
            } else {
              res.locals[this.token] = payload;
              const obj = (payload as any)[this.key];
              if (!obj) {
                res.status(403).end('Payload must contain ' + this.key);
              } else {
                if (!exist<T>(obj as any, v)) {
                  res.status(403).end('invalid ' + this.key);
                } else {
                  const userId = (payload as any)[this.user];
                  if (!userId) {
                    res.status(403).end('Payload must contain ' + this.user);
                  } else {
                    this.privilege(userId, privilege).then(p => {
                      if (p === none) {
                        res.status(403).end('no permission for ' + userId);
                      } else {
                        if (!action) {
                          next();
                        } else {
                          if (this.exact) {
                            // tslint:disable-next-line:no-bitwise
                            const sum = action & p;
                            if (sum === action) {
                              return next();
                            } else {
                              res.status(403).end('no permission');
                            }
                          } else {
                            if (p >= action) {
                              return next();
                            } else {
                              res.status(403).end('no permission');
                            }
                          }
                        }
                      }
                    }).catch(err => {
                      const { status, body } = this.buildError(err);
                      res.status(status).end(body);
                    });
                  }
                }
              }
            }
          }).catch(err => {
            const { status, body } = this.buildError(err);
            res.status(status).end(body);
          });
        }
      } else {
        res.status(401).end(`Require 'Authorization' in header`);
      }
    };
  }
}
export class Authorizer<P> {
  buildError: (err: any) => StatusError;
  key: string;
  prefix: string;
  exact: boolean;
  token: string;
  constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, public privilege: (userId: string, privilegeId: string) => Promise<number>, buildErr?: (err: any) => StatusError, exact?: boolean, key?: string, prefix?: string, token?: string) {
    this.buildError = (buildErr ? buildErr : buildError);
    this.prefix = (prefix ? prefix : 'Bearer ');
    this.key = (key ? key : 'userId');
    this.token = (token ? token : 'token');
    this.exact = (exact !== undefined ? exact : true);
    this.authorize = this.authorize.bind(this);
  }
  authorize(privilege: string, action?: number): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      const data = req.headers['authorization'];
      if (data) {
        if (!data.startsWith(this.prefix)) {
          res.status(401).end(`Authorization must start with '${this.prefix.trim()}'`);
        } else {
          const token = data.substr(this.prefix.length);
          this.verify(token, this.secret).then(payload => {
            if (payload === undefined) {
              res.status(401).end('Payload cannot be undefined');
            } else {
              res.locals[this.token] = payload;
              const userId = (payload as any)[this.key];
              if (!userId) {
                res.status(403).end('Payload must contain ' + this.key);
              } else {
                this.privilege(userId, privilege).then(p => {
                  if (p === none) {
                    res.status(403).end('no permission for ' + userId);
                  } else {
                    if (!action) {
                      next();
                    } else {
                      if (this.exact) {
                        // tslint:disable-next-line:no-bitwise
                        const sum = action & p;
                        if (sum === action) {
                          return next();
                        } else {
                          res.status(403).end('no permission');
                        }
                      } else {
                        if (p >= action) {
                          return next();
                        } else {
                          res.status(403).end('no permission');
                        }
                      }
                    }
                  }
                }).catch(err => {
                  const { status, body } = this.buildError(err);
                  res.status(status).end(body);
                });
              }
            }
          }).catch(err => {
            const { status, body } = this.buildError(err);
            res.status(status).end(body);
          });
        }
      } else {
        res.status(401).end(`Require 'Authorization' in header`);
      }
    };
  }
}
export const none = 0;
export const read = 1;
export const write = 2;
export const approve = 4;
export const all = 2147483647;
export class PrivilegeLoader {
  constructor(public sql: string, public query: <T>(sql: string, args?: any[]) => Promise<T[]>) {
    this.privilege = this.privilege.bind(this);
  }
  privilege(userId: string, privilegeId: string): Promise<number> {
    return this.query<any>(this.sql, [userId, privilegeId]).then(v => {
      if (!v || v.length === 0) {
        return none;
      }
      const keys = Object.keys(v[0]);
      if (keys.length === 0) {
        return all;
      }
      const k: string = keys[0];
      let permissions = 0;
      let ok = false;
      for (const p of v) {
        const x = p[k];
        if (typeof x === 'number') {
          // tslint:disable-next-line:no-bitwise
          permissions = permissions | x;
          ok = true;
        }
      }
      return (ok ? permissions : all);
    });
  }
}
export function buildError(err: any): StatusError {
  return { status: 401, body: 'Invalid token: ' + toString(err) };
}
export function toString(err: any): string {
  return (typeof err === 'string' ? err : JSON.stringify(err));
}
