/**
 * @prettier
 */
import { OrderedMap, Map, List } from "immutable"
import { createSelector } from "reselect"

import { getDefaultRequestBodyValue } from "./components/request-body"
import { stringify } from "core/utils"

// Helpers

const onlyOAS3 =
  (selector) =>
  (state, ...args) =>
  (system) => {
    if (system.getSystem().specSelectors.isOAS3()) {
      const selectedValue = selector(state, ...args)
      return typeof selectedValue === "function"
        ? selectedValue(system)
        : selectedValue
    } else {
      return null
    }
  }

function validateRequestBodyIsRequired(selector) {
  return (...args) =>
    (system) => {
      const specJson = system.getSystem().specSelectors.specJson()
      const argsList = [...args]
      // expect argsList[0] = state
      let pathMethod = argsList[1] || []
      let isOas3RequestBodyRequired = specJson.getIn([
        "paths",
        ...pathMethod,
        "requestBody",
        "required",
      ])

      if (isOas3RequestBodyRequired) {
        return selector(...args)
      } else {
        // validation pass b/c not required
        return true
      }
    }
}

const validateRequestBodyValueExists = (state, pathMethod) => {
  pathMethod = pathMethod || []
  let oas3RequestBodyValue = state.getIn([
    "requestData",
    ...pathMethod,
    "bodyValue",
  ])
  // context: bodyValue can be a String, or a Map
  if (!oas3RequestBodyValue) {
    return false
  }
  // validation pass if String is not empty, or if Map exists
  return true
}

export const selectedServer = onlyOAS3((state, namespace) => {
  const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"]
  return state.getIn(path) || ""
})

export const requestBodyValue = onlyOAS3((state, path, method) => {
  return state.getIn(["requestData", path, method, "bodyValue"]) || null
})

export const shouldRetainRequestBodyValue = onlyOAS3((state, path, method) => {
  return state.getIn(["requestData", path, method, "retainBodyValue"]) || false
})

export const selectDefaultRequestBodyValue =
  (state, path, method) => (system) => {
    const { oas3Selectors, specSelectors, fn } = system.getSystem()

    if (specSelectors.isOAS3()) {
      const currentMediaType = oas3Selectors.requestContentType(path, method)
      if (currentMediaType) {
        return getDefaultRequestBodyValue(
          specSelectors.specResolvedSubtree([
            "paths",
            path,
            method,
            "requestBody",
          ]),
          currentMediaType,
          oas3Selectors.activeExamplesMember(
            path,
            method,
            "requestBody",
            "requestBody"
          ),
          fn
        )
      }
    }
    return null
  }

export const hasUserEditedBody = onlyOAS3((state, path, method) => (system) => {
  const { oas3Selectors, specSelectors, fn } = system

  let userHasEditedBody = false
  const currentMediaType = oas3Selectors.requestContentType(path, method)
  let userEditedRequestBody = oas3Selectors.requestBodyValue(path, method)
  const requestBody = specSelectors.specResolvedSubtree([
    "paths",
    path,
    method,
    "requestBody",
  ])

  /**
   * The only request body that can currently be edited is for Path Items that are direct values of OpenAPI.paths.
   * Path Item contained within the Callback Object or OpenAPI.webhooks (OpenAPI 3.1.0) have `Try it out`
   * disabled and thus body cannot be edited.
   */
  if (!requestBody) {
    return false
  }

  if (Map.isMap(userEditedRequestBody)) {
    // context is not application/json media-type
    userEditedRequestBody = stringify(
      userEditedRequestBody
        .mapEntries((kv) =>
          Map.isMap(kv[1]) ? [kv[0], kv[1].get("value")] : kv
        )
        .toJS()
    )
  }
  if (List.isList(userEditedRequestBody)) {
    userEditedRequestBody = stringify(userEditedRequestBody)
  }

  if (currentMediaType) {
    const currentMediaTypeDefaultBodyValue = getDefaultRequestBodyValue(
      requestBody,
      currentMediaType,
      oas3Selectors.activeExamplesMember(
        path,
        method,
        "requestBody",
        "requestBody"
      ),
      fn
    )
    userHasEditedBody =
      !!userEditedRequestBody &&
      userEditedRequestBody !== currentMediaTypeDefaultBodyValue
  }
  return userHasEditedBody
})

