<template>
  <div class="flex items-center px-4 border-b border-dividerLight">
    <label class="truncate font-semibold text-secondaryLight">
      {{ t("authorization.oauth.grant_type") }}
    </label>

    <tippy
      placement="bottom"
      interactive
      trigger="click"
      theme="popover"
      :on-shown="() => grantTypeTippyActions?.focus()"
      class="!flex-initial"
    >
      <HoppSmartSelectWrapper>
        <HoppButtonSecondary
          class="ml-2 rounded-none pr-8"
          :label="selectedGrantType?.label"
        />
      </HoppSmartSelectWrapper>
      <template #content="{ hide }">
        <div
          ref="grantTypeTippyActions"
          class="flex flex-col focus:outline-none"
          tabindex="0"
          @keyup.escape="hide()"
        >
          <HoppSmartItem
            v-for="grantType in supportedGrantTypes"
            :key="grantType.id"
            :label="grantType.label"
            :icon="
              selectedGrantTypeID === grantType.id ? IconCircleDot : IconCircle
            "
            :active="selectedGrantTypeID === grantType.id"
            @click="
              () => {
                changeSelectedGrantType(grantType.id)

                hide()
              }
            "
          />
        </div>
      </template>
    </tippy>
  </div>

  <div class="flex flex-col">
    <div
      v-for="element in currentOAuthGrantTypeFormElements"
      :key="element.id"
      class="flex flex-1 border-b border-dividerLight"
    >
      <SmartEnvInput
        v-if="element.type === 'text'"
        v-model="element.ref.value"
        :placeholder="element.label"
        :envs="envs"
        :auto-complete-env="true"
      />
      <div
        v-else-if="element.type === 'checkbox'"
        class="px-4 py-2 flex items-center"
      >
        <span class="text-secondaryLight font-semibold mr-6">{{
          element.label
        }}</span>
        <HoppSmartCheckbox
          class="text-secondaryLight flex"
          :on="element.ref.value"
          @change="element.ref.value = !element.ref.value"
        ></HoppSmartCheckbox>
      </div>
      <div
        v-else-if="element.type === 'dropdown'"
        class="flex items-center px-4"
      >
        <label class="truncate text-secondaryLight">
          {{ element.label }}
        </label>
        <tippy
          v-if="element.ref.value"
          interactive
          trigger="click"
          theme="popover"
          class="!flex-initial"
        >
          <HoppSmartSelectWrapper>
            <HoppButtonSecondary
              class="ml-2 rounded-none pr-8"
              :label="element.ref.value.label"
            />
          </HoppSmartSelectWrapper>
          <template #content="{ hide }">
            <div
              :ref="element.tippyRefName"
              class="flex flex-col focus:outline-none"
              tabindex="0"
              @keyup.escape="hide()"
            >
              <HoppSmartItem
                v-for="item in element.options"
                :key="item.id"
                :label="item.label"
                :icon="
                  element.ref.value.id === item.id ? IconCircleDot : IconCircle
                "
                :active="element.ref.value.id === item.id"
                @click="
                  () => {
                    element.ref.value = item
                    hide()
                  }
                "
              ></HoppSmartItem>
            </div>
          </template>
        </tippy>
      </div>
    </div>

    <div class="flex items-center border-b border-dividerLight">
      <span class="flex items-center">
        <label class="ml-4 text-secondaryLight">
          {{ t("authorization.pass_key_by") }}
        </label>
        <tippy
          interactive
          trigger="click"
          theme="popover"
          :on-shown="() => authTippyActions?.focus()"
        >
          <HoppSmartSelectWrapper>
            <HoppButtonSecondary
              :label="passBy"
              class="ml-2 rounded-none pr-8"
            />
          </HoppSmartSelectWrapper>
          <template #content="{ hide }">
            <div
              ref="authTippyActions"
              class="flex flex-col focus:outline-none"
              tabindex="0"
              @keyup.escape="hide()"
            >
              <HoppSmartItem
                v-for="addToTarget in addToTargets"
                :key="addToTarget.id"
                :label="addToTarget.label"
                :icon="
                  auth.addTo === addToTarget.id ? IconCircleDot : IconCircle
                "
                :active="auth.addTo === addToTarget.id"
                @click="
                  () => {
                    auth.addTo = addToTarget.id
                    hide()
                  }
                "
              />
            </div>
          </template>
        </tippy>
      </span>
    </div>

    <div class="p-2">
      <HoppButtonSecondary
        filled
        :label="`${t('authorization.generate_token')}`"
        @click="generateOAuthToken()"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import {
  HoppGQLAuthOAuth2,
  HoppRESTAuthOAuth2,
  parseTemplateStringE,
} from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as E from "fp-ts/Either"
import { Ref, computed, ref } from "vue"
import { z } from "zod"
import { useI18n } from "~/composables/i18n"
import { refWithCallbackOnChange } from "~/composables/ref"
import { useToast } from "~/composables/toast"
import { getCombinedEnvVariables } from "~/helpers/preRequest"
import { AggregateEnvironment } from "~/newstore/environments"
import authCode, {
  AuthCodeOauthFlowParams,
  getDefaultAuthCodeOauthFlowParams,
} from "~/services/oauth/flows/authCode"
import clientCredentials, {
  ClientCredentialsFlowParams,
  getDefaultClientCredentialsFlowParams,
} from "~/services/oauth/flows/clientCredentials"
import implicit, {
  ImplicitOauthFlowParams,
  getDefaultImplicitOauthFlowParams,
} from "~/services/oauth/flows/implicit"
import passwordFlow, {
  PasswordFlowParams,
  getDefaultPasswordFlowParams,
} from "~/services/oauth/flows/password"
import {
  PersistedOAuthConfig,
  grantTypesInvolvingRedirect,
} from "~/services/oauth/oauth.service"
import { PersistenceService } from "~/services/persistence"
import { GQLTabService } from "~/services/tab/graphql"
import { RESTTabService } from "~/services/tab/rest"
import IconCircle from "~icons/lucide/circle"
import IconCircleDot from "~icons/lucide/circle-dot"

