'use strict'; 

const fetch = require('node-fetch');
const { URLSearchParams } = require('url');

var apiProto = {};

apiProto.__getApiDoc = async function(host){
   try{
      let setupRes = await fetch(host || this.host+'/pve-docs/api-viewer/apidoc.js', { method: 'GET'});
      let text = await setupRes.text()

      text = text.split('// avoid errors when running without development tools')[0];

      return eval(text += '; pveapi;');

   }catch(error){
      throw error;
   }
}

apiProto.__makeMap = async function(APItree){
   APItree = APItree || await this.__getApiDoc();
   let apiMap = {};

   function __rec(obj){
      for(let item of obj){
         if(item.children){
            __rec(item.children);
         }
         if(item.path){
            delete item.children;
            apiMap[item.path] = item;
            item.methods = item.info;
            delete item.info;
         }
      }
   }

   __rec(APItree);

   return apiMap
};

//these helper functions are to build the fetch call
apiProto.__buildHeaders = function(){
   let headers = {};
   if(this.authInfo.apiToken){
      headers = {
         "Authorization": `PVEAPIToken=${this.authInfo.username}=${this.authInfo.apiToken}`
      }
   }else{   
      headers = {
         'Cookie': 'PVEAuthCookie='+this.user.ticket,
         'CSRFPreventionToken': this.user.CSRFPreventionToken
      }
   }

   return headers;
};

// this function takes an iterable of 'key':'value' pairs
apiProto.__buildBody = function(props, data){
   const params = new URLSearchParams();
   for (let key in props){
      if(data[key]){
         params.append(key, data[key])
         continue;
      }
      if(props[key].optional !==1){
         // console.log(props[key])
         throw new Error('MissingKey');
      }
   }
   return params;
};

apiProto.__getEndPoint = function(args){
   let endpoint = this.apiMap[args.path]
   if(!args || !endpoint) throw new Error('endpointNotFound');
   if(!endpoint.methods[args.method]) throw new Error('methodNotFound');

   return endpoint;
};

apiProto.__fetch = async function(method, path, params, data){
   try{
      let endpoint = this.__getEndPoint({path, method});

      let fetchOptions = {
         method: method,
         headers: this.__buildHeaders(),
      }

      if(['PUT', 'POST'].includes(method)){
         fetchOptions.body = this.__buildBody(
            endpoint.methods[method].parameters.properties,
            data
         )
      }

      let HTTPres = await fetch(this.BASEURL+path, fetchOptions);

      // console.log(HTTPres)

      return {
         statusCode: HTTPres.status,
         statusText: HTTPres.statusText,
         json: (await HTTPres.json()).data,
      }

   }catch(error){

      throw error;
   }
};

apiProto.GET = async function(args){
   try{

      return await this.__fetch(
         'GET',
         args.path,
         args.parama || {},
      );

   }catch(error){
      throw error;
   }
};

apiProto.DELETE = async function(args){
   try{

      return await this.__fetch(
         'DELETE',
         args.path,
         args.parama || {},
      );

   }catch(error){
      throw error;
   }
};

apiProto.POST = async function(args){
   try{

      return await this.__fetch(
         'POST',
         args.path,
         args.parama || {},
         args.data || {}
      );
      
   }catch(error){
      throw error;
   }
};

apiProto.PUT = async function(args){
   try{

      return await this.__fetch(
         'PUT',
         args.path,
         args.parama || {},
         args.data || {}
      );
      
   }catch(error){
      throw error;
   }
};

apiProto.auth = async function(authInfo){
   try{
      this.user = (await this.POST({
         path: '/access/ticket',
         data: authInfo || this.authInfo
      })).json

      return authInfo;

   }catch(error){
      throw error;
   }
};

apiProto.__init = async function(conf){
   try{
      this.user = {};
      this.BASEURL = conf.host+'/api2/json';
      this.apiMap = await this.__makeMap();

      if(!this.authInfo.apiToken) await this.auth(); 

      return this;

   }catch(error){
      throw(error);
   }
};

async function API(conf){
   let instance = Object.create(apiProto);
   Object.assign(instance, conf);

   return await instance.__init(conf);
}


module.exports = API;

