import { Buffer } from 'buffer';
import SealedBox from 'tweetnacl-sealedbox-js';
import { getCookieOptions, setVisitorId, getVisitorId, isUrl } from '@arcblock/ux/lib/Util';
import Cookie from 'js-cookie';
import isNil from 'lodash/isNil';
import isString from 'lodash/isString';
import isUndefined from 'lodash/isUndefined';
import { createAxios as _createAxios, getBlockletSDK } from '@blocklet/js-sdk';
import { withQuery } from 'ufo';
import { isFromPublicKey, isValid as isValidDid } from '@arcblock/did';
import { getSafeUrl } from '@arcblock/ux/lib/Util/security';
import { createDebug, createLogger } from '@arcblock/ux/lib/Util/logger';

import pkg from '../package.json';
import { DID_CONNECT_URL_PARAMS_NAME, DEFAULT_WINDOW_TIMEOUT } from './constant';
import { NotOpenError } from './error';

const { version } = pkg;

export const debug = createDebug('did-connect');
export const debugTmp = createDebug('did-connect:tmp');
export const debugTmpInterval = createDebug('did-connect:tmp-interval');
export const logger = createLogger('did-connect');

export * from '@arcblock/ux/lib/Util/wallet';

export { setVisitorId, getVisitorId, version };

