import {Api, ApiObject, fetcherResult, Payload} from '../types';
import {fetcherConfig} from '../factory';
import {tokenize, dataMapping} from './tpl-builtin';
import qs from 'qs';
import axios from 'axios'
import {evalExpression} from './tpl';
import {
  isObject,
  isObjectShallowModified,
  hasFile,
  object2formData,
  qsstringify,
  cloneObject,
  createObject
} from './helper';
import isPlainObject from 'lodash/isPlainObject';

const rSchema = /(?:^|raw\:)(get|post|put|delete|patch|options|head|jsonp):/i;

interface ApiCacheConfig extends ApiObject {
  cachedPromise: Promise<any>;
  requestTime: number;
}

const apiCaches: Array<ApiCacheConfig> = [];

export function normalizeApi(
  api: Api,
  defaultMethod: string = 'get'
): ApiObject {
  if (typeof api === 'string') {
    let method = rSchema.test(api) ? RegExp.$1 : '';
    method && (api = api.replace(method + ':', ''));

    api = {
      method: (method || defaultMethod) as any,
      url: api
    };
  } else {
    api = {
      ...api
    };
  }
  return api;
}

export function buildApi(
  api: Api,
  data?: object,
  options: {
    autoAppend?: boolean;
    ignoreData?: boolean;
    [propName: string]: any;
  } = {}
): ApiObject {
  api = normalizeApi(api, options.method);
  const {autoAppend, ignoreData, ...rest} = options;

  api.config = {
    ...rest
  };
  api.method = (api.method || (options as any).method || 'get').toLowerCase();

  if (!data) {
    return api;
  } else if (
    data instanceof FormData ||
    data instanceof Blob ||
    data instanceof ArrayBuffer
  ) {
    api.data = data;
    return api;
  }

  const raw = (api.url = api.url || '');
  const idx = api.url.indexOf('?');

  if (~idx) {
    const hashIdx = api.url.indexOf('#');
    const params = qs.parse(
      api.url.substring(idx + 1, ~hashIdx ? hashIdx : undefined)
    );
    api.url =
      tokenize(api.url.substring(0, idx + 1), data, '| url_encode') +
      qsstringify((api.query = dataMapping(params, data))) +
      (~hashIdx ? api.url.substring(hashIdx) : '');
  } else {
    api.url = tokenize(api.url, data, '| url_encode');
  }

  if (ignoreData) {
    return api;
  }

  if (api.data) {
    api.body = api.data = dataMapping(api.data, data);
  } else if (api.method === 'post' || api.method === 'put' || api.method === 'delete') {
    api.body = api.data = cloneObject(data);
  }

  // get 类请求，把 data 附带到 url 上。
  if (api.method === 'get') {
    if (!~raw.indexOf('$') && !api.data && autoAppend) {
      api.query = api.data = data;
    } else if (
      api.attachDataToQuery === false &&
      api.data &&
      !~raw.indexOf('$') &&
      autoAppend
    ) {
      const idx = api.url.indexOf('?');
      if (~idx) {
        let params = (api.query = {
          ...qs.parse(api.url.substring(idx + 1)),
          ...data
        });
        api.url = api.url.substring(0, idx) + '?' + qsstringify(params);
      } else {
        api.query = data;
        api.url += '?' + qsstringify(data);
      }
    }

    if (api.data && api.attachDataToQuery !== false) {
      const idx = api.url.indexOf('?');
      if (~idx) {
        let params = (api.query = {
          ...qs.parse(api.url.substring(idx + 1)),
          ...api.data
        });
        api.url = api.url.substring(0, idx) + '?' + qsstringify(params);
      } else {
        api.query = api.data;
        api.url += '?' + qsstringify(api.data);
      }
      delete api.data;
    }
  }

  if (api.headers) {
    api.headers = dataMapping(api.headers, data);
  }

  if (api.requestAdaptor && typeof api.requestAdaptor === 'string') {
    api.requestAdaptor = str2function(api.requestAdaptor, 'api') as any;
  }

  // csp
  if (api.adaptor && typeof api.adaptor === 'string') {
    console.warn('api.adaptor type not support string')
    api.adaptor = str2function(
      api.adaptor,
      'payload',
      'response',
      'api'
    ) as any;
  }

  return api;
}

