import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
import { ERRORS } from '../components/Error/Errors';
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
import useGuestBookStore from '../store/store';
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import {
  User,
  UserCreate,
  UserDelete,
  UserImageUpdate,
  UserList,
  UserPermissionsUpdate,
  UserUpdate,
} from '../types/User';

const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';

interface ApiContext {
  hasAuth: boolean;
  authenticatedUser?: User;
  currentSession: [string?, string?];

  logIn?: (email: string, password: string) => Promise<void>;
  logOut?: () => Promise<boolean>;

  posts?: (page?: number) => Promise<PostListNonAuth | PostListAuth>;
  newPost?: (data: PostCreate) => Promise<PostNew>;
  updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
  deletePost?: (id: number) => Promise<PostDelete>;

  users?: (page?: number) => Promise<UserList>;
  user?: (id?: number) => Promise<User>;
  createUser?: (data: UserCreate) => Promise<User>;
  confirmUser?: (code: string) => Promise<User>;
  updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
  updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
  updateUserPermissions?: (data: UserPermissionsUpdate, id: number) => Promise<User>;
  deleteUser?: (id: number) => Promise<UserDelete>;

  userPosts?: (id?: number) => Promise<PostListAuth>;
}

const ApiContext = createContext<ApiContext>({
  hasAuth: false,
  currentSession: [undefined, undefined],
});

//eslint-disable-next-line  react-refresh/only-export-components
export const useApi = () => {
  const {
    hasAuth,
    authenticatedUser,
    currentSession,

    logIn,
    logOut,
    posts,
    newPost,
    updatePost,
    deletePost,

    users,
    user,
    createUser,
    confirmUser,
    updateUser,
    updateUserImage,
    updateUserPermissions,
    deleteUser,

    userPosts,
  } = useContext(ApiContext);

  if (
    logIn &&
    logOut &&
    posts &&
    newPost &&
    updatePost &&
    deletePost &&
    users &&
    user &&
    createUser &&
    confirmUser &&
    updateUser &&
    updateUserImage &&
    updateUserPermissions &&
    deleteUser &&
    userPosts
  ) {
    return {
      hasAuth,
      authenticatedUser,
      currentSession,

      logIn,
      logOut,
      posts,
      newPost,
      updatePost,
      deletePost,

      users,
      user,
      createUser,
      confirmUser,
      updateUser,
      updateUserImage,
      updateUserPermissions,
      deleteUser,

      userPosts,
    };
  }

  throw new Error("Couldn't find context. Is your component inside an ApiProvider?");
};

