import path from "node:path"
import fs from "node:fs"
import { PROFILES_PATH } from "../../../utils/global"
import { parseContent } from "../../../utils/common"
import { getConfigData } from "../../../utils/config"
import { logError, logInfo } from "../../../utils/logger"
import runCustomScript from "../shared/scriptExecutor"
import { HTTPMethod } from "./restHttpMethod"
import {
  execPostRequestScript,
  execPreRequestScript
} from "../shared/requestScript"

const getEnvData = (
  profile?: string,
  env?: string
): { path: string, content: string } | undefined => {
  try {
    const configData = getConfigData()

    const targetProfile = profile || configData?.defaultProfile
    const envDirPath = path.join(PROFILES_PATH, targetProfile, "env")
    const targetEnv = `${env || "default"}.jsonc`
    const envFilePath = path.join(envDirPath, targetEnv)
    const envContent = fs.readFileSync(envFilePath, "utf-8")

    return {
      path: envFilePath,
      content: envContent,
    }
  } catch (err) {
    throw new Error(`Failed to get env data: ${err}`)
  }
}

const getEndpointData = (
  name: string,
  profile?: string
): { path: string, data: Record<string, any> } | undefined => {
  if (!name || name.trim() === "") {
    throw new Error("Name is required")
  }

  try {
    const configData = getConfigData()

    const targetProfile = profile || configData?.defaultProfile
    const endpointsDirPath = path.join(
      PROFILES_PATH, targetProfile, "endpoints"
    )

    const targetEndpointPath = name.split(":").slice(0, -1).join("/")
    const targetEndpointName = name.split(":").slice(-1)[0]
    const endpointFilePath = path.join(
      endpointsDirPath, `${targetEndpointPath || "default"}.jsonc`
    )

    const endpointContent = fs.readFileSync(endpointFilePath, "utf-8")
    const content = parseContent(endpointContent)
    const data = Object.entries(content.parsed)
      .filter(([key]) => key === targetEndpointName)[0]

    return {
      path: endpointFilePath,
      data: data[1] as Record<string, any>
    }
  } catch (err) {
    logError(`${err}`)
    return undefined
  }
}

const assignValueFromVar = (
  src: Record<string, any>,
  target: Record<string, any>
) => {
  let result = { ...target }

  const hasBracket = (str: string) => {
    return str[0] === "{" && str[1] === "{"
      && str[str.length - 1] === "}" && str[str.length - 2] === "}"
  }

  for (const [key, value] of Object.entries(target)) {
    if (!hasBracket(value)) {
      continue
    }

    const paramKey = value.slice(2, value.length - 2)
    if (src[paramKey]) {
      result[key] = src[paramKey]
    } else {
      result[key] = ""
    }
  }

  return result
}

const assignValueFromArgs = (
  target: Record<string, any>,
  args?: string
) => {
  let result = { ...target }

  if (!args || args.trim() === "") {
    return result
  }

  const argObjects = args.split(":").map((arg) => {
    const [key, value] = arg.split("=")
    return { [key]: value }
  })

  for (const arg of argObjects) {
    const key = Object.keys(arg)[0]
    const value = Object.values(arg)[0]
    if (target[key]) {
      result[key] = value
    }
  }

  return result
}

const assignValueFromScript = (target: Record<string, any>) => {
  let result = { ...target }

  const hasScriptSign = (str: string) => {
    return str[0] === "<" && str[1] === "%"
      && str[str.length - 2] === "%" && str[str.length - 1] === ">"
  }

  const getScriptName = (key: string): string => {
    const openParenthesisIndex = key.indexOf("(")
    return key.slice(2, openParenthesisIndex)
  }

  const getArgs = (key: string): string[] => {
    const openParenthesisIndex = key.indexOf("(")
    const closeParenthesisIndex = key.indexOf(")")

    return key.slice(openParenthesisIndex + 1, closeParenthesisIndex)
      .split(",")
  }

  for (const [key, value] of Object.entries(target)) {
    if (!hasScriptSign(value)) {
      continue
    }

    const scriptName = getScriptName(value)
    const scriptFn = value.slice(2, value.length - 2)
    const args = getArgs(scriptFn)
    const valueFromScript = runCustomScript(scriptName, ...args)
    if (valueFromScript) {
      result[key] = valueFromScript
    }
  }

  return result
}

const assignEndpointVariables = (
  src: Record<string, unknown>,
  endpoint: string
) => {

  const endpointVariables = endpoint.match(/\{\{(.+?)\}\}/g)

  if (!endpointVariables) {
    return endpoint
  }

  let result = endpoint

  for (const variable of endpointVariables) {
    const variableName = variable.slice(2, variable.length - 2)
    const variableValue = src[variableName]
    if (!variableValue) {
      continue
    }

    const formattedVariable = `${variableValue}`.trim().replace(/\s+/g, " ")
    const finalVariable = formattedVariable.replaceAll(/\s/g, "%20")

    result = result.replace(variable, finalVariable)
  }

  return result
}

