'use strict'

const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const url = require('url');
const { GithubActions } = require('./githubActions');
const { convertToJSON, convertToFormData, retry } = require('./helper');

const METHOD_GET = 'GET'
const METHOD_POST = 'POST'

const HEADER_CONTENT_TYPE = 'Content-Type'

const CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded'

/**
 * @param {Object} param0
 * @param {string} param0.method HTTP Method
 * @param {axios.AxiosRequestConfig} param0.instanceConfig
 * @param {string} param0.data Request Body as string, default {}
 * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {}
 * @param {string} param0.file Single request file (absolute path)
 * @param {GithubActions} param0.actions 
 * @param {{ 
 *  ignoredCodes: number[];
 *  preventFailureOnNoResponse: boolean,
 *  escapeData: boolean;
 *  retry: number;
 *  retryWait: number;
 * }} param0.options
 *
 * @returns {Promise<axios.AxiosResponse>}
 */
const request = async({ method, instanceConfig, data, files, file, actions, options }) => {
  actions.debug(`options: ${JSON.stringify(options)}`)
  
  try {
    if (options.escapeData) {
      data = data.replace(/"[^"]*"/g, (match) => { 
        return match.replace(/[\n\r]\s*/g, "\\n");
      }); 
    }

    if (method === METHOD_GET) {
      data = undefined;
    }

    if (files && files !== '{}') {
      let filesJson = convertToJSON(files)
      let dataJson = convertToJSON(data)

      if (Object.keys(filesJson).length > 0) {
        try {
          data = convertToFormData(dataJson, filesJson)
          instanceConfig = await updateConfig(instanceConfig, data, actions)
        } catch(error) {
          actions.setFailed(JSON.stringify({ message: `Unable to convert Data and Files into FormData: ${error.message}`, data: dataJson, files: filesJson }))
          return
        }
      }
    }

    // Only consider file if neither data nor files provided
    if ((!data || data === '{}') && (!files || files === '{}') && file) {
      data = fs.createReadStream(file)
      updateConfigForFile(instanceConfig, file, actions)
    }

    if (instanceConfig.headers[HEADER_CONTENT_TYPE] === CONTENT_TYPE_URLENCODED) {
      let dataJson = convertToJSON(data)
      if (typeof dataJson === 'object' && Object.keys(dataJson).length) {
        data = (new url.URLSearchParams(dataJson)).toString();
      }
    }

    const requestData = {
      method,
      data,
      maxContentLength: Infinity,
      maxBodyLength: Infinity
    }

    actions.debug('Instance Configuration: ' + JSON.stringify(instanceConfig))
    
    /** @type {axios.AxiosInstance} */
    const instance = axios.create(instanceConfig);

    actions.debug('Request Data: ' + JSON.stringify(requestData))

    const execRequest = async () => {
      try {
        return await instance.request(requestData)
      } catch(error) {
        if (error.response && options.ignoredCodes.includes(error.response.status)) {
          actions.warning(`ignored status code: ${JSON.stringify({ code: error.response.status, message: error.response.data })}`)
          
          return null
        }
        
        if (!error.response && error.request && options.preventFailureOnNoResponse) {
          actions.warning(`no response received: ${JSON.stringify(error)}`);

          return null
        }

        throw error
      }
    }

    /** @type {axios.AxiosResponse|null} */
    const response = await retry(execRequest, {
      actions,
      retry: options.retry || 0,
      sleep: options.retryWait // wait 3s after each retry
    })

    if (!response) {
      return null
    }

    actions.setOutput('response', JSON.stringify(response.data))
    actions.setOutput('headers', response.headers)

    return response
  } catch (error) {
    if ((typeof error === 'object') && (error.isAxiosError === true)) {
      const { name, message, code, response } = error
      actions.setOutput('requestError', JSON.stringify({ name, message, code, status: response && response.status ? response.status : null }));
    }

    if (error.response) {
      actions.setFailed(JSON.stringify({ code: error.response.status, message: error.response.data }))
    } else if (error.request) {
      actions.setFailed(JSON.stringify({ error: "no response received" }));
    } else {
      actions.setFailed(JSON.stringify({ message: error.message, data }));
    }
  }
}

/**
 * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig
 * @param {FormData} formData
 * @param {*} actions
 *
 * @returns {Promise<{ baseURL: string; timeout: number; headers: { [name: string]: string } }>}
 */
const updateConfig = async (instanceConfig, formData, actions) => {
  try {
    const formHeaders = formData.getHeaders()
    const contentType = formHeaders['content-type']

    delete formHeaders['content-type']

    return { 
      ...instanceConfig, 
      headers: { 
        ...instanceConfig.headers, 
        ...formHeaders,
        'Content-Length': await contentLength(formData),
        'Content-Type': contentType
      }
    }
  } catch(error) {
    actions.setFailed({ message: `Unable to read Content-Length: ${error.message}`, data, files })
  }
}

/**
 * @param instanceConfig
 * @param filePath
 * @param {*} actions
 *
 * @returns {{ baseURL: string; timeout: number; headers: { [name: string]: string } }}
 */
const updateConfigForFile = (instanceConfig, filePath, actions) => {
  try {
    const { size } = fs.statSync(filePath)

    return {
      ...instanceConfig,
      headers: {
        ...instanceConfig.headers,
        'Content-Length': size,
        'Content-Type': 'application/octet-stream'
      }
    }
  } catch(error) {
    actions.setFailed({ message: `Unable to read Content-Length: ${error.message}`, data, files })
  }
}

/**
 * @param {FormData} formData
 *
 * @returns {Promise<number>}
 */
const contentLength = (formData) => new Promise((resolve, reject) => {
  formData.getLength((err, length) => {
    if (err) {
      reject (err)
      return
    }

    resolve(length)
  })
})

module.exports = {
  request,
  METHOD_POST,
  METHOD_GET,
}