export function str2function(
  contents: string,
  ...args: Array<string>
): Function | null {
  try {
    let fn = new Function(...args, contents);
    return fn;
  } catch (e) {
    console.warn(e);
    return null;
  }
}

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;

export function str2AsyncFunction(
  contents: string,
  ...args: Array<string>
): Function | null {
  try {
    let fn = new AsyncFunction(...args, contents);
    return fn;
  } catch (e) {
    console.warn(e);
    return null;
  }
}

export function responseAdaptor(ret: fetcherResult, api: ApiObject) {
  const data = ret.data;
  let hasStatusField = true;

  if (!data) {
    throw new Error('Response is empty!');
  }

  // 兼容几种常见写法
  if (data.hasOwnProperty('errorCode')) {
    // 阿里 Java 规范
    data.status = data.errorCode;
    data.msg = data.errorMessage;
  } else if (data.hasOwnProperty('errno')) {
    data.status = data.errno;
    data.msg = data.errmsg || data.errstr || data.msg;
  } else if (data.hasOwnProperty('no')) {
    data.status = data.no;
    data.msg = data.error || data.msg;
  } else if (data.hasOwnProperty('error')) {
    // Google JSON guide
    // https://google.github.io/styleguide/jsoncstyleguide.xml#error
    if (typeof data.error === 'object' && data.error.hasOwnProperty('code')) {
      data.status = data.error.code;
      data.msg = data.error.message;
    } else {
      data.status = data.error;
      data.msg = data.errmsg || data.msg;
    }
  }

  if (!data.hasOwnProperty('status')) {
    hasStatusField = false;
  }

  const payload: Payload = {
    ok: hasStatusField === false || data.status == 0,
    status: hasStatusField === false ? 0 : data.status,
    msg: data.msg || data.message,
    msgTimeout: data.msgTimeout,
    data: !data.data && !hasStatusField ? data : data.data // 兼容直接返回数据的情况
  };

  // 兼容返回 schema 的情况，用于 app 模式
  if (data && data.type) {
    payload.data = data;
  }

  if (payload.status == 422) {
    payload.errors = data.errors;
  }

  if (payload.ok && api.responseData) {
    payload.data = dataMapping(
      api.responseData,

      createObject(
        {api},
        (Array.isArray(payload.data)
          ? {
              items: payload.data
            }
          : payload.data) || {}
      )
    );
  }

  return payload;
}

export function wrapFetcher(
  fn: (config: fetcherConfig) => Promise<fetcherResult>
): (api: Api, data: object, options?: object) => Promise<Payload | void> {
  return function (api, data, options) {
    api = buildApi(api, data, options) as ApiObject;

    api.requestAdaptor && (api = api.requestAdaptor(api) || api);

    if (api.data && (hasFile(api.data) || api.dataType === 'form-data')) {
      api.data = object2formData(api.data, api.qsOptions);
    } else if (
      api.data &&
      typeof api.data !== 'string' &&
      api.dataType === 'form'
    ) {
      api.data = qsstringify(api.data, api.qsOptions) as any;
      api.headers = api.headers || (api.headers = {});
      api.headers['Content-Type'] = 'application/x-www-form-urlencoded';
    } else if (
      api.data &&
      typeof api.data !== 'string' &&
      api.dataType === 'json'
    ) {
      api.data = JSON.stringify(api.data) as any;
      api.headers = api.headers || (api.headers = {});
      api.headers['Content-Type'] = 'application/json';
    }

    if (typeof api.cache === 'number' && api.cache > 0) {
      const apiCache = getApiCache(api);
      return wrapAdaptor(
        apiCache
          ? (apiCache as ApiCacheConfig).cachedPromise
          : setApiCache(api, fn(api)),
        api
      );
    }
    return wrapAdaptor(fn(api), api);
  };
}

export function wrapAdaptor(promise: Promise<fetcherResult>, api: ApiObject) {
  const adaptor = api.adaptor;
  return adaptor
    ? promise
        .then(async response => {
          let result = adaptor((response as any).data, response, api);

          if (result?.then) {
            result = await result;
          }

          return {
            ...response,
            data: result
          };
        })
        .then(ret => responseAdaptor(ret, api))
    : promise.then(ret => responseAdaptor(ret, api));
}