// https://github.com/joaquimserafim/base64-url/blob/54d9c9ede66a8724f280cf24fd18c38b9a53915f/index.js#L10
function unescape(str) {
  return (str + '==='.slice((str.length + 3) % 4)).replace(/-/g, '+').replace(/_/g, '/');
}
function escape(str) {
  return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// 如果需要 did-connect 加入一个已经存在的 session, 可以通过 url 传递一个 "__connect_url__" 查询参数, did-connect 会解析参数值中的 token 和 connect url 并加入该 session
// "__connect_url__" 需要编码/解码才可以正常使用
// 该方法只针对 browser 环境, nodejs 环境可以使用 escape(Buffer.from(url).toString('base64')) 进行编码
export const encodeConnectUrl = (url) => {
  return escape(window.btoa(url));
};

export const decodeConnectUrl = (encoded) => {
  return window.atob(unescape(encoded));
};

// connectUrl 对应 create token 响应数据中的 url 字段值
export const parseTokenFromConnectUrl = (connectUrl) => {
  const connectUrlObj = new URL(connectUrl);
  const url = decodeURIComponent(connectUrlObj.searchParams.get('url'));
  const token = new URL(url).searchParams.get('_t_');
  return token;
};

/**
 * 获取 appInfo 的 appId
 * @param {{agentDid: string; publisher: string}} appInfo
 * @returns {string} appId
 */
export const getAppId = (appInfo, memberAppInfo) => {
  // 优先读取 agentDid，在 federated 登录模式下，当前应用会被存在 agentDid 中，而 publisher 中存储的是 federated master 的 appPid
  let publisher = memberAppInfo?.publisher;
  if (!publisher) {
    // @deprecated 兼容原来的 agentDid 字段，新版中，将使用 memberAppInfo 来代替
    if (appInfo?.agentDid) {
      return appInfo.agentDid;
    }
    publisher = appInfo?.publisher;
  }
  if (publisher) {
    return publisher.replace('did:abt:', '');
  }
  return globalThis.blocklet?.appPid || globalThis.blocklet?.appId || globalThis.env?.appId || '';
};

export const getConnectedInfo = (data) => {
  const result = {
    connected_did: data.did || '',
    connected_pk: data.pk || '',
    connected_app: getAppId(data.appInfo, data.memberAppInfo),
  };
  if (data?.connectedWallet?.os) {
    result.connected_wallet_os = data.connectedWallet.os;
  }
  return result;
};

/**
 *
 * @param {object} connectedInfo 服务端返回的 did-connect 连接信息，可通过格式化解析后成为可以写入 cookie 的值（也可以直接传入解析后的值，但第二个参数要求填写为 true）
 * @param {boolean} parsed connectedInfo 是否被格式化解析过
 * @returns
 */
export const updateConnectedInfo = (connectedInfo, parsed = false) => {
  if (!connectedInfo) return;
  // 如果输入的是原始数据，需要通过 getConnectedInfo 处理一下（兼容之前的调用方式）
  if (!parsed) {
    // eslint-disable-next-line no-param-reassign
    connectedInfo = getConnectedInfo(connectedInfo);
  }
  const cookieOptions = getCookieOptions({
    expireInDays: 7,
    returnDomain: false,
  });
  // connected_did and connected_pk are used to skip authPrincipal
  if (!isNil(connectedInfo.connected_did) && !isNil(connectedInfo.connected_pk)) {
    // NOTICE: 需要严格校验 connected_did 和 connected_pk 是否合法
    if (
      isValidDid(connectedInfo.connected_did) &&
      isFromPublicKey(connectedInfo.connected_did, connectedInfo.connected_pk)
    ) {
      Cookie.set('connected_did', connectedInfo.connected_did, cookieOptions);
      Cookie.set('connected_pk', connectedInfo.connected_pk, cookieOptions);
    }
  }
  // connected_app is used to check session validity
  if (!isNil(connectedInfo.connected_app) && isValidDid(connectedInfo.connected_app))
    Cookie.set('connected_app', connectedInfo.connected_app, cookieOptions);
  if (connectedInfo.connected_wallet_os) {
    Cookie.set('connected_wallet_os', connectedInfo.connected_wallet_os, cookieOptions);
  }
};

export const encodeKey = (key) => escape(Buffer.from(key).toString('base64'));
export const decodeKey = (str) => Uint8Array.from(Buffer.from(unescape(str), 'base64'));

export const decrypt = (value, encryptKey, decryptKey) => {
  const decrypted = SealedBox.open(
    Uint8Array.from(Buffer.from(value, 'base64')),
    decodeKey(encryptKey),
    decodeKey(decryptKey)
  );

  return JSON.parse(Buffer.from(decrypted).toString('utf8'));
};

export const encrypt = (value, encryptKey) => {
  return Buffer.from(SealedBox.seal(Uint8Array.from(Buffer.from(value)), decodeKey(encryptKey))).toString('base64');
};

export const parseNextWorkflow = (url, tokenKey = '_t_') => {
  const tmp = new URL(url);
  const nw = {
    baseUrl: tmp.origin,
    prefix: tmp.pathname.replace(/\/auth$/, ''),
    token: tmp.searchParams.get(tokenKey) || '',
  };

  if (!nw.baseUrl) {
    throw new Error('Invalid next workflow: origin empty');
  }

  if (!nw.token) {
    throw new Error('Invalid next workflow: token not found');
  }

  return nw;
};

export const getApiErrorMessage = (err, defaultMessage) => {
  let errorMessage = err.message;
  const { response } = err;
  if (response) {
    errorMessage = response.data?.error || response.data?.message || response.data || defaultMessage;
    errorMessage = typeof errorMessage === 'object' ? defaultMessage : errorMessage;
  }
  return errorMessage;
};

/**
 * Handle WebAuthn specific errors with user-friendly messages
 * @param {Error} err The error object from WebAuthn operations
 * @param {string} defaultMessage Default fallback message
 * @param {Function} t Translation function
 * @returns {string} User-friendly error message
 */
export const getWebAuthnErrorMessage = (err, defaultMessage, t) => {
  if (!err) {
    return err;
  }
  // If it's not a WebAuthn specific error, use the general API error handler
  if (!err.name) {
    return getApiErrorMessage(err, defaultMessage);
  }

  // Handle specific WebAuthn error types
  switch (err.name) {
    case 'NotAllowedError':
      // User canceled the operation or it timed out
      return t ? t('webauthn.error.canceled') : 'Authentication was canceled';
    case 'SecurityError':
      return t ? t('webauthn.error.security') : 'A security error occurred during authentication';
    case 'NotSupportedError':
      return t ? t('webauthn.error.notSupported') : 'This browser does not support passkey authentication';
    case 'AbortError':
      return t ? t('webauthn.error.aborted') : 'Authentication was aborted';
    default:
      // For unknown error types, log the error name for debugging
      console.warn('Unhandled WebAuthn error type:', err.name);
      return getApiErrorMessage(err, defaultMessage);
  }
};

export const getExtraHeaders = (baseUrl) => {
  const headers = {};
  // 如果 baseUrl 是有效的 url，则使用 baseUrl，否则使用当前页面的 url
  // 避免传入的 baseUrl 是相对路径，导致 new URL 失败
  const authUrl = isUrl(baseUrl) ? baseUrl : window.location.href;

  if (authUrl) {
    const { hostname, protocol, port } = new URL(authUrl);
    headers['x-real-hostname'] = hostname;
    headers['x-real-port'] = port;
    headers['x-real-protocol'] = protocol.endsWith(':') ? protocol.substring(0, protocol.length - 1) : protocol;
  }
  return headers;
};

export const createAxios = (options = {}, { lazy = false, lazyTime = 300 } = {}) => {
  const mergedHeaders = {
    ...getExtraHeaders(options.baseURL),
    ...options.headers,
    'x-did-connect-version': version,
  };

  const instance = _createAxios(
    {
      ...options,
      headers: mergedHeaders,
    },
    { lazy, lazyTime }
  );
  instance.interceptors.request.use((config) => {
    const visitorId = getVisitorId();
    // FIXME: @zhanghan 需要在 js-sdk 改为动态获取 visitorId 后，删除该逻辑
    if (visitorId) config.headers['x-blocklet-visitor-id'] = visitorId;
    return config;
  });
  return instance;
};

export const sleep = (time = 0) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
};

