import { createSelector } from "reselect"
import constant from "lodash/constant"
import { sorters, paramToIdentifier } from "core/utils"
import { fromJS, Set, Map, OrderedMap, List } from "immutable"

const DEFAULT_TAG = "default"

const OPERATION_METHODS = [
  "get", "put", "post", "delete", "options", "head", "patch", "trace"
]

const state = state => {
  return state || Map()
}

export const lastError = createSelector(
  state,
  spec => spec.get("lastError")
)

export const url = createSelector(
  state,
  spec => spec.get("url")
)

export const specStr = createSelector(
  state,
  spec => spec.get("spec") || ""
)

export const specSource = createSelector(
  state,
  spec => spec.get("specSource") || "not-editor"
)

export const specJson = createSelector(
  state,
  spec => spec.get("json", Map())
)

export const specJS = createSelector(
  specJson,
  (spec) => spec.toJS()
)

export const specResolved = createSelector(
  state,
  spec => spec.get("resolved", Map())
)

export const specResolvedSubtree = (state, path) => {
  return state.getIn(["resolvedSubtrees", ...path], undefined)
}

const mergerFn = (oldVal, newVal) => {
  if(Map.isMap(oldVal) && Map.isMap(newVal)) {
    if(newVal.get("$$ref")) {
      // resolver artifacts indicated that this key was directly resolved
      // so we should drop the old value entirely
      return newVal
    }

    return OrderedMap().mergeWith(
      mergerFn,
      oldVal,
      newVal
    )
  }

  return newVal
}

export const specJsonWithResolvedSubtrees = createSelector(
  state,
  spec => OrderedMap().mergeWith(
    mergerFn,
    spec.get("json"),
    spec.get("resolvedSubtrees")
  )
)

// Default Spec ( as an object )
export const spec = state => {
  let res = specJson(state)
  return res
}

export const isOAS3 = createSelector(
  // isOAS3 is stubbed out here to work around an issue with injecting more selectors
  // in the OAS3 plugin, and to ensure that the function is always available.
  // It's not perfect, but our hybrid (core+plugin code) implementation for OAS3
  // needs this. //KS
  spec,
	() => false
)

export const info = createSelector(
  spec,
	spec => returnSelfOrNewMap(spec && spec.get("info"))
)

export const externalDocs = createSelector(
  spec,
	spec => returnSelfOrNewMap(spec && spec.get("externalDocs"))
)

export const version = createSelector(
	info,
	info => info && info.get("version")
)

export const semver = createSelector(
	version,
	version => /v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(version).slice(1)
)

export const paths = createSelector(
	specJsonWithResolvedSubtrees,
	spec => spec.get("paths")
)

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

export const operations = createSelector(
  paths,
  paths => {
    if(!paths || paths.size < 1)
      return List()

    let list = List()

    if(!paths || !paths.forEach) {
      return List()
    }

    paths.forEach((path, pathName) => {
      if(!path || !path.forEach) {
        return {}
      }
      path.forEach((operation, method) => {
        if(OPERATION_METHODS.indexOf(method) < 0) {
          return
        }
        list = list.push(fromJS({
          path: pathName,
          method,
          operation,
          id: `${method}-${pathName}`
        }))
      })
    })

    return list
  }
)

export const consumes = createSelector(
  spec,
  spec => Set(spec.get("consumes"))
)

export const produces = createSelector(
  spec,
  spec => Set(spec.get("produces"))
)

export const security = createSelector(
    spec,
    spec => spec.get("security", List())
)

export const securityDefinitions = createSelector(
    spec,
    spec => spec.get("securityDefinitions")
)


export const findDefinition = ( state, name ) => {
  const resolvedRes = state.getIn(["resolvedSubtrees", "definitions", name], null)
  const unresolvedRes = state.getIn(["json", "definitions", name], null)
  return resolvedRes || unresolvedRes || null
}

export const definitions = createSelector(
  spec,
  spec => {
    const res = spec.get("definitions")
    return Map.isMap(res) ? res : Map()
  }
)

export const basePath = createSelector(
    spec,
    spec => spec.get("basePath")
)

export const host = createSelector(
    spec,
    spec => spec.get("host")
)

