import { getStoredAccessToken } from '@/services/authSession'

const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
const authUnauthorizedErrorCodes = new Set(['AUTH_TOKEN_REQUIRED', 'AUTH_SESSION_INVALID'])

type UnauthorizedHandler = (() => void) | null

let unauthorizedHandler: UnauthorizedHandler = null

export class ApiError extends Error {
  readonly status: number
  readonly details: unknown

  constructor(message: string, status: number, details: unknown) {
    super(message)
    this.name = 'ApiError'
    this.status = status
    this.details = details
  }
}

export async function getJson<T>(path: string): Promise<T> {
  return requestJson<T>(path, {
    method: 'GET',
  })
}

export async function sendJson<T>(path: string, method: 'POST' | 'PUT', body?: unknown): Promise<T> {
  return requestJson<T>(path, {
    method,
    headers: {
      'Content-Type': 'application/json',
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  })
}

export function setUnauthorizedHandler(handler: UnauthorizedHandler): void {
  unauthorizedHandler = handler
}

export function notifyUnauthorized(): void {
  unauthorizedHandler?.()
}

export function isAuthSessionInvalidPayload(payload: unknown): boolean {
  return authUnauthorizedErrorCodes.has(errorCodeFromPayload(payload))
}

export function isAuthUnauthorizedError(error: unknown): boolean {
  return error instanceof ApiError && isAuthUnauthorized(error.status, error.details)
}

async function requestJson<T>(path: string, init: RequestInit): Promise<T> {
  const response = await fetch(`${apiBaseUrl}${path}`, {
    ...init,
    headers: buildRequestHeaders(init.headers),
  })

  const payload = await readResponsePayload(response)
  if (!response.ok) {
    if (isAuthUnauthorized(response.status, payload)) {
      notifyUnauthorized()
    }
    throw new ApiError(`Request failed with status ${response.status}`, response.status, payload)
  }

  return payload as T
}

function buildRequestHeaders(initHeaders?: HeadersInit): Record<string, string> {
  const headers: Record<string, string> = {
    Accept: 'application/json',
  }
  mergeHeaders(headers, initHeaders)

  const token = getStoredAccessToken()
  if (token) {
    headers.Authorization = `Bearer ${token}`
  }
  return headers
}

function mergeHeaders(target: Record<string, string>, source?: HeadersInit): void {
  if (!source) {
    return
  }
  if (source instanceof Headers) {
    source.forEach((value, key) => {
      target[key] = value
    })
    return
  }
  if (Array.isArray(source)) {
    source.forEach(([key, value]) => {
      target[key] = value
    })
    return
  }
  Object.entries(source).forEach(([key, value]) => {
    if (typeof value === 'string') {
      target[key] = value
    }
  })
}

function isAuthUnauthorized(status: number, payload: unknown): boolean {
  if (isAuthSessionInvalidPayload(payload)) {
    return true
  }
  return status === 401 && errorCodeFromPayload(payload) !== 'AUTH_INVALID_CREDENTIALS'
}

function errorCodeFromPayload(payload: unknown): string {
  if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
    return ''
  }
  const value = (payload as { error_code?: unknown }).error_code
  return typeof value === 'string' ? value : ''
}

async function readResponsePayload(response: Response): Promise<unknown> {
  const contentType = response.headers.get('content-type') ?? ''
  if (!contentType.includes('application/json')) {
    return response.text()
  }
  return response.json()
}
