import path from "path"
import z from "zod"
import { Global } from "../global"
import { Effect, Layer, ServiceMap } from "effect"
import { AppFileSystem } from "@/filesystem"
import { makeRuntime } from "@/effect/run-service"

export namespace McpAuth {
  export const Tokens = z.object({
    accessToken: z.string(),
    refreshToken: z.string().optional(),
    expiresAt: z.number().optional(),
    scope: z.string().optional(),
  })
  export type Tokens = z.infer<typeof Tokens>

  export const ClientInfo = z.object({
    clientId: z.string(),
    clientSecret: z.string().optional(),
    clientIdIssuedAt: z.number().optional(),
    clientSecretExpiresAt: z.number().optional(),
  })
  export type ClientInfo = z.infer<typeof ClientInfo>

  export const Entry = z.object({
    tokens: Tokens.optional(),
    clientInfo: ClientInfo.optional(),
    codeVerifier: z.string().optional(),
    oauthState: z.string().optional(),
    serverUrl: z.string().optional(),
  })
  export type Entry = z.infer<typeof Entry>

  const filepath = path.join(Global.Path.data, "mcp-auth.json")

  export interface Interface {
    readonly all: () => Effect.Effect<Record<string, Entry>>
    readonly get: (mcpName: string) => Effect.Effect<Entry | undefined>
    readonly getForUrl: (mcpName: string, serverUrl: string) => Effect.Effect<Entry | undefined>
    readonly set: (mcpName: string, entry: Entry, serverUrl?: string) => Effect.Effect<void>
    readonly remove: (mcpName: string) => Effect.Effect<void>
    readonly updateTokens: (mcpName: string, tokens: Tokens, serverUrl?: string) => Effect.Effect<void>
    readonly updateClientInfo: (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) => Effect.Effect<void>
    readonly updateCodeVerifier: (mcpName: string, codeVerifier: string) => Effect.Effect<void>
    readonly clearCodeVerifier: (mcpName: string) => Effect.Effect<void>
    readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect<void>
    readonly getOAuthState: (mcpName: string) => Effect.Effect<string | undefined>
    readonly clearOAuthState: (mcpName: string) => Effect.Effect<void>
    readonly isTokenExpired: (mcpName: string) => Effect.Effect<boolean | null>
  }

  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/McpAuth") {}

  export const layer = Layer.effect(
    Service,
    Effect.gen(function* () {
      const fs = yield* AppFileSystem.Service

      const all = Effect.fn("McpAuth.all")(function* () {
        return yield* fs.readJson(filepath).pipe(
          Effect.map((data) => data as Record<string, Entry>),
          Effect.catch(() => Effect.succeed({} as Record<string, Entry>)),
        )
      })

      const get = Effect.fn("McpAuth.get")(function* (mcpName: string) {
        const data = yield* all()
        return data[mcpName]
      })

      const getForUrl = Effect.fn("McpAuth.getForUrl")(function* (mcpName: string, serverUrl: string) {
        const entry = yield* get(mcpName)
        if (!entry) return undefined
        if (!entry.serverUrl) return undefined
        if (entry.serverUrl !== serverUrl) return undefined
        return entry
      })

      const set = Effect.fn("McpAuth.set")(function* (mcpName: string, entry: Entry, serverUrl?: string) {
        const data = yield* all()
        if (serverUrl) entry.serverUrl = serverUrl
        yield* fs.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600).pipe(Effect.orDie)
      })

      const remove = Effect.fn("McpAuth.remove")(function* (mcpName: string) {
        const data = yield* all()
        delete data[mcpName]
        yield* fs.writeJson(filepath, data, 0o600).pipe(Effect.orDie)
      })

      const updateField = <K extends keyof Entry>(field: K, spanName: string) =>
        Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string, value: NonNullable<Entry[K]>, serverUrl?: string) {
          const entry = (yield* get(mcpName)) ?? {}
          entry[field] = value
          yield* set(mcpName, entry, serverUrl)
        })

      const clearField = <K extends keyof Entry>(field: K, spanName: string) =>
        Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) {
          const entry = yield* get(mcpName)
          if (entry) {
            delete entry[field]
            yield* set(mcpName, entry)
          }
        })

      const updateTokens = updateField("tokens", "updateTokens")
      const updateClientInfo = updateField("clientInfo", "updateClientInfo")
      const updateCodeVerifier = updateField("codeVerifier", "updateCodeVerifier")
      const updateOAuthState = updateField("oauthState", "updateOAuthState")
      const clearCodeVerifier = clearField("codeVerifier", "clearCodeVerifier")
      const clearOAuthState = clearField("oauthState", "clearOAuthState")

      const getOAuthState = Effect.fn("McpAuth.getOAuthState")(function* (mcpName: string) {
        const entry = yield* get(mcpName)
        return entry?.oauthState
      })

      const isTokenExpired = Effect.fn("McpAuth.isTokenExpired")(function* (mcpName: string) {
        const entry = yield* get(mcpName)
        if (!entry?.tokens) return null
        if (!entry.tokens.expiresAt) return false
        return entry.tokens.expiresAt < Date.now() / 1000
      })

      return Service.of({
        all,
        get,
        getForUrl,
        set,
        remove,
        updateTokens,
        updateClientInfo,
        updateCodeVerifier,
        clearCodeVerifier,
        updateOAuthState,
        getOAuthState,
        clearOAuthState,
        isTokenExpired,
      })
    }),
  )

  const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))

  const { runPromise } = makeRuntime(Service, defaultLayer)

  // Async facades for backward compat (used by McpOAuthProvider, CLI)

  export const get = async (mcpName: string) => runPromise((svc) => svc.get(mcpName))

  export const getForUrl = async (mcpName: string, serverUrl: string) =>
    runPromise((svc) => svc.getForUrl(mcpName, serverUrl))

  export const all = async () => runPromise((svc) => svc.all())

  export const set = async (mcpName: string, entry: Entry, serverUrl?: string) =>
    runPromise((svc) => svc.set(mcpName, entry, serverUrl))

  export const remove = async (mcpName: string) => runPromise((svc) => svc.remove(mcpName))

  export const updateTokens = async (mcpName: string, tokens: Tokens, serverUrl?: string) =>
    runPromise((svc) => svc.updateTokens(mcpName, tokens, serverUrl))

  export const updateClientInfo = async (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) =>
    runPromise((svc) => svc.updateClientInfo(mcpName, clientInfo, serverUrl))

  export const updateCodeVerifier = async (mcpName: string, codeVerifier: string) =>
    runPromise((svc) => svc.updateCodeVerifier(mcpName, codeVerifier))

  export const clearCodeVerifier = async (mcpName: string) => runPromise((svc) => svc.clearCodeVerifier(mcpName))

  export const updateOAuthState = async (mcpName: string, oauthState: string) =>
    runPromise((svc) => svc.updateOAuthState(mcpName, oauthState))

  export const getOAuthState = async (mcpName: string) => runPromise((svc) => svc.getOAuthState(mcpName))

  export const clearOAuthState = async (mcpName: string) => runPromise((svc) => svc.clearOAuthState(mcpName))

  export const isTokenExpired = async (mcpName: string) => runPromise((svc) => svc.isTokenExpired(mcpName))
}