export function getBrowserLang() {
  if (typeof window === 'undefined') {
    return null;
  }

  const lang =
    (window.navigator.languages && window.navigator.languages[0]) ||
    window.navigator.language ||
    window.navigator.browserLanguage ||
    window.navigator.userLanguage ||
    window.navigator.systemLanguage ||
    null;

  const langMap = {
    'zh-CN': 'zh',
    'zh-TW': 'zh',
    zh: 'zh',
    en: 'en',
  };

  return langMap[lang] || 'en';
}

export function formatCacheTtl(ttl, defaultVal) {
  if ([undefined, null, ''].includes(ttl)) {
    return defaultVal;
  }
  return ttl / 86400;
}

/**
 * @returns {import('./types').DIDConnectUrlOptions}
 */
export async function decodeUrlParams() {
  const currentUrl = window.location.href;
  try {
    const urlInstance = new URL(currentUrl);
    const encodedParams = urlInstance.searchParams.get(DID_CONNECT_URL_PARAMS_NAME);
    if (encodedParams) {
      urlInstance.searchParams.delete(DID_CONNECT_URL_PARAMS_NAME);
      const params = JSON.parse(Buffer.from(encodedParams, 'base64').toString('utf-8'));
      if (isString(params.forceConnected) && isUndefined(params.sourceAppPid)) {
        const sdk = getBlockletSDK();
        const userInfo = await sdk.user.getUserPublicInfo({ did: params.forceConnected });
        if (!isUndefined(userInfo?.sourceAppPid)) {
          params.sourceAppPid = userInfo.sourceAppPid;
        }
      }
      return {
        params,
        url: urlInstance.href,
      };
    }
  } catch {
    console.warn('Failed to decode did-connect url params');
  }

  return {
    params: null,
    url: currentUrl,
  };
}

/**
 *
 * @param {string} url - 打开一个弹窗
 * @returns
 */
export const openPopup = (
  _url,
  { width = 680, height = 720, name = 'did-connect:popup', offsetX = 0, offsetY = 0 } = {}
) => {
  // 需要处理为安全的 url
  const url = getSafeUrl(_url, { allowDomains: null });

  const left = window.screenX + (window.innerWidth - width) / 2 + offsetX;
  const top = window.screenY + (window.innerHeight - height) / 2 + offsetY;

  const windowFeatures = [
    `left=${left}`,
    `top=${top}`,
    `width=${width}`,
    `height=${height}`,
    'resizable=no', // not working
    'scrollbars=yes',
    'status=yes',
    'popup=yes',
  ];

  // HACK: IOS 必须以这种形式打开，才能被识别为 dialog 模式
  const popup = window.open('', name, windowFeatures.join(','));
  if (popup === null) {
    throw new NotOpenError();
  }
  popup.location.href = withQuery(url, {
    // NOTICE: 携带当前页面的 origin，用于在 popup 中通过该参数判断是否可以发送 postMessage，即使该参数被伪造，最终也只有该域名能接收到消息，所以没有关系
    opener: window.location.origin,
  });
  return popup;
};

/**
 * @typedef {Object} PopupConfig
 * @property {number} [timeoutInSeconds] - 等待弹窗响应的超时时间
 * @property {number} [closeTimeout] - 弹窗关闭前的等待时间
 * @property {any} [popup] - 弹窗的实例
 */

/**
 *
 * @param {PopupConfig} config
 * @returns
 */
export const runPopup = (config) => {
  return new Promise((resolve, reject) => {
    let popupTimer;
    let timeoutId;
    const popupEventListener = ({ data }) => {
      if (!data) {
        return;
      }

      if (data.type !== 'authorization_response') {
        return;
      }

      if (data?.error) {
        // HACK: 捕获到报错时，如果弹窗未关闭，则不应该 reject，否则调用 runPopup 的地方拿不到成功后的值
        console.error(data.error);
        return;
      }

      setTimeout(() => {
        config.popup?.close();
        // 弹窗关闭后才取消事件监听
        clearTimeout(timeoutId);
        clearInterval(popupTimer);
        window.removeEventListener('message', popupEventListener, false);
      }, config.closeTimeout || 0);
      // 不用等弹窗关闭，可以直接返回结果
      resolve(data);
    };

    timeoutId = setTimeout(
      () => {
        clearInterval(popupTimer);
        reject(new Error('Timeout'));
        window.removeEventListener('message', popupEventListener, false);
      },
      (config.timeoutInSeconds || DEFAULT_WINDOW_TIMEOUT) * 1000
    );
    popupTimer = setInterval(() => {
      if (config.popup?.closed) {
        clearInterval(popupTimer);
        clearTimeout(timeoutId);
        window.removeEventListener('message', popupEventListener, false);
        reject(new Error('Popup closed'));
      }
    }, 1000);

    window.addEventListener('message', popupEventListener);
  });
};
