import { Environment } from "@hoppscotch/data"
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
import { runTestScript } from "@hoppscotch/js-sandbox/web"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import { flow, pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es"
import { Observable, Subject } from "rxjs"
import { filter } from "rxjs/operators"
import { Ref } from "vue"

import {
  environmentsStore,
  getCurrentEnvironment,
  getEnvironment,
  getGlobalVariables,
  setGlobalEnvVariables,
  updateEnvironment,
} from "~/newstore/environments"
import { HoppTab } from "~/services/tab"
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
import { createRESTNetworkRequestStream } from "./network"
import {
  getCombinedEnvVariables,
  getFinalEnvsFromPreRequest,
} from "./preRequest"
import { HoppRESTDocument } from "./rest/document"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { isJSONContentType } from "./utils/contenttypes"
import {
  SecretEnvironmentService,
  SecretVariable,
} from "~/services/secret-environment.service"
import { getService } from "~/modules/dioc"

const secretEnvironmentService = getService(SecretEnvironmentService)

const getTestableBody = (
  res: HoppRESTResponse & { type: "success" | "fail" }
) => {
  const contentTypeHeader = res.headers.find(
    (h) => h.key.toLowerCase() === "content-type"
  )

  const rawBody = new TextDecoder("utf-8")
    .decode(res.body)
    .replaceAll("\x00", "")

  const x = pipe(
    // This pipeline just decides whether JSON parses or not
    contentTypeHeader && isJSONContentType(contentTypeHeader.value)
      ? O.of(rawBody)
      : O.none,

    // Try parsing, if failed, go to the fail option
    O.chain((body) => O.tryCatch(() => JSON.parse(body))),

    // If JSON, return that (get), else return just the body string (else)
    O.getOrElse<any | string>(() => rawBody)
  )

  return x
}

const combineEnvVariables = (envs: {
  global: Environment["variables"]
  selected: Environment["variables"]
}) => [...envs.selected, ...envs.global]

export const executedResponses$ = new Subject<
  HoppRESTResponse & { type: "success" | "fail " }
>()

/**
 * Used to update the environment schema with the secret variables
 * and store the secret variable values in the secret environment service
 * @param envs The environment variables to update
 * @param type Whether the environment variables are global or selected
 * @returns the updated environment variables
 */
const updateEnvironmentsWithSecret = (
  envs: Environment["variables"] &
    {
      secret: true
      value: string | undefined
      key: string
    }[],
  type: "global" | "selected"
) => {
  const currentEnvID =
    type === "selected" ? getCurrentEnvironment().id : "Global"

  const updatedSecretEnvironments: SecretVariable[] = []

  const updatedEnv = pipe(
    envs,
    A.mapWithIndex((index, e) => {
      if (e.secret) {
        updatedSecretEnvironments.push({
          key: e.key,
          value: e.value ?? "",
          varIndex: index,
        })

        // delete the value from the environment
        // so that it doesn't get saved in the environment
        delete e.value
        return e
      }
      return e
    })
  )
  if (currentEnvID) {
    secretEnvironmentService.addSecretEnvironment(
      currentEnvID,
      updatedSecretEnvironments
    )
  }
  return updatedEnv
}

export function runRESTRequest$(
  tab: Ref<HoppTab<HoppRESTDocument>>
): [
  () => void,
  Promise<
    | E.Left<"script_fail" | "cancellation">
    | E.Right<Observable<HoppRESTResponse>>
  >,
] {
  let cancelCalled = false
  let cancelFunc: (() => void) | null = null

  const cancel = () => {
    cancelCalled = true
    cancelFunc?.()
  }

  const res = getFinalEnvsFromPreRequest(
    tab.value.document.request.preRequestScript,
    getCombinedEnvVariables()
  ).then((envs) => {
    if (cancelCalled) return E.left("cancellation" as const)

    if (E.isLeft(envs)) {
      console.error(envs.left)
      return E.left("script_fail" as const)
    }

    const requestAuth =
      tab.value.document.request.auth.authType === "inherit" &&
      tab.value.document.request.auth.authActive
        ? tab.value.document.inheritedProperties?.auth.inheritedAuth
        : tab.value.document.request.auth

    let requestHeaders

    const inheritedHeaders =
      tab.value.document.inheritedProperties?.headers.map((header) => {
        if (header.inheritedHeader) {
          return header.inheritedHeader
        }
        return []
      })

    if (inheritedHeaders) {
      requestHeaders = [
        ...inheritedHeaders,
        ...tab.value.document.request.headers,
      ]
    } else {
      requestHeaders = [...tab.value.document.request.headers]
    }

    const finalRequest = {
      ...tab.value.document.request,
      auth: requestAuth ?? { authType: "none", authActive: false },
      headers: requestHeaders,
    }

    const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
      name: "Env",
      variables: combineEnvVariables(envs.right),
    })

    const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
    cancelFunc = cancelRun

    const subscription = stream
      .pipe(filter((res) => res.type === "success" || res.type === "fail"))
      .subscribe(async (res) => {
        if (res.type === "success" || res.type === "fail") {
          executedResponses$.next(
            // @ts-expect-error Typescript can't figure out this inference for some reason
            res
          )

          const runResult = await runTestScript(
            res.req.testScript,
            envs.right,
            {
              status: res.statusCode,
              body: getTestableBody(res),
              headers: res.headers,
            }
          )

          if (E.isRight(runResult)) {
            const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
              cloneDeep(runResult.right.envs.global),
              "global"
            )

            const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
              cloneDeep(runResult.right.envs.selected),
              "selected"
            )

            // set the response in the tab so that multiple tabs can run request simultaneously
            tab.value.document.response = res

            const updatedRunResult = {
              ...runResult.right,
              envs: {
                global: updatedGlobalEnvVariables,
                selected: updatedSelectedEnvVariables,
              },
            }

            tab.value.document.testResults =
              translateToSandboxTestResults(updatedRunResult)

            setGlobalEnvVariables(
              updateEnvironmentsWithSecret(
                runResult.right.envs.global,
                "global"
              )
            )
            if (
              environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
            ) {
              const env = getEnvironment({
                type: "MY_ENV",
                index: environmentsStore.value.selectedEnvironmentIndex.index,
              })
              updateEnvironment(
                environmentsStore.value.selectedEnvironmentIndex.index,
                {
                  name: env.name,
                  v: 1,
                  id: env.id ?? "",
                  variables: updatedRunResult.envs.selected,
                }
              )
            } else if (
              environmentsStore.value.selectedEnvironmentIndex.type ===
              "TEAM_ENV"
            ) {
              const env = getEnvironment({
                type: "TEAM_ENV",
              })
              pipe(
                updateTeamEnvironment(
                  JSON.stringify(updatedRunResult.envs.selected),
                  environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
                  env.name
                )
              )()
            }
          } else {
            tab.value.document.testResults = {
              description: "",
              expectResults: [],
              tests: [],
              envDiff: {
                global: {
                  additions: [],
                  deletions: [],
                  updations: [],
                },
                selected: {
                  additions: [],
                  deletions: [],
                  updations: [],
                },
              },
              scriptError: true,
            }
          }

          subscription.unsubscribe()
        }
      })

    return E.right(stream)
  })

  return [cancel, res]
}

