import type { AsyncThunkPayloadCreator } from '@reduxjs/toolkit';
import type { AxiosError } from 'axios';

import { notify } from '@/components/common/notify';

// Request config for the endpoints that read POST bodies as form data.
export const formEncoded = { headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } };

// The serializable part of a failed request. RTK replaces a rejection with a
// SerializedError unless the thunk rejects with a plain value like this one.
export interface ApiErrorPayload {
  data?: unknown;
  status?: number;
}

export const rejectPayload = (error: unknown): ApiErrorPayload => {
  const { response } = error as AxiosError;
  return { status: response?.status, data: response?.data };
};

export interface ApiThunkConfig {
  rejectValue: ApiErrorPayload;
}

type ThunkApi<Returned, Arg> = Parameters<AsyncThunkPayloadCreator<Returned, Arg, ApiThunkConfig>>[1];

// Message to notify on failure: a fixed string, or a resolver that can return
// undefined for statuses the caller renders itself (e.g. a 404 empty state).
type ErrorMessage<Arg> = string | ((payload: ApiErrorPayload, arg: Arg) => string | undefined);

export const withApiError =
  <Returned, Arg = void>(
    request: (arg: Arg, api: ThunkApi<Returned, Arg>) => Promise<Returned>,
    errorMessage?: ErrorMessage<Arg>
  ): AsyncThunkPayloadCreator<Returned, Arg, ApiThunkConfig> =>
  async (arg, api) => {
    try {
      return await request(arg, api);
    } catch (error) {
      const payload = rejectPayload(error);
      const message = typeof errorMessage === 'function' ? errorMessage(payload, arg) : errorMessage;
      if (message) {
        notify.error(message);
      }
      return api.rejectWithValue(payload);
    }
  };

// A failure body is a bare string, {msg, details} coming from the API, or the
// {errors: [{field, message}]} shape its request validators reject with.
interface ApiErrorBody {
  details?: unknown;
  errors?: { field?: string; message?: string }[];
  msg?: string;
}

export const apiErrorMessage = (rejection: unknown, fallback: string): string => {
  const { data } = (rejection ?? {}) as ApiErrorPayload;
  if (typeof data === 'string') {
    return data || fallback;
  }
  const body = data as ApiErrorBody;
  const validationErrors = (body?.errors ?? []).map(({ message }) => message).filter(Boolean);
  return body?.msg || validationErrors.join(' ') || fallback;
};

// RTK Query counterpart of withApiError: `onQueryStarted` is the one place a mutation
// can report its outcome once per dispatch, whether or not the caller stays mounted.
export const notifyResult =
  (failure: string, success?: string) =>
  async (_arg: unknown, { queryFulfilled }: { queryFulfilled: Promise<unknown> }): Promise<void> => {
    try {
      await queryFulfilled;
      if (success) {
        notify.success(success);
      }
    } catch (rejection) {
      notify.error(apiErrorMessage((rejection as { error?: unknown }).error, failure));
    }
  };

export const apiErrorDetails = (rejection: unknown): string[] => {
  const { details } = (((rejection ?? {}) as ApiErrorPayload).data ?? {}) as ApiErrorBody;
  return Array.isArray(details) ? (details as string[]) : [];
};
