import type { Manifest } from '@types'
import { buildAppUrl } from '@urls'
import { AsyncLocalStorage } from 'node:async_hooks'

const DEFAULT_HOST = process.env.DEV ? 'http://localhost:3000' : 'http://toes.local'
const signalStore = new AsyncLocalStorage<AbortSignal>()

const normalizeUrl = (url: string) =>
  url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`

function tryParseError(text: string): string | undefined {
  try {
    const json = JSON.parse(text)
    return json.error
  } catch {
    return undefined
  }
}

export const HOST = process.env.TOES_URL
  ? normalizeUrl(process.env.TOES_URL)
  : DEFAULT_HOST

export const gitUrl = (name: string) => `${buildAppUrl('git', HOST)}/${name}`

export const getSignal = () => signalStore.getStore()

export function withSignal<T>(signal: AbortSignal, fn: () => T): T {
  return signalStore.run(signal, fn)
}

export function makeUrl(path: string): string {
  return `${HOST}${path}`
}

export function handleError(error: unknown): void {
  if (error instanceof Error && 'code' in error && error.code === 'ConnectionRefused') {
    console.error(`🐾 Can't connect to toes server at ${HOST}`)
    console.error(`   Set TOES_URL to connect to a different host`)
    return
  }
  if (error instanceof Error) {
    console.error(`Error: ${error.message}`)
    return
  }
  console.error(error)
}

export async function get<T>(url: string): Promise<T | undefined> {
  try {
    const res = await fetch(makeUrl(url), { signal: getSignal() })
    if (!res.ok) {
      const text = await res.text()
      const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
      throw new Error(msg)
    }
    return await res.json()
  } catch (error) {
    handleError(error)
  }
}

export async function getManifest(appName: string): Promise<{ exists: boolean, manifest?: Manifest } | null> {
  try {
    const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`), { signal: getSignal() })
    if (res.status === 404) return { exists: false }
    if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
    const manifest = await res.json()
    return { exists: true, manifest }
  } catch (error) {
    handleError(error)
    return null
  }
}

export async function post<T, B = unknown>(url: string, body?: B): Promise<T | undefined> {
  try {
    const res = await fetch(makeUrl(url), {
      method: 'POST',
      headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
      body: body !== undefined ? JSON.stringify(body) : undefined,
      signal: getSignal(),
    })
    if (!res.ok) {
      const text = await res.text()
      const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
      throw new Error(msg)
    }
    return await res.json()
  } catch (error) {
    handleError(error)
  }
}

export async function put(url: string, body: Buffer | Uint8Array): Promise<boolean> {
  try {
    const res = await fetch(makeUrl(url), {
      method: 'PUT',
      body: body as BodyInit,
      signal: getSignal(),
    })
    if (!res.ok) {
      const text = await res.text()
      const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
      throw new Error(msg)
    }
    return true
  } catch (error) {
    handleError(error)
    return false
  }
}

export async function download(url: string): Promise<Buffer | undefined> {
  try {
    const fullUrl = makeUrl(url)
    const res = await fetch(fullUrl, { signal: getSignal() })
    if (!res.ok) {
      const text = await res.text()
      const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
      throw new Error(msg)
    }
    return Buffer.from(await res.arrayBuffer())
  } catch (error) {
    handleError(error)
  }
}

export async function del(url: string): Promise<boolean> {
  try {
    const res = await fetch(makeUrl(url), {
      method: 'DELETE',
      signal: getSignal(),
    })
    if (!res.ok) {
      const text = await res.text()
      const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
      throw new Error(msg)
    }
    return true
  } catch (error) {
    handleError(error)
    return false
  }
}
