import crypto from 'crypto'
import fs from 'fs/promises'
import path from 'path'
import http from 'http'
import https from 'https'
import { URL } from 'url'
import { encode } from '../util.mjs'

export default class Client {
  constructor() {

  }

  customRequest(method = 'GET', path, body, options = {}) {
    if (path.slice(0, 4) !== 'http') {
      throw new Error('Http client path invalid')
    }
    let urlObj = new URL(path)
    let d1 = new Date()

    return new Promise((resolve, reject) => {
      const opts = {
        method: options.method || method,
        timeout: options.timeout || 60000,
        protocol: options.protocol || urlObj.protocol,
        username: options.username || urlObj.username,
        password: options.password || urlObj.password,
        host: options.host || urlObj.hostname,
        port: options.port || Number(urlObj.port),
        path: options.path || urlObj.pathname + urlObj.search,
        headers: options.headers || {},
      }

      if (options.agent) {
        opts.agent = options.agent
      }

      // opts.agent = agent

      let req
      if (path.startsWith('https')) {
        req = https.request(opts)
      } else {
        req = http.request(opts)
      }

      req.on('error', (err) => {
        reject(err)
      })
      req.on('timeout', function() {
        req.destroy()
        let d2 = new Date()
        reject(new Error(`Request ${method} ${path} timed out`))
      })
      req.on('response', res => {
        let output = ''
        res.setEncoding('utf8')

        res.on('data', function (chunk) {
          output += chunk.toString()
        })

        res.on('end', function () {
          if (options.getRaw) {
            output = {
              status: res.statusCode,
              data: output,
              headers: res.headers,
            }
          } else {
            if (!output) return resolve(null)
            try {
              output = JSON.parse(output)
            } catch (e) {
              return reject(new Error(`${e.message} while decoding: ${output}`))
            }
          }
          if (!options.getRaw && output.status && typeof(output.status) === 'number') {
            let err = new Error(`Request failed [${output.status}]: ${output.message}`)
            err.url = path
            err.body = output
            return reject(err)
          }
          resolve(output)
        })
      })

      if (opts.returnRequest) {
        return resolve(req)
      }
      
      if (body) {
        req.write(body)
      }
      req.end()
      return req
    })
  }

  createJwt(body, secret) {
    let header = {
      typ: 'JWT',
      alg: 'HS256',
    }

    body.iat = Math.floor(Date.now() / 1000)
    body.exp = body.iat + 300

    // Base64 encode header and body
    let headerBase64 = encode(Buffer.from(JSON.stringify(header)))
    let bodyBase64 = encode(Buffer.from(JSON.stringify(body)))
    let headerBodyBase64 = headerBase64 + '.' + bodyBase64

    const hmac = crypto.createHmac('sha256', secret)
    hmac.update(headerBodyBase64)
    let signatureBuffer = hmac.digest()

    // Construct final JWT
    let signatureBase64 = encode(signatureBuffer)
    return headerBodyBase64 + '.' + signatureBase64
  }

  random(length = 8) {
    // Declare all characters
    let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    // Pick characers randomly
    let str = '';
    for (let i = 0; i < length; i++) {
        str += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return str;
  }

  get(url) {
    return this.customRequest('GET', url, null)
  }

  post(url, body) {
    let parsed = JSON.stringify(body)
    return this.customRequest('POST', url, parsed, {
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': parsed.length,
      },
    })
  }
  
  delete(url, body) {
    let parsed = JSON.stringify(body)
    return this.customRequest('DELETE', url, parsed, {
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': parsed.length,
      },
    })
  }

  upload(url, files, method = 'POST', body = {}) {
    const boundary = `---------${this.random(32)}`
    const crlf = '\r\n'
    let upload = files

    if (typeof(upload) === 'string') {
      upload = {
        file: files
      }
    }
    let keys = Object.keys(upload)
    let uploadBody = []

    return Promise.all(keys.map(key => {
      let file = upload[key]
      let filename = ''
      if (typeof(file) === 'object') {
        if (typeof(file.file) !== 'string' || typeof(file.filename) !== 'string') {
          throw new Error('Invalid value in client.upload for key ' + key)
        }
        filename = file.filename
        file = file.file
      } else {
        filename = path.basename(file)
      }
      return fs.readFile(file).then(data => {
        uploadBody.push(Buffer.from(
          `${crlf}--${boundary}${crlf}` +
          `Content-Disposition: form-data; name="${key}"; filename="${filename}"` + crlf + crlf
        ))
        uploadBody.push(data)
      })
    }))
    .then(() => {
      uploadBody.push(
        Buffer.concat(Object.keys(body).map(function(key) {
          return Buffer.from(''
            + `${crlf}--${boundary}${crlf}`
            + `Content-Disposition: form-data; name="${key}"` + crlf + crlf
            + JSON.stringify(body[key])
          )
        }))
      )
      uploadBody.push(Buffer.from(`${crlf}--${boundary}--`))

      let multipartBody = Buffer.concat(uploadBody)

      return this.customRequest(method, url, multipartBody, {
        headers: {
          'Content-Type': 'multipart/form-data; boundary=' + boundary,
          'Content-Length': multipartBody.length,
        },
      })
    })
  }
}
