import * as Sentry from "@sentry/nextjs"
import { initTRPC } from "@trpc/server"
import { experimental_nextAppDirCaller } from "@trpc/server/adapters/next-app-dir"
import { ZodError } from "zod"

import { env } from "@/env/server"

import {
  badRequestError,
  internalServerError,
  sessionExpiredError,
  unauthorizedError,
} from "./errors/trpc"
import { type Context, createContext } from "./context"
import { getServiceToken } from "./tokenManager"
import { transformer } from "./transformer"
import { langInput } from "./utils"

import type { Session } from "next-auth"

import type { Meta } from "@/types/trpc/meta"

const t = initTRPC
  .context<Context>()
  .meta<Meta>()
  .create({
    transformer,
    errorFormatter({ shape, error }) {
      return {
        ...shape,
        data: {
          ...shape.data,
          cause:
            error.cause instanceof ZodError
              ? undefined
              : JSON.parse(JSON.stringify(error.cause)),
          zodError:
            error.cause instanceof ZodError ? error.cause.flatten() : null,
        },
      }
    },
  })

const sentryMiddleware = t.middleware(
  Sentry.trpcMiddleware({
    attachRpcInput: true,
  })
)

export const { createCallerFactory, mergeRouters, router } = t
const baseProcedure = t.procedure.use(sentryMiddleware)

export const publicProcedure = baseProcedure

export const languageProcedure = baseProcedure.use(async function (opts) {
  if (!opts.ctx.lang) {
    // When fetching data client side with TRPC we don't pass through middlewares and therefore do not get the lang through headers
    // We can then pass lang as an input in the request and set it to the context in the procedure

    const input = await opts.getRawInput()
    const parsedInput = langInput.safeParse(input)
    if (!parsedInput.success) {
      throw badRequestError("Missing Lang in tRPC context")
    }

    return opts.next({
      ctx: {
        lang: parsedInput.data.lang,
      },
    })
  }

  return opts.next({
    ctx: {
      lang: opts.ctx.lang,
    },
  })
})

export const contentstackBaseProcedure = languageProcedure

export const contentstackExtendedProcedureUID = contentstackBaseProcedure.use(
  async function (opts) {
    if (!opts.ctx.uid) {
      throw badRequestError("Missing UID in tRPC context")
    }

    return opts.next({
      ctx: {
        uid: opts.ctx.uid,
      },
    })
  }
)
export const protectedProcedure = baseProcedure.use(async function (opts) {
  const authRequired = opts.meta?.authRequired ?? true
  const session = await opts.ctx.auth()

  if (!authRequired && env.NODE_ENV === "development") {
    console.info(
      `❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
    )
    console.info(`path: ${opts.path} | type: ${opts.type}`)
  }

  if (!session) {
    throw unauthorizedError()
  }

  if (session?.error === "RefreshAccessTokenError") {
    throw sessionExpiredError()
  }

  return opts.next({
    ctx: {
      session,
    },
  })
})

export const safeProtectedProcedure = baseProcedure.use(async function (opts) {
  const authRequired = opts.meta?.authRequired ?? true
  let session: Session | null = await opts.ctx.auth()
  if (!authRequired && env.NODE_ENV === "development") {
    console.info(
      `❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
    )
    console.info(`path: ${opts.path} | type: ${opts.type}`)
  }

  if (!session || session.error === "RefreshAccessTokenError") {
    session = null
  }

  return opts.next({
    ctx: {
      session,
    },
  })
})

export const serviceProcedure = baseProcedure.use(async (opts) => {
  const token = await getServiceToken()
  const { access_token } = token
  if (!access_token) {
    throw internalServerError(`[serviceProcedure] No service token`)
  }
  return opts.next({
    ctx: {
      serviceToken: access_token,
    },
  })
})

export const serverActionProcedure = baseProcedure.experimental_caller(
  experimental_nextAppDirCaller({
    createContext,
    normalizeFormData: true,
  })
)

export const serviceServerActionProcedure = serverActionProcedure.use(
  async (opts) => {
    const { access_token } = await getServiceToken()
    if (!access_token) {
      throw internalServerError(
        "[serviceServerActionProcedure]: No service token"
      )
    }
    return opts.next({
      ctx: {
        serviceToken: access_token,
      },
    })
  }
)

export const protectedServerActionProcedure = serverActionProcedure.use(
  async (opts) => {
    const session = await opts.ctx.auth()
    if (!session) {
      throw unauthorizedError()
    }

    if (session && session.error === "RefreshAccessTokenError") {
      throw sessionExpiredError()
    }

    return opts.next({
      ctx: {
        ...opts.ctx,
        session,
      },
    })
  }
)

export const contentStackUidWithServiceProcedure =
  contentstackExtendedProcedureUID.concat(serviceProcedure)

export const contentStackBaseWithServiceProcedure =
  contentstackBaseProcedure.concat(serviceProcedure)

export const contentStackBaseWithProtectedProcedure =
  contentstackBaseProcedure.concat(protectedProcedure)

export const safeProtectedServiceProcedure =
  safeProtectedProcedure.concat(serviceProcedure)

export const languageProtectedProcedure =
  protectedProcedure.concat(languageProcedure)