const t = useI18n()
const toast = useToast()

const gqlTabsService = useService(GQLTabService)
const persistenceService = useService(PersistenceService)
const restTabsService = useService(RESTTabService)

const props = defineProps<{
  modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
  isCollectionProperty?: boolean
  envs?: AggregateEnvironment[]
  source: "REST" | "GraphQL"
}>()

const auth = ref(props.modelValue)

const addToTargets = [
  {
    id: "HEADERS" as const,
    label: "Headers",
  },
  {
    id: "QUERY_PARAMS" as const,
    label: "Query Params",
  },
]

const passBy = computed(() => {
  return (
    addToTargets.find((target) => target.id === auth.value.addTo)?.label ||
    t("state.none")
  )
})

const supportedGrantTypes = [
  {
    // used for both authCode and authCodePKCE
    id: "authCode" as const,
    label: t("authorization.oauth.label_auth_code"),
    formElements: computed(() => {
      if (!(auth.value.grantTypeInfo.grantType === "AUTHORIZATION_CODE")) {
        return
      }

      const grantType = auth.value.grantTypeInfo

      const authEndpoint = refWithCallbackOnChange(
        grantType?.authEndpoint,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            authEndpoint: value,
          }
        }
      )

      const tokenEndpoint = refWithCallbackOnChange(
        grantType?.tokenEndpoint,
        (value) => {
          if (!("tokenEndpoint" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            tokenEndpoint: value,
          }
        }
      )

      const clientID = refWithCallbackOnChange(grantType?.clientID, (value) => {
        auth.value.grantTypeInfo = {
          ...auth.value.grantTypeInfo,
          clientID: value,
        }
      })

      const clientSecret = refWithCallbackOnChange(
        grantType?.clientSecret,
        (value) => {
          if (!("clientSecret" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            clientSecret: value,
          }
        }
      )

      const scopes = refWithCallbackOnChange(
        grantType?.scopes ? grantType.scopes : undefined,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            scopes: value,
          }
        }
      )

      const isPKCE = refWithCallbackOnChange(
        auth.value.grantTypeInfo.isPKCE,
        (value) => {
          if (!("isPKCE" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            isPKCE: value,
          }
        }
      )

      const codeChallenge: Ref<{
        id: "plain" | "S256"
        label: string
      } | null> = refWithCallbackOnChange(
        auth.value.grantTypeInfo.codeVerifierMethod
          ? {
              id: auth.value.grantTypeInfo.codeVerifierMethod,
              label:
                auth.value.grantTypeInfo.codeVerifierMethod === "plain"
                  ? "Plain"
                  : "SHA-256",
            }
          : null,
        (value) => {
          if (!("codeVerifierMethod" in auth.value.grantTypeInfo) || !value) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            codeVerifierMethod: value.id,
          }
        }
      )

      const runAction = () => {
        const params: AuthCodeOauthFlowParams = {
          authEndpoint: authEndpoint.value,
          tokenEndpoint: tokenEndpoint.value,
          clientID: clientID.value,
          clientSecret: clientSecret.value,
          scopes: scopes.value,
          isPKCE: isPKCE.value,
          codeVerifierMethod: codeChallenge.value?.id,
        }

        const unwrappedParams = replaceTemplateStringsInObjectValues(params)

        const parsedArgs = authCode.params.safeParse(unwrappedParams)

        if (!parsedArgs.success) {
          return E.left("VALIDATION_FAILED" as const)
        }

        authCode.init(parsedArgs.data)

        return E.right(undefined)
      }

      const pkceElements = computed(() => {
        const checkbox = {
          id: "isPKCE",
          label: t("authorization.oauth.label_use_pkce"),
          type: "checkbox" as const,
          ref: isPKCE,
          onChange: (e: Event) => {
            const target = e.target as HTMLInputElement

            isPKCE.value = target.checked
          },
        }

        return isPKCE.value
          ? [
              checkbox,
              {
                id: "codeChallenge",
                label: t("authorization.oauth.label_code_challenge"),
                type: "dropdown" as const,
                ref: codeChallenge,
                tippyRefName: "pkceTippyActions",
                tippyRef: pkceTippyActions,
                options: [
                  {
                    id: "plain" as const,
                    label: "Plain",
                  },
                  {
                    id: "S256" as const,
                    label: "SHA-256",
                  },
                ],
              },
            ]
          : [checkbox]
      })

      const elements = computed(() => {
        return [
          ...pkceElements.value,
          {
            id: "authEndpoint",
            label: t("authorization.oauth.label_authorization_endpoint"),
            type: "text" as const,
            ref: authEndpoint,
          },
          {
            id: "tokenEndpoint",
            label: t("authorization.oauth.label_token_endpoint"),
            type: "text" as const,
            ref: tokenEndpoint,
          },
          {
            id: "clientId",
            label: t("authorization.oauth.label_client_id"),
            type: "text" as const,
            ref: clientID,
          },
          {
            id: "clientSecret",
            label: t("authorization.oauth.label_client_secret"),
            type: "text" as const,
            ref: clientSecret,
          },
          {
            id: "scopes",
            label: t("authorization.oauth.label_scopes"),
            type: "text" as const,
            ref: scopes,
          },
        ]
      })

      return {
        runAction,
        elements,
      }
    }),
  },
  {
    id: "clientCredentials" as const,
    label: t("authorization.oauth.label_client_credentials"),
    formElements: computed(() => {
      if (!(auth.value.grantTypeInfo.grantType === "CLIENT_CREDENTIALS")) {
        return
      }

      const grantTypeInfo = auth.value.grantTypeInfo

      const authEndpoint = refWithCallbackOnChange(
        grantTypeInfo?.authEndpoint,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            authEndpoint: value,
          }
        }
      )

      const clientID = refWithCallbackOnChange(
        grantTypeInfo?.clientID,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            clientID: value,
          }
        }
      )

      const clientSecret = refWithCallbackOnChange(
        grantTypeInfo?.clientSecret,
        (value) => {
          if (!("clientSecret" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            clientSecret: value,
          }
        }
      )

      const scopes = refWithCallbackOnChange(
        grantTypeInfo?.scopes ? grantTypeInfo.scopes : undefined,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            scopes: value,
          }
        }
      )

      const runAction = async () => {
        const values: ClientCredentialsFlowParams =
          replaceTemplateStringsInObjectValues({
            authEndpoint: authEndpoint.value,
            clientID: clientID.value,
            clientSecret: clientSecret.value,
            scopes: scopes.value,
          })

        const parsedArgs = clientCredentials.params.safeParse(values)

        if (!parsedArgs.success) {
          return E.left("VALIDATION_FAILED" as const)
        }

        const res = await clientCredentials.init(parsedArgs.data)

        if (E.isLeft(res)) {
          return E.left("OAUTH_TOKEN_FETCH_FAILED" as const)
        }

        setAccessTokenInActiveContext(res.right?.access_token)

        toast.success(t("authorization.oauth.token_fetched_successfully"))

        return E.right(undefined)
      }

      const elements = computed(() => {
        return [
          {
            id: "authEndpoint",
            label: "Authorization Endpoint",
            type: "text" as const,
            ref: authEndpoint,
          },
          {
            id: "clientId",
            label: "Client ID",
            type: "text" as const,
            ref: clientID,
          },
          {
            id: "clientSecret",
            label: "Client Secret",
            type: "text" as const,
            ref: clientSecret,
          },
          {
            id: "scopes",
            label: "Scopes",
            type: "text" as const,
            ref: scopes,
          },
        ]
      })

      return {
        runAction,
        elements,
      }
    }),
  },
  {
    id: "password" as const,
    label: "Password",
    formElements: computed(() => {
      if (!(auth.value.grantTypeInfo.grantType === "PASSWORD")) {
        return
      }

      const grantTypeInfo = auth.value.grantTypeInfo

      const authEndpoint = refWithCallbackOnChange(
        grantTypeInfo?.authEndpoint,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            authEndpoint: value,
          }
        }
      )

      const clientID = refWithCallbackOnChange(
        grantTypeInfo?.clientID,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            clientID: value,
          }
        }
      )

      const clientSecret = refWithCallbackOnChange(
        grantTypeInfo?.clientSecret,
        (value) => {
          if (!("clientSecret" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            clientSecret: value,
          }
        }
      )

      const scopes = refWithCallbackOnChange(
        grantTypeInfo?.scopes ? grantTypeInfo.scopes : undefined,
        (value) => {
          auth.value.grantTypeInfo.scopes = value
        }
      )

      const username = refWithCallbackOnChange(
        grantTypeInfo?.username,
        (value) => {
          if (!("username" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            username: value,
          }
        }
      )

      const password = refWithCallbackOnChange(
        grantTypeInfo?.password,
        (value) => {
          if (!("password" in auth.value.grantTypeInfo)) {
            return
          }

          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            password: value,
          }
        }
      )

      const runAction = async () => {
        const values: PasswordFlowParams = replaceTemplateStringsInObjectValues(
          {
            authEndpoint: authEndpoint.value,
            clientID: clientID.value,
            clientSecret: clientSecret.value,
            scopes: scopes.value,
            username: username.value,
            password: password.value,
          }
        )

        const parsedArgs = passwordFlow.params.safeParse(values)

        if (!parsedArgs.success) {
          return E.left("VALIDATION_FAILED" as const)
        }

        const res = await passwordFlow.init(parsedArgs.data)

        if (E.isLeft(res)) {
          return E.left("OAUTH_TOKEN_FETCH_FAILED" as const)
        }

        setAccessTokenInActiveContext(res.right?.access_token)

        toast.success(t("authorization.oauth.token_fetched_successfully"))

        return E.right(undefined)
      }

      const elements = computed(() => {
        return [
          {
            id: "authEndpoint",
            label: t("authorization.oauth.label_authorization_endpoint"),
            type: "text" as const,
            ref: authEndpoint,
          },
          {
            id: "clientId",
            label: t("authorization.oauth.label_client_id"),
            type: "text" as const,
            ref: clientID,
          },
          {
            id: "clientSecret",
            label: t("authorization.oauth.label_client_secret"),
            type: "text" as const,
            ref: clientSecret,
          },
          {
            id: "username",
            label: t("authorization.oauth.label_username"),
            type: "text" as const,
            ref: username,
          },
          {
            id: "password",
            label: t("authorization.oauth.label_password"),
            type: "text" as const,
            ref: password,
          },
          {
            id: "scopes",
            label: t("authorization.oauth.label_scopes"),
            type: "text" as const,
            ref: scopes,
          },
        ]
      })

      return {
        runAction,
        elements,
      }
    }),
  },
  {
    id: "implicit" as const,
    label: t("authorization.oauth.label_implicit"),
    formElements: computed(() => {
      const grantTypeInfo = auth.value.grantTypeInfo

      const authEndpoint = refWithCallbackOnChange(
        grantTypeInfo?.authEndpoint,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            authEndpoint: value,
          }
        }
      )

      const clientID = refWithCallbackOnChange(
        grantTypeInfo?.clientID,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            clientID: value,
          }
        }
      )

      const scopes = refWithCallbackOnChange(
        grantTypeInfo?.scopes ? grantTypeInfo.scopes : undefined,
        (value) => {
          auth.value.grantTypeInfo = {
            ...auth.value.grantTypeInfo,
            scopes: value,
          }
        }
      )

      const runAction = () => {
        const values: ImplicitOauthFlowParams =
          replaceTemplateStringsInObjectValues({
            authEndpoint: authEndpoint.value,
            clientID: clientID.value,
            scopes: scopes.value,
          })

        const unwrappedValues = replaceTemplateStringsInObjectValues(values)

        const parsedArgs = implicit.params.safeParse(unwrappedValues)

        if (!parsedArgs.success) {
          return E.left("VALIDATION_FAILED" as const)
        }

        implicit.init(parsedArgs.data)

        return E.right(undefined)
      }

      const elements = computed(() => {
        return [
          {
            id: "authEndpoint",
            label: t("authorization.oauth.label_authorization_endpoint"),
            type: "text" as const,
            ref: authEndpoint,
          },
          {
            id: "clientId",
            label: t("authorization.oauth.label_client_id"),
            type: "text" as const,
            ref: clientID,
          },
          {
            id: "scopes",
            label: t("authorization.oauth.label_scopes"),
            type: "text" as const,
            ref: scopes,
          },
        ]
      })

      return {
        runAction,
        elements,
      }
    }),
  },
]

