import http from 'node:http'
import https from 'node:https'
import type { JsonObject } from '../../../types/json.js'

import { createHttpError } from '../../../utils/http.js'
import { logInfo, logWarn } from '../../../utils/logger.js'
import { notifyCloudtentaclesAuthExpired } from '../../notification/domain-notifications.js'
import {
  buildCloudtentaclesHeaders,
  buildCloudtentaclesUrl,
  resolveCloudtentaclesConfig,
} from './helpers.js'

type HeaderAdapter = {
  get(name: unknown): string | null
}
type NodeHttpResponse = {
  ok: boolean
  status: number
  headers: HeaderAdapter
  bodyText: string
}

/** 通用接口默认最小间隔；bind_info 等敏感接口由调用方覆盖 */
const DEFAULT_RATE_LIMIT_INTERVAL_MS = 500
const DEFAULT_RATE_LIMIT_RETRIES = 2
const DEFAULT_RATE_LIMIT_RETRY_DELAY_MS = 1_200
const MAX_RETRY_JITTER_MS = 400
/** 命中上游限频后，额外冷却桶时间 */
const RATE_LIMIT_COOLDOWN_MS = 3_000
const bucketNextAvailableAt = new Map<string, number>()
let rateLimitQueue = Promise.resolve()

export async function cloudtentaclesRequest(pathname: unknown, options: JsonObject = {}) {
  const normalizedPathname = String(pathname || '').trim()
  const config = resolveCloudtentaclesConfig(options)
  const url = buildCloudtentaclesUrl(config.baseUrl, normalizedPathname, options.searchParams)
  const timeoutMs = Number(options.timeoutMs || config.timeoutMs || 5000)
  const method = String(options.method || 'GET')
    .trim()
    .toUpperCase()
  const headers = buildCloudtentaclesHeaders({
    token: options.token,
    contentType:
      options.contentType === null ? '' : options.contentType || inferContentType(options.body),
    deviceId: options.deviceId ?? config.deviceId,
    deviceType: options.deviceType ?? config.deviceType,
    extra: options.headers,
  })
  const rateLimitBucketKey = resolveRateLimitBucketKey({
    baseUrl: config.baseUrl,
    token: options.token,
    sourceKey: options.sourceKey,
    deviceId: options.deviceId ?? config.deviceId,
    pathname: normalizedPathname,
    isolatePath: options.rateLimitIsolatePath === true,
  })
  const rateLimitIntervalMs = normalizeNonNegativeInteger(
    options.rateLimitIntervalMs,
    resolveDefaultRateLimitIntervalMs(normalizedPathname),
  )
  const maxRetries = normalizeNonNegativeInteger(
    options.rateLimitRetries,
    DEFAULT_RATE_LIMIT_RETRIES,
  )
  const retryDelayMs = normalizeNonNegativeInteger(
    options.rateLimitRetryDelayMs,
    resolveDefaultRateLimitRetryDelayMs(normalizedPathname),
  )

  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    try {
      await waitForCloudtentaclesSlot(rateLimitBucketKey, rateLimitIntervalMs)
      const body = normalizeRequestBody(options.body, headers['content-type'])
      const response = await executeCloudtentaclesRequest({
        url,
        method,
        headers,
        timeoutMs,
        ...(body !== undefined ? { body } : {}),
      })
      const rawText = response.bodyText
      const payload = tryParseJson(rawText)

      if (!response.ok) {
        if (isCloudtentaclesHttpRateLimited(response.status) && attempt < maxRetries) {
          await waitBeforeRateLimitRetry({
            attempt,
            pathname: normalizedPathname,
            method,
            status: response.status,
            retryDelayMs,
            bucketKey: rateLimitBucketKey,
          })
          continue
        }

        logWarn('[cloudtentacles/http]', '请求 HTTP 失败', {
          method,
          pathname: normalizedPathname,
          status: response.status,
          attempt: attempt + 1,
          sourceKey: options.sourceKey,
          accountLabel: options.accountLabel,
          context: options.context,
          upstreamPayload: payload,
          rawText: truncateText(rawText),
        })

        throw createHttpError(`cloudtentacles 请求失败，HTTP ${response.status}`, {
          statusCode: isCloudtentaclesHttpRateLimited(response.status) ? 429 : 502,
          errorCode: isCloudtentaclesHttpRateLimited(response.status)
            ? 'cloudtentacles_rate_limited'
            : 'cloudtentacles_http_error',
          context: {
            method,
            pathname: normalizedPathname,
            upstreamStatus: response.status,
            upstreamPayload: payload,
            requestContext: options.context,
          },
        })
      }

      if (options.requireBusinessSuccess !== false && Number(payload?.code ?? 1) !== 0) {
        const message = String(payload?.message || payload?.msg || 'cloudtentacles 接口返回失败')
        const statusCode = Number(options.businessErrorStatusCode || 400)
        const errorCode = String(options.businessErrorCode || 'cloudtentacles_business_error')
        const upstreamCode = payload?.code

        if (isHighFrequencyMessage(message, upstreamCode)) {
          if (attempt < maxRetries) {
            await waitBeforeRateLimitRetry({
              attempt,
              pathname: normalizedPathname,
              method,
              status: response.status,
              retryDelayMs,
              bucketKey: rateLimitBucketKey,
              exponential: true,
            })
            continue
          }

          throw createHttpError('cloudtentacles 请求过于频繁，请稍后重试', {
            statusCode: 429,
            errorCode: 'cloudtentacles_rate_limited',
            context: {
              pathname: normalizedPathname,
              upstreamMessage: message,
              upstreamCode,
            },
          })
        }

        if (isCloudtentaclesExpiredMessage(message)) {
          await notifyCloudtentaclesAuthExpired({
            pathname: normalizedPathname,
            errorCode,
            message,
            sourceKey: options.sourceKey,
            accountLabel: options.accountLabel,
          })
        }

        logWarn('[cloudtentacles/http]', '请求业务失败', {
          method,
          pathname: normalizedPathname,
          status: response.status,
          attempt: attempt + 1,
          errorCode,
          upstreamCode,
          upstreamMessage: message,
          sourceKey: options.sourceKey,
          accountLabel: options.accountLabel,
          context: options.context,
          upstreamPayload: payload,
        })

        throw createHttpError(message, {
          statusCode,
          errorCode,
          context: {
            method,
            pathname: normalizedPathname,
            upstreamStatus: response.status,
            upstreamCode,
            upstreamMessage: message,
            upstreamPayload: payload,
            requestContext: options.context,
          },
        })
      }

      logInfo('[cloudtentacles/http]', '请求完成', {
        method,
        pathname: normalizedPathname,
        status: response.status,
        attempt: attempt + 1,
        sourceKey: options.sourceKey,
        accountLabel: options.accountLabel,
        context: options.context,
      })

      return {
        url: url.toString(),
        status: response.status,
        headers: response.headers,
        payload,
        rawText,
      }
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') {
        throw createHttpError(`cloudtentacles 请求超时（${timeoutMs}ms）`, {
          statusCode: 504,
          errorCode: 'cloudtentacles_request_timeout',
        })
      }

      throw error
    }
  }

  throw createHttpError('cloudtentacles 请求过于频繁，请稍后重试', {
    statusCode: 429,
    errorCode: 'cloudtentacles_rate_limited',
  })
}

