import log from "@racletteCore/lib/logger"
import {
  promisifyActionResolve,
  markDataAsModified,
  handleErrors,
} from "@racletteCore/lib/dataHelper"
import { $api, $store } from "@racletteCore"
import {
  pluginReadOperations,
  handleOperationTarget,
  prefixOperationTarget,
} from "@racletteCore/setup/plugin-system"
import {
  getCacheQueryAndHash,
  isParentResultFresh,
} from "@racletteCore/store/reducers/queriesCache/queryCacheHelper"
import { $eventbus } from "@racletteCore"

// to be replaced with actual open search availability state
let isOnline = true

// Store active controllers for each actionId - structured for potential extraction
const requestControllerManager = {
  controllers: {},

  create(actionId) {
    this.abort(actionId) // Cancel existing request
    const controller = new AbortController()
    this.controllers[actionId] = controller
    return controller
  },

  abort(actionId) {
    if (this.controllers[actionId]) {
      this.controllers[actionId].abort()
      delete this.controllers[actionId]
    }
  },

  cleanup(actionId) {
    delete this.controllers[actionId]
  },
}

type ItemDispatcherResultObject = {
  result: Array<Object>
  error?: Object | null
  backendQueryParams?: Object
}

type ItemDispatcherActionIdentifier = {
  cacheKey: String
  actionId: String
}

const itemDispatcher = async (
  actionIdentifier: ItemDispatcherActionIdentifier = {
    cacheKey: "",
    actionId: "",
  },
  resultObj: ItemDispatcherResultObject = {
    result: [],
    backendQueryParams: {},
    error: null,
  },
  remote: Boolean = false,
) => {
  const { actionId, cacheKey } = actionIdentifier
  const { result, backendQueryParams } = resultObj
  const resolveId = actionId + "_resolve"
  if (remote) {
    $store.dispatch({
      type: "data/addFromServer",
      payload: { actionIdentifier, result },
    })
  } else {
    $store.dispatch({
      type: "data/addFromClient",
      payload: { actionIdentifier, result },
    })
  }

  if (backendQueryParams && Object.keys(backendQueryParams).length > 0) {
    $store.dispatch({
      type: "queries/update",
      payload: {
        actionId,
        backend: backendQueryParams,
      },
    })
  }

  $store.dispatch({
    type: "queries/updateItems",
    payload: { items: result, actionId, cacheKey, resolveId },
  })

  if (resultObj.error) {
    throw resultObj.error
  }
  $store.dispatch({
    type: "queries/update/loading",
    payload: {
      loading: false,
      actionId: actionId,
    },
  })
  return Promise.resolve(result.map((res) => res._source))
}

// Cache Management Functions
const cacheOperations = {
  async setupCache(actionId, target, params, options) {
    if (options?.useStore === false)
      return { cachedQuery: null, actionIdentifier: { actionId } }
    const cachedQuery = getCacheQueryAndHash(target, params)
    const actionIdentifier = {
      actionId,
      cacheKey: cachedQuery.hash,
    }

    await $store.dispatch({
      type: "queriesCache/add",
      payload: {
        cacheKey: cachedQuery.hash,
        params,
        target,
      },
    })
    await $store.dispatch({
      type: "queries/setCacheKey",
      payload: {
        cacheKey: cachedQuery.hash,
        actionId,
      },
    })
    return { cachedQuery, actionIdentifier }
  },

  async handleCacheHit(cachedQuery, actionIdentifier) {
    if (cachedQuery.query.loading === true) {
      // Return promise to be resolved once cache query resolves
      return promisifyActionResolve(
        `cacheQuery_${cachedQuery.hash}`,
        async (result) => {
          const items = result?.payload?.items || []
          return await itemDispatcher(
            actionIdentifier,
            { result: items },
            false,
          )
        },
      )
    }

    // Return fresh cached result
    if (isParentResultFresh(cachedQuery.hash)) {
      const uuids = cachedQuery.query.uuids
      const currentData = $store.getState("data")
      const result = uuids.map((uuid) => currentData[uuid]) || []
      return await itemDispatcher(actionIdentifier, { result }, false)
    }

    return null // No valid cache hit
  },
}

const finishRemoteQuery = (actionIdentifier, result = {}, params, options) => {
  const { actionId, cacheKey } = actionIdentifier
  log.api("finishRemoteQuery", { actionId, params, result })

  if (!result?.data) {
    return
  }
  let resultBody = structuredClone(result.data.body)
  const { type } = result.data
  if (type === "dataReadModified") {
    resultBody = markDataAsModified(resultBody)
  }

  const resultObj = {
    result: [],
    backendQueryParams: {},
    error: result.error,
  }

  if (resultBody?.items?.length) {
    resultBody.items.forEach((item) => resultObj.result.push(item))
  }

  if (options?.useStore === false) {
    $store.dispatch({
      type: "queries/update/loading",
      payload: {
        loading: false,
        actionId: actionId,
      },
    })
    return {
      result: resultObj,
      response: result.data,
    }
  }

  return itemDispatcher(actionIdentifier, resultObj, true)
}
// Unified remote data handler for both GET and POST
const handleRemoteRequest = async (
  actionIdentifier,
  method,
  target,
  params,
  options,
  signal,
) => {
  const appRequestCode =
    params.requestCode || `${method.toLowerCase()}RemoteData`
  const config = {
    headers: {
      "app-request-code": appRequestCode,
    },
    signal,
  }

  // For GET requests, params go in config.params; for POST, they go in the body
  if (method === "GET") {
    config.params = params
  }

  log.api(`${method.toLowerCase()}RemoteData`, {
    actionIdentifier,
    target,
    params,
    config,
  })

  try {
    const response =
      method === "GET"
        ? await $api.axios.get(target, config).catch(handleErrors)
        : await $api.axios.post(target, params, config).catch(handleErrors)

    return {
      response,
      result: finishRemoteQuery(actionIdentifier, response, params, options),
    }
  } catch (e) {
    if (e.name === "AbortError" || e.name === "CanceledError") {
      log.api(`${method} request was canceled for actionId:`, actionIdentifier)
      throw e
    }

    log.api(`${method.toLowerCase()}RemoteData catch`, {
      actionIdentifier,
      target,
      config,
      e,
    })

    return {
      response: e,
      result: finishRemoteQuery(
        actionIdentifier,
        { data: [], error: e?.response?.data || e },
        params,
        options,
      ),
    }
  }
}