type GrantTypes = z.infer<
  typeof HoppRESTAuthOAuth2
>["grantTypeInfo"]["grantType"]

const grantTypeMap: Record<
  GrantTypes,
  (typeof supportedGrantTypes)[number]["id"]
> = {
  AUTHORIZATION_CODE: "authCode",
  CLIENT_CREDENTIALS: "clientCredentials",
  IMPLICIT: "implicit",
  PASSWORD: "password",
} as const

const grantTypeDefaultPayload = {
  AUTHORIZATION_CODE: getDefaultAuthCodeOauthFlowParams,
  CLIENT_CREDENTIALS: getDefaultClientCredentialsFlowParams,
  IMPLICIT: getDefaultImplicitOauthFlowParams,
  PASSWORD: getDefaultPasswordFlowParams,
} as const

const selectedGrantTypeID = computed(() => {
  const currentGrantType = auth.value.grantTypeInfo.grantType
  return grantTypeMap[currentGrantType]
})

const selectedGrantType = computed(() => {
  return supportedGrantTypes.find(
    (grantType) => grantType.id === selectedGrantTypeID.value
  )
})

const setAccessTokenInActiveContext = (accessToken?: string) => {
  if (props.isCollectionProperty && accessToken) {
    auth.value.grantTypeInfo = {
      ...auth.value.grantTypeInfo,
      token: accessToken,
    }

    return
  }

  const tabService = props.source === "REST" ? restTabsService : gqlTabsService

  if (
    tabService.currentActiveTab.value.document.request.auth.authType ===
      "oauth-2" &&
    accessToken
  ) {
    tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token =
      accessToken
  }
}