const configureRequest = (
  envData: Record<string, any> | undefined,
  endpointData: Record<string, any> | undefined,
  additionalArgs: string
): {
  method: HTTPMethod,
  finalURL: string,
  requestData: Record<string, any>
} => {
  if (!envData || !endpointData) {
    logError("Environment or endpoint data not found")
    return { method: "GET", finalURL: "", requestData: {} }
  }

  const endpointPath = assignEndpointVariables(envData, endpointData["path"])
  const fullURL = `${envData.protocol}://${envData.baseURL}${endpointPath}`
  const method: HTTPMethod = endpointData["method"]
  const headers: Record<string, string> = assignValueFromScript(
    assignValueFromArgs(
      assignValueFromVar(envData, endpointData["headers"]),
      additionalArgs
    )
  )
  const params = assignValueFromScript(
    assignValueFromArgs(
      assignValueFromVar(envData, endpointData["params"]),
      additionalArgs
    )
  )

  let finalURL = fullURL
  let requestData: Record<string, any> = {}
  switch (method) {
    case "GET":
      // TODO: Need to revisit this part
      finalURL = params && Object.keys(params).length > 0
        ? `${fullURL}?${new URLSearchParams(params).toString()}`
        : fullURL
      requestData = {
        method,
        headers
      }
      break
    case "POST":
    case "PUT":
    case "PATCH":
      requestData = {
        method,
        headers,
        body: params ? params : ""
      }
      break
    case "DELETE":
      requestData = {
        method,
        headers
      }
      break
  }

  return { method, finalURL, requestData }
}

const constructResponse = async (
  method: string,
  response: Response
) => {
  let responseHeaders: Record<string, string> = {}
  for (const [key, value] of response.headers.entries()) {
    responseHeaders[key] = value
  }

  const contentType = responseHeaders["content-type"]
  if (contentType.includes("text/html")) {
    return await response.text()
  }

  const result: Record<string, unknown> = {}

  if (response.status > 499) {
    result["status"] = response.status
    result["error"] = response.statusText

    return result
  }

  const responseBody = await response.json()
  result[method] = response.url
  result["status"] = response.status
  result["headers"] = responseHeaders
  result["cookie"] = response.headers.getSetCookie()
  result["body"] = responseBody

  return result
}

const httpRequest = async (opt: Record<string, string | boolean>) => {
  const verbose = opt?.verbose as boolean

  try {
    const env = getEnvData(opt?.profile as string, opt?.env as string)
    if (!env) {
      logError("Environment data not found")
      return
    }

    const envContent = parseContent(env.content)
    const envData = envContent.parsed
    const protocol = envData.protocol as string
    const baseURL = envData.baseURL as string
    if (!protocol || !baseURL) {
      logError("Base URL not found")
      return
    }

    const name = opt?.call as string
    const endpoint = getEndpointData(name, opt?.profile as string)
    if (!endpoint) {
      logError("Endpoint data not found")
      return
    }

    const endpointData = endpoint.data
    const preRequestScript = endpointData.scripts?.pre as string
    let updatedEnv: Record<string, any> = envData
    if (preRequestScript) {
      updatedEnv = execPreRequestScript(preRequestScript, env, verbose)
    }

    const additionalArgs = opt?.args as string
    const { method, finalURL, requestData } = configureRequest(
      envData,
      endpointData,
      additionalArgs
    )

    if (verbose) {
      logInfo("[REST] Request Payload:")
      console.log({ destination: finalURL, ...requestData })
      console.log()
    }

    const response = await fetch(
      finalURL,
      {
        ...requestData,
        body: JSON.stringify(requestData.body)
      } as RequestInit
    )

    const httpResponse = await constructResponse(method, response)
    if (typeof httpResponse === "string") {
      return httpResponse
    }

    const postRequestScript = endpointData.scripts?.post as string
    if (postRequestScript) {
      execPostRequestScript(
        postRequestScript,
        {
          path: env.path,
          unparseable: envContent.unparseable,
          parsed: updatedEnv
        },
        httpResponse,
        verbose
      )
    }

    return httpResponse
  } catch (err) {
    logError(`${err}`)
  }
}

export default async function executeRestApi(
  options: Record<string, string | boolean>
) {
  console.time("[REST] API Request Time")

  const response = await httpRequest(options)
  console.log("[REST] API Response:")
  console.dir(response, { depth: 5 })
  console.log()

  console.timeEnd("[REST] API Request Time")
}
