import http from 'node:http'
import https from 'node:https'

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

type JsonObject = Record<string, any>
type HeaderAdapter = {
  get(name: unknown): string | null
}
type NodeHttpResponse = {
  ok: boolean
  status: number
  headers: HeaderAdapter
  bodyText: string
}

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 controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)
  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,
  })

  try {
    const response = await requestViaNodeHttp(url, {
      method,
      headers,
      body: normalizeRequestBody(options.body, headers['content-type']),
      signal: controller.signal,
    })

    const rawText = response.bodyText
    const payload = tryParseJson(rawText)

    if (!response.ok) {
      throw createHttpError(`cloudtentacles 请求失败，HTTP ${response.status}`, {
        statusCode: 502,
        errorCode: 'cloudtentacles_http_error',
      })
    }

    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')

      if (statusCode === 401 || isCloudtentaclesExpiredMessage(message)) {
        await notifyCloudtentaclesAuthExpired({
          pathname: normalizedPathname,
          errorCode,
          message,
        })
      }

      throw createHttpError(message, {
        statusCode,
        errorCode,
      })
    }

    logInfo('[cloudtentacles/http]', '请求完成', {
      method,
      pathname: normalizedPathname,
      status: response.status,
    })

    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
  } finally {
    clearTimeout(timer)
  }
}

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

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
    },
  }
}