/** 导出供单测：识别上游限频文案/错误码（含中文 110001） */
export function isHighFrequencyMessage(message: unknown, code: unknown = null) {
  const text = String(message || '')
    .trim()
    .toLowerCase()
  const codeText = String(code ?? '').trim()

  if (codeText === '110001' || Number(code) === 110001) {
    return true
  }

  if (!text) {
    return false
  }

  return (
    text.includes('high-frequency request') ||
    text.includes('high frequency') ||
    text.includes('110001') ||
    text.includes('过于频繁') ||
    text.includes('请求过于频繁') ||
    text.includes('too frequent') ||
    text.includes('rate limit') ||
    text.includes('too many requests')
  )
}

function isCloudtentaclesExpiredMessage(message: unknown) {
  const normalized = String(message || '')
    .trim()
    .toLowerCase()
  return (
    normalized.includes('expired') || normalized.includes('过期') || normalized.includes('失效')
  )
}

function isCloudtentaclesHttpRateLimited(status: unknown) {
  return Number(status) === 429
}

function resolveDefaultRateLimitIntervalMs(pathname: string) {
  if (isBindInfoPath(pathname)) {
    // bind_info 是 110001 高发接口，单独加长间隔
    return 1_500
  }
  return DEFAULT_RATE_LIMIT_INTERVAL_MS
}

function resolveDefaultRateLimitRetryDelayMs(pathname: string) {
  if (isBindInfoPath(pathname)) {
    return 2_500
  }
  return DEFAULT_RATE_LIMIT_RETRY_DELAY_MS
}

function isBindInfoPath(pathname: string) {
  const normalized = String(pathname || '')
    .trim()
    .toLowerCase()
  return normalized.includes('bind_info') || normalized.endsWith('/vn/bind_info')
}

function truncateText(value: unknown, maxLength = 1000) {
  const text = String(value || '')
  if (text.length <= maxLength) {
    return text
  }

  return `${text.slice(0, maxLength)}...`
}

async function executeCloudtentaclesRequest({
  url,
  method,
  headers,
  body,
  timeoutMs,
}: {
  url: URL
  method: string
  headers: JsonObject
  body?: string | URLSearchParams
  timeoutMs: number
}) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)

  try {
    return await requestViaNodeHttp(url, {
      method,
      headers,
      signal: controller.signal,
      ...(body !== undefined ? { body } : {}),
    })
  } finally {
    clearTimeout(timer)
  }
}

