import {NcConfig} from "../../interface/config";
import Noco from "./Noco";
import {Router} from "express";
import {RestApiBuilder} from "./rest/RestApiBuilder";
import {GqlApiBuilder} from "./gql/GqlApiBuilder";

import {SqlClientFactory, Tele} from 'nc-help';
import path from "path";
import fs from "fs";
import Migrator from '../migrator/SqlMigrator/lib/KnexMigrator';
import axios from "axios";
import {XCEeError} from "./meta/NcMetaMgr";

export default class NcProjectBuilder {

  public readonly id: string;
  public readonly title: string;
  public readonly description: string;
  public readonly router: Router;
  public readonly apiBuilders: Array<RestApiBuilder | GqlApiBuilder> = [];
  public readonly config: any;

  protected startTime;
  protected app: Noco;
  protected appConfig: NcConfig;
  protected apiInfInfoList: any[] = [];
  protected aggregatedApiInfo: any;
  protected authHook: any;

  constructor(app: Noco, appConfig: NcConfig, project: any) {
    this.app = app;
    this.appConfig = appConfig;

    if (project) {
      this.id = project.id;
      this.title = project.title;
      this.description = project.description;
      this.config = {...this.appConfig, ...JSON.parse(project.config)};
      this.router = Router();
    }
  }


  public async init(isFirstTime?: boolean) {

    try {

      await this.addAuthHookToMiddleware();

      this.startTime = Date.now();
      const allRoutesInfo: any[] = [];
      await this.app.ncMeta.projectStatusUpdate(this.title, 'starting');
      await this.syncMigration();
      await this._createApiBuilder();
      this.initApiInfoRoute();

      /* Create REST APIs / GraphQL Resolvers */
      for (const meta of this.apiBuilders) {

        let routeInfo;
        if (meta instanceof RestApiBuilder) {
          console.log(`Creating REST APIs ${meta.getDbType()} - > ${meta.getDbName()}`);
          routeInfo = await (meta as RestApiBuilder).loadRoutes(null);
        } else if (meta instanceof GqlApiBuilder) {
          console.log(`Creating GraphQL APIs ${meta.getDbType()} - > ${meta.getDbName()}`);
          routeInfo = await (meta as GqlApiBuilder).loadResolvers(null);
        }
        allRoutesInfo.push(routeInfo);
        this.progress(routeInfo, allRoutesInfo, isFirstTime);
      }

      this.app.projectRouter.use(`/nc/${this.id}`, this.router);
      await this.app.ncMeta.projectStatusUpdate(this.title, 'started');

    } catch (e) {
      console.log(e);
      await this.app.ncMeta.projectStatusUpdate(this.title, 'stopped');
    }
  }


  public async handleRunTimeChanges(data: any): Promise<any> {
    const curBuilder = this.apiBuilders.find(builder => {
      return (data.req?.dbAlias || data.req?.args?.dbAlias) === builder.getDbAlias();
    });

    switch (data?.req?.api) {

      case 'xcAuthHookSet':
        this.authHook = await this.app.ncMeta.metaGet(this.id, 'db', 'nc_hooks', {
          type: 'AUTH_MIDDLEWARE'
        });
        break;

      case 'relationCreate':
        await curBuilder.onRelationCreate(data.req.args.parentTable, data.req.args.childTable, data.req.args);

        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'RELATION',
          op_sub_type: 'CREATED',
          user: data.user.email,
          description: `created relation between tables ${data.req.args.childTable} and ${data.req.args.parentTable} `,
          ip: data.ctx.req.clientIp
        });
        console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
        break;