const changeSelectedGrantType = (
  grantType: (typeof supportedGrantTypes)[number]["id"]
) => {
  const keys = Object.keys(grantTypeMap) as GrantTypes[]

  const grantTypeToSet = keys.find((key) => grantTypeMap[key] === grantType)

  if (grantTypeToSet) {
    auth.value.grantTypeInfo.grantType = grantTypeToSet

    const getDefaultPayload = grantTypeDefaultPayload[grantTypeToSet]

    // set the default payload for the grant type
    // for eg: if the grant type was auth code, and then the user selected the password grant type,
    // there wont be a password key in the payload
    // so we set the default payload, and it'll add all the keys that are missing
    if (getDefaultPayload) {
      auth.value.grantTypeInfo = {
        ...getDefaultPayload(),
        ...auth.value.grantTypeInfo,
      }
    }
  }
}

const runAction = computed(() => {
  return selectedGrantType.value?.formElements.value?.runAction
})

const currentOAuthGrantTypeFormElements = computed(() => {
  return selectedGrantType.value?.formElements.value?.elements.value
})

const generateOAuthToken = async () => {
  if (
    grantTypesInvolvingRedirect.includes(auth.value.grantTypeInfo.grantType)
  ) {
    const authConfig: PersistedOAuthConfig = {
      source: props.source,
      context: props.isCollectionProperty
        ? { type: "collection-properties", metadata: {} }
        : { type: "request-tab", metadata: {} },
      grant_type: auth.value.grantTypeInfo.grantType,
    }
    persistenceService.setLocalConfig(
      "oauth_temp_config",
      JSON.stringify(authConfig)
    )
  }

  const res = await runAction.value?.()

  if (res && E.isLeft(res)) {
    const errorMessages = {
      VALIDATION_FAILED: t("authorization.oauth.validation_failed"),
      OAUTH_TOKEN_FETCH_FAILED: t("authorization.oauth.token_fetch_failed"),
    }

    toast.error(errorMessages[res.left])
    return
  }
}