export const schemes = createSelector(
    spec,
    spec => spec.get("schemes", Map())
)

export const operationsWithRootInherited = createSelector(
  [
    operations,
    consumes,
    produces
  ],
  (operations, consumes, produces) => {
    return operations.map( ops => ops.update("operation", op => {
      if(op) {
        if(!Map.isMap(op)) { return }
        return op.withMutations( op => {
          if ( !op.get("consumes") ) {
            op.update("consumes", a => Set(a).merge(consumes))
          }
          if ( !op.get("produces") ) {
            op.update("produces", a => Set(a).merge(produces))
          }
          return op
        })
      } else {
        // return something with Immutable methods
        return Map()
      }

    }))
  }
)

export const tags = createSelector(
  spec,
  json => {
    const tags = json.get("tags", List())
    return List.isList(tags) ? tags.filter(tag => Map.isMap(tag)) : List()
  }
)

export const tagDetails = (state, tag) => {
  let currentTags = tags(state) || List()
  return currentTags.filter(Map.isMap).find(t => t.get("name") === tag, Map())
}

export const operationsWithTags = createSelector(
  operationsWithRootInherited,
  tags,
  (operations, tags) => {
    return operations.reduce( (taggedMap, op) => {
      let tags = Set(op.getIn(["operation","tags"]))
      if(tags.count() < 1)
        return taggedMap.update(DEFAULT_TAG, List(), ar => ar.push(op))
      return tags.reduce( (res, tag) => res.update(tag, List(), (ar) => ar.push(op)), taggedMap )
    }, tags.reduce( (taggedMap, tag) => {
      return taggedMap.set(tag.get("name"), List())
    } , OrderedMap()))
  }
)

export const taggedOperations = (state) => ({ getConfigs }) => {
  let { tagsSorter, operationsSorter } = getConfigs()
  return operationsWithTags(state)
    .sortBy(
      (val, key) => key, // get the name of the tag to be passed to the sorter
      (tagA, tagB) => {
        let sortFn = (typeof tagsSorter === "function" ? tagsSorter : sorters.tagsSorter[ tagsSorter ])
        return (!sortFn ? null : sortFn(tagA, tagB))
      }
    )
    .map((ops, tag) => {
      let sortFn = (typeof operationsSorter === "function" ? operationsSorter : sorters.operationsSorter[ operationsSorter ])
      let operations = (!sortFn ? ops : ops.sort(sortFn))

      return Map({ tagDetails: tagDetails(state, tag), operations: operations })
    })
}

export const responses = createSelector(
  state,
  state => state.get( "responses", Map() )
)

export const requests = createSelector(
    state,
    state => state.get( "requests", Map() )
)

export const mutatedRequests = createSelector(
    state,
    state => state.get( "mutatedRequests", Map() )
)

export const responseFor = (state, path, method) => {
  return responses(state).getIn([path, method], null)
}

export const requestFor = (state, path, method) => {
  return requests(state).getIn([path, method], null)
}

export const mutatedRequestFor = (state, path, method) => {
  return mutatedRequests(state).getIn([path, method], null)
}

export const allowTryItOutFor = () => {
  // This is just a hook for now.
  return true
}

export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
  const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], OrderedMap())
  const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], OrderedMap())

  const mergedParams = opParams.map((currentParam) => {
    const inNameKeyedMeta = metaParams.get(`${param.get("in")}.${param.get("name")}`)
    const hashKeyedMeta = metaParams.get(`${param.get("in")}.${param.get("name")}.hash-${param.hashCode()}`)
    return OrderedMap().merge(
      currentParam,
      inNameKeyedMeta,
      hashKeyedMeta
    )
  })
  return mergedParams.find(curr => curr.get("in") === param.get("in") && curr.get("name") === param.get("name"), OrderedMap())
}

export const parameterInclusionSettingFor = (state, pathMethod, paramName, paramIn) => {
  const paramKey = `${paramIn}.${paramName}`
  return state.getIn(["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey], false)
}


export const parameterWithMeta = (state, pathMethod, paramName, paramIn) => {
  const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], OrderedMap())
  const currentParam = opParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, OrderedMap())
  return parameterWithMetaByIdentity(state, pathMethod, currentParam)
}