const getAddedEnvVariables = (
  current: Environment["variables"],
  updated: Environment["variables"]
) => updated.filter((x) => current.findIndex((y) => y.key === x.key) === -1)

const getRemovedEnvVariables = (
  current: Environment["variables"],
  updated: Environment["variables"]
) => current.filter((x) => updated.findIndex((y) => y.key === x.key) === -1)

const getUpdatedEnvVariables = (
  current: Environment["variables"],
  updated: Environment["variables"]
) =>
  pipe(
    updated,
    A.filterMap(
      flow(
        O.of,
        O.bindTo("env"),
        O.bind("index", ({ env }) =>
          pipe(
            current.findIndex((x) => x.key === env.key),
            O.fromPredicate((x) => x !== -1)
          )
        ),
        O.chain(
          O.fromPredicate(
            ({ env, index }) => env.value !== current[index].value
          )
        ),
        O.map(({ env, index }) => ({
          ...env,
          previousValue: current[index].value,
        }))
      )
    )
  )

function translateToSandboxTestResults(
  testDesc: SandboxTestResult
): HoppTestResult {
  const translateChildTests = (child: TestDescriptor): HoppTestData => {
    return {
      description: child.descriptor,
      expectResults: child.expectResults,
      tests: child.children.map(translateChildTests),
    }
  }

  const globals = cloneDeep(getGlobalVariables())
  const env = getCurrentEnvironment()
  return {
    description: "",
    expectResults: testDesc.tests.expectResults,
    tests: testDesc.tests.children.map(translateChildTests),
    scriptError: false,
    envDiff: {
      global: {
        additions: getAddedEnvVariables(globals, testDesc.envs.global),
        deletions: getRemovedEnvVariables(globals, testDesc.envs.global),
        updations: getUpdatedEnvVariables(globals, testDesc.envs.global),
      },
      selected: {
        additions: getAddedEnvVariables(env.variables, testDesc.envs.selected),
        deletions: getRemovedEnvVariables(
          env.variables,
          testDesc.envs.selected
        ),
        updations: getUpdatedEnvVariables(
          env.variables,
          testDesc.envs.selected
        ),
      },
    },
  }
}