export function isApiOutdated(
  prevApi: Api | undefined,
  nextApi: Api | undefined,
  prevData: any,
  nextData: any
): nextApi is Api {
  if (!nextApi) {
    return false;
  } else if (!prevApi) {
    return true;
  }

  nextApi = normalizeApi(nextApi);

  if (nextApi.autoRefresh === false) {
    return false;
  }

  const trackExpression = nextApi.trackExpression ?? nextApi.url;

  if (typeof trackExpression !== 'string' || !~trackExpression.indexOf('$')) {
    return false;
  }
  prevApi = normalizeApi(prevApi);

  let isModified = false;

  if (nextApi.trackExpression || prevApi.trackExpression) {
    isModified =
      tokenize(prevApi.trackExpression || '', prevData) !==
      tokenize(nextApi.trackExpression || '', nextData);
  } else {
    prevApi = buildApi(prevApi as Api, prevData as object, {ignoreData: true});
    nextApi = buildApi(nextApi as Api, nextData as object, {ignoreData: true});
    isModified = prevApi.url !== nextApi.url;
  }

  return !!(
    isModified &&
    isValidApi(nextApi.url) &&
    (!nextApi.sendOn || evalExpression(nextApi.sendOn, nextData))
  );
}

export function isValidApi(api: string) {
  return (
    api &&
    /^(?:(https?|wss?|taf):\/\/[^\/]+)?(\/?[^\s\/\?]*){1,}(\?.*)?$/.test(api)
  );
}

export function isEffectiveApi(
  api?: Api,
  data?: any,
  initFetch?: boolean,
  initFetchOn?: string
): api is Api {
  if (!api) {
    return false;
  }
  if (initFetch === false) {
    return false;
  }
  if (initFetchOn && data && !evalExpression(initFetchOn, data)) {
    return false;
  }
  if (typeof api === 'string' && api.length) {
    return true;
  } else if (isObject(api) && (api as ApiObject).url) {
    if (
      (api as ApiObject).sendOn &&
      data &&
      !evalExpression((api as ApiObject).sendOn as string, data)
    ) {
      return false;
    }
    return true;
  }
  return false;
}

export function isSameApi(
  apiA: ApiObject | ApiCacheConfig,
  apiB: ApiObject | ApiCacheConfig
): boolean {
  return (
    apiA.method === apiB.method &&
    apiA.url === apiB.url &&
    !isObjectShallowModified(apiA.data, apiB.data, false)
  );
}

export function getApiCache(api: ApiObject): ApiCacheConfig | undefined {
  // 清理过期cache
  const now = Date.now();
  let result: ApiCacheConfig | undefined;

  for (let idx = 0, len = apiCaches.length; idx < len; idx++) {
    const apiCache = apiCaches[idx];

    if (now - apiCache.requestTime > (apiCache.cache as number)) {
      apiCaches.splice(idx, 1);
      len--;
      idx--;
      continue;
    }

    if (isSameApi(api, apiCache)) {
      result = apiCache;
      break;
    }
  }

  return result;
}

export function setApiCache(
  api: ApiObject,
  promise: Promise<any>
): Promise<any> {
  apiCaches.push({
    ...api,
    cachedPromise: promise,
    requestTime: Date.now()
  });
  return promise;
}

export function clearApiCache() {
  apiCaches.splice(0, apiCaches.length);
}

export function normalizeApiResponseData(data: any) {
  if (typeof data === 'undefined') {
    data = {};
  } else if (!isPlainObject(data)) {
    data = {
      [Array.isArray(data) ? 'items' : 'result']: data
    };
  }

  return data;
}

// window.apiCaches = apiCaches;