      case 'relationDelete':
        await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, data.req.args);
        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'RELATION',
          op_sub_type: 'DELETED',
          user: data.user.email,
          description: `deleted relation between tables ${data.req.args.childTable} and ${data.req.args.parentTable} `,
          ip: data.ctx.req.clientIp
        });
        console.log(`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
        break;


      case 'xcVirtualRelationCreate':
        await curBuilder.onRelationCreate(data.req.args.parentTable, data.req.args.childTable, {
          ...data.req.args,
          virtual: true
        });
        console.log(`Added new relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
        break;

      case 'xcVirtualRelationDelete':
        await curBuilder.onRelationDelete(data.req.args.parentTable, data.req.args.childTable, {
          ...data.req.args,
          virtual: true
        });
        console.log(`Deleted relation between : ${data.req.args.parentTable} ==> ${data.req.args.childTable}`)
        break;


      case 'tableCreate':
        await curBuilder.onTableCreate(data.req.args.tn, data.req.args);

        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'TABLE',
          op_sub_type: 'CREATED',
          user: data.user.email,
          description: `created table ${data.req.args.tn} with alias ${data.req.args._tn}  `,
          ip: data.ctx.req.clientIp
        })
        console.log(`Added new routes for table : ${data.req.args.tn}`)
        break;

      case 'viewCreate':
        await curBuilder.onViewCreate(data.req.args.view_name, data.req.args);
        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'VIEW',
          op_sub_type: 'CREATED',
          user: data.user.email,
          description: `created view ${data.req.args.view_name} `, ip: data.ctx.req.clientIp
        })
        console.log(`Added new routes for table : ${data.req.args.tn}`)
        break;

      case 'viewUpdate':
        await curBuilder.onViewUpdate(data.req.args.view_name);
        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'VIEW',
          op_sub_type: 'UPDATED',
          user: data.user.email,
          description: `updated view ${data.req.args.view_name} `, ip: data.ctx.req.clientIp
        })
        console.log(`Added new routes for table : ${data.req.args.tn}`)
        break;

      case 'tableDelete':
        await curBuilder.onTableDelete(data.req.args.tn);
        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'TABLE',
          op_sub_type: 'DELETED',
          user: data.user.email,
          description: `deleted table ${data.req.args.tn} `, ip: data.ctx.req.clientIp
        })
        console.log(`Deleted routes for table : ${data.req.args.tn}`)
        break;

      case 'tableRename':
        await curBuilder.onTableRename(data.req.args.tn_old, data.req.args.tn);

        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'TABLE',
          op_sub_type: 'RENAMED',
          user: data.user.email,
          description: `renamed table ${data.req.args.tn_old} to  ${data.req.args.tn}  `,
          ip: data.ctx.req.clientIp
        })
        console.log(`Updated routes for table : ${data.req.args.tn}`)
        break;


      case 'xcRoutesHandlerUpdate':
      case 'xcResolverHandlerUpdate':
      case 'xcRpcHandlerUpdate':
        // todo: implement separate function
        await curBuilder.onHandlerCodeUpdate(data.req.args.tn);
        console.log(`Updated routes handler for table : ${data.req.tn}`)
        break;


      case 'xcRoutesMiddlewareUpdate':
      case 'xcResolverMiddlewareUpdate':
        // todo: implement separate function
        await curBuilder.onMiddlewareCodeUpdate(data.req.args.tn);
        console.log(`Updated routes handler for table : ${data.req.args.tn}`)
        break;

      case 'xcModelSet':
        await curBuilder.onValidationUpdate(data.req.args.tn);
        console.log(`Updated validations for table : ${data.req.args.tn}`)
        break;

      case 'xcModelSchemaSet':
        await curBuilder.onGqlSchemaUpdate(data.req.args.tn, data.req.args.schema);
        console.log(`Updated validations for table : ${data.req.args.tn}`)
        break;


      case 'tableXcHooksSet':
        await curBuilder.onHooksUpdate(data.req.args.tn);
        console.log(`Updated validations for table : ${data.req.args.tn}`)
        break;

      case 'xcModelSwaggerDocSet':
        await (curBuilder as RestApiBuilder).onSwaggerDocUpdate(data.req.args.tn);
        console.log(`Updated validations for table : ${data.req.args.tn}`)
        break;

      case 'tableUpdate':
        await curBuilder.onTableUpdate(data.req.args);
        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'TABLE',
          op_sub_type: 'UPDATED',
          user: data.user.email,
          description: `updated table ${data.req.args.tn} with alias ${data.req.args._tn} `,
          ip: data.ctx.req.clientIp
        })
        console.log(`Updated validations for table : ${data.req.args.tn}`)
        break;

      case 'procedureCreate':
        await curBuilder.onProcedureCreate(data.req.args.procedure_name)
        break;

      case 'functionUpdate':
        await curBuilder.onFunctionDelete(data.req.args.function_name)
        await curBuilder.onFunctionCreate(data.req.args.function_name)
        break;

      case 'procedureUpdate':
        await curBuilder.onProcedureDelete(data.req.args.procedure_name)
        await curBuilder.onProcedureCreate(data.req.args.procedure_name)
        break;

      case 'procedureDelete':
        await curBuilder.onProcedureDelete(data.req.args.procedure_name)
        break;

      case 'functionCreate':
        await curBuilder.onFunctionCreate(data.req.args.function_name)
        break;

      case 'functionDelete':
        await curBuilder.onFunctionDelete(data.req.args.function_name)
        break;

      case 'xcRoutesPolicyUpdate':
      case 'xcResolverPolicyUpdate':
        await curBuilder.onPolicyUpdate(data.req.args.tn);
        console.log(`Updated validations for table : ${data.req.args.tn}`)
        break;

      case 'xcModelsEnable':
        await curBuilder.onToggleModels(data.req.args);
        break;

      case 'xcTableModelsEnable':
        await curBuilder.onToggleModels(data.req.args);
        break;

      case 'xcViewModelsEnable':
        await curBuilder.onToggleModels(data.req.args);
        break;

      case 'xcProcedureModelsEnable':
        await curBuilder.onToggleModels(data.req.args);
        break;

      case 'xcFunctionModelsEnable':
        await curBuilder.onToggleModels(data.req.args);
        break;

      case 'xcRelationsSet':
        XCEeError.throw();
        break;

      case 'xcCronSave':
        await curBuilder.restartCron(data.req.args)
        break;

      case 'cronDelete':
        await curBuilder.removeCron(data.req.args)
        break;


        // todo: optimize
        if (Array.isArray(data.req.args.tableNames)) {
          for (const procedure of data.req.args.tableNames) {
            await curBuilder.onProcedureCreate(procedure)
          }
        }
      case 'tableMetaCreate':
        XCEeError.throw();
        break;

      case 'viewMetaCreate':
        XCEeError.throw();
        break;

      case 'tableMetaDelete':
        XCEeError.throw();
        break;

      case 'viewMetaDelete':
        XCEeError.throw();
        break;
      case 'viewDelete':
        await curBuilder.onViewDelete(data.req.args.view_name)
        this.app.ncMeta.audit(this.id, curBuilder.getDbAlias(), 'nc_audit', {
          op_type: 'VIEW',
          op_sub_type: 'DELETED',
          user: data.user.email,
          description: `deleted view ${data.req.args.view_name} `, ip: data.ctx.req.clientIp
        })
        break;

      case 'tableMetaRecreate':
        XCEeError.throw();
        break;

      case 'viewMetaRecreate':
        XCEeError.throw();
        break;

      case 'procedureMetaCreate':
        XCEeError.throw();
        break;

      case 'functionMetaRecreate':
        XCEeError.throw();
        break;

      case 'xcAclSave':
        await curBuilder.onAclUpdate(data.req.args.tn || data.req.args.name);
        break;

      case 'xcAclAggregatedSave':
        for (const builder of this.apiBuilders) {
          await builder.loadXcAcl();
        }
        break;

      case 'procedureMetaDelete':
        await curBuilder.onProcedureDelete(data.req.args.procedure_name)
        break;

      case 'procedureMetaRecreate':
        await curBuilder.onProcedureDelete(data.req.args.tn)
        await curBuilder.onProcedureCreate(data.req.args.tn)
        break;

      case 'functionMetaCreate':
        // todo: optimize
        if (Array.isArray(data.req.args.tableNames)) {
          for (const functionName of data.req.args.tableNames) {
            await curBuilder.onFunctionCreate(functionName)
          }
        }
        break;

      case 'functionMetaDelete':
        await curBuilder.onFunctionDelete(data.req.args.procedure_name)
        break;


      case 'projectStop':
        this.router.stack.splice(0, this.router.stack.length);
        this.apiBuilders.splice(0, this.apiBuilders.length);
        await this.app.ncMeta.projectStatusUpdate(this.title, 'stopped');
        NcProjectBuilder.triggerGarbageCollect();
        this.app.ncMeta.audit(this.id, null, 'nc_audit', {
          op_type: 'PROJECT',
          op_sub_type: 'STOPPED',
          user: data.user.email,
          description: `stopped project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
        })
        break;

      case 'projectStart':
        await this.init();
        this.app.ncMeta.audit(this.id, null, 'nc_audit', {
          op_type: 'PROJECT',
          op_sub_type: 'STARTED',
          user: data.user.email,
          description: `started project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
        })
        break;

      case 'projectDelete':
        this.router.stack.splice(0, this.router.stack.length);
        this.apiBuilders.splice(0, this.apiBuilders.length);
        await this.app.ncMeta.projectDelete(this.title);
        await this.app.ncMeta.knex('nc_projects_users').where({project_id: this.id}).del();
        for (const db of (this.config?.envs?.[this.appConfig?.workingEnv]?.db || [])) {
          const dbAlias = db?.meta?.dbAlias;
          const apiType = db?.meta?.api?.type;
          await this.app.ncMeta.metaReset(this.id, dbAlias, apiType);
        }
        NcProjectBuilder.triggerGarbageCollect();
        this.app.ncMeta.audit(this.id, null, 'nc_audit', {
          op_type: 'PROJECT',
          op_sub_type: 'DELETED',
          user: data.user.email,
          description: `deleted project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
        })
        break;

      case 'xcMetaTablesImportLocalFsToDb':
      case 'xcMetaTablesImportZipToLocalFsAndDb':
      case 'projectRestart':
        this.router.stack.splice(0, this.router.stack.length);
        this.apiBuilders.splice(0, this.apiBuilders.length);
        await this.app.ncMeta.projectStatusUpdate(this.title, 'stopped');
        await this.init();
        NcProjectBuilder.triggerGarbageCollect();
        this.app.ncMeta.audit(this.id, null, 'nc_audit', {
          op_type: 'PROJECT',
          op_sub_type: 'RESTARTED',
          user: data.user.email,
          description: `restarted project ${this.title}(${this.id}) `, ip: data?.ctx?.req?.clientIp
        })
        break;

      default:
        console.log('DB OPS', data.req.api);
    }


    // export metadata to filesystem after meta changes
    switch (data?.req?.api) {

      case 'procedureCreate':
      case 'functionUpdate':
      case 'procedureUpdate':
      case 'procedureDelete':
      case 'functionCreate':
      case 'functionDelete':
      case 'relationCreate':
      case 'relationDelete':
      case 'tableCreate':
      case 'tableDelete':
      case 'tableRename':
      case 'xcRoutesHandlerUpdate':
      case 'xcResolverHandlerUpdate':
      case 'xcRoutesMiddlewareUpdate':
      case 'xcResolverMiddlewareUpdate':
      case 'xcModelSet':
      case 'xcModelSchemaSet':
      case 'tableXcHooksSet':
      case 'projectCreateByWeb':
      case 'projectUpdateByWeb':
      case 'projectChangeEnv':
      case 'tableUpdate':
      case 'xcRoutesPolicyUpdate':
      case 'xcResolverPolicyUpdate':
      case 'xcModelsEnable':
      case 'xcTableModelsEnable':
      case 'xcViewModelsEnable':
      case 'xcProcedureModelsEnable':
      case 'xcFunctionModelsEnable':
      case 'functionMetaCreate':
        if (!this.config.try) {
        }
        break;
    }

  }


  protected async _createApiBuilder() {

    this.apiBuilders.splice(0, this.apiBuilders.length);
    let i = 0;

    const connectionConfigs = [];

    /* for each db create an api builder */
    for (const db of (this.config?.envs?.[this.appConfig?.workingEnv]?.db || [])) {

      let Builder;
      switch (db.meta.api.type) {
        case "graphql":
          Builder = GqlApiBuilder;
          break;

        case 'rest':
          Builder = RestApiBuilder;
          break;
      }

      if ((db?.connection as any)?.database) {

        const connectionConfig = {
          ...db,
          meta: {
            ...db.meta,
            api: {
              ...db.meta.api,
              prefix: db.meta.api.prefix || this.genVer(i)
            }
          }
        };


        this.apiBuilders.push(new Builder(this.app, this, this.config, connectionConfig, this.app.ncMeta));
        connectionConfigs.push(connectionConfig);
        i++;
      } else if (db.meta?.allSchemas) {

        /* get all schemas and create APIs for all of them */
        const sqlClient = SqlClientFactory.create({
          ...db,
          connection: {...db.connection, database: undefined}
        });

        // @ts-ignore
        const schemaList = (await sqlClient.schemaList({}))?.data?.list;
        for (const schema of schemaList) {
          const connectionConfig = {
            ...db,
            connection: {...db.connection, database: schema.schema_name},
            meta: {
              ...db.meta,
              dbAlias: i ? db.meta.dbAlias + i : db.meta.dbAlias,
              api: {
                ...db.meta.api,
                prefix: db.meta.api.prefix || this.genVer(i)
              }
            }
          };

          this.apiBuilders.push(new Builder(this.app, this, this.config, connectionConfig, this.app.ncMeta));
          connectionConfigs.push(connectionConfig);

          i++;
        }

        sqlClient.knex.destroy();

      }
    }
    if (this.config?.envs?.[this.appConfig.workingEnv]?.db) {
      this.config.envs[this.appConfig.workingEnv].db.splice(0, this.config.envs[this.appConfig.workingEnv].db.length, ...connectionConfigs);
    }
  }


  protected genVer(i): string {
    const l = 'vwxyzabcdefghijklmnopqrstu';
    return i
      .toString(26)
      .split('')
      .map(v => l[parseInt(v, 26)])
      .join('') + '1';
  }


  protected async syncMigration(): Promise<void> {

    if (this.appConfig?.toolDir
      // && !('NC_MIGRATIONS_DISABLED' in process.env)
    ) {

      const dbs = this.config?.envs?.[this.appConfig.workingEnv]?.db

      if (!dbs || !dbs.length) {
        return;
      }

      for (const connectionConfig of dbs) {

        try {

          /* create migrator */
          const migrator = new Migrator({
            project_id: this.id,
            config: this.config,
            metaDb: this.app?.ncMeta?.knex
          });

          /* if migrator folder doesn't exist for project - call migratior init */
          const migrationFolder = path.join(this.config.toolDir, 'nc', this.id, connectionConfig.meta.dbAlias, 'migrations');
          if (!fs.existsSync(migrationFolder)) {
            await migrator.init({
              folder: this.config?.toolDir,
              env: this.appConfig.workingEnv,
              dbAlias: connectionConfig.meta.dbAlias
            });
          }

          /* migrator : sync & up */
          await migrator.sync({
            folder: this.config?.toolDir,
            env: this.appConfig.workingEnv,
            dbAlias: connectionConfig.meta.dbAlias
          });

          await migrator.migrationsUp({
            folder: this.config?.toolDir,
            env: this.appConfig.workingEnv,
            dbAlias: connectionConfig.meta.dbAlias,
            migrationSteps: 99999,
            sqlContentMigrate: 1,
          });


        } catch (e) {
          console.log(e);
          // throw e;
        }
      }
    } else {
    }
  }


  protected static triggerGarbageCollect() {
    try {
      if (global.gc) {
        global.gc();
      }
    } catch (e) {
      console.log("`node --expose-gc index.js`");
      process.exit();
    }
  }


  protected initApiInfoRoute(): void {

    this.router.get(`/projectApiInfo`, (req: any, res): any => {

      // auth to admin
      if (this.config.auth) {
        if (this.config.auth.jwt) {
          if (!(req.session.passport.user.roles.creator
            || req.session.passport.user.roles.editor
            || req.session.passport.user.roles.commenter
            || req.session.passport.user.roles.viewer)) {
            return res.status(401).json({
              msg: 'Unauthorized access : xc-auth does not have admin permission'
            })
          }
        } else if (this.config.auth.masterKey) {
          if (req.headers['xc-master-key'] !== this.config.auth.masterKey.secret) {
            return res.status(401).json({
              msg: 'Unauthorized access : xc-admin header missing or not matching'
            })
          }
        }
      }


      const info: any = {};

      for (const builder of this.apiBuilders) {
        info[builder.getDbAlias()] = {
          swaggerUrl: `/nc/${this.id}/${builder.getDbAlias()}/swagger`,
          apiUrl: `/nc/${this.id}//api/${builder.apiPrefix}`,
          gqlApiUrl: `/nc/${this.id}/${builder.apiPrefix}/graphql`,
          grpcApiUrl: ``,
          apiType: builder.apiType,
          database: builder.getDbName(),
        }
      }

      res.json({
        info,
        aggregatedInfo: {
          list: this.apiInfInfoList,
          aggregated: this.aggregatedApiInfo
        }
      });
    });
  }


  protected async progress(info, allInfo, isFirstTime?) {

    const aggregatedInfo = allInfo.reduce((arrSum, infoObj) => [
        '',
        arrSum[1] + +infoObj.tablesCount,

        arrSum[2] + (infoObj.type === 'graphql' ? 1 : 0),
        arrSum[3] + +(infoObj.type === 'rest' ? 1 : 0),

        arrSum[4] + (+infoObj.apiCount || +infoObj.resolversCount || 0),
        // arrSum[3] + +info.timeTaken
        (Date.now() - this.startTime) / 1000
      ],
      [0, 0, 0, 0, 0, 0])
      .map((v, i) => (i === 5 ? v.toFixed(1) + 's' : (i === 2 ? v.toLocaleString() : v)));


    this.apiInfInfoList.push(info);
    this.aggregatedApiInfo = aggregatedInfo;
    if (isFirstTime) {
      Tele.emit('evt_api_created', info);
    }

  }


  protected async addAuthHookToMiddleware(): Promise<any> {
    this.authHook = await this.app.ncMeta.metaGet(this.id, 'db', 'nc_hooks', {
      type: 'AUTH_MIDDLEWARE'
    });

    this.router.use(async (req: any, _res, next) => {

      if (this.authHook && this.authHook.url) {
        try {
          const result = await axios.post(this.authHook.url, {}, {
            headers: req.headers
          });
          req.locals = req.locals || {};
          req.locals = {user: result.data};
        } catch (e) {
          console.log(e)
        }
      }
      next();
    })
  }


  public get prefix(): String {
    return this.config?.prefix;
  }


}

/**
 * @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/>.
 *
 */
