import axios, {
  type AxiosRequestConfig,
  type AxiosResponse,
} from 'axios'

const DEFAULT_TIMEOUT_MS = 15_000
const DEFAULT_MAX_ATTEMPTS = 2
const DEFAULT_RETRY_DELAY_MS = 200
const MAX_ERROR_BODY_CHARS = 300

type ProviderAxiosOptions = Omit<AxiosRequestConfig, 'redact'>

export interface ProviderRequestOptions extends ProviderAxiosOptions {
  operation: string
  timeoutMs?: number
  maxAttempts?: number
  retryDelayMs?: number
  redact?: Array<string | null | undefined>
}

export async function providerRequest<T = unknown>(
  options: ProviderRequestOptions,
): Promise<AxiosResponse<T>> {
  const {
    operation,
    timeoutMs = DEFAULT_TIMEOUT_MS,
    maxAttempts = DEFAULT_MAX_ATTEMPTS,
    retryDelayMs = DEFAULT_RETRY_DELAY_MS,
    redact = [],
    ...axiosOptions
  } = options
  const attempts = Math.max(1, maxAttempts)

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    try {
      return await axios.request<T>({
        timeout: timeoutMs,
        ...axiosOptions,
      })
    } catch (err: unknown) {
      if (attempt < attempts && shouldRetryProviderRequest(err, axiosOptions.method)) {
        await sleep(retryDelayFor(err, retryDelayMs))
        continue
      }

      throw new Error(`${operation}: ${extractProviderHttpMessage(err, redact)}`)
    }
  }

  throw new Error(`${operation}: falha HTTP inesperada`)
}

export function shouldRetryProviderRequest(err: unknown, method: string | undefined): boolean {
  if (!axios.isAxiosError(err)) return false

  const status = err.response?.status
  if (status === 429) return true
  if (!isRetrySafeMethod(method)) return false
  if (status != null && status >= 500) return true

  return isTimeoutOrConnectError(err)
}

export function extractProviderHttpMessage(
  err: unknown,
  redact: Array<string | null | undefined> = [],
): string {
  if (axios.isAxiosError(err)) {
    const status = err.response?.status
    if (status != null) {
      const body = err.response?.data == null
        ? ''
        : `: ${sanitizeProviderErrorBody(stringifyErrorBody(err.response.data), redact)}`
      return `HTTP ${status} (${httpErrorKind(status)})${body}`
    }

    const kind = isTimeoutOrConnectError(err) ? 'timeout_or_connect' : 'network_error'
    return `${kind}: ${sanitizeProviderErrorBody(err.message, redact)}`
  }

  return sanitizeProviderErrorBody(err instanceof Error ? err.message : String(err), redact)
}

export function sanitizeProviderErrorBody(
  body: string,
  redact: Array<string | null | undefined> = [],
): string {
  let text = body

  for (const rawValue of redact) {
    const value = rawValue?.trim()
    if (!value || value.length < 4) continue
    text = text.split(value).join('[REDACTED]')
    if (value.startsWith('Bearer ')) {
      text = text.split(value.slice('Bearer '.length)).join('[REDACTED]')
    }
    if (value.startsWith('Basic ')) {
      text = text.split(value.slice('Basic '.length)).join('[REDACTED]')
    }
  }

  text = text
    .replace(/Basic\s+[A-Za-z0-9+/=._-]+/g, 'Basic [REDACTED]')
    .replace(/Bearer\s+[A-Za-z0-9+/=._-]+/g, 'Bearer [REDACTED]')
    .replace(/github_pat_[A-Za-z0-9_]+/g, 'github_pat_[REDACTED]')
    .replace(/gh[pousr]_[A-Za-z0-9_]+/g, (match) => `${match.slice(0, 4)}[REDACTED]`)

  return text.slice(0, MAX_ERROR_BODY_CHARS)
}

function isRetrySafeMethod(method: string | undefined): boolean {
  const normalized = (method ?? 'GET').toUpperCase()
  return normalized === 'GET' || normalized === 'HEAD' || normalized === 'OPTIONS'
}

function isTimeoutOrConnectError(err: { code?: string; message?: string }): boolean {
  const code = err.code ?? ''
  return code === 'ECONNABORTED'
    || code === 'ETIMEDOUT'
    || code === 'ECONNRESET'
    || code === 'ECONNREFUSED'
    || code === 'ENOTFOUND'
    || (err.message ?? '').toLowerCase().includes('timeout')
}

function httpErrorKind(status: number): string {
  if (status === 401 || status === 403) return 'auth_forbidden'
  if (status === 404) return 'not_found'
  if (status === 429) return 'rate_limited'
  if (status >= 500) return 'server_error'
  return 'http_error'
}

function retryDelayFor(err: unknown, fallbackMs: number): number {
  if (!axios.isAxiosError(err)) return fallbackMs
  const retryAfter = err.response?.headers?.['retry-after']
  const value = Array.isArray(retryAfter) ? retryAfter[0] : retryAfter
  if (typeof value !== 'string') return fallbackMs

  const seconds = Number(value)
  if (Number.isFinite(seconds) && seconds >= 0) {
    return Math.min(seconds * 1000, 5_000)
  }

  const dateMs = Date.parse(value)
  if (Number.isFinite(dateMs)) {
    return Math.min(Math.max(0, dateMs - Date.now()), 5_000)
  }

  return fallbackMs
}

function stringifyErrorBody(body: unknown): string {
  if (typeof body === 'string') return body
  try {
    return JSON.stringify(body)
  } catch {
    return String(body)
  }
}

function sleep(ms: number): Promise<void> {
  if (ms <= 0) return Promise.resolve()
  return new Promise((resolve) => setTimeout(resolve, ms))
}