const searchQuery = async (
  actionIdentifier,
  params,
  options,
  signal,
  target = "auto",
) => {
  const { actionId, cacheKey } = actionIdentifier
  const project = $store.getState("project")
  const backend = $store.getState("backend")

  let localProjects = params.project?.length > 0 ? params.project : project._id
  localProjects = Array.isArray(localProjects) ? localProjects : [localProjects]
  const projectsToQueryFrom = params.projects || localProjects

  if (!params.options) {
    params.options = {}
  }

  if (params.find) {
    isOnline = backend?.socket?.status === 1
    const finalParams = structuredClone(params)

    if (isOnline || target === "remote" || (isOnline && target === "auto")) {
      const query = {
        find: finalParams.find,
        types: finalParams.types,
        options: finalParams.options,
        project: projectsToQueryFrom,
      }

      log.api("searchQuery", {
        actionIdentifier,
        target: "core/find/query",
        query,
        isOnline,
      })

      return handleRemoteRequest(
        actionIdentifier,
        "POST",
        "core/find/query",
        query,
        options,
        signal,
      )
    } else {
      console.warn("no backend connection!")
    }
  }
}

const handlePluginReadRoutes = async (actionId, target, params) => {
  let routeUpdated = false
  const updatedRouteData = { actionId, target, params, validTarget: target }

  if (!pluginReadOperations[actionId]) return updatedRouteData

  const pluginReadOperation = pluginReadOperations[actionId]
  const updatedTarget = prefixOperationTarget(
    handleOperationTarget(pluginReadOperation.target, params),
    pluginReadOperation.targetPrefix,
  )

  if (updatedTarget !== pluginReadOperation.target) {
    updatedRouteData.validTarget = updatedTarget
    routeUpdated = true
  }

  if (routeUpdated) {
    await $store.dispatch({
      type: "queries/update",
      payload: {
        target: updatedRouteData.validTarget,
        actionId: actionId,
      },
    })
  }

  return updatedRouteData
}

// Route Handler Registry
const routeHandlers = {
  "remote/get": async (actionIdentifier, target, params, options, signal) => {
    const cleanTarget = target.split("remote/get")[1]
    return handleRemoteRequest(
      actionIdentifier,
      "GET",
      cleanTarget,
      params,
      options,
      signal,
    )
  },

  "remote/post": async (actionIdentifier, target, params, options, signal) => {
    const cleanTarget = target.split("remote/post")[1]
    return handleRemoteRequest(
      actionIdentifier,
      "POST",
      cleanTarget,
      params,
      options,
      signal,
    )
  },

  "search/query": async (actionIdentifier, target, params, options, signal) => {
    if (params.search?.q === "@showall") {
      const dataState = $store.getState("data")
      return {
        response: "dataStore",
        result: itemDispatcher(
          actionIdentifier,
          {
            result: Object.values(dataState),
          },
          true,
        ),
      }
    }

    return {
      response: "dataStore",
      result: itemDispatcher(
        actionIdentifier,
        {
          result: [],
        },
        true,
      ),
    }
  },
}

const executeRoute = async (actionIdentifier, validTarget, params, options) => {
  // Find matching route handler
  for (const [routePrefix, handler] of Object.entries(routeHandlers)) {
    if (validTarget.indexOf(routePrefix) === 0 || validTarget === routePrefix) {
      const controller = requestControllerManager.create(
        actionIdentifier.actionId,
      )

      try {
        const result = await handler(
          actionIdentifier,
          validTarget,
          params,
          options,
          controller.signal,
        ).catch((e) => {
          log.error(e.error, e.message)
        })
        requestControllerManager.cleanup(actionIdentifier.actionId)
        return result
      } catch (error) {
        requestControllerManager.cleanup(actionIdentifier.actionId)

        if (error.name === "AbortError" || error.name === "CanceledError") {
          log.api(
            "HTTP request was canceled for actionId:",
            actionIdentifier.actionId,
          )
          throw error
        }

        throw error
      }
    }
  }

  // No route handler found
  throw new Error(`No route handler found for target: ${validTarget}`)
}

const getData = async (actionId, target, params, options) => {
  if (!target) {
    return log.api("getData", "no target given", params)
  }

  // Handle plugin routing
  const { validTarget } = await handlePluginReadRoutes(actionId, target, params)
  if (options?.useCache !== false) {
    // Setup cache
    const { cachedQuery, actionIdentifier } = await cacheOperations.setupCache(
      actionId,
      validTarget,
      params,
      options,
    )
    // Check cache
    if (cachedQuery?.query) {
      const cacheResult = await cacheOperations.handleCacheHit(
        cachedQuery,
        actionIdentifier,
      )
      if (cacheResult !== null) {
        return {
          response: "cached_" + cachedQuery.hash,
          result: cacheResult,
        }
      }
    }

    // Execute route
    return executeRoute(actionIdentifier, validTarget, params, options)
  }

  // Execute route
  return executeRoute({ actionId }, validTarget, params, options)
}

export default getData