export const operationWithMeta = (state, path, method) => {
  const op = specJsonWithResolvedSubtrees(state).getIn(["paths", path, method], OrderedMap())
  const meta = state.getIn(["meta", "paths", path, method], OrderedMap())

  const mergedParams = op.get("parameters", List()).map((param) => {
    return parameterWithMetaByIdentity(state, [path, method], param)
  })

  return OrderedMap()
    .merge(op, meta)
    .set("parameters", mergedParams)
}

// Get the parameter value by parameter name
export function getParameter(state, pathMethod, name, inType) {
  pathMethod = pathMethod || []
  let params = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
  return params.find( (p) => {
    return Map.isMap(p) && p.get("name") === name && p.get("in") === inType
  }) || Map() // Always return a map
}

export const hasHost = createSelector(
  spec,
  spec => {
    const host = spec.get("host")
    return typeof host === "string" && host.length > 0 && host[0] !== "/"
  }
)

// Get the parameter values, that the user filled out
export function parameterValues(state, pathMethod, isXml) {
  pathMethod = pathMethod || []
  let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List())
  return paramValues.reduce( (hash, p) => {
    let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value")
    if (List.isList(value)) {
      value = value.filter(v => v !== "")
    }
    return hash.set(paramToIdentifier(p, { allowHashes: false }), value)
  }, fromJS({}))
}

// True if any parameter includes `in: ?`
export function parametersIncludeIn(parameters, inValue="") {
  if(List.isList(parameters)) {
    return parameters.some( p => Map.isMap(p) && p.get("in") === inValue )
  }
}

// True if any parameter includes `type: ?`
export function parametersIncludeType(parameters, typeValue="") {
  if(List.isList(parameters)) {
    return parameters.some( p => Map.isMap(p) && p.get("type") === typeValue )
  }
}

// Get the consumes/produces value that the user selected
export function contentTypeValues(state, pathMethod) {
  pathMethod = pathMethod || []
  let op = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod], fromJS({}))
  let meta = state.getIn(["meta", "paths", ...pathMethod], fromJS({}))
  let producesValue = currentProducesFor(state, pathMethod)

  const parameters = op.get("parameters") || new List()

  const requestContentType = (
    meta.get("consumes_value") ? meta.get("consumes_value")
      : parametersIncludeType(parameters, "file") ? "multipart/form-data"
      : parametersIncludeType(parameters, "formData") ? "application/x-www-form-urlencoded"
      : undefined
  )

  return fromJS({
    requestContentType,
    responseContentType: producesValue
  })
}

// Get the currently selected produces value for an operation
export function currentProducesFor(state, pathMethod) {
  pathMethod = pathMethod || []

  const operation = specJsonWithResolvedSubtrees(state).getIn([ "paths", ...pathMethod], null)

  if(operation === null) {
    // return nothing if the operation does not exist
    return
  }

  const currentProducesValue = state.getIn(["meta", "paths", ...pathMethod, "produces_value"], null)
  const firstProducesArrayItem = operation.getIn(["produces", 0], null)

  return currentProducesValue || firstProducesArrayItem || "application/json"

}

// Get the produces options for an operation
export function producesOptionsFor(state, pathMethod) {
  pathMethod = pathMethod || []

  const spec = specJsonWithResolvedSubtrees(state)
  const operation = spec.getIn([ "paths", ...pathMethod], null)

  if(operation === null) {
    // return nothing if the operation does not exist
    return
  }

  const [path] = pathMethod

  const operationProduces = operation.get("produces", null)
  const pathItemProduces = spec.getIn(["paths", path, "produces"], null)
  const globalProduces = spec.getIn(["produces"], null)

  return operationProduces || pathItemProduces || globalProduces
}

// Get the consumes options for an operation
export function consumesOptionsFor(state, pathMethod) {
  pathMethod = pathMethod || []

  const spec = specJsonWithResolvedSubtrees(state)
  const operation = spec.getIn(["paths", ...pathMethod], null)

  if (operation === null) {
    // return nothing if the operation does not exist
    return
  }

  const [path] = pathMethod

  const operationConsumes = operation.get("consumes", null)
  const pathItemConsumes = spec.getIn(["paths", path, "consumes"], null)
  const globalConsumes = spec.getIn(["consumes"], null)

  return operationConsumes || pathItemConsumes || globalConsumes
}

