import crypto from 'node:crypto'
import { createRemoteJWKSet, jwtVerify, errors as joseErrors, type JWTPayload } from 'jose'
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js'
import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js'
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'
import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
import type { McpEnv } from './config.js'

/** Who is calling — available to every tool via `extra.authInfo.extra`. */
export interface Caller {
  subject: string
  clientId: string
  scopes: string[]
}

export const callerOf = (auth: AuthInfo | undefined): Caller => ({
  subject: String(auth?.extra?.subject ?? 'unknown'),
  clientId: auth?.clientId ?? 'unknown',
  scopes: auth?.scopes ?? [],
})

function scopesFrom(payload: JWTPayload, claim: string): string[] {
  const v = payload[claim]
  if (typeof v === 'string') return v.split(' ').filter(Boolean)
  if (Array.isArray(v)) return v.map(String)
  return []
}

/** Fetch the IdP's authorization-server metadata (RFC 8414, falling back to OIDC discovery). */
export async function discoverIssuer(issuer: string): Promise<OAuthMetadata> {
  const base = issuer.replace(/\/$/, '')
  for (const path of ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration']) {
    const res = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(10_000) }).catch(() => null)
    if (res?.ok) return (await res.json()) as OAuthMetadata
  }
  throw new Error(`Could not discover OAuth metadata for issuer ${issuer}`)
}

/** Validates IdP-issued JWT access tokens: signature (JWKS), issuer, audience = this MCP server, expiry. */
export function jwtVerifier(env: McpEnv, metadata: OAuthMetadata): OAuthTokenVerifier {
  const jwksUri = (metadata as { jwks_uri?: string }).jwks_uri
  if (!jwksUri) throw new Error('Issuer metadata has no jwks_uri — the IdP must issue JWT access tokens')
  const jwks = createRemoteJWKSet(new URL(jwksUri))
  const audience = env.OAUTH_AUDIENCE ?? env.MCP_PUBLIC_URL

  return {
    async verifyAccessToken(token: string): Promise<AuthInfo> {
      try {
        const { payload } = await jwtVerify(token, jwks, { issuer: metadata.issuer, audience })
        return {
          token,
          clientId: String(payload.azp ?? payload.client_id ?? payload.sub ?? 'unknown'),
          scopes: scopesFrom(payload, env.OAUTH_SCOPE_CLAIM),
          expiresAt: payload.exp,
          resource: new URL(env.MCP_PUBLIC_URL),
          extra: { subject: payload.sub ?? 'unknown', email: payload.email },
        }
      } catch (err) {
        if (err instanceof joseErrors.JOSEError) throw new InvalidTokenError(`Invalid access token: ${err.code}`)
        throw err
      }
    },
  }
}

/** Dev/testing only: fixed tokens from MCP_STATIC_TOKENS. Refused in production by config validation. */
export function staticVerifier(spec: string): OAuthTokenVerifier {
  const entries = spec.split(';').map(s => s.trim()).filter(Boolean).map(entry => {
    const [token, subject, scopes] = entry.split('|')
    if (!token || !subject) throw new Error('MCP_STATIC_TOKENS entries must be token|subject|scopes')
    return { token: Buffer.from(token), subject, scopes: (scopes ?? '').split(' ').filter(Boolean) }
  })
  return {
    async verifyAccessToken(token: string): Promise<AuthInfo> {
      const given = Buffer.from(token)
      const match = entries.find(e => e.token.length === given.length && crypto.timingSafeEqual(e.token, given))
      if (!match) throw new InvalidTokenError('Invalid access token')
      return {
        token,
        clientId: 'static',
        scopes: match.scopes,
        expiresAt: Math.floor(Date.now() / 1000) + 3600,
        extra: { subject: match.subject },
      }
    },
  }
}
