import http from 'http'
import https from 'https'
import stream from 'stream/promises'
import fs from 'fs'
import Util from './util.mjs'

function resolveRelative(from, to) {
  const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
  if (resolvedUrl.protocol === 'resolve:') {
    // `from` is a relative URL.
    const { pathname, search, hash } = resolvedUrl;
    return pathname + search + hash;
  }
  return resolvedUrl.toString();
}

export function request(config, path, filePath = null, redirects, fastRaw = false) {
  if (!config || typeof(config) !== 'object' || Array.isArray(config)) {
    return Promise.reject(new Error('Request must be called with config in first parameter'))
  }
  let newRedirects = (redirects || 0) + 1
  if (!path || typeof(path) !== 'string' || !path.startsWith('http')) {
    return Promise.reject(new Error('URL was empty or invalid' + (typeof(path) === 'string' ? ': ' + path : '')))
  }
  let parsed
  try {
    parsed = new URL(path)
  } catch {
    return Promise.reject(new Error('URL was empty or invalid: ' + path))
  }

  let h = http
  if (parsed.protocol === 'https:') {
    h = https
  }

  let req = null

  let orgErr = new Error(); return new Promise(function(resolve, reject) {
    if (!path) {
      return reject(new Error('Request path was empty'))
    }
    let headers = {
      'User-Agent': 'TheThing/service-core',
      Accept: 'application/vnd.github.v3+json'
    }
    if (config.token) {
      headers['Authorization'] = `token ${config.token}`
    }
    let timeout = config.timeout || 10000

    let timedout = false
    let timer = setTimeout(function() {
      timedout = true
      if (req) { req.destroy() }
      reject(Util.combineStack(new Error(`Request ${path} timed out after ${timeout}ms`), orgErr))
    }, timeout)

    req = h.request({
      path: parsed.pathname + parsed.search,
      port: parsed.port,
      method: 'GET',
      headers: headers,
      timeout: timeout,
      hostname: parsed.hostname
    }, function(res) {
      if (timedout) { return }
      clearTimeout(timer)

      const ac = new AbortController()

      let output = ''
      if (filePath) {
        stream.pipeline(res, fs.createWriteStream(filePath), { signal: ac.signal })
        .then(function() {
          resolve({
            statusCode: res.statusCode,
            status: res.statusCode,
            statusMessage: res.statusMessage,
            headers: res.headers,
            body: null
          })
        }, function(err) {
          if (err.code === 'ABORT_ERR') return
          reject(Util.combineStack(err, orgErr))
        })
        // let file = fs.createWriteStream(filePath)
        // res.pipe(file)
      } else {
        res.on('data', function(chunk) {
          output += chunk
        })
      }
      res.on('end', function() {
        let err = null

        if (res.statusCode >= 300 && res.statusCode < 400) {
          if (newRedirects > 5) {
            err = new Error(`Too many redirects (last one was ${res.headers.location})`)
          }
          else if (!res.headers.location) {
            err = new Error('Redirect returned no path in location header')
          }
          else if (res.headers.location.startsWith('http')) {
            ac.abort()
            return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw))
          } else {
            ac.abort()
            return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw))
          }
        } else if (res.statusCode >= 400) {
          err = new Error(`HTTP Error ${res.statusCode}: ${output}`)
        }

        if (err) {
          ac.abort()
          if (!filePath) return reject(Util.combineStack(err, orgErr))

          // Do some cleanup in case we were in the middle of downloading file
          return fs.rm(filePath, function() {
            reject(Util.combineStack(err, orgErr))
          })
        }
        // Let the pipeline do the resolving so it can finish flusing before calling resolve
        if (!filePath) {
          resolve({
            statusCode: res.statusCode,
            status: res.statusCode,
            statusMessage: res.statusMessage,
            headers: res.headers,
            body: output
          })
        }
      })
    })
    
    req.on('error', function(err) {
      if (timedout) return
      let wrapped = new Error(`Error during request ${path}: ${err.message}`)
      wrapped.code = err.code
      reject(Util.combineStack(wrapped, orgErr))
    })
    req.on('timeout', function(err) {
      if (timedout) return
      reject(Util.combineStack(err, orgErr))
    })

    req.end()
  }).then(function(res) {
    if (req) {
      req.destroy()
    }
    if (!filePath && !fastRaw) {
      if (typeof(res.body) === 'string') {
        try {
          res.body = JSON.parse(res.body)
        } catch(e) {
          if (res.body.indexOf('<!DOCTYPE') < 100 || res.body.indexOf('<html') < 100) {
            return Promise.reject(Util.combineStack(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body), orgErr))
          }
          return Promise.reject(Util.combineStack(new Error(`Error parsing body ${res.body}: ${e.message}`), orgErr))
        }
      }
    }
    return res
  })
}