const replaceTemplateStringsInObjectValues = <
  T extends Record<string, unknown>,
>(
  obj: T
) => {
  const envs = getCombinedEnvVariables()

  const requestVariables =
    props.source === "REST"
      ? restTabsService.currentActiveTab.value.document.request.requestVariables.map(
          ({ key, value }) => ({
            key,
            value,
            secret: false,
          })
        )
      : []

  // Ensure request variables are prioritized by removing any selected/global environment variables with the same key
  const selectedEnvVars = envs.selected.filter(
    ({ key }) =>
      !requestVariables.some(({ key: reqVarKey }) => reqVarKey === key)
  )
  const globalEnvVars = envs.global.filter(
    ({ key }) =>
      !requestVariables.some(({ key: reqVarKey }) => reqVarKey === key)
  )

  const envVars = [...selectedEnvVars, ...globalEnvVars, ...requestVariables]

  const newObj: Partial<T> = {}

  for (const key in obj) {
    const val = obj[key]

    if (typeof val === "string") {
      const parseResult = parseTemplateStringE(val, envVars)

      newObj[key] = E.isRight(parseResult)
        ? (parseResult.right as T[typeof key])
        : (val as T[typeof key])
    } else {
      newObj[key] = val
    }
  }

  return newObj as T
}

const grantTypeTippyActions = ref<HTMLElement | null>(null)
const pkceTippyActions = ref<HTMLElement | null>(null)
const authTippyActions = ref<HTMLElement | null>(null)
</script>