export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ children }) => {
  const [hasAuth, setHasAuth] = useState(false);
  const [authenticatedUser, setAuthenticatedUser] = useState<User>();

  const [currentSession, setCurrentSession] = useGuestBookStore((state) => [
    state.currentSession,
    state.setCurrentSession,
  ]);

  const token = useRef<string | undefined>();

  useEffect(() => {
    if (currentSession[0] && !token.current) {
      token.current = currentSession[0];
      refresh();
    }
  }, [currentSession]); //eslint-disable-line react-hooks/exhaustive-deps

  const logIn = async (email: string, password: string): Promise<void> => {
    const { user, token: _token, refreshToken } = await (await post('login', { email, password })).json();
    setAuthenticatedUser(user);
    setCurrentSession([_token, refreshToken]);
    setHasAuth(true);
    token.current = _token;
  };

  const logOut = async (): Promise<boolean> => {
    try {
      if (token.current) return await (await reAuth(() => postAuth('logout'))).json();
      return true;
    } catch {
      return false;
    } finally {
      setAuthenticatedUser(undefined);
      setCurrentSession([undefined, undefined]);
      setHasAuth(false);
      token.current = undefined;
    }
  };

  const posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
    const url = `posts?p=${page ?? 0}&l=${POST_LIMIT}`;

    if (hasAuth) return await (await reAuth(() => getAuth(url))).json();

    return await (await get(url)).json();
  };

  const newPost = async (data: PostCreate): Promise<PostNew> => {
    return await (
      await reAuth(() => postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>))
    ).json();
  };

  const updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
    return await (await reAuth(() => patchAuth(`posts/${id}`, data as Record<string, unknown>))).json();
  };

  const deletePost = async (id: number): Promise<PostDelete> => {
    return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
  };

  const users = async (page?: number): Promise<UserList> => {
    const url = `users?p=${page ?? 0}&l=${POST_LIMIT}`;

    return await (await reAuth(() => getAuth(url))).json();
  };

  const user = async (id?: number): Promise<User> => {
    return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
  };

  const createUser = async (data: UserCreate): Promise<User> => {
    return await (await post(`register`, data as unknown as Record<string, unknown>)).json();
  };

  const confirmUser = async (code: string): Promise<User> => {
    return await (await patch(`register`, { code })).json();
  };

  const updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
    const _user = await (
      await reAuth(() => patchAuth(`users/${id ?? 'self'}`, data as Record<string, unknown>))
    ).json();
    setAuthenticatedUser(_user);
    return _user;
  };

  const updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
    const formData = new FormData();
    if (data.image) formData.append('image', data.image);
    if (!data.image && data.predefined) formData.append('predefined', data.predefined);

    const _user = await (await reAuth(() => postAuthRaw(`users/${id ?? 'self'}/image`, formData))).json();
    setAuthenticatedUser(_user);
    return _user;
  };

  const updateUserPermissions = async (data: UserPermissionsUpdate, id: number): Promise<User> => {
    const _user = await (
      await reAuth(() => patchAuth(`users/${id}/permissions`, data as Record<string, unknown>))
    ).json();
    return _user;
  };

  const userPosts = async (id?: number): Promise<PostListAuth> => {
    return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
  };

  const deleteUser = async (id: number): Promise<UserDelete> => {
    return await (await reAuth(() => _delete(`users/${id}?l=${POST_LIMIT}`))).json();
  };

  /* IMPL */

  const post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'post',
      headers,
      body: JSON.stringify(body),
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const postAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'post',
      headers: { token: token.current ?? '', ...headers },
      body: JSON.stringify(body),
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'post',
      headers: { token: token.current ?? '', ...headers },
      body,
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const get = async (endpoint: string, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'get',
      headers,
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const getAuth = async (endpoint: string, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'get',
      headers: { token: token.current ?? '', ...headers },
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const _delete = async (endpoint: string, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'delete',
      headers: { token: token.current ?? '', ...headers },
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'patch',
      headers: headers,
      body: JSON.stringify(body),
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const patchAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
    const response = await fetch(`${BASE}${endpoint}`, {
      mode: 'cors',
      method: 'patch',
      headers: { token: token.current ?? '', ...headers },
      body: JSON.stringify(body),
    });
    if (response.ok) return response;
    throw await response.json();
  };

  const reAuth = async <T,>(callback: () => Promise<T>): Promise<T> => {
    try {
      console.log('[REAUTH] fetching');
      const ret = await callback();

      return ret;
      //eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (error: any) {
      console.log('[REAUTH] failed once', error);
      if (error.code !== ERRORS.UNAUTHORIZED) throw error;

      try {
        console.log('[REAUTH] failed due to authentication, try refreshing session');
        // REAUTH
        await refresh();
        // DO AGAIN
        console.log('[REAUTH] refreshed, fetching again');
        const ret = await callback();

        return ret;
      } catch (_error) {
        console.log('[REAUTH] terminating session', _error);
        setAuthenticatedUser(undefined);
        setHasAuth(false);
        setCurrentSession([undefined, undefined]);
        token.current = undefined;
        throw _error;
      }
    }
  };

  const refresh = async () => {
    const {
      user: _user,
      token: _token,
      refreshToken,
    } = await (await postAuth('refresh', { refreshToken: currentSession[1] ?? 'INVALID DEFAULT' })).json();
    setAuthenticatedUser(_user);
    setCurrentSession([_token, refreshToken]);
    setHasAuth(true);
    token.current = _token;
  };

  return (
    <ApiContext.Provider
      value={{
        hasAuth,
        authenticatedUser,
        currentSession,

        logIn,
        logOut,

        posts,
        newPost,
        updatePost,
        deletePost,

        users,
        user,
        createUser,
        confirmUser,
        updateUser,
        updateUserImage,
        updateUserPermissions,
        deleteUser,

        userPosts,
      }}
    >
      {children}
    </ApiContext.Provider>
  );
};

export default ApiProvider;
