/* tslint:disable:prefer-const */
import colors from 'colors';
import fs from 'fs';
import glob from 'glob';
import jsonfile from 'jsonfile';
import path from 'path';
import Util from '../util/Util';

import Table from 'cli-table3';

class PermissionsMgr {

  public static async set(args) {

    if (Util.isProjectGraphql()) {
      try {

        if (args._.length < 4) {
          console.warn('Invalid arguments for : xc permissions.set')
          return;
        }


        // @ts-ignore
        let [_, models, users, ...resolvers] = args._;
        models = models.split('.');
        users = users.split('.');

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];

          if (models.includes(modelName) || models[0] === '$') {
            const filePermissions = require(file);

            const roles = this.extractUniqueGqlPolicyRoles(filePermissions);

            if (users[0] === '$') {
              for (const [route, rolesObj] of Object.entries(filePermissions)) {

                if (resolvers[0] === '$=1') {
                  const permObj = roles.reduce((obj, role) => {
                    obj[role] = true;
                    return obj;
                  }, {});

                  Object.assign(rolesObj, permObj)
                  console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')}  resolver: ${route}`);
                } else if (resolvers[0] === '$=0') {
                  const permObj = roles.reduce((obj, role) => {
                    obj[role] = false;
                    return obj;
                  }, {});

                  Object.assign(rolesObj, permObj)
                  console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')}  resolver: ${route}`);
                } else {
                  resolvers.forEach(permission => {
                    const permTuple = permission.split('=')
                    if (route === permTuple[0]) {
                      const permObj = roles.reduce((obj, role) => {
                        const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0);
                        obj[role] = val;
                        return obj;
                      }, {});
                      Object.assign(rolesObj, permObj)
                      console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} , resolver: ${route}`);
                    }
                  })
                }

              }

            } else {


              for (const [route, rolesObj] of Object.entries(filePermissions)) {
                resolvers.forEach(permission => {
                  const permTuple = permission.split('=')
                  if (route === permTuple[0] || permTuple[0] === '$') {
                    const permObj = users.reduce((obj, role) => {
                      const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0);
                      obj[role] = val;
                      return obj;
                    }, {});
                    Object.assign(rolesObj, permObj)
                    console.log(`Setting Permissions for model:${modelName} roles:${users} , resolver: ${route}`);
                  }
                })
              }
            }

            const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`;
            fs.writeFileSync(file, policyFileContent)

          }

        });

      } catch (e) {
        console.error(`Error in xc permissions.set`, e);
      }
    } else {
      try {
        if (args._.length < 4) {
          console.warn('Invalid arguments for : xc permissions.set')
          return;
        }

        // @ts-ignore
        let [_, models, users, ...permissions] = args._;

        models = models.split('.');
        users = users.split('.');

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];
          if (models.includes(modelName) || models[0] === '$') {

            const routesList: Route[] = require(file);
            const roles = this.extractUniqueRoles(routesList);

            if (users[0] === '$') {
              for (const route of routesList) {

                if (permissions[0] === '$=1') {
                  const permObj = roles.reduce((obj, role) => {
                    obj[role] = true;
                    return obj;
                  }, {});
                  Object.assign(route.acl, permObj)
                  console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} method: ${route.type}=true, route: ${route.path}`);
                } else if (permissions[0] === '$=0') {
                  const permObj = roles.reduce((obj, role) => {
                    obj[role] = false;
                    return obj;
                  }, {});
                  Object.assign(route.acl, permObj)
                  console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} method: ${route.type}=false, route: ${route.path}`);
                } else {
                  permissions.forEach(permission => {
                    const permTuple = permission.split('=')
                    const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0);
                    const permObj = roles.reduce((obj, role) => {
                      obj[role] = val;
                      return obj;
                    }, {});
                    if (route.type === permTuple[0]) {
                      Object.assign(route.acl, permObj)
                      console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} method: ${permTuple[0]}=${val}, route: ${route.path}`);
                    }
                  })
                }
              }
            } else {
              for (const route of routesList) {
                permissions.forEach(permission => {
                  const permTuple = permission.split('=')
                  const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0);
                  const permObj = users.reduce((obj, role) => {
                    obj[role] = val;
                    return obj;
                  }, {});

                  if (route.type === permTuple[0]) {
                    Object.assign(route.acl, permObj)
                    console.log(`Setting Permissions for model:${modelName} roles:${users.join(', ')} method: ${permTuple[0]}=${val}, route: ${route.path}`);
                  } else if (permTuple[0] === '*') {
                    Object.assign(route.acl, permObj)
                    console.log(`Setting Permissions for model:${modelName} roles:${users.join(', ')} method: ${route.type}=${val}, route: ${route.path}`);
                  }
                })
              }
            }

            const policyFileContent = `module.exports = ${JSON.stringify(routesList, null, 2)};\n`;
            fs.writeFileSync(file, policyFileContent)

          }
        });
      } catch (e) {
        console.error(`Error in xc permissions.set`, e);
      }
    }
  }

  public static async get(args) {

    if (Util.isProjectGraphql()) {
      try {
        if (args._.length < 2) {
          console.warn('Invalid arguments for : xc permissions.get')
          return;
        }

        let {1: models} = args._;
        models = models.split('.');

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js');


        // instantiate
        let rows: any[] = [];
        let roles: any[] = [];


        glob.sync(policiesPath).forEach((file) => {
          // let filePermissions = require(file);

          const modelName = path.basename(file).split('.')[0];

          if (models.includes(modelName)) {
            const filePermissions = require(file);


            roles = this.extractUniqueGqlPolicyRoles(filePermissions);

            rows.push([{
              colSpan: roles.length + 1,
              content: colors.green(file),
              hAlign: 'center'
            }])

            for (const [route, methods] of Object.entries(filePermissions)) {
              const row: any[] = [{content: route, vAlign: 'center'}];
              for (const role of roles) {
                row.push(methods[role] ? colors.green('✔') : colors.red('x'));
              }
              rows.push(row)
            }
          }
        })


        const table = new Table({
          head: ['Route', ...roles]
        });

        table.push(...rows);
        console.log(table.toString());

      } catch (e) {
        console.error(`Error in xc permissions.get`, e);
      }
    } else {


      try {

        if (args._.length < 2) {
          console.warn('Invalid arguments for : xc permissions.get')
          return;
        }

        let {1: models} = args._;
        models = models.split('.');

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js');


        // instantiate
        const table = new Table({
          head: ['Route', 'Role', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH']
        });


        glob.sync(policiesPath)
          .sort(this.sortFiles)
          .forEach((file) => {
            const modelName = path.basename(file).split('.')[0];

            if (models.includes(modelName)) {
              const routesList: Route[] = require(file);
              const groupedRoutes = this.groupRoutes(routesList);


              // extract unique roles
              const roles = this.extractUniqueRoles(routesList);

              table.push([{
                colSpan: 7,
                content: colors.green(file),
                hAlign: 'center'
              }])


              for (const [routePath, methods] of Object.entries(groupedRoutes)) {
                let i = 0;
                for (const role of roles) {
                  {
                    table.push([...(i++ ? [] : [{
                      content: routePath,
                      rowSpan: roles.length,
                      vAlign: 'center'
                    }]), role,
                      methods?.get?.acl?.[role] ? colors.green('✔') : colors.red('x'),
                      methods?.post?.acl?.[role] ? colors.green('✔') : colors.red('x'),
                      methods?.put?.acl?.[role] ? colors.green('✔') : colors.red('x'),
                      methods?.delete?.acl?.[role] ? colors.green('✔') : colors.red('x'),
                      methods?.patch?.acl?.[role] ? colors.green('✔') : colors.red('x'),
                    ] as any)
                  }
                }
              }

            }

          })

        console.log(table.toString());


      } catch (e) {
        console.error(`Error in xc permissions.get`, e);
      }
    }
  }


  public static async userAdd(args) {

    if (Util.isProjectGraphql()) {

      try {
        if (args._.length < 2) {
          console.warn('Invalid arguments for : xc permissions.userAdd')
          return;
        }

        const {1: user} = args._;

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];


          const filePermissions = require(file);

          const roles = this.extractUniqueGqlPolicyRoles(filePermissions);


          if (roles.includes(user)) {
            console.warn(`${user} already exist in ${modelName} policy`);
            return;
          }

          for (const roles1 of Object.values(filePermissions)) {
            roles1[user] = true;
          }


          console.log(`Adding new role permission for model:${modelName} roles:${user}`);


          const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`;
          fs.writeFileSync(file, policyFileContent)

        });

      } catch (e) {
        console.error(`Error in xc permissions.user.add`, e);
      }

    } else {

      try {
        if (args._.length < 2) {
          console.warn('Invalid arguments for : xc permissions.userAdd')
          return;
        }

        const {1: user} = args._;

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];


          const routes: Route[] = require(file);

          const roles = this.extractUniqueRoles(routes);


          if (roles.includes(user)) {
            console.warn(`${user} already exist in ${modelName} policy`);
            return;
          }

          for (const route of routes) {
            route.acl[user] = true;
          }


          console.log(`Adding new role permission for model:${modelName} roles:${user}`);


          const policyFileContent = `module.exports = ${JSON.stringify(routes, null, 2)};\n`;
          fs.writeFileSync(file, policyFileContent)

        });

      } catch (e) {
        console.error(`Error in xc permissions.user.add`, e);
      }
    }


  }


  public static async userDelete(args) {

    if (Util.isProjectGraphql()) {
      try {
        if (args._.length < 2) {
          console.warn('Invalid arguments for : xc permissions.userAdd')
          return;
        }

        const {1: user} = args._;

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];


          const filePermissions = require(file);


          const roles = this.extractUniqueGqlPolicyRoles(filePermissions);


          if (!roles.includes(user)) {
            console.warn(`${user} not exist in ${modelName} policy`);
            return;
          }

          for (const roles1 of Object.values(filePermissions)) {
            delete roles1[user];
          }


          console.log(`Deleting user permission for model:${modelName} roles:${user}`);


          const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`;
          fs.writeFileSync(file, policyFileContent)

        });

      } catch (e) {
        console.error(`Error in xc permissions.user.delete`, e);
      }
    } else {
      try {
        if (args._.length < 2) {
          console.warn('Invalid arguments for : xc permissions.userAdd')
          return;
        }

        const {1: user} = args._;

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];


          const routes: Route[] = require(file);


          const roles = this.extractUniqueRoles(routes)


          if (!roles.includes(user)) {
            console.warn(`${user} not exist in ${modelName} policy`);
            return;
          }

          for (const route of routes) {
            delete route.acl[user];
          }


          console.log(`Deleting user permission for model:${modelName} roles:${user}`);


          const policyFileContent = `module.exports = ${JSON.stringify(routes, null, 2)};\n`;
          fs.writeFileSync(file, policyFileContent)

        });

      } catch (e) {
        console.error(`Error in xc permissions.user.delete`, e);
      }
    }
  }

  public static async userRename(args) {

    if (Util.isProjectGraphql()) {
      try {
        if (args._.length < 3) {
          console.warn('Invalid arguments for : xc permissions.userAdd')
          return;
        }

        const {1: oldUser, 2: newUser} = args._;

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];


          const filePermissions = require(file);

          const roles = this.extractUniqueGqlPolicyRoles(filePermissions);


          if (!roles.includes(oldUser)) {
            console.warn(`${oldUser} not exist in ${modelName} policy`);
            return;
          }
          if (roles.includes(newUser)) {
            console.warn(`${newUser} is already exist in ${modelName} policy`);
            return;
          }

          for (const roles1 of Object.values(filePermissions)) {
            roles1[newUser] = roles1[oldUser];
            delete roles1[oldUser];
          }


          console.log(`Renaming user permission ${oldUser} to ${newUser} for model:${modelName}`);
          const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`;
          fs.writeFileSync(file, policyFileContent)

        });

      } catch (e) {
        console.error(`Error in xc permissions.user.delete`, e);
      }
    } else {
      try {
        if (args._.length < 3) {
          console.warn('Invalid arguments for : xc permissions.userAdd')
          return;
        }

        const {1: oldUser, 2: newUser} = args._;

        /* get all policies */
        const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js');

        glob.sync(policiesPath).forEach((file) => {

          const modelName = path.basename(file).split('.')[0];


          const routes: Route[] = require(file);

          const roles = this.extractUniqueRoles(routes);

          if (!roles.includes(oldUser)) {
            console.warn(`${oldUser} not exist in ${modelName} policy`);
            return;
          }
          if (roles.includes(newUser)) {
            console.warn(`${newUser} is already exist in ${modelName} policy`);
            return;
          }

          for (const route of routes) {
            route.acl[newUser] = route.acl[oldUser];
            delete route.acl[oldUser];
          }


          console.log(`Renaming user permission ${oldUser} to ${newUser} for model:${modelName}`);
          const policyFileContent = `module.exports = ${JSON.stringify(routes, null, 2)};\n`;
          fs.writeFileSync(file, policyFileContent)

        });

      } catch (e) {
        console.error(`Error in xc permissions.user.delete`, e);
      }
    }
  }

  public static getPolicyPath() {

    const projectConfig = jsonfile.readFileSync(path.join(process.cwd(), 'config.xc.json'));
    return projectConfig.meta.projectType === 'rest' ? 'routers' : 'resolvers';
  }

  private static extractUniqueGqlPolicyRoles(filePermissions) {
    return Object.values(filePermissions)
      .flatMap(roles1 => Object.keys(roles1))
      .filter((v, i, arr) => arr.indexOf(v) === i);
  }

  private static extractUniqueRoles(routesList: Route[]) {
    const roles = routesList
      .flatMap(
        route => Object.keys(route.acl)
      )
      .filter((v, i, arr) => arr.indexOf(v) === i);
    return roles;
  }


  private static groupRoutes(routes: Route[]): GroupedRoutes {
    const groupedRoutes: GroupedRoutes = {};
    for (const route of routes) {
      groupedRoutes[route.path] = groupedRoutes[route.path] || {};
      groupedRoutes[route.path][route.type] = route;
    }
    return groupedRoutes;
  }

  private static sortFiles(file1: string, file2: string): number {
    return ((file1.indexOf('.bt.') > -1 ? 1 : 0) || (file1.indexOf('.hm.') > -1 ? 2 : 0))
      - ((file2.indexOf('.bt.') > -1 ? 1 : 0) || (file2.indexOf('.hm.') > -1 ? 2 : 0));
  }
}


export default PermissionsMgr;

export interface Route {
  type: string;
  path: string;
  acl: {
    [key: string]: boolean
  };
}

type httpMethods = 'get' | 'post' | 'patch' | 'put' | 'delete';

export interface GroupedRoutes {
  [key: string]: {
    [key in httpMethods]?: {
      type: string;
      path: string;
      acl: {
        [key: string]: boolean
      };
    }
  }
}
/**
 * @copyright Copyright (c) 2021, Xgene Cloud Ltd
 *
 * @author Naveen MR <oof1lab@gmail.com>
 * @author Pranav C Balan <pranavxc@gmail.com>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */
