import { removeAccessToken } from '@/app/components/share/utils';
import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config';

import { asyncRunSafe } from '@/utils';
import { basePath } from '@/utils/var';
import type { FetchOptionType, ResponseError } from './fetch';
import { ContentType, base, getAccessToken, getBaseOptions } from './fetch';
const TIME_OUT = 100000;

export type IOnDataMoreInfo = {
  conversationId?: string;
  taskId?: string;
  messageId: string;
  errorMessage?: string;
  errorCode?: string;
};

export type IOnData = (
  message: string,
  isFirstMessage: boolean,
  moreInfo: IOnDataMoreInfo,
) => void;

const baseFetch = base;

type UploadOptions = {
  xhr: XMLHttpRequest;
  method: string;
  url?: string;
  headers?: Record<string, string>;
  data: FormData;
  onprogress?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void;
};

type UploadResponse = {
  id: string;
  [key: string]: unknown;
};

export const upload = async (
  options: UploadOptions,
  isPublicAPI?: boolean,
  url?: string,
  searchParams?: string,
): Promise<UploadResponse> => {
  const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX;
  const token = await getAccessToken(isPublicAPI);
  const defaultOptions = {
    method: 'POST',
    url:
      (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) +
      (searchParams || ''),
    headers: {
      Authorization: `Bearer ${token}`,
    },
  };
  const mergedOptions = {
    ...defaultOptions,
    ...options,
    url: options.url || defaultOptions.url,
    headers: { ...defaultOptions.headers, ...options.headers } as Record<
      string,
      string
    >,
  };
  return new Promise((resolve, reject) => {
    const xhr = mergedOptions.xhr;
    xhr.open(mergedOptions.method, mergedOptions.url);
    for (const key in mergedOptions.headers)
      xhr.setRequestHeader(key, mergedOptions.headers[key]);

    xhr.withCredentials = true;
    xhr.responseType = 'json';
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 201) resolve(xhr.response);
        else reject(xhr);
      }
    };
    if (mergedOptions.onprogress)
      xhr.upload.onprogress = mergedOptions.onprogress;
    xhr.send(mergedOptions.data);
  });
};

export const ssePost = async (
  url: string,
  fetchOptions: FetchOptionType,
  otherOptions: IOtherOptions,
) => {
  const {
    isPublicAPI = false,
    onData,
    onCompleted,
    onThought,
    onFile,
    onMessageEnd,
    onMessageReplace,
    onWorkflowStarted,
    onWorkflowFinished,
    onNodeStarted,
    onNodeFinished,
    onIterationStart,
    onIterationNext,
    onIterationFinish,
    onNodeRetry,
    onParallelBranchStarted,
    onParallelBranchFinished,
    onTextChunk,
    onTTSChunk,
    onTTSEnd,
    onTextReplace,
    onAgentLog,
    onError,
    getAbortController,
    onLoopStart,
    onLoopNext,
    onLoopFinish,
    onDataSourceNodeProcessing,
    onDataSourceNodeCompleted,
    onDataSourceNodeError,
  } = otherOptions;
  const abortController = new AbortController();

  const token = localStorage.getItem('console_token');

  const baseOptions = getBaseOptions();
  const options = Object.assign(
    {},
    baseOptions,
    {
      method: 'POST',
      signal: abortController.signal,
      headers: new Headers({
        Authorization: `Bearer ${token}`,
      }),
    } as RequestInit,
    fetchOptions,
  );

  const contentType = (options.headers as Headers).get('Content-Type');
  if (!contentType)
    (options.headers as Headers).set('Content-Type', ContentType.json);

  getAbortController?.(abortController);

  const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX;
  const urlWithPrefix =
    url.startsWith('http://') || url.startsWith('https://')
      ? url
      : `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`;

  const { body } = options;
  if (body) options.body = JSON.stringify(body);

  const accessToken = await getAccessToken(isPublicAPI);
  (options.headers as Headers).set('Authorization', `Bearer ${accessToken}`);

  globalThis
    .fetch(urlWithPrefix, options as RequestInit)
    .then((res) => {
      if (!/^[23]\d{2}$/.test(String(res.status))) {
        if (res.status === 401) {
          if (isPublicAPI) {
            res.json().then((data: { code?: string; message?: string }) => {
              if (isPublicAPI) {
                if (data.code === 'web_app_access_denied')
                  requiredWebSSOLogin(data.message, 403);

                if (data.code === 'web_sso_auth_required') {
                  removeAccessToken();
                  requiredWebSSOLogin();
                }

                if (data.code === 'unauthorized') {
                  removeAccessToken();
                  requiredWebSSOLogin();
                }
              }
            });
          } else {
            refreshAccessTokenOrRelogin(TIME_OUT)
              .then(() => {
                ssePost(url, fetchOptions, otherOptions);
              })
              .catch((err) => {
                console.error(err);
              });
          }
        } else {
          res.json().then((data) => {
            Toast.notify({
              type: 'error',
              message: data.message || 'Server Error',
            });
          });
          onError?.('Server Error');
        }
        return;
      }
      return handleStream(
        res,
        (str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
          if (moreInfo.errorMessage) {
            onError?.(moreInfo.errorMessage, moreInfo.errorCode);
            // TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
            if (
              moreInfo.errorMessage !==
                'AbortError: The user aborted a request.' &&
              !moreInfo.errorMessage.includes(
                'TypeError: Cannot assign to read only property',
              )
            )
              Toast.notify({ type: 'error', message: moreInfo.errorMessage });
            return;
          }
          onData?.(str, isFirstMessage, moreInfo);
        },
        onCompleted,
        onThought,
        onMessageEnd,
        onMessageReplace,
        onFile,
        onWorkflowStarted,
        onWorkflowFinished,
        onNodeStarted,
        onNodeFinished,
        onIterationStart,
        onIterationNext,
        onIterationFinish,
        onLoopStart,
        onLoopNext,
        onLoopFinish,
        onNodeRetry,
        onParallelBranchStarted,
        onParallelBranchFinished,
        onTextChunk,
        onTTSChunk,
        onTTSEnd,
        onTextReplace,
        onAgentLog,
        onDataSourceNodeProcessing,
        onDataSourceNodeCompleted,
        onDataSourceNodeError,
      );
    })
    .catch((e) => {
      if (
        e.toString() !== 'AbortError: The user aborted a request.' &&
        !e.toString().includes('TypeError: Cannot assign to read only property')
      )
        Toast.notify({ type: 'error', message: e });
      onError?.(e);
    });
};

