import { toStakeAddress, toTokenAddress, toTokenFactoryAddress } from '@arcblock/did-util/cbor';
import { fromPublicKeyHash, toTypeInfo } from '@arcblock/did';
import stringify from 'json-stable-stringify';
import { BN, fromTokenToUnit, fromUnitToToken, toBN, toBase58, toBase64 } from '@ocap/util';
import { types } from '@ocap/mcrypto';
import { verify as verifyVC } from '@arcblock/vc';
import { BlockletService } from '@blocklet/sdk/service/blocklet';

import { PaymentMethod } from '../../store/models';
import type { TPaymentCurrency } from '../../store/models';
import { wallet } from '../../libs/auth';
import logger from '../../libs/logger';
import env from '../../libs/env';
import { sleep } from '../../libs/util';
import { getGasPayerExtra } from '../../libs/payment';

const blockletService = new BlockletService();

export function isOnchainCredit(paymentCurrency: TPaymentCurrency) {
  return paymentCurrency.type === 'credit' && !!paymentCurrency.token_config;
}

/**
 * Stake ABT for token creation
 */
async function stakeForTokenCreation(tokenSymbol: string, client: any) {
  try {
    // ensure context for stake
    await client.getContext();

    // Get chain configuration
    const { state: forgeState } = await client.getForgeState({});
    const requiredStake = new BN(forgeState.txConfig.txStake.createCreditToken || 0);
    const mainToken = forgeState.token;

    // Check if already staked for this token
    const stakeAddress = toStakeAddress(wallet.address, mainToken.address, tokenSymbol);
    const { state: stakeState } = await client.getStakeState({ address: stakeAddress });

    if (stakeState?.revocable) {
      logger.info('Already staked for token creation', { stakeState, tokenSymbol });

      const stakedToken = stakeState.tokens?.find((item: any) => item.address === mainToken.address);
      const actualStake = new BN(stakedToken?.value || 0);

      if (actualStake.gte(requiredStake)) {
        return { client, forgeState, stakeAddress };
      }
    }

    // Get account state to check balance
    const { state: account } = await client.getAccountState({ address: wallet.address });

    // Check if we have enough balance to stake
    const holding = (account?.tokens || []).find((x: any) => x.address === mainToken.address);
    const balance = toBN(holding?.value || '0');

    if (balance.lt(requiredStake)) {
      throw new Error(
        `Insufficient balance to stake. Required: ${fromUnitToToken(requiredStake, mainToken.decimal)} ${mainToken.symbol}, Current: ${fromUnitToToken(balance, mainToken.decimal)} ${mainToken.symbol}`
      );
    }

    logger.info('Staking for token creation', {
      amount: fromUnitToToken(requiredStake, mainToken.decimal),
      token: mainToken.symbol,
      tokenSymbol,
    });

    // @ts-ignore
    const [hash] = await client.stake({
      to: mainToken.address,
      nonce: tokenSymbol,
      message: 'stake for create token factory',
      tokens: [{ address: mainToken.address, value: fromUnitToToken(requiredStake, mainToken.decimal) }],
      wallet,
    });

    logger.info('Stake transaction successful', { hash, stakeAddress, tokenSymbol });

    return { client, forgeState, stakeAddress };
  } catch (error) {
    logger.error('Failed to stake for token creation', { tokenSymbol, error });
    throw error;
  }
}

async function createTokenVC(data: { tokenAddress: string; symbol: string; website: string; chainId: string }) {
  // Compatible with different chainId for main chain and Payment Kit
  const chainId = data.chainId === 'main' ? 'xenon-2020-01-15' : data.chainId;
  const subject = {
    id: wallet.address,
    issued: {
      address: data.tokenAddress,
      symbol: data.symbol,
      website: data.website,
      chainId,
    },
    display: {
      type: 'url',
      content: `${data.website.replace(/\/$/, '')}/.well-known/ocap/tokens.json`,
    },
  };

  const typeInfo = toTypeInfo(wallet.address);
  const vcType = { ...typeInfo, role: types.RoleType.ROLE_VC };
  const vcDid = fromPublicKeyHash(wallet.hash(stringify(subject)!), vcType);

  const vc = {
    '@context': 'https://schema.arcblock.io/v0.1/context.jsonld',
    id: vcDid,
    type: 'TokenIssueCredential',
    issuer: {
      id: wallet.address,
      pk: toBase58(wallet.publicKey),
      name: wallet.address,
    },
    issuanceDate: new Date().toISOString(),
    credentialSubject: subject,
  };

  const signature = await wallet.sign(stringify(vc)!);

  const proofType = {
    [types.KeyType.ED25519]: 'Ed25519Signature2018',
    [types.KeyType.SECP256K1]: 'Secp256k1Signature2019',
    [types.KeyType.ETHEREUM]: 'EthereumEip712Signature2021',
  }[typeInfo.pk!];

  const proof = {
    type: proofType,
    created: vc.issuanceDate,
    proofPurpose: 'assertionMethod',
    jws: toBase64(signature),
  };

  // @ts-ignore
  vc.proof = proof;

  const isValid = await verifyVC({
    vc: vc as any,
    ownerDid: wallet.address,
    trustedIssuers: wallet.address,
  });

  if (!isValid) {
    throw new Error('Failed to verify token VC, please try again');
  }

  return vc;
}

