import crypto from 'crypto';
import { Readable } from 'stream';
import { buffer } from 'node:stream/consumers';
import { getUrl } from '@blocklet/sdk/lib/component';
import { env } from '@blocklet/sdk/lib/env';
import { getWalletDid } from '@blocklet/sdk/lib/did';
import { toStakeAddress } from '@arcblock/did-util';
import { customAlphabet } from 'nanoid';
import type { LiteralUnion } from 'type-fest';
import { joinURL, withQuery, withTrailingSlash } from 'ufo';

import axios from 'axios';
import { ethers } from 'ethers';
import { fromUnitToToken } from '@ocap/util';
import get from 'lodash/get';
import numbro from 'numbro';
import { trimEnd } from 'lodash';
import dayjs from './dayjs';
import { blocklet, wallet } from './auth';
import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
import logger from './logger';

export const OCAP_PAYMENT_TX_TYPE = 'fg:t:transfer_v2';

export const MAX_SUBSCRIPTION_ITEM_COUNT = 20;

export const MAX_RETRY_COUNT = 20; // 2^20 seconds ~~ 12 days, total retry time: 24 days
export const MIN_RETRY_MAIL = 13; // total retry time before sending first mail: 6 hours

export const CHECKOUT_SESSION_TTL = 6 * 60 * 60; // expires in 6 hours, then removed after 12 hours

export const STRIPE_API_VERSION = '2023-08-16';
export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook');
export const STRIPE_EVENTS: any[] = [
  'checkout.session.async_payment_failed',
  'checkout.session.async_payment_succeeded',
  'checkout.session.completed',
  'checkout.session.expired',

  // 'customer.subscription.created',
  'customer.subscription.deleted',
  'customer.subscription.paused',
  'customer.subscription.pending_update_applied',
  'customer.subscription.pending_update_expired',
  'customer.subscription.resumed',
  'customer.subscription.trial_will_end',
  'customer.subscription.updated',
  'customer.updated',

  'invoice.created',
  'invoice.deleted',
  'invoice.finalization_failed',
  'invoice.finalized',
  'invoice.marked_uncollectible',
  'invoice.paid',
  'invoice.payment_action_required',
  'invoice.payment_failed',
  'invoice.payment_succeeded',
  'invoice.sent',
  'invoice.upcoming',
  'invoice.updated',
  'invoice.voided',

  'payment_intent.canceled',
  'payment_intent.created',
  'payment_intent.partially_funded',
  'payment_intent.payment_failed',
  'payment_intent.processing',
  'payment_intent.requires_action',
  'payment_intent.succeeded',

  'setup_intent.canceled',
  // 'setup_intent.created',
  'setup_intent.requires_action',
  'setup_intent.setup_failed',
  'setup_intent.succeeded',

  'refund.created',
  'refund.updated',
];

export const api = axios.create({
  timeout: 10 * 1000,
});

export function md5(input: string) {
  return crypto.createHash('md5').update(input).digest('hex');
}

export function createIdGenerator(prefix: string, size: number = 24) {
  const generator = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', size);
  return prefix ? () => `${prefix}_${generator()}` : generator;
}

export function createCodeGenerator(prefix: string, size: number = 24) {
  const generator = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', size);
  return prefix ? () => `${prefix}_${generator()}` : generator;
}

export function hasObjectChanged<T extends Record<string, any>>(
  updates: Partial<T>,
  original: T,
  options?: {
    deepCompare?: string[];
  }
): boolean {
  const deepFields = options?.deepCompare || [];

  for (const [key, newValue] of Object.entries(updates)) {
    if (newValue !== undefined) {
      const oldValue = original[key];

      if (deepFields.includes(key)) {
        if (typeof newValue === 'object' && newValue !== null && typeof oldValue === 'object' && oldValue !== null) {
          const newObj = newValue as Record<string, any>;
          const oldObj = (oldValue || {}) as Record<string, any>;
          for (const subKey of Object.keys(newObj)) {
            if (newObj[subKey] !== oldObj[subKey]) {
              return true;
            }
          }
        } else if (newValue !== oldValue) {
          return true;
        }
      } else if (newValue !== oldValue) {
        return true;
      }
    }
  }

  return false;
}

