import { fnRef } from "fn-ref";
import { useEffect, useRef, useState } from "react";

function debounce(
  func: Function,
  wait: number,
  setStatus?: (status: StatusType) => void
) {
  let timeout: any;
  return function (...args: any[]): any {
    clearTimeout(timeout);
    setStatus?.("loading");
    // @ts-ignore
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

function throttle(
  func: Function,
  limit: number,
  setStatus?: (status: StatusType) => void
) {
  let inThrottle: boolean;
  return function (...args: any[]): any {
    if (!inThrottle) {
      // @ts-ignore
      func.apply(this, args);
      inThrottle = true;
      setStatus?.("loading");
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

type StatusType = "intial" | "loading" | "success" | "error" | "cancelled";

export type ActionType<T, E, P extends Record<string, any> = {}> = {
  run: (payload: P, signal?: AbortSignal) => Promise<T>;
  abort: () => void;
  onRun: (subscriber: any) => (subscriber: any) => void;
  onSuccess: (subscriber: any) => (subscriber: any) => void;
  subscribe: (subscriber: any) => (subscriber: any) => void;
  onError: (subscriber: any) => (subscriber: any) => void;
  getState: () => ActionReturnType<T, E, P>;
  intercept: (interceptor: (props: P) => P) => void;
  interceptAfter: (interceptor: (data: any) => T) => void;
  interceptError: (interceptor: (error: any) => E) => void;
  // subscribe: (subscriber: any) => (subscriber: any) => void;
};
export function asyncAction<T, E, P extends Record<string, any>>(
  // p is the props type
  fn: (payload: P, signal?: AbortSignal) => any,
  options: { debounce?: number; throttle?: number } = {}
): ActionType<T, E, P> & ActionReturnType<T, E, P> {
  let data: T | null = null;
  let error: E | null = null;
  let status: "intial" | "loading" | "success" | "error" | "cancelled" =
    "intial";
  let interceptorAfter: ((props: any) => T) | null = null;
  let interceptorError: ((props: any) => E) | null = null;
  let isDebounced = false;
  let isDebouncing = false;
  let abortController = new AbortController();
  let subscribers: any[] = [];
  let successSubscriber: any[] = [];
  let errorSubscriber: any[] = [];
  let stateSubscriber: any[] = [];
  let isThrottled = false;
  let payload: P = {} as P;
  const onSuccess = (subscriber: any) => {
    successSubscriber.push(subscriber);
    return (subscriber: any) => {
      successSubscriber = successSubscriber.filter((s) => s !== subscriber);
    };
  };
  const onError = (subscriber: any) => {
    errorSubscriber.push(subscriber);
    return (subscriber: any) => {
      errorSubscriber = errorSubscriber.filter((s) => s !== subscriber);
    };
  };
  const onRun = (subscriber: any) => {
    subscribers.push(subscriber);
    return (subscriber: any) => {
      subscribers = subscribers.filter((s) => s !== subscriber);
    };
  };
  const onStateChange = (subscriber: any) => {
    stateSubscriber.push(subscriber);
    return (subscriber: any) => {
      stateSubscriber = stateSubscriber.filter((s) => s !== subscriber);
    };
  };
  const callSubs = debounce((subscriber: any) => {
    subscribers.forEach((s) => s(subscriber));
  }, 0);
  const callSuccessSubs = debounce((subscriber: any) => {
    successSubscriber.forEach((s) => s(subscriber));
  }, 0);
  const callErrorSubs = debounce((subscriber: any) => {
    errorSubscriber.forEach((s) => s(subscriber));
  }, 0);
  const callStateChangeSubs = debounce((subscriber: any) => {
    buildState();
    stateSubscriber.forEach((s) => s(subscriber));
  }, 0);
  const execute = async function (_payload: P = {} as P) {
    payload = _payload;
    if (interceptor) {
      try {
        payload = await interceptor(payload);
      } catch (err) {
        console.error("error in interceptor", err);
      }
    }
    let _data: T | null = null;
    let _error: E | null = null;
    abortController.abort();
    setStatus("loading");
    try {
      abortController = new AbortController();
      // @ts-ignore
      _data = await fn.call(this, payload, abortController.signal);
      if (interceptorAfter) {
        try {
          _data = await interceptorAfter(_data);
        } catch (err) {
          console.error("error in interceptorAfter", err);
        }
      }
      setStatus("success");
    } catch (err) {
      if (err instanceof Error && err.name === "AbortError") {
        setStatus("cancelled");
        return null;
      }

      _error = err as E;
      if (interceptorError) {
        try {
          _error = await interceptorError(_error);
        } catch (err) {
          console.error("error in interceptorError", err);
        }
      }
      setStatus("error");
    } finally {
      callSubs();
      if (status === "success") {
        setData(_data);
        setError(null);
        callSuccessSubs();
      }
      if (status === "error") {
        setError(_error);
        setData(null);
        callErrorSubs();
      }
      _data = null;
      _error = null;
    }
    return data;
  };

  const abort = () => {
    abortController.abort();
  };

  let currentPage = 1;
  let countPerPage = 10;
  const setPage = (page: number) => {
    currentPage = page;
    callStateChangeSubs();
  };
  const setCountPerPage = (count: number) => {
    countPerPage = count;
    callStateChangeSubs();
  };
  let extraState: any = {};
  const setExtraState = (state: any) => {
    extraState = state;
    callStateChangeSubs();
  };
  let filters: any = {};
  const setFilters = (filters: any) => {
    filters = filters;
    callStateChangeSubs();
  };
  const setData = (_data: T | null) => {
    data = _data;
    callStateChangeSubs();
  };
  const setError = (_error: E | null) => {
    error = _error;
    callStateChangeSubs();
  };
  const setStatus = (
    _status: "intial" | "loading" | "success" | "error" | "cancelled"
  ) => {
    status = _status;
    callStateChangeSubs();
  };
  let run = execute;

  if (options.debounce) {
    run = debounce(execute, options.debounce, setStatus);
    isDebounced = true;
  }

  if (options.throttle) {
    run = throttle(execute, options.throttle, setStatus);
    isThrottled = true;
  }

  let interceptor: ((props: P) => P) | null = null;

  let state: ActionReturnType<T, E, P> = {} as ActionReturnType<T, E, P>;

  const buildState = () => {
    state = {
      get data() {
        return data;
      },
      run,
      abort,
      get error() {
        return error;
      },
      get isRunning() {
        return status === "loading";
      },
      get isSuccess() {
        return status === "success";
      },
      get isError() {
        return status === "error";
      },
      get isDebounced() {
        return isDebounced;
      },
      get isThrottled() {
        return isThrottled;
      },
      get isDebouncing() {
        return isDebouncing;
      },
      get status() {
        return status;
      },
      get abortController() {
        return abortController;
      },
      get currentPage() {
        return currentPage;
      },
      get countPerPage() {
        return countPerPage;
      },
      get extraState() {
        return extraState;
      },
      get filters() {
        return filters;
      },
      get payload() {
        return payload;
      },
      setData,
      setError,
      setPage,
      setCountPerPage,
      setExtraState,
      setFilters,
      setStatus,
    };
  };

  buildState();
  let returnVal = {
    run,
    abort,
    getState: () => state,
    onRun,
    intercept(_interceptor: (props: P) => P) {
      interceptor = _interceptor;
    },
    interceptAfter(_interceptor: (props: T) => T) {
      interceptorAfter = _interceptor;
    },
    interceptError(_interceptor: (props: E) => E) {
      interceptorError = _interceptor;
    },
    onSuccess,
    onError,
    subscribe: onStateChange,
    get data() {
      return data;
    },
    get error() {
      return error;
    },
    get isRunning() {
      return status === "loading";
    },
    get isSuccess() {
      return status === "success";
    },
    get isError() {
      return status === "error";
    },
    get isDebounced() {
      return isDebounced;
    },
    get isThrottled() {
      return isThrottled;
    },
    get isDebouncing() {
      return isDebouncing;
    },
    get status() {
      return status;
    },
    get abortController() {
      return abortController;
    },
    get currentPage() {
      return currentPage;
    },
    get countPerPage() {
      return countPerPage;
    },
    get extraState() {
      return extraState;
    },
    get filters() {
      return filters;
    },
    get payload() {
      return payload;
    },
    setData,
    setError,
    setPage,
    setCountPerPage,
    setExtraState,
    setFilters,
    setStatus,
  };
  return returnVal;
}

type ActionReturnType<T, E, P extends Record<string, any>> = {
  data: T | null;
  error: E | null;
  isRunning: boolean;
  isSuccess: boolean;
  isError: boolean;
  isDebounced: boolean;
  isThrottled: boolean;
  isDebouncing: boolean;
  status: StatusType;
  abortController: AbortController;
  currentPage: number;
  countPerPage: number;
  extraState: any;
  filters: any;
  payload: P;
  setData: (data: T | null) => void;
  setError: (error: E | null) => void;
  setPage: (page: number) => void;
  setCountPerPage: (count: number) => void;
  setExtraState: (state: any) => void;
  setFilters: (filters: any) => void;
  setStatus: (status: StatusType) => void;
  run: (payload: P, signal?: AbortSignal) => Promise<T>;
  abort: () => void;
};
export function useAction<T, E, P extends Record<string, any>>(
  action: ActionType<T, E, P>
): ActionReturnType<T, E, P> {
  const [actionData, setActionData] = useState(action.getState());
  useEffect(() => {
    action.subscribe(() => {
      setActionData(action.getState());
    });
  }, [setActionData]);
  return actionData;
}

export function useOnMount(callback: () => void) {
  // Implement logic to run callback on component mount
  useEffect(() => {
    callback();
  }, []);
}

export function useOnRun(
  callback: () => void,
  deps: ActionType<any, any, any>[]
) {
  // Implement logic to run callback after action is called
  const callbackRef = useRef(callback);
  callbackRef.current = callback;
  useEffect(() => {
    deps.forEach((action) => {
      action.onRun(() => callbackRef.current());
    });
  }, []);
}

export function useOnSuccess(
  callback: () => void,
  deps: ActionType<any, any, any>[]
) {
  // Implement logic to run callback on success
  const callbackRef = useRef(callback);
  callbackRef.current = callback;
  useEffect(() => {
    deps.forEach((action) => {
      action.onSuccess(() => callbackRef.current());
    });
  }, []);
}

export function useOnError(
  callback: () => void,
  deps: ActionType<any, any, any>[]
) {
  // Implement logic to run callback on error
  const callbackRef = useRef(callback);
  callbackRef.current = callback;
  useEffect(() => {
    deps.forEach((action) => {
      action.onError(() => callbackRef.current());
    });
  }, []);
}

type ActionFunction<T, P> = (props: P) => T;

interface EnhancedAction<T, P> extends ActionFunction<T, P> {
  intercept: (interceptor: (props: P) => P) => void;
  subscribe: (callback: (result: T) => void) => () => void;
  callCount: number;
  callCountTotal: number;
  retry: () => void;
  getState: () => T;
  forwardCall: (callback: (props: P) => T) => void;
}

export function action<T, P extends Record<string, any>>(
  fn: ActionFunction<T, P>
): EnhancedAction<T, P> {
  let state: T;
  let subscribers: ((result: T) => void)[] = [];

  let interceptor: ((props: P) => P) | null = null;
  // Create the enhanced function
  let forward: (props: P) => T;
  let forwardInProgress: boolean = false;
  let _props: P = {} as P;
  const enhancedFn = function (props: P): T {
    _props = props;

    enhancedFn.callCountTotal++;

    if (forward && !forwardInProgress) {
      forwardInProgress = true;
      forward(_props);
      forwardInProgress = false;
    }
    // Check for interceptor
    else {
      if (interceptor) {
        _props = interceptor(_props);
      }
      state = fn(_props);
      forwardInProgress = false;
      enhancedFn.callCount++;
    }

    // Notify subscribers
    subscribers.forEach((callback) => callback(state));

    return state;
  } as EnhancedAction<T, P>;

  enhancedFn.callCount = 0;
  enhancedFn.callCountTotal = 0;
  // Add properties
  enhancedFn.intercept = (_interceptor: (props: P) => P) => {
    interceptor = _interceptor;
  };
  enhancedFn.forwardCall = (callback: (props: P) => T) => {
    forward = callback;
  };
  enhancedFn.subscribe = (callback: (result: T) => void) => {
    subscribers.push(callback);
    return () => {
      subscribers = subscribers.filter((cb) => cb !== callback);
    };
  };
  enhancedFn.retry = () => {
    enhancedFn(_props);
  };
  enhancedFn.getState = () => state;

  return enhancedFn;
}


export const interceptorFetch = fnRef((payload: FetchPayloadType) => payload);
export const interceptorFetchSuccess = fnRef((data) => data);
export const interceptorFetchError = fnRef((error) => error);
type FetchPayloadType = {
  url: string;
  qsObj?: Record<string, any>;
} & RequestInit;
export function Fetch(
  options: { debounce?: number; throttle?: number } = { debounce: 1000 }
) {
  const action = asyncAction(
    async (
      payload: { url: string; qsObj?: Record<string, any> } & RequestInit,
      signal
    ) => {
      return fetch(payload.url, { signal, ...payload }).then((res) => {
        if (
          res.ok &&
          res.headers.get("content-type")?.includes("application/json")
        ) {
          return res.json();
        }
        return res;
      });
    },
    options
  );

  //@ts-ignore
  action.intercept(interceptorFetch);
  //@ts-ignore
  action.interceptAfter(interceptorFetchSuccess);
  //@ts-ignore
  action.interceptError(interceptorFetchError);

  return action;
}