/**
 * Minimal async-data hooks (no react-query dependency).
 *
 * `useApi` runs an async loader and tracks loading/error/data, re-running when
 * `deps` change. `reload()` re-fetches on demand (e.g. after a mutation).
 *
 * Design notes (to satisfy the strict react-hooks rules):
 *  - `loading` is DERIVED (resolved-key !== current-request-key), never set
 *    synchronously inside the effect.
 *  - the loader is read through a ref so the effect can key purely on the
 *    request signature without re-running on every render.
 */
import { useCallback, useEffect, useRef, useState } from 'react'
import { ApiError } from '../api/client'
import { de } from '../strings/de'

export interface AsyncState<T> {
  data: T | null
  loading: boolean
  error: string | null
  reload: () => void
}

function messageFor(err: unknown): string {
  if (err instanceof ApiError) return err.message
  if (err instanceof Error && err.message) return err.message
  return de.api.errors.unknown
}

interface Resolved<T> {
  key: string | null
  data: T | null
  error: string | null
}

export function useApi<T>(loader: () => Promise<T>, deps: unknown[]): AsyncState<T> {
  const loaderRef = useRef(loader)
  // Keep the loader ref current (declared before the request effect so it is
  // synced first within a commit). Writing refs in render is disallowed.
  useEffect(() => {
    loaderRef.current = loader
  })

  const [nonce, setNonce] = useState(0)
  const requestKey = `${nonce}:${JSON.stringify(deps)}`

  const [resolved, setResolved] = useState<Resolved<T>>({ key: null, data: null, error: null })

  useEffect(() => {
    let cancelled = false
    loaderRef
      .current()
      .then((result) => {
        if (!cancelled) setResolved({ key: requestKey, data: result, error: null })
      })
      .catch((err) => {
        if (!cancelled) setResolved({ key: requestKey, data: null, error: messageFor(err) })
      })
    return () => {
      cancelled = true
    }
  }, [requestKey])

  const reload = useCallback(() => setNonce((n) => n + 1), [])
  const loading = resolved.key !== requestKey

  return { data: resolved.data, loading, error: loading ? null : resolved.error, reload }
}

/**
 * Outcome of a mutation. Discriminated on `ok` so callers MUST branch before
 * touching `value` — this makes error handling compile-time-visible and means
 * `run()` NEVER throws (no more unhandled rejections from async event handlers).
 * Works for void mutations too (delete): success is `{ ok: true, value: undefined }`.
 */
export type MutationOutcome<T> =
  | { ok: true; value: T }
  | { ok: false; error: string; cause: unknown }

/**
 * Wraps a mutation (create/update/delete) with pending + error state.
 * `run()` resolves to a MutationOutcome and never rejects; `error` also drives
 * inline UI as before. Call sites: `const r = await m.run(x); if (r.ok) …`.
 */
export function useMutation<TArgs extends unknown[], TResult>(
  mutator: (...args: TArgs) => Promise<TResult>,
): {
  run: (...args: TArgs) => Promise<MutationOutcome<TResult>>
  pending: boolean
  error: string | null
} {
  const [pending, setPending] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const run = useCallback(
    async (...args: TArgs): Promise<MutationOutcome<TResult>> => {
      setPending(true)
      setError(null)
      try {
        const value = await mutator(...args)
        return { ok: true, value }
      } catch (err) {
        const message = messageFor(err)
        setError(message)
        return { ok: false, error: message, cause: err }
      } finally {
        setPending(false)
      }
    },
    [mutator],
  )

  return { run, pending, error }
}
