/**
 * Feishu OAuth Device Authorization Grant (RFC 8628).
 *
 * Two-step flow:
 *   1. requestDeviceAuthorization – obtains device_code + user_code + verification_uri
 *   2. pollToken – polls the token endpoint until the user authorises
 *
 * Plus refreshToken – transparent token refresh before expiry.
 *
 * All HTTP calls use the built-in fetch (Node 18+).
 * This is a simplified port of openclaw-lark's device-flow.ts (MIT license).
 */

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface DeviceAuthResponse {
  deviceCode: string
  userCode: string
  verificationUri: string
  verificationUriComplete: string
  expiresIn: number // seconds
  interval: number  // recommended polling interval (seconds)
}

export interface TokenResponse {
  accessToken: string
  refreshToken: string
  expiresIn: number     // seconds
  refreshExpiresIn: number // seconds
  scope: string
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const ENDPOINTS: Record<string, { deviceAuthorization: string; token: string }> = {
  lark: {
    deviceAuthorization: 'https://accounts.larksuite.com/oauth/v1/device_authorization',
    token: 'https://open.larksuite.com/open-apis/authen/v2/oauth/token',
  },
  // Default: feishu (China mainland)
  feishu: {
    deviceAuthorization: 'https://accounts.feishu.cn/oauth/v1/device_authorization',
    token: 'https://open.feishu.cn/open-apis/authen/v2/oauth/token',
  },
}

function resolveEndpoints(brand: string): { deviceAuthorization: string; token: string } {
  return ENDPOINTS[brand] ?? ENDPOINTS.feishu
}

// ---------------------------------------------------------------------------
// Step 1 – Device Authorization Request
// ---------------------------------------------------------------------------

/**
 * Request a device authorization code from the Feishu OAuth server.
 * Uses Confidential Client authentication (HTTP Basic with appId:appSecret).
 * The `offline_access` scope is always appended so the response includes a refresh_token.
 */
export async function requestDeviceAuthorization(
  appId: string,
  appSecret: string,
  brand?: string,
  scopes?: string[],
): Promise<DeviceAuthResponse> {
  const endpoints = resolveEndpoints(brand ?? 'feishu')
  const basicAuth = Buffer.from(`${appId}:${appSecret}`).toString('base64')

  // Build scope list: business scopes (if any) + offline_access for refresh_token
  const scopeSet = new Set(scopes ?? [])
  scopeSet.add('offline_access')
  const scopeList = Array.from(scopeSet)

  const body = new URLSearchParams({
    client_id: appId,
    scope: scopeList.join(' '),
  })

  const resp = await fetch(endpoints.deviceAuthorization, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${basicAuth}`,
    },
    body: body.toString(),
  })

  const data = (await resp.json()) as Record<string, unknown>

  if (!resp.ok || data.error) {
    const msg = (data.error_description as string) ?? (data.error as string) ?? 'Unknown error'
    throw new Error(`Device authorization failed: ${msg}`)
  }

  return {
    deviceCode: data.device_code as string,
    userCode: data.user_code as string,
    verificationUri: data.verification_uri as string,
    verificationUriComplete: (data.verification_uri_complete as string) ?? (data.verification_uri as string),
    expiresIn: (data.expires_in as number) ?? 240,
    interval: (data.interval as number) ?? 5,
  }
}

// ---------------------------------------------------------------------------
// Step 2 – Poll Token Endpoint
// ---------------------------------------------------------------------------

/**
 * Poll the token endpoint until the user authorises, rejects, or the code expires.
 * Throws on terminal errors (access_denied, expired_token).
 */
export async function pollToken(
  appId: string,
  appSecret: string,
  deviceCode: string,
  interval: number,
  signal?: AbortSignal,
  brand?: string,
): Promise<TokenResponse> {
  const endpoints = resolveEndpoints(brand ?? 'feishu')
  const deadline = Date.now() + 240 * 1000 // 4 minutes

  while (Date.now() < deadline) {
    await sleep(interval * 1000, signal)

    const resp = await fetch(endpoints.token, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
        device_code: deviceCode,
        client_id: appId,
        client_secret: appSecret,
      }).toString(),
    })
    const data = (await resp.json()) as Record<string, unknown>
    const error = data.error as string | undefined

    if (!error && data.access_token) {
      return {
        accessToken: data.access_token as string,
        refreshToken: (data.refresh_token as string) ?? '',
        expiresIn: (data.expires_in as number) ?? 7200,
        refreshExpiresIn: (data.refresh_token_expires_in as number) ?? 604800,
        scope: (data.scope as string) ?? '',
      }
    }

    if (error === 'authorization_pending') continue
    if (error === 'slow_down') {
      interval = Math.min(interval + 5, 60)
      continue
    }
    if (error === 'access_denied') throw new Error('用户拒绝了授权')
    if (error === 'expired_token' || error === 'invalid_grant') throw new Error('授权码已过期，请重新发起')

    // Unknown error — treat as terminal
    throw new Error((data.error_description as string) ?? error ?? 'Unknown error')
  }

  throw new Error('授权超时，请重新发起')
}

function sleep(ms: number, signal?: AbortSignal): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    const timer = setTimeout(resolve, ms)
    signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')) }, { once: true })
  })
}

// ---------------------------------------------------------------------------
// Token Verification
// ---------------------------------------------------------------------------

/** Feishu API error codes that indicate the user access token is invalid/revoked. */
const AUTH_REJECTED_CODES = new Set([99991668, 99991677, 20005])

/**
 * Verify an access_token against the server (GET /authen/v1/user_info).
 *
 * Returns true when the token is accepted. Returns false ONLY when the server
 * explicitly rejects the token (known auth error codes or HTTP 401) — other
 * errors (rate limits, 5xx) do NOT invalidate the token. Throws on network
 * failure so callers can decide to keep using the token.
 */
export async function verifyUserToken(accessToken: string, brand?: string): Promise<boolean> {
  const base = brand === 'lark' ? 'https://open.larksuite.com' : 'https://open.feishu.cn'
  const resp = await fetch(`${base}/open-apis/authen/v1/user_info`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  })
  if (resp.status === 401) return false
  const data = (await resp.json()) as Record<string, unknown>
  const code = data.code as number | undefined
  if (code === 0) return true
  return !AUTH_REJECTED_CODES.has(code ?? -1)
}

// ---------------------------------------------------------------------------
// Token Refresh
// ---------------------------------------------------------------------------

/**
 * Refresh an access_token using its refresh_token.
 * The response includes a new (rotated) refresh_token.
 */
export async function refreshUserToken(
  appId: string,
  appSecret: string,
  refreshToken: string,
  brand?: string,
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number; scope?: string }> {
  const endpoints = resolveEndpoints(brand ?? 'feishu')

  const resp = await fetch(endpoints.token, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: appId,
      client_secret: appSecret,
    }).toString(),
  })
  const data = (await resp.json()) as Record<string, unknown>

  const code = data.code as number | undefined
  if (code !== undefined && code !== 0) {
    throw new Error(`Token refresh failed: code=${code} ${(data.msg as string) ?? ''}`)
  }

  if (!data.access_token) {
    throw new Error('Token refresh returned no access_token')
  }

  return {
    accessToken: data.access_token as string,
    refreshToken: (data.refresh_token as string) ?? refreshToken,
    expiresIn: (data.expires_in as number) ?? 7200,
    scope: (data.scope as string) ?? '',
  }
}