export function defaultFetcher(env: any={}) {

  const attachmentAdpator = (response: any) => {
    if (
      response &&
      response.headers &&
      response.headers['content-disposition']
    ) {
      const disposition = response.headers['content-disposition'];
      let filename = '';

      if (disposition && disposition.indexOf('attachment') !== -1) {
        // disposition 有可能是 attachment; filename="??.xlsx"; filename*=UTF-8''%E4%B8%AD%E6%96%87.xlsx
        // 这种情况下最后一个才是正确的文件名
        let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)$/;

        let matches = disposition.match(filenameRegex);
        if (matches && matches.length) {
          filename = matches[1].replace(`UTF-8''`, '').replace(/['"]/g, '');
        }

        // 很可能是中文被 url-encode 了
        if (filename && filename.replace(/[^%]/g, '').length > 2) {
          filename = decodeURIComponent(filename);
        }

        let type = response.headers['content-type'];
        let blob =
          response.data.toString() === '[object Blob]'
            ? response.data
            : new Blob([response.data], {type: type});
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
          // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
          window.navigator.msSaveBlob(blob, filename);
        } else {
          let URL = window.URL || (window as any).webkitURL;
          let downloadUrl = URL.createObjectURL(blob);
          if (filename) {
            // use HTML5 a[download] attribute to specify filename
            let a = document.createElement('a');
            // safari doesn't support this yet
            if (typeof a.download === 'undefined') {
              (window as any).location = downloadUrl;
            } else {
              a.href = downloadUrl;
              a.download = filename;
              document.body.appendChild(a);
              a.click();
            }
          } else {
            (window as any).location = downloadUrl;
          }
          setTimeout(function () {
            URL.revokeObjectURL(downloadUrl);
          }, 100); // cleanup
        }

        return {
          ...response,
          data: {
            status: 0,
            msg: '文件即将开始下载。。'
          }
        };
      }
    } else if (response.data.toString() === '[object Blob]') {
      return new Promise((resolve, reject) => {
        let reader = new FileReader();
        reader.addEventListener('loadend', e => {
          const text = reader.result as string;

          try {
            resolve({
              ...response,
              data: {
                ...JSON.parse(text)
              }
            });
          } catch (e) {
            reject(e);
          }
        });

        reader.readAsText(response.data);
      });
    }

    return response;
  };

  const responseAdaptor = (api: any) => (value: any) => {
    let response = value.data || {}; // blob 下可能会返回内容为空？
    if (env && env.responseAdaptor) {
      const idx = api.url.indexOf('?');
      const query = ~idx ? qs.parse(api.url.substring(idx)) : {};
      const request = {
        ...api,
        query: query,
        body: api.data
      };
      response = env.responseAdaptor(api, response, query, request);
    } else {
      if (response.hasOwnProperty('errno')) {
        response.status = response.errno;
        response.msg = response.errmsg;
      } else if (response.hasOwnProperty('no')) {
        response.status = response.no;
        response.msg = response.error;
      }else{
        if(response.code !== void 0){
          response.status = response.code === 'OK' ? 0 : response.code;
        }
        response.msg = response.message || response.msg;
      }
    }

    const result = {
      ...value,
      data: response
    };
    return result;
  };

  return async (api: any) => {
    let {url, method, data, responseType, config, headers} = api;
    config = config || {};
    config.withCredentials = true;
    responseType && (config.responseType = responseType);

    if (config.cancelExecutor) {
      config.cancelToken = new (axios as any).CancelToken(
        config.cancelExecutor
      );
    }

    config.headers = headers || {};
    config.method = method;

    if (method === 'get' && data) {
      config.params = data;
    } else if (data && data instanceof FormData) {
      // config.headers['Content-Type'] = 'multipart/form-data';
    } else if (
      data &&
      typeof data !== 'string' &&
      !(data instanceof Blob) &&
      !(data instanceof ArrayBuffer)
    ) {
      data = JSON.stringify(data);
      config.headers['Content-Type'] = 'application/json';
    }

    // 支持返回各种报错信息
    config.validateStatus = function (status:any) {
      return true;
    };

    data && (config.data = data);

    if(env.baseURL){
      config.baseURL = env.baseURL
    }

    let response = await axios(url, config);
    response = await attachmentAdpator(response);
    response = responseAdaptor(api)(response);

    if (response.status >= 400) {
      if (response.data) {
        // 主要用于 raw: 模式下，后端自己校验登录，
        if (
          response.status === 401 &&
          response.data.location &&
          response.data.location.startsWith('http')
        ) {
          location.href = response.data.location.replace(
            '{{redirect}}',
            encodeURIComponent(location.href)
          );
          return new Promise(() => {});
        } else if (response.data.msg) {
          throw new Error(response.data.msg);
        } else {
          throw new Error(
            '接口报错：' + JSON.stringify(response.data, null, 2)
          );
        }
      } else {
        throw new Error(`接口出错，状态码是 ${response.status}`);
      }
    }

    return response;
  }
}
