import { Application, NextFunction, Request, Response } from "express"

export type Handle = (req: Request, res: Response, next: NextFunction) => void
export type Authorize = (privilege: string, action?: number) => Handle
export type MultiAuthorize<T> = (v: T[], privilege: string, action?: number) => Handle
export type Authorizes<T> = (v: T[], privilege: string, action?: number) => Handle
export type Check<T> = (v: T[]) => Handle
export interface Token {
  secret: string
  expires: number
}
export type TokenConfig = Token
export type TokenConf = Token
export interface StatusError {
  status: number
  body: string
}
export type GetToken<P> = (req: Request, res: Response) => Promise<PayloadToken<P>>
export interface StringToken {
  token: string
  error?: string
}
export interface PayloadToken<P> {
  payload?: P
  token?: string
  error?: string
  end?: boolean
}
export function handleToken<P>(
  res: Response,
  secret: string,
  name: string,
  t: StringToken,
  verify: (token: string, secret: string) => Promise<P>,
  buildErr: (err: any) => StatusError,
): Promise<PayloadToken<P>> {
  if (t.error) {
    res.status(401).end(t.error)
    return Promise.resolve({ error: t.error, end: true })
  } else {
    return verify(t.token, secret)
      .then((payload) => {
        res.locals[name] = payload
        return Promise.resolve({ payload, token: t.token })
      })
      .catch((err) => {
        const { status, body } = buildErr(err)
        res.status(status).end(body)
        return Promise.resolve({ token: t.token, error: t.error, end: true })
      })
  }
}
export function fromCookies(req: Request, name: string): StringToken {
  if (!req.cookies) {
    return { token: "", error: `Require cookies` }
  } else {
    const token = req.cookies[name]
    if (!token) {
      return { token: "", error: `Require '${name}' in cookies` }
    } else {
      return { token }
    }
  }
}
export function fromAuthorization(req: Request, prefix: string): StringToken {
  const data = req.headers["authorization"]
  if (data) {
    if (!data.startsWith(prefix)) {
      return { token: "", error: `Authorization must start with '${prefix.trim()}'` }
    } else {
      const token = data.substring(prefix.length)
      return { token }
    }
  } else {
    return { token: "", error: `Require 'Authorization' in header` }
  }
}
export interface TokenHandler<P> {
  getToken(req: Request, res: Response): Promise<PayloadToken<P>>
}
export class LocalsToken<P> implements TokenHandler<P> {
  token: string
  constructor(token?: string) {
    this.token = token ? token : "token"
    this.getToken = this.getToken.bind(this)
  }
  getToken(req: Request, res: Response): Promise<PayloadToken<P>> {
    const payload = res.locals[this.token]
    return Promise.resolve({ payload })
  }
}
// tslint:disable-next-line:max-classes-per-file
export class CookiesToken<P> implements TokenHandler<P> {
  token: string
  payload: string
  buildError: (err: any) => StatusError
  constructor(
    public secret: string,
    public verify: (token: string, secret: string) => Promise<P>,
    buildErr?: (err: any) => StatusError,
    payload?: string,
    token?: string,
  ) {
    this.token = token ? token : "token"
    this.payload = payload ? payload : "token"
    this.buildError = buildErr ? buildErr : buildError
    this.getToken = this.getToken.bind(this)
  }
  getToken(req: Request, res: Response): Promise<PayloadToken<P>> {
    const t = fromCookies(req, this.token)
    return handleToken<P>(res, this.secret, this.payload, t, this.verify, this.buildError)
  }
}
// tslint:disable-next-line:max-classes-per-file
export class AuthorizationToken<P> implements TokenHandler<P> {
  prefix: string
  payload: string
  buildError: (err: any) => StatusError
  constructor(
    public secret: string,
    public verify: (token: string, secret: string) => Promise<P>,
    buildErr?: (err: any) => StatusError,
    payload?: string,
    prefix?: string,
  ) {
    this.prefix = prefix ? prefix : "Bearer "
    this.payload = payload ? payload : "token"
    this.buildError = buildErr ? buildErr : buildError
    this.getToken = this.getToken.bind(this)
  }
  getToken(req: Request, res: Response): Promise<PayloadToken<P>> {
    const t = fromAuthorization(req, this.prefix)
    return handleToken<P>(res, this.secret, this.payload, t, this.verify, this.buildError)
  }
}
// tslint:disable-next-line:max-classes-per-file
export class TokenService<P> implements TokenHandler<P> {
  prefix: string
  token: string
  payload: string
  buildError: (err: any) => StatusError
  constructor(
    public secret: string,
    public verify: (token: string, secret: string) => Promise<P>,
    buildErr?: (err: any) => StatusError,
    payload?: string,
    token?: string,
    prefix?: string,
  ) {
    this.prefix = prefix ? prefix : "Bearer "
    this.token = token ? token : "token"
    this.payload = payload ? payload : "token"
    this.buildError = buildErr ? buildErr : buildError
    this.getToken = this.getToken.bind(this)
  }
  getToken(req: Request, res: Response): Promise<PayloadToken<P>> {
    let t = fromCookies(req, this.token)
    if (t.error) {
      t = fromAuthorization(req, this.prefix)
    }
    return handleToken<P>(res, this.secret, this.payload, t, this.verify, this.buildError)
  }
}
export function useToken<P>(
  secret: string,
  verify: (token: string, secret: string) => Promise<P>,
  buildErr?: (err: any) => StatusError,
  cookie?: boolean,
  payload?: string,
  token?: string,
  prefix?: string,
): GetToken<P> {
  if (cookie === true) {
    return new CookiesToken(secret, verify, buildErr, payload, token).getToken
  } else if (cookie === false) {
    return new AuthorizationToken(secret, verify, buildErr, payload, prefix).getToken
  } else {
    return new TokenService(secret, verify, buildErr, payload, token, prefix).getToken
  }
}
export const getToken = useToken
// tslint:disable-next-line:max-classes-per-file
export class Handler<P> {
  prefix: string
  token: string
  payload: string
  constructor(public secret: string, public verify: (token: string, secret: string) => Promise<P>, prefix?: string, token?: string, payload?: string) {
    this.prefix = prefix ? prefix : "Bearer "
    this.token = token ? token : "token"
    this.payload = payload ? payload : "token"
    this.handle = this.handle.bind(this)
  }
  handle(): (req: Request, res: Response, next: NextFunction) => void {
    return (req: Request, res: Response, next: NextFunction) => {
      let t = fromCookies(req, this.token)
      if (t.error) {
        t = fromAuthorization(req, this.prefix)
      }
      if (t.error) {
        next()
      } else {
        this.verify(t.token, this.secret)
          .then((payload) => {
            res.locals[this.payload] = payload
            next()
          })
          .catch((err) => {
            next()
          })
      }
    }
  }
}
export const AuthorizationHandler = Handler
// tslint:disable-next-line:max-classes-per-file
export class AuthorizationChecker<P> {
  getToken: GetToken<P>
  constructor(gt: GetToken<P>) {
    this.getToken = gt
    this.check = this.check.bind(this)
    this.require = this.require.bind(this)
  }
  require(): Handle {
    return (req: Request, res: Response, next: NextFunction) => {
      this.getToken(req, res).then((t) => {
        if (!t.end) {
          next()
        }
      })
    }
  }
  check(): Handle {
    return this.require()
  }
}
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
}
// tslint:disable-next-line:max-classes-per-file
export class Checker<T, P> {
  key: string
  getToken: GetToken<P>
  constructor(gt: GetToken<P>, key?: string) {
    this.getToken = gt
    this.key = key ? key : "id"
    this.check = this.check.bind(this)
  }
  check(v: T[]): Handle {
    return (req: Request, res: Response, next: NextFunction) => {
      this.getToken(req, res).then((t) => {
        if (!t.end) {
          if (t.payload) {
            const obj = (t.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)
              }
            }
          } else {
            res.status(403).end("Payload cannot be undefined")
          }
        }
      })
    }
  }
}
// tslint:disable-next-line:max-classes-per-file
export class Authorizer<P> {
  payloadId: string
  userId: string
  permissions: string
  exact: boolean
  getToken: GetToken<P>
  buildError: (err: any) => StatusError
  constructor(
    gt: GetToken<P>,
    public privilege: (userId: string, privilegeId: string) => Promise<number>,
    buildErr?: (err: any) => StatusError,
    exact?: boolean,
    payloadId?: string,
    userId?: string,
    permissions?: string,
  ) {
    this.getToken = gt
    this.buildError = buildErr ? buildErr : buildError
    this.payloadId = payloadId ? payloadId : "id"
    this.userId = userId ? userId : "userId"
    this.permissions = permissions ? permissions : "permissions"
    this.exact = exact !== undefined ? exact : true
    this.authorize = this.authorize.bind(this)
  }
  authorize(privilege: string, action?: number): Handle {
    return (req: Request, res: Response, next: NextFunction) => {
      this.getToken(req, res).then((t) => {
        if (!t.end) {
          const payload = t.payload
          if (payload === undefined) {
            res.status(401).end("Payload cannot be undefined")
          } else {
            const userId = (payload as any)[this.payloadId]
            if (!userId) {
              res.status(403).end("Payload must contain " + this.payloadId)
            } else {
              this.privilege(userId, privilege)
                .then((p) => {
                  if (p === none) {
                    res.status(403).end("no permission for " + userId)
                  } else {
                    res.locals[this.userId] = userId
                    res.locals[this.permissions] = p
                    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)
                })
            }
          }
        }
      })
    }
  }
}
export const none = 0
export const read = 1
export const write = 2
export const approve = 4
export const all = 2147483647
// tslint:disable-next-line:max-classes-per-file
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)
}

export function get(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void {
  if (secure) {
    app.get(path, authorize, handle)
  } else {
    app.get(path, handle)
  }
}
export function post(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void {
  if (secure) {
    app.post(path, authorize, handle)
  } else {
    app.post(path, handle)
  }
}
export function put(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void {
  if (secure) {
    app.put(path, authorize, handle)
  } else {
    app.put(path, handle)
  }
}
export function patch(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void {
  if (secure) {
    app.patch(path, authorize, handle)
  } else {
    app.patch(path, handle)
  }
}
export function del(app: Application, path: string, authorize: Handle, handle: Handle, secure?: boolean): void {
  if (secure) {
    app.delete(path, authorize, handle)
  } else {
    app.delete(path, handle)
  }
}