async function waitForCloudtentaclesSlot(bucketKey: string, intervalMs: number) {
  if (intervalMs <= 0) {
    return
  }

  const previousQueue = rateLimitQueue
  let releaseQueue: () => void = () => {}
  rateLimitQueue = new Promise<void>((resolve) => {
    releaseQueue = resolve
  })

  await previousQueue

  let waitMs
  try {
    const now = Date.now()
    const nextAvailableAt = bucketNextAvailableAt.get(bucketKey) || 0
    waitMs = Math.max(0, nextAvailableAt - now)
    bucketNextAvailableAt.set(bucketKey, Math.max(now, nextAvailableAt) + intervalMs)
  } finally {
    releaseQueue()
  }

  if (waitMs > 0) {
    await sleep(waitMs)
  }
}

async function waitBeforeRateLimitRetry(options: {
  attempt: number
  pathname: string
  method: string
  status: number
  retryDelayMs: number
  bucketKey: string
  exponential?: boolean
}) {
  const { attempt, pathname, method, status, retryDelayMs, bucketKey, exponential } = options
  const factor = exponential ? 2 ** attempt : attempt + 1
  const delayMs = retryDelayMs * factor + Math.floor(Math.random() * MAX_RETRY_JITTER_MS)

  // 命中限频后额外冷却账号桶，避免紧接着再打
  const coolUntil = Date.now() + Math.max(delayMs, RATE_LIMIT_COOLDOWN_MS)
  const current = bucketNextAvailableAt.get(bucketKey) || 0
  if (coolUntil > current) {
    bucketNextAvailableAt.set(bucketKey, coolUntil)
  }

  logWarn('[cloudtentacles/http]', '请求触发云触手限频，准备重试', {
    method,
    pathname,
    status,
    attempt: attempt + 1,
    retryDelayMs: delayMs,
  })
  await sleep(delayMs)
}

function resolveRateLimitBucketKey({
  baseUrl,
  token,
  sourceKey,
  deviceId,
  pathname,
  isolatePath,
}: {
  baseUrl: unknown
  token: unknown
  sourceKey: unknown
  deviceId: unknown
  pathname?: unknown
  isolatePath?: boolean
}) {
  const parts = [
    String(baseUrl || '').trim(),
    String(sourceKey || '').trim() || String(token || '').trim() || 'anonymous',
    String(deviceId || '').trim(),
  ]
  if (isolatePath) {
    parts.push(String(pathname || '').trim())
  }
  return parts.join('|')
}

function normalizeNonNegativeInteger(value: unknown, fallback: number) {
  const numericValue = Number(value)
  if (Number.isInteger(numericValue) && numericValue >= 0) {
    return numericValue
  }

  return fallback
}

function sleep(ms: number) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}

function inferContentType(body: unknown) {
  if (body == null) {
    return ''
  }

  if (typeof body === 'string' || body instanceof URLSearchParams) {
    return 'application/json'
  }

  return 'application/json'
}

function normalizeRequestBody(body: unknown, contentType: unknown) {
  if (body == null) {
    return undefined
  }

  if (typeof body === 'string' || body instanceof URLSearchParams) {
    return body
  }

  if (String(contentType || '').includes('application/json')) {
    return JSON.stringify(body)
  }

  return String(body)
}

function tryParseJson(text: string) {
  try {
    return JSON.parse(text)
  } catch {
    return null
  }
}

async function requestViaNodeHttp(
  url: URL,
  {
    method,
    headers,
    body,
    signal,
  }: {
    method: string
    headers: JsonObject
    body?: string | URLSearchParams
    signal?: AbortSignal
  },
): Promise<NodeHttpResponse> {
  const isHttps = url.protocol === 'https:'
  const transport = isHttps ? https : http

  return new Promise((resolve, reject) => {
    const request = transport.request(
      url,
      {
        method,
        headers,
        rejectUnauthorized: false,
      },
      (response) => {
        const chunks: Buffer[] = []

        response.on('data', (chunk) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
        })

        response.on('end', () => {
          const bodyText = Buffer.concat(chunks).toString('utf8')
          resolve({
            ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300,
            status: Number(response.statusCode || 0),
            headers: createHeaderAdapter(response.headers),
            bodyText,
          })
        })
      },
    )

    request.on('error', reject)

    if (signal) {
      if (signal.aborted) {
        const error = new Error('Request aborted')
        error.name = 'AbortError'
        request.destroy(error)
      } else {
        signal.addEventListener(
          'abort',
          () => {
            const error = new Error('Request aborted')
            error.name = 'AbortError'
            request.destroy(error)
          },
          { once: true },
        )
      }
    }

    if (typeof body !== 'undefined') {
      request.write(body)
    }

    request.end()
  })
}

function createHeaderAdapter(headers: http.IncomingHttpHeaders): HeaderAdapter {
  const normalized = new Map()

  for (const [key, value] of Object.entries(headers || {})) {
    if (Array.isArray(value)) {
      normalized.set(String(key || '').toLowerCase(), value.join(', '))
      continue
    }

    if (typeof value === 'string') {
      normalized.set(String(key || '').toLowerCase(), value)
    }
  }

  return {
    get(name: unknown) {
      return normalized.get(String(name || '').toLowerCase()) || null
    },
  }
}