export function stringToStream(str: string): Readable {
  const stream = new Readable();
  stream.push(str);
  stream.push(null);

  return stream;
}

export const streamToString = async (stream: any, encoding?: BufferEncoding): Promise<string> => {
  const data = await buffer(stream);
  return data.toString(encoding);
};

// FIXME: merge with old metadata
export function formatMetadata(metadata?: Record<string, any>): Record<string, any> {
  if (!metadata) {
    return {};
  }

  if (Array.isArray(metadata)) {
    return metadata.reduce((acc: Record<string, any>, x: { key: string; value: any }) => {
      if (x.value !== '') {
        acc[x.key] = x.value;
      }
      return acc;
    }, {});
  }

  // remove empty values
  return Object.keys(metadata).reduce((acc: Record<string, any>, key: string) => {
    if (metadata[key] !== '') {
      acc[key] = metadata[key];
    }
    return acc;
  }, {});
}

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

export function tryWithTimeout(asyncFn: Function, timeout = 5000) {
  if (typeof asyncFn !== 'function') {
    throw new Error('Must provide a valid asyncFn function');
  }

  /* eslint-disable no-async-promise-executor */
  return new Promise(async (resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Operation timed out after ${timeout} ms`));
    }, timeout);

    try {
      const result = await asyncFn();
      resolve(result);
    } catch (err) {
      reject(err);
    } finally {
      clearTimeout(timer);
    }
  });
}

// simple exponential delay: 2^retryCount
export const getNextRetry = (retryCount: number, eventCreatedAt?: Date) => {
  const delay = 2 ** retryCount;
  const baseTime = eventCreatedAt ? dayjs(eventCreatedAt).unix() : dayjs().unix();
  return baseTime + delay;
};

export const getWebhookJobId = (eventId: string, webhookId: string) => {
  return md5([eventId, webhookId].join('-'));
};

export function getTxMetadata(extra: Record<string, any> = {}): any {
  return {
    type: 'json',
    value: {
      paymentKit: {
        // FIXME: this should be customizable
        description: `Manage subscriptions and payments on ${env.appName}`,
        customerPortal: getUrl('/customer'),
        ...extra,
      },
    },
  };
}

export function getDataObjectFromQuery(
  query: Record<string, any> = {},
  prefix: string = 'metadata'
): Record<string, any> {
  const result: Record<string, any> = {};
  Object.keys(query).forEach((key) => {
    if (key.startsWith(`${prefix}.`) && query[key]) {
      result[key.replace(`${prefix}.`, '')] = query[key];
    }
  });

  return result;
}

export function safeJsonParse(input: any, defaultValue: any) {
  try {
    return JSON.parse(input);
  } catch {
    if (defaultValue === undefined) {
      return input;
    }
    return defaultValue;
  }
}

const cachedBlockletJsonResult = new Map<string, { data: any; expiry: number }>();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

export async function getBlockletJson(url?: string) {
  const blockletKey = url || process.env.BLOCKLET_APP_URL || 'default';
  const now = Date.now();

  if (cachedBlockletJsonResult.has(blockletKey)) {
    const cached = cachedBlockletJsonResult.get(blockletKey);
    if (cached && now < cached.expiry) {
      return cached.data;
    }
  }
  const scriptUrl = new URL('__blocklet__.js?type=json', withTrailingSlash(url || process.env.BLOCKLET_APP_URL));
  try {
    const { data: blockletMeta } = await api.get(scriptUrl.href);
    cachedBlockletJsonResult.set(blockletKey, { data: blockletMeta, expiry: now + CACHE_TTL });
    return blockletMeta;
  } catch (err) {
    logger.error(`getBlockletJson error for ${scriptUrl}`, err);
    if (process.env.BLOCKLET_MOUNT_POINTS) {
      const BLOCKLET_MOUNT_POINTS = safeJsonParse(process.env.BLOCKLET_MOUNT_POINTS, []);
      return {
        componentMountPoints: BLOCKLET_MOUNT_POINTS,
        appId: process.env.BLOCKLET_APP_ID,
        appName: process.env.BLOCKLET_APP_NAME,
        appLogo: '/.well-known/service/blocklet/logo',
        appUrl: process.env.BLOCKLET_APP_URL,
      };
    }
    return null;
  }
}

export async function getUserOrAppInfo(
  address: string,
  blockletJson?: any
): Promise<{ name: string; avatar: string; type: 'dapp' | 'user'; url: string } | null> {
  if (blockletJson) {
    if (blockletJson?.appId === address) {
      return {
        name: blockletJson?.appName,
        avatar: `${blockletJson?.appUrl}${blockletJson?.appLogo}`,
        type: 'dapp',
        url: blockletJson?.appUrl,
      };
    }
    const appInfo = blockletJson?.componentMountPoints?.find((x: any) => x.appId === address);
    if (appInfo) {
      return {
        name: appInfo.name,
        avatar: joinURL(process.env.BLOCKLET_APP_URL!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),
        type: 'dapp',
        url: joinURL(process.env.BLOCKLET_APP_URL!, appInfo.mountPoint),
      };
    }
  }
  const { user } = await blocklet.getUser(address);
  if (user) {
    const locale = get(user, 'locale', 'en');
    return {
      name: user?.fullName,
      avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
      type: 'user',
      url: getCustomerProfileUrl({ userDid: address, locale }),
    };
  }
  return {
    name: 'anonymous',
    avatar: getUrl('/methods/default.png'),
    type: 'user',
    url: getCustomerProfileUrl({ userDid: address, locale: 'en' }),
  };
}

// @FIXME: 这个应该封装在某个通用类库里面 @jianchao @wangshijun
export function getExplorerLink({
  type,
  did,
  chainHost = undefined,
  queryParams = {},
}: {
  chainHost: string | undefined;
  did: string | undefined;
  type: LiteralUnion<'asset' | 'account' | 'tx' | 'token' | 'factory' | ' bridge', string>;
  queryParams?: Record<string, string>;
}) {
  if (!chainHost) return undefined;
  try {
    const chainUrl = new URL(chainHost);
    switch (type) {
      case 'asset':
        chainUrl.pathname = `/explorer/assets/${did}`;
        break;
      case 'account':
        chainUrl.pathname = `/explorer/accounts/${did}`;
        break;
      case 'tx':
        chainUrl.pathname = `/explorer/txs/${did}`;
        break;
      case 'token':
        chainUrl.pathname = `/explorer/tokens/${did}`;
        break;
      case 'factory':
        chainUrl.pathname = `/explorer/factories/${did}`;
        break;
      case 'bridge':
        chainUrl.pathname = `/explorer/bridges/${did}`;
        break;
      default:
        chainUrl.pathname = '/';
    }

    return withQuery(chainUrl.href, queryParams);
  } catch {
    return undefined;
  }
}

type DIDConnectCustomQuery = {
  // 填写用户的 DID
  forceConnected: string;
  sourceAppPid?: string;
  // auto 代表引导切换（进行弹窗提示，用户可选择不切换）；
  // disabled 代表不提示（适用于支付页面，允许用户不以当前登录账户来进行支付）；
  // required 代表强制要求切换（进行页面跳转强制切换账号）
  // switchAccount?: boolean || 'auto'; // 默认 'auto'
  switchBehavior?: 'auto' | 'disabled' | 'required';
  showClose?: true | false;
};
/**
 * @see https://github.com/blocklet/payment-kit/issues/315#issuecomment-1902978895
 * @description
 * @export
 * @param {{ userDid: string }} { userDid }
 * @return {*}  {string}
 */
export function getConnectQueryParam({ userDid }: { userDid: string }): {
  '__did-connect__': string;
} {
  const data: DIDConnectCustomQuery = {
    forceConnected: userDid,
    sourceAppPid: env.appPid,
    switchBehavior: 'auto',
    showClose: false,
  };

  return {
    '__did-connect__': Buffer.from(JSON.stringify(data), 'utf8').toString('base64'),
  };
}

export function formatAmountPrecisionLimit(
  amount: string,
  precision: number = 6,
  amountLabel: LiteralUnion<'Amount' | 'Price', string> = 'Amount'
) {
  const [, decimal] = amount.split('.');
  if (decimal && decimal.length > precision) {
    return `${amountLabel} decimal places must be less than or equal to ${precision}`;
  }
  return '';
}

export async function getCustomerStakeAddress(customerDid: string, nonce?: string) {
  const { user } = await blocklet.getUser(customerDid, { enableConnectedAccount: true });
  if (user) {
    return toStakeAddress(getWalletDid(user), wallet.address, nonce);
  }

  return toStakeAddress(customerDid, wallet.address, nonce);
}

export function getCustomerProfileUrl({
  locale = 'en',
  userDid,
}: {
  locale: LiteralUnion<'en' | 'zh', string>;
  userDid: string;
}) {
  const { BLOCKLET_APP_URL } = process.env;
  return joinURL(
    BLOCKLET_APP_URL as string,
    withQuery('.well-known/service/user', {
      locale,
      did: userDid,
    })
  );
}

export function getCustomerRechargeLink({
  locale = 'en',
  userDid,
  subscriptionId,
}: {
  locale: LiteralUnion<'en' | 'zh', string>;
  userDid: string;
  subscriptionId: string;
}) {
  return getUrl(
    withQuery(`customer/subscription/${subscriptionId}/recharge`, {
      locale,
      ...getConnectQueryParam({ userDid }),
    })
  );
}

export async function getOwnerDid() {
  try {
    const { user } = await blocklet.getOwner();
    return user?.did;
  } catch (error) {
    logger.error('getOwnerDid error', error);
    return undefined;
  }
}

export async function getDidListByRole(role: string | string[]) {
  try {
    if (Array.isArray(role)) {
      const didSet = new Set<string>();
      await Promise.all(
        role.map(async (r: string) => {
          const { users } = await blocklet.getUsers({ query: { role: r } });
          users.forEach((u: any) => didSet.add(u.did));
        })
      );
      return Array.from(didSet);
    }
    const { users } = await blocklet.getUsers({ query: { role } });
    return users.map((x: any) => x?.did);
  } catch (error) {
    logger.error('getDidListByRole error', error);
    return null;
  }
}

export function getSubscriptionNotificationCustomActions(
  subscription: Subscription,
  eventType: string,
  locale: string
) {
  if (!subscription || !subscription?.service_actions || !subscription?.service_actions?.length) {
    return [];
  }
  const actions = subscription.service_actions?.filter(
    (x: any) => x?.type === 'notification' && x?.triggerEvents?.includes(eventType)
  );
  return actions?.map((x: any) => ({
    name: x?.name || x?.text?.[locale],
    title: x?.text?.[locale],
    link: x?.link,
  }));
}

export function getAdminBillingSubscriptionsUrl({ locale, userDid }: { locale: string; userDid: string }) {
  return getUrl(withQuery('admin/billing/subscriptions', { locale, ...getConnectQueryParam({ userDid }) }));
}

export function getAdminBillingSubscriptionUrl({
  locale,
  userDid,
  subscriptionId,
}: {
  locale: string;
  userDid: string;
  subscriptionId: string;
}) {
  return getUrl(withQuery(`admin/billing/${subscriptionId}`, { locale, ...getConnectQueryParam({ userDid }) }));
}

export function getCustomerIndexUrl({ locale, userDid }: { locale: string; userDid: string }) {
  return getUrl(withQuery('customer', { locale, ...getConnectQueryParam({ userDid }) }));
}

export function getCustomerAutoRechargeSettingsUrl({
  locale,
  userDid,
  currencyId,
}: {
  locale: string;
  userDid: string;
  currencyId?: string;
}) {
  const query: Record<string, string> = {
    locale,
    action: 'auto-recharge',
    ...getConnectQueryParam({ userDid }),
  };
  if (currencyId) {
    query.currencyId = currencyId;
  }
  return getUrl(withQuery('customer', query));
}

// Check if user is in blocklist
export async function isUserInBlocklist(did: string, paymentMethod: PaymentMethod): Promise<boolean> {
  try {
    // Get API host from payment method settings
    // @ts-ignore
    const apiHost = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
    if (!apiHost) {
      logger.warn('No API host found in payment method settings', { paymentMethodId: paymentMethod.id });
      return false;
    }

    // Get user information
    const { user }: { user: any } = await blocklet.getUser(did);
    if (!user) {
      logger.error('User not found', { did });
      return false;
    }
    // Get all wallet DIDs associated with the user
    const connectedAccounts = user.connectedAccounts || user.extraConfigs?.connectedAccounts || [];
    const walletDids = connectedAccounts
      .filter((account: any) => account.provider === 'wallet')
      .map((account: any) => account.did);

    if (!walletDids.length) {
      logger.info('No wallet DIDs found for user', { did });
      return false;
    }

    const blockedUrl = joinURL(apiHost, '/blocked');
    const blockedResponse = await api.get(blockedUrl);
    const blockedDids = blockedResponse.data || [];

    if (!Array.isArray(blockedDids) || blockedDids.length === 0) {
      return false;
    }

    // Check if any of the user's DIDs are in the blocklist
    const isBlocked = walletDids.some((walletDid: string) => blockedDids.includes(walletDid));

    if (isBlocked) {
      logger.info('User is in blocklist', { did, walletDids, blockedDids });
    }

    return isBlocked;
  } catch (error) {
    logger.error('Error checking blocklist', { error: error.message, did });
    return false; // Default to allowing payment on error
  }
}

export function resolveAddressChainTypes(address: string): LiteralUnion<'ethereum' | 'base' | 'arcblock', string>[] {
  if (ethers.isAddress(address)) {
    return ['ethereum', 'base', 'arcblock'];
  }
  return ['arcblock'];
}

export function formatCurrencyInfo(
  amount: string | number,
  paymentCurrency: PaymentCurrency,
  paymentMethod?: PaymentMethod | null,
  isToken?: boolean
) {
  const defaultPaymentCurrency = {
    symbol: '',
    decimal: 18,
  };

  const decimal = paymentCurrency.decimal ?? defaultPaymentCurrency.decimal;
  const symbol = paymentCurrency.symbol ?? defaultPaymentCurrency.symbol;

  let formattedAmount: string;
  if (isToken) {
    // Already in token format, apply precision formatting
    const numericValue = Number(amount || '0');
    if (!Number.isFinite(numericValue)) {
      formattedAmount = String(amount || '0');
    } else {
      const abs = Math.abs(numericValue);
      // If amount > 0.01, show 2 decimal places; otherwise show full precision
      const targetPrecision = abs > 0 && abs < 0.01 ? decimal : 2;
      formattedAmount = formatNumber(numericValue, targetPrecision, true, false) || '0';
    }
  } else {
    // Convert from unit to token format, then apply precision formatting
    const tokenAmount = fromUnitToToken(amount || '0', decimal);
    const numericValue = Number(tokenAmount);
    if (!Number.isFinite(numericValue)) {
      formattedAmount = tokenAmount;
    } else {
      const abs = Math.abs(numericValue);
      // If amount > 0.01, show 2 decimal places; otherwise show full precision
      const targetPrecision = abs > 0 && abs < 0.01 ? decimal : 2;
      formattedAmount = formatNumber(numericValue, targetPrecision, true, false) || '0';
    }
  }

  const amountStr = `${formattedAmount} ${symbol}`;
  return paymentMethod && paymentMethod.type !== 'arcblock' ? `${amountStr} (${paymentMethod.name})` : amountStr;
}

/**
 * Format amount from unit to token with consistent precision
 * - If amount > 0.01, show 2 decimal places
 * - If 0 < amount <= 0.01, show full precision
 * - Returns formatted string without symbol
 */
export function formatTokenAmount(amount: string | number, decimal: number): string {
  const tokenAmount = fromUnitToToken(amount || '0', decimal);
  const numericValue = Number(tokenAmount);
  if (!Number.isFinite(numericValue)) {
    return tokenAmount;
  }
  const abs = Math.abs(numericValue);
  const targetPrecision = abs > 0 && abs < 0.01 ? decimal : 2;
  return formatNumber(numericValue, targetPrecision, true, false) || '0';
}

export function getExplorerTxUrl({
  explorerHost,
  txHash,
  type,
}: {
  explorerHost: string;
  txHash: string;
  type: LiteralUnion<'ethereum' | 'base' | 'arcblock' | 'bitcoin', string>;
}) {
  if (!explorerHost || !txHash) {
    return '';
  }
  if (type === 'arcblock') {
    return joinURL(explorerHost, '/txs', txHash);
  }
  return joinURL(explorerHost, '/tx', txHash);
}

export function formatNumber(
  n: number | string,
  precision: number = 6,
  trim: boolean = true,
  thousandSeparated: boolean = true
) {
  if (!n || n === '0') {
    return '0';
  }
  const num = numbro(n);
  const options = {
    thousandSeparated,
    ...((precision || precision === 0) && { mantissa: precision }),
  };
  const result = num.format(options);
  if (!trim) {
    return result;
  }
  const [left, right] = result.split('.');
  return right ? [left, trimEnd(right, '0')].filter(Boolean).join('.') : left;
}

const CURRENCY_SYMBOLS: Record<string, string> = {
  USD: '$',
  EUR: '€',
  GBP: '£',
  JPY: '¥',
  CNY: '¥',
  KRW: '₩',
  INR: '₹',
  AUD: 'A$',
  CAD: 'C$',
  CHF: 'Fr',
  HKD: 'HK$',
  SGD: 'S$',
  NZD: 'NZ$',
};

/**
 * Format credit amount for display in data contexts (balances, transactions, notifications)
 * - If currency has symbol mapping (USD -> $): shows "$20"
 * - If no symbol mapping: shows "20 minutes" or "20 arcsphere credits"
 *
 * @param formattedAmount - Already formatted amount string (e.g., "20", "1,000.50")
 * @param currencySymbol - Currency symbol (USD, EUR, minutes, etc.)
 */
export function formatCreditAmount(formattedAmount: string, currencySymbol: string | undefined): string {
  const mappedSymbol = CURRENCY_SYMBOLS[currencySymbol || ''];
  if (mappedSymbol) {
    return `${mappedSymbol}${formattedAmount} credits`;
  }
  return `${formattedAmount} ${currencySymbol || ''}`;
}

export function formatLinkWithLocale(url: string, locale?: string) {
  if (!locale || !url) {
    return url;
  }
  try {
    const urlObj = new URL(url);
    urlObj.searchParams.set('locale', locale);
    return urlObj.toString();
  } catch (error) {
    if (/[?&]locale=[^&]*/.test(url)) {
      return url.replace(/([?&])locale=[^&]*/, `$1locale=${locale}`);
    }
    const separator = url.includes('?') ? '&' : '?';
    return `${url}${separator}locale=${locale}`;
  }
}