// base request
export const request = async <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  try {
    const otherOptionsForBaseFetch = otherOptions || {};
    const [err, resp] = await asyncRunSafe<T>(
      baseFetch(url, options, otherOptionsForBaseFetch),
    );
    if (err === null) return resp;
    const errResp: Response = err as any;
    if (errResp.status === 401) {
      const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(
        errResp.json(),
      );
      const loginUrl = `${globalThis.location.origin}${basePath}/signin`;
      if (parseErr) {
        globalThis.location.href = loginUrl;
        return Promise.reject(err);
      }
      if (/\/login/.test(url)) return Promise.reject(errRespData);
      // special code
      const { code, message } = errRespData;
      // webapp sso
      if (code === 'web_app_access_denied') {
        requiredWebSSOLogin(message, 403);
        return Promise.reject(err);
      }
      if (code === 'web_sso_auth_required') {
        removeAccessToken();
        requiredWebSSOLogin();
        return Promise.reject(err);
      }
      if (code === 'unauthorized_and_force_logout') {
        localStorage.removeItem('console_token');
        localStorage.removeItem('refresh_token');
        globalThis.location.reload();
        return Promise.reject(err);
      }
      const { isPublicAPI = false, silent } = otherOptionsForBaseFetch;
      if (isPublicAPI && code === 'unauthorized') {
        removeAccessToken();
        requiredWebSSOLogin();
        return Promise.reject(err);
      }
      if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) {
        Toast.notify({ type: 'error', message, duration: 4000 });
        return Promise.reject(err);
      }
      if (code === 'not_init_validated' && IS_CE_EDITION) {
        globalThis.location.href = `${globalThis.location.origin}${basePath}/init`;
        return Promise.reject(err);
      }
      if (code === 'not_setup' && IS_CE_EDITION) {
        globalThis.location.href = `${globalThis.location.origin}${basePath}/install`;
        return Promise.reject(err);
      }

      // refresh token
      const [refreshErr] = await asyncRunSafe(
        refreshAccessTokenOrRelogin(TIME_OUT),
      );
      if (refreshErr === null)
        return baseFetch<T>(url, options, otherOptionsForBaseFetch);
      if (location.pathname !== `${basePath}/signin` || !IS_CE_EDITION) {
        globalThis.location.href = loginUrl;
        return Promise.reject(err);
      }
      if (!silent) {
        Toast.notify({ type: 'error', message });
        return Promise.reject(err);
      }
      globalThis.location.href = loginUrl;
      return Promise.reject(err);
    } else {
      return Promise.reject(err);
    }
  } catch (error) {
    console.error(error);
    return Promise.reject(error);
  }
};

// request methods
export const get = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return request<T>(
    url,
    Object.assign({}, options, { method: 'GET' }),
    otherOptions,
  );
};

// For public API
export const getPublic = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return get<T>(url, options, { ...otherOptions, isPublicAPI: true });
};

// For Marketplace API
export const getMarketplace = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return get<T>(url, options, { ...otherOptions, isMarketplaceAPI: true });
};

export const post = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return request<T>(
    url,
    Object.assign({}, options, { method: 'POST' }),
    otherOptions,
  );
};

// For Marketplace API
export const postMarketplace = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true });
};

export const postPublic = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return post<T>(url, options, { ...otherOptions, isPublicAPI: true });
};

export const put = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return request<T>(
    url,
    Object.assign({}, options, { method: 'PUT' }),
    otherOptions,
  );
};

export const putPublic = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return put<T>(url, options, { ...otherOptions, isPublicAPI: true });
};

export const del = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return request<T>(
    url,
    Object.assign({}, options, { method: 'DELETE' }),
    otherOptions,
  );
};

export const delPublic = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return del<T>(url, options, { ...otherOptions, isPublicAPI: true });
};

export const patch = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return request<T>(
    url,
    Object.assign({}, options, { method: 'PATCH' }),
    otherOptions,
  );
};

export const patchPublic = <T>(
  url: string,
  options = {},
  otherOptions?: IOtherOptions,
) => {
  return patch<T>(url, options, { ...otherOptions, isPublicAPI: true });
};