export const operationScheme = ( state, path, method ) => {
  let url = state.get("url")
  let matchResult = url.match(/^([a-z][a-z0-9+\-.]*):/)
  let urlScheme = Array.isArray(matchResult) ? matchResult[1] : null

  return state.getIn(["scheme", path, method]) || state.getIn(["scheme", "_defaultScheme"]) || urlScheme || ""
}

export const canExecuteScheme = ( state, path, method ) => {
  return ["http", "https"].indexOf(operationScheme(state, path, method)) > -1
}

export const validationErrors = (state, pathMethod) => {
  pathMethod = pathMethod || []
  const paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
  const result = []

  if (paramValues.length === 0) return result

  const getErrorsWithPaths = (errors, path = []) => {
    const getNestedErrorsWithPaths = (e, path) => {
      const currPath = [...path, e.get("propKey") || e.get("index")]
      return Map.isMap(e.get("error")) 
        ? getErrorsWithPaths(e.get("error"), currPath) 
        : { error: e.get("error"), path: currPath }
    }

    return List.isList(errors)
     ? errors.map((e) => (Map.isMap(e) ? getNestedErrorsWithPaths(e, path) : { error: e, path }))
     : getNestedErrorsWithPaths(errors, path)
  }

  const formatError = (error, path, paramName) => {
    path = path.reduce((acc, curr) => {
      return typeof curr === "number" 
        ? `${acc}[${curr}]` 
        : acc 
        ? `${acc}.${curr}` 
        : curr
    }, "")
    return `For '${paramName}'${path ? ` at path '${path}'` : ""}: ${error}.`
  }

  paramValues.forEach( (p, key) => {
    const paramName = key.split(".").slice(1, -1).join(".")
    const errors = p.get("errors")
    if (errors && errors.count()) {
      const errorsWithPaths = getErrorsWithPaths(errors)
      errorsWithPaths.forEach(({error, path}) => {
        result.push(formatError(error, path, paramName))
      })
    }
  })
  return result
}

export const validateBeforeExecute = (state, pathMethod) => {
  return validationErrors(state, pathMethod).length === 0
}

export const getOAS3RequiredRequestBodyContentType = (state, pathMethod) => {
  let requiredObj = {
    requestBody: false,
    requestContentType: {}
  }
  let requestBody = state.getIn(["resolvedSubtrees", "paths", ...pathMethod, "requestBody"], fromJS([]))
  if (requestBody.size < 1) {
    return requiredObj
  }
  if (requestBody.getIn(["required"])) {
    requiredObj.requestBody = requestBody.getIn(["required"])
  }
  requestBody.getIn(["content"]).entrySeq().forEach((contentType) => { // e.g application/json
    const key = contentType[0]
    if (contentType[1].getIn(["schema", "required"])) {
      const val = contentType[1].getIn(["schema", "required"]).toJS()
      requiredObj.requestContentType[key] = val
    }
  })
  return requiredObj
}

export const isMediaTypeSchemaPropertiesEqual = ( state, pathMethod, currentMediaType, targetMediaType) => {
  if((currentMediaType || targetMediaType) && currentMediaType === targetMediaType ) {
    return true
  }
  let requestBodyContent = state.getIn(["resolvedSubtrees", "paths", ...pathMethod, "requestBody", "content"], fromJS([]))
  if (requestBodyContent.size < 2 || !currentMediaType || !targetMediaType) {
    // nothing to compare
    return false
  }
  let currentMediaTypeSchemaProperties = requestBodyContent.getIn([currentMediaType, "schema", "properties"], fromJS([]))
  let targetMediaTypeSchemaProperties = requestBodyContent.getIn([targetMediaType, "schema", "properties"], fromJS([]))
  return !!currentMediaTypeSchemaProperties.equals(targetMediaTypeSchemaProperties)
}

function returnSelfOrNewMap(obj) {
  // returns obj if obj is an Immutable map, else returns a new Map
  return Map.isMap(obj) ? obj : new Map()
}
