const socketClient = require("socket.io-client")
const EventEmmitter = require("events")

function attach(_socket, attachOpts={}) {
  let socket
  let handleConnect = null
  let promises = {}
  let refreshToken = attachOpts.refreshToken
  const throwErrorOnStatus = attachOpts.throwErrorOnStatus == null? true: !!attachOpts.throwErrorOnStatus
  let token
  let keepConnectionTimer
  const emitter = new EventEmmitter()

  let locks = 0

  async function callApi(body, opts={}) {
    socket = await handleConnect()
    const TIMEOUT = opts.timeout || attachOpts.timeout || 5000
    body = body || {}
    body.action == "auth.login" && await callApi({action: "auth.logout"})
    body.action == "auth.logout" && (token = refreshToken = null)
    const messageId = callApi.id = (callApi.id || 0)+1
    socket.emit("api", Object.assign(body, {messageId}))
    locks++
    return await new Promise((resolve, reject) => {
      const timerId = setTimeout(() => {
        if(promises[messageId]) {
          delete promises[messageId]
          opts.preparedError.message = "Timeout expired"
          if(reject) {
            locks--
            reject(opts.preparedError)
            resolve = reject = null
          }
        }
      }, TIMEOUT)
      promises[messageId] = Object.assign({}, {request: body}, opts, {
        resolve(...args) {
          clearTimeout(timerId)
          if(resolve) {
            locks--
            resolve(...args)
            resolve = reject = null
          }
        },
        reject(...args) {
          clearTimeout(timerId)
          if(reject) {
            locks--
            reject(...args)
            resolve = reject = null
          }
        }
      })
    })
  }

  function close() {
    if(socket) {
      const _soc = socket
      socket = null
      _soc.close()
      locks = 0
      emitter.emit("closeConnection", _soc)

      keepConnectionTimer && clearTimeout(keepConnectionTimer)
      keepConnectionTimer = null
      // const _promises = promises
      promises = {}
      //
      // for(const i in _promises) {
      //   _promises[i].reject(new Error("disconnect"))
      //   delete _promises[i]
      // }
    }
  }

  const apiProxy = (path, hash = {}, preparedError) => {
    if(!preparedError) {
      preparedError = new Error("")
      Error.captureStackTrace && Error.captureStackTrace(preparedError, apiProxy)
    }
    path = path || []

    function get(target, prop) {
      if(typeof prop === "string" && prop !== "call" && prop !== "inspect" && prop !== "valueOf" && prop !== "name") {
        if(!(prop in hash)) {
          if(!path.length && prop != "domain" && prop in emitter) {
            hash[prop] = (...args) => emitter[prop](...args)
          } else {
            const [sub, ...other] = prop.split(".")
            const preparedError = new Error("")
            Error.captureStackTrace && Error.captureStackTrace(preparedError, get)
            let proxy = hash[sub] = apiProxy([...path, sub], {}, preparedError)
            for(let i=0; i < other.length; i++) proxy = proxy[other[i]]
            return proxy
            // hash[prop] = apiProxy([...path, prop])
          }
        }
        return hash[prop]
      }
    }

    return new Proxy(body => callApi(Object.assign({}, body, {action: path.join(".")}), {preparedError}), {
      get,
      set(target, prop, value) {
        hash[prop] = value
        return true
      },
      deleteProperty(target, prop) {
        delete hash[prop]
        return true
      }
    })
  }

  handleConnect = async () => socket || await new Promise((resolve, reject) => {
    socket = typeof _socket == "string"? socketClient(_socket, attachOpts.socket || {}): _socket

    function keepConnection() {
      function onKeepConnectionTimeout() {
        if(locks == 0) {
          close()
          emitter.listeners("notify").length && handleConnect()
        }
      }
      keepConnectionTimer && clearTimeout(keepConnectionTimer)
      keepConnectionTimer = attachOpts.keepConnection && setTimeout(onKeepConnectionTimeout, attachOpts.keepConnection)
    }

    socket.on("connect", async args => {
      emitter.emit("connect", args)
      refreshToken && await callApi({action: "auth.login", query: {refreshToken}}).catch(error => {
        error.status == 401 && (refreshToken = token = null)
        reject && reject(error)
        resolve = reject = null
        close()
      })
      keepConnection()
      resolve && resolve(socket)
      resolve = reject = null
    })
    socket.on("api", async data => {
      try {
        if(data) {
          if(!data.messageId) {
            emitter.emit("notify", data)
          } else {
            if(!promises[data.messageId]) {
              /*eslint no-console: ["error", { allow: ["warn", "error"] }]*/
              console.warn("There is no handle for response ", data)
              return
            }
            keepConnection()
            const promise = promises[data.messageId]
            delete promises[data.messageId]
            promise.request.action == "auth.login" && data.data && (
              refreshToken = (await callApi({action: "socket.refreshToken"})).data,
              token = (await callApi({action: "socket.token"})).data
            )
            data.request = promise.request
            if(data.error && throwErrorOnStatus) {
              Object.assign(promise.preparedError, data.error)
              promise.preparedError.message = data.error.message || ""+data.error
              promise.preparedError.data = data
              promise.reject(promise.preparedError)
            } else {
              promise.resolve(data)
            }
          }
        }
      } catch(error) {
        console.error(error, error.stack)
      }
    })

    socket.on("error", error => {
      emitter.emit("error", error)
      reject && reject(error)
      resolve = reject = null
    })

    socket.on("disconnect", arg => {
      emitter.emit("disconnect", arg)
      close()
    })
    socket.on("close", arg => {
      emitter.emit("closeConnection", arg)
    })
  })

  const preparedError = new Error("")
  Error.captureStackTrace && Error.captureStackTrace(preparedError, apiProxy)
  return apiProxy(null, {
    close,
    get refreshToken() {return refreshToken},
    get token() {return token},
    get rawSocket() {return socket},
  }, preparedError)
}

module.exports = attach