export const requestBodyInclusionSetting = onlyOAS3((state, path, method) => {
  return state.getIn(["requestData", path, method, "bodyInclusion"]) || Map()
})

export const requestBodyErrors = onlyOAS3((state, path, method) => {
  return state.getIn(["requestData", path, method, "errors"]) || null
})

export const activeExamplesMember = onlyOAS3(
  (state, path, method, type, name) => {
    return (
      state.getIn(["examples", path, method, type, name, "activeExample"]) ||
      null
    )
  }
)

export const requestContentType = onlyOAS3((state, path, method) => {
  return (
    state.getIn(["requestData", path, method, "requestContentType"]) || null
  )
})

export const responseContentType = onlyOAS3((state, path, method) => {
  return (
    state.getIn(["requestData", path, method, "responseContentType"]) || null
  )
})

export const serverVariableValue = onlyOAS3((state, locationData, key) => {
  let path

  // locationData may take one of two forms, for backwards compatibility
  // Object: ({server, namespace?}) or String:(server)
  if (typeof locationData !== "string") {
    const { server, namespace } = locationData
    if (namespace) {
      path = [namespace, "serverVariableValues", server, key]
    } else {
      path = ["serverVariableValues", server, key]
    }
  } else {
    const server = locationData
    path = ["serverVariableValues", server, key]
  }

  return state.getIn(path) || null
})

export const serverVariables = onlyOAS3((state, locationData) => {
  let path

  // locationData may take one of two forms, for backwards compatibility
  // Object: ({server, namespace?}) or String:(server)
  if (typeof locationData !== "string") {
    const { server, namespace } = locationData
    if (namespace) {
      path = [namespace, "serverVariableValues", server]
    } else {
      path = ["serverVariableValues", server]
    }
  } else {
    const server = locationData
    path = ["serverVariableValues", server]
  }

  return state.getIn(path) || OrderedMap()
})

export const serverEffectiveValue = onlyOAS3((state, locationData) => {
  var varValues, serverValue

  // locationData may take one of two forms, for backwards compatibility
  // Object: ({server, namespace?}) or String:(server)
  if (typeof locationData !== "string") {
    const { server, namespace } = locationData
    serverValue = server
    if (namespace) {
      varValues = state.getIn([namespace, "serverVariableValues", serverValue])
    } else {
      varValues = state.getIn(["serverVariableValues", serverValue])
    }
  } else {
    serverValue = locationData
    varValues = state.getIn(["serverVariableValues", serverValue])
  }

  varValues = varValues || OrderedMap()
  let str = serverValue

  varValues.map((val, key) => {
    str = str.replace(new RegExp(`{${key}}`, "g"), val)
  })

  return str
})

export const validateBeforeExecute = validateRequestBodyIsRequired(
  (state, pathMethod) => validateRequestBodyValueExists(state, pathMethod)
)

export const validateShallowRequired = (
  state,
  {
    oas3RequiredRequestBodyContentType,
    oas3RequestContentType,
    oas3RequestBodyValue,
  }
) => {
  let missingRequiredKeys = []
  // context: json => String; urlencoded, form-data => Map
  if (!Map.isMap(oas3RequestBodyValue)) {
    return missingRequiredKeys
  }
  let requiredKeys = []
  // Cycle through list of possible contentTypes for matching contentType and defined requiredKeys
  Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach(
    (contentType) => {
      if (contentType === oas3RequestContentType) {
        let contentTypeVal =
          oas3RequiredRequestBodyContentType.requestContentType[contentType]
        contentTypeVal.forEach((requiredKey) => {
          if (requiredKeys.indexOf(requiredKey) < 0) {
            requiredKeys.push(requiredKey)
          }
        })
      }
    }
  )
  requiredKeys.forEach((key) => {
    let requiredKeyValue = oas3RequestBodyValue.getIn([key, "value"])
    if (!requiredKeyValue) {
      missingRequiredKeys.push(key)
    }
  })
  return missingRequiredKeys
}

export const validOperationMethods = createSelector(() => [
  "get",
  "put",
  "post",
  "delete",
  "options",
  "head",
  "patch",
  "trace",
])