/**
 * Step 2: Publish VC (Verifiable Credential) for token via routing rule
 */
async function publishTokenVC(vc: any) {
  const { blocklet: blockletInfo } = await blockletService.getBlocklet();
  const site = blockletInfo?.site;

  if (!site?.id) {
    throw new Error('Blocklet site configuration missing');
  }

  const prefix = '/.well-known/ocap/tokens.json';
  const existingRule =
    (site.rules || []).find(
      (rule: any) => rule?.from?.pathPrefix === prefix || rule?.from?.pathPrefix === `${prefix}/`
    ) || null;

  let body: any[] = [];

  // parse data from existing rule
  if (existingRule) {
    const rawBody = existingRule.to?.response?.body || '[]';
    try {
      body = JSON.parse(rawBody);
      if (!Array.isArray(body)) {
        body = [];
      }
    } catch (err) {
      try {
        body = JSON.parse(rawBody.replace(/\\n/g, '').replace(/\\"/g, '"'));
      } catch (parseErr) {
        logger.warn('Failed to parse existing tokens routing body, fallback to empty array', {
          error: parseErr,
        });
        body = [];
      }
    }

    // replace rule if it already exists
    // create new rule if it doesn't exist
    const index = body.findIndex(
      (item: any) => item?.credentialSubject?.issued?.address === vc?.credentialSubject?.issued?.address
    );
    if (index > -1) {
      body[index] = vc;
    } else {
      body.push(vc);
    }
  } else {
    body = [vc];
  }

  const rule = {
    from: { pathPrefix: prefix },
    to: {
      type: 'direct_response',
      response: {
        body: JSON.stringify(body, null, 2),
        contentType: 'text/plain',
        status: 200,
      },
    },
  };

  const isUpdate = !!existingRule;
  const method = isUpdate ? 'updateRoutingRule' : 'addRoutingRule';

  logger.info('Calling routing rule API', {
    method,
    siteId: site.id,
    hasExistingRule: isUpdate,
  });

  try {
    let result;
    if (isUpdate) {
      result = await blockletService.updateRoutingRule({
        id: site.id,
        rule: {
          id: existingRule.id,
          ...rule,
        },
      });
    } else {
      result = await blockletService.addRoutingRule({
        id: site.id,
        rule,
      });
    }

    logger.info('Token VC published successfully', {
      method,
      prefix,
      rulesCount: result?.site?.rules?.length,
    });
  } catch (error) {
    logger.error(`Failed to call ${method} via SDK`, { error });
    throw error;
  }
}

export async function createToken(data: { name: string; symbol: string; decimal?: number; livemode?: boolean }) {
  const livemode = data.livemode ?? true;

  try {
    // Find the arcblock payment method for the current environment
    const paymentMethod = await PaymentMethod.findOne({
      where: {
        livemode: !!livemode,
        type: 'arcblock',
      },
    });
    if (!paymentMethod) {
      throw new Error('ArcBlock payment method not found');
    }

    const chainId = paymentMethod.settings.arcblock?.chain_id;
    if (!chainId) {
      throw new Error('Chain configuration incomplete');
    }

    const client = paymentMethod.getOcapClient();
    const { state: forgeState } = await client.getForgeState({});

    const factoryItx: any = {
      token: {
        name: data.name,
        symbol: data.symbol,
        description: `Token created by ${env.appName || 'Payment Kit'}`,
        website: env.appUrl || process.env.BLOCKLET_APP_HOST,
        icon: '',
        maxTotalSupply: null,
        decimal: data.decimal ?? 10,
        unit: 'A',
        type: 'CreditToken',
        spenders: [wallet.address],
        minters: [wallet.address],
        metadata: {
          type: 'json',
          value: {
            issuer: env.appName || 'Payment Kit',
          },
        },
      },
      feeRate: 0,
      reserveAddress: forgeState.token.address,
    };

    factoryItx.token.address = toTokenAddress(factoryItx.token, 'protobuf');
    factoryItx.address = toTokenFactoryAddress(factoryItx, 'protobuf');

    // Step 1: Create and publish VC
    logger.info('Creating and publishing token VC', { symbol: data.symbol });

    const vc = await createTokenVC({
      tokenAddress: factoryItx.token.address,
      symbol: data.symbol,
      website: env.appUrl || process.env.BLOCKLET_APP_HOST!,
      chainId,
    });

    await publishTokenVC(vc);
    // wait for 1 second to ensure the VC is published
    await sleep(1000);

    // Step 2: Stake ABT for token creation
    logger.info('Staking for token creation', { symbol: data.symbol });

    await stakeForTokenCreation(data.symbol, client);

    // Step 3: Create token factory on chain
    logger.info('Step 3: Creating token factory transaction', { symbol: data.symbol });

    const hash = await client.sendCreateTokenFactoryTx({
      tx: {
        itx: factoryItx,
      },
      wallet,
    });
    const { state: tokenFactoryState } = await client.getTokenFactoryState({ address: factoryItx.address });

    logger.info('Token created successfully', { hash, ...tokenFactoryState });

    return tokenFactoryState;
  } catch (err) {
    logger.error('Failed to create token', { data, error: err });
    throw err;
  }
}

export async function getTokenContext(paymentCurrency: TPaymentCurrency) {
  const tokenConfig = paymentCurrency.token_config;

  if (!tokenConfig?.address) {
    throw new Error(`Payment currency ${paymentCurrency.id} is missing token_config`);
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);

  if (paymentMethod?.type !== 'arcblock') {
    throw new Error(`Payment method ${paymentMethod?.id} must be ArcBlock`);
  }

  return {
    tokenConfig,
    client: paymentMethod.getOcapClient() as any,
  };
}

/**
 * Get the latest DID address for a customer, handling DID migration.
 */
export async function getAccountState(paymentCurrency: TPaymentCurrency, did?: string) {
  if (!did) return null;

  try {
    const { client } = await getTokenContext(paymentCurrency);
    const { state } = await client.getAccountState({ address: did, traceMigration: true });
    return state;
  } catch (error) {
    logger.error('Failed to get account state', { did, error });
    return null;
  }
}

export async function mintToken({
  paymentCurrency,
  amount,
  receiver,
  creditGrantId,
}: {
  paymentCurrency: TPaymentCurrency;
  amount: string;
  receiver: string;
  creditGrantId?: string;
}): Promise<string> {
  const { tokenConfig, client } = await getTokenContext(paymentCurrency);

  logger.info('Minting credit token', {
    creditGrantId,
    paymentCurrencyId: paymentCurrency.id,
    tokenFactory: tokenConfig.token_factory_address,
    amount,
    receiver,
    tokenConfig,
  });

  const hash = await (client as any).mintToken({
    wallet,
    tokenFactory: tokenConfig.token_factory_address,
    amount,
    receiver,
    data: {
      typeUrl: 'json',
      value: {
        creditGrantId,
      },
    },
  });

  logger.info('Successfully minted credit token', {
    creditGrantId,
    tokenFactory: tokenConfig.token_factory_address,
    hash,
  });

  return hash;
}

export async function burnToken({
  paymentCurrency,
  amount,
  sender,
  data,
}: {
  paymentCurrency: TPaymentCurrency;
  amount: string | number;
  sender: string;
  data?: any;
}): Promise<string> {
  const { tokenConfig, client } = await getTokenContext(paymentCurrency);

  logger.info('Burning on-chain credit token via transfer then burn', {
    paymentCurrencyId: paymentCurrency.id,
    tokenFactory: tokenConfig.token_factory_address,
    amount,
    sender,
  });

  // Step 1: Transfer tokens from sender to owner using multi-signature
  logger.info('Step 1: Transferring tokens from sender to owner', {
    from: sender,
    to: wallet.address,
    amount,
  });

  const transferHash = await transferTokenFromCustomer({
    paymentCurrency,
    customerDid: sender,
    amount: fromTokenToUnit(amount.toString(), paymentCurrency.decimal).toString(),
    data: {
      reason: 'burn_expired_credit',
      ...(data || {}),
    },
    checkBalance: false, // Don't check balance as we're burning expired tokens
  });

  if (!transferHash) {
    throw new Error('Failed to transfer tokens from customer');
  }

  logger.info('Step 1 completed: Tokens transferred to owner', {
    transferHash,
    from: sender,
    to: wallet.address,
    amount,
  });

  // Step 2: Burn tokens from owner wallet
  logger.info('Step 2: Burning tokens from owner wallet', {
    amount,
    tokenFactory: tokenConfig.token_factory_address,
    transferHash,
  });

  const burnHash = await client.burnToken({
    tokenFactory: tokenConfig.token_factory_address,
    wallet,
    amount,
    receiver: wallet.address,
    data: {
      typeUrl: 'json',
      value: {
        reason: 'burn_expired_credit',
        transferHash,
        ...(data || {}),
      },
    },
  });

  logger.info('Step 2 completed: Tokens burned successfully', {
    paymentCurrencyId: paymentCurrency.id,
    burnHash,
    transferHash,
    tokenFactory: tokenConfig.token_factory_address,
    originalSender: sender,
  });

  return burnHash;
}

/**
 * Get customer's on-chain token balance
 */
export async function getCustomerTokenBalance(customerDid: string, paymentCurrency: TPaymentCurrency): Promise<string> {
  const { client, tokenConfig } = await getTokenContext(paymentCurrency);

  const { tokens } = await client.getAccountTokens({
    address: customerDid,
    token: tokenConfig.address,
  });
  const [token] = tokens;

  return token?.balance || '0';
}

/**
 * Transfer token from user wallet to system wallet
 * Uses multi-signature mechanism where system initiates and user authorizes
 * This is used for credit consumption, expired token burn, etc.
 */
export async function transferTokenFromCustomer({
  paymentCurrency,
  customerDid,
  amount,
  data,
  checkBalance = true,
}: {
  paymentCurrency: TPaymentCurrency;
  customerDid: string;
  amount: string;
  data: any; // Transaction data (reason, metadata, etc)
  checkBalance?: boolean; // Whether to check balance before transfer
}) {
  // Only process for on-chain credit
  if (!isOnchainCredit(paymentCurrency)) {
    return null;
  }

  const { client, tokenConfig } = await getTokenContext(paymentCurrency);

  // Optional balance check
  if (checkBalance) {
    const balance = await getCustomerTokenBalance(customerDid, paymentCurrency);

    if (!balance || new BN(balance).lt(new BN(amount))) {
      logger.error('User does not have enough token balance', {
        from: customerDid,
        tokenAddress: tokenConfig.address,
        required: amount,
        actual: balance || '0',
        data,
      });
      throw new Error('INSUFFICIENT_TOKEN_BALANCE');
    }
  }

  logger.info('Transferring tokens from customer wallet', {
    amount,
    from: customerDid,
    to: wallet.address,
    tokenAddress: tokenConfig.address,
    data,
  });

  // transfer tokens from user to system owner via multi-signature
  const signed = await client.signTransferV3Tx({
    tx: {
      itx: {
        inputs: [
          {
            owner: customerDid,
            tokens: [{ address: tokenConfig.address, value: amount }],
          },
        ],
        outputs: [
          {
            owner: wallet.address,
            tokens: [{ address: tokenConfig.address, value: amount }],
          },
        ],
        data: {
          typeUrl: 'json',
          // @ts-ignore
          value: data,
        },
      },
      signatures: [
        {
          signer: customerDid,
          pk: '',
          signature: '',
        },
      ],
    },
    wallet,
  });

  // @ts-ignore
  const { buffer } = await client.encodeTransferV3Tx({ tx: signed });
  // @ts-ignore
  const txHash = await client.sendTransferV3Tx({ tx: signed, wallet }, await getGasPayerExtra(buffer));

  logger.info('Token transferred from customer wallet', {
    txHash,
    amount,
    from: customerDid,
    tokenAddress: tokenConfig.address,
    data,
  });

  return txHash;
}
