import http from 'node:http'
import https from 'node:https'
import type { IncomingHttpHeaders } from 'node:http'

import { parseJsonObject } from '../../../utils/json.js'
import { createHttpError } from '../../../utils/http.js'
import { logInfo } from '../../../utils/logger.js'
import { buildKuaishouEticketHeaders, buildKuaishouEticketUrl, resolveKuaishouEticketConfig } from './shared.js'

type JsonObject = Record<string, any>

type KuaishouEticketRequestOptions = JsonObject & {
  method?: string
  body?: unknown
  headers?: Record<string, unknown>
  timeoutMs?: number | string
  cookie?: string
  contentType?: string | null
}

type HeaderAdapter = {
  get(name: string): string | null
}

type NodeHttpResponse = {
  ok: boolean
  status: number
  headers: HeaderAdapter
  bodyText: string
}

export async function kuaishouEticketRequest(pathname: string, options: KuaishouEticketRequestOptions = {}) {
  const config = resolveKuaishouEticketConfig(options)
  const url = buildKuaishouEticketUrl(config.baseUrl, pathname)
  const timeoutMs = Number(options.timeoutMs || config.timeoutMs || 8000)
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)
  const method = String(options.method || 'POST').trim().toUpperCase()
  const headers = buildKuaishouEticketHeaders({
    baseUrl: config.baseUrl,
    refererPath: config.refererPath,
    userAgent: config.userAgent,
    cookie: options.cookie ?? config.cookie,
    contentType: options.contentType === null ? '' : options.contentType || inferContentType(options.body),
    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 = parseJsonObject(rawText, { preserveLargeIntegers: true })

    if (!response.ok) {
      throw createHttpError(`快手小店核销请求失败，HTTP ${response.status}`, {
        statusCode: 502,
        errorCode: 'kuaishou_eticket_http_error',
      })
    }

    logInfo('[kuaishou-eticket/http]', '请求完成', {
      method,
      pathname,
      status: response.status,
      result: Number(payload?.result || 0),
    })

    return {
      url: url.toString(),
      status: response.status,
      headers: response.headers,
      payload,
      rawText,
    }
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      throw createHttpError(`快手小店核销请求超时（${timeoutMs}ms）`, {
        statusCode: 504,
        errorCode: 'kuaishou_eticket_request_timeout',
      })
    }
    throw error
  } finally {
    clearTimeout(timer)
  }
}

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

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

  return 'application/json'
}

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

  if (typeof body === 'string') {
    return body
  }

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

  return String(body)
}

async function requestViaNodeHttp(
  url: URL,
  { method, headers, body, signal }: {
    method: string
    headers: Record<string, string>
    body?: string
    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: IncomingHttpHeaders): HeaderAdapter {
  const normalized = new Map<string, string>()

  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: string) {
      return normalized.get(String(name || '').toLowerCase()) || null
    },
  }
}
