import { getUrl } from '@blocklet/sdk/lib/component';
import { fromTokenToUnit } from '@ocap/util';
import { ethers } from 'ethers';
import { Router } from 'express';
import pick from 'lodash/pick';
import { InferAttributes, Op, WhereOptions } from 'sequelize';
import cloneDeep from 'lodash/cloneDeep';
import merge from 'lodash/merge';
import Joi from 'joi';
import { ensureWebhookRegistered, cleanupStripeWebhook, validateStripeKeys } from '../integrations/stripe/setup';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { PaymentCurrency } from '../store/models/payment-currency';
import { PaymentMethod, TPaymentMethod } from '../store/models/payment-method';
import type { EVMChainType, PaymentMethodSettings } from '../store/models/types';
import { ethWallet, wallet } from '../libs/auth';
import { EVM_CHAIN_TYPES } from '../libs/constants';
import { getTokenSummaryByDid } from '../integrations/arcblock/stake';

const router = Router();

const auth = authenticate<PaymentMethod>({ component: true, roles: ['owner', 'admin'] });

const paymentMethodCreateSchema = Joi.object({
  name: Joi.string().max(32).required(),
  description: Joi.string().max(255).required(),
}).unknown(true);

router.post('/', auth, async (req, res) => {
  const raw: Partial<TPaymentMethod> = req.body;

  raw.livemode = req.livemode;
  raw.locked = false;
  raw.active = true;
  const { error } = paymentMethodCreateSchema.validate(pick(req.body, ['name', 'description']));
  if (error) {
    return res.status(400).json({ error: error.message });
  }

  if (!raw.name) {
    return res.status(400).json({ error: 'payment method name is required' });
  }
  if (!raw.description) {
    return res.status(400).json({ error: 'payment method description is required' });
  }

  if (!raw.settings) {
    return res.status(400).json({ error: 'payment method settings is required' });
  }

  if (raw.type === 'stripe') {
    const exist = await PaymentMethod.findOne({ where: { type: 'stripe', livemode: raw.livemode } });
    if (exist) {
      return res.status(400).json({ error: 'stripe payment method already exist' });
    }

    if (!raw.settings.stripe?.publishable_key) {
      return res.status(400).json({ error: 'stripe publishable key is required' });
    }
    if (!raw.settings.stripe?.secret_key) {
      return res.status(400).json({ error: 'stripe secret key is required' });
    }
    const isValid = await validateStripeKeys(raw.settings.stripe.secret_key);
    if (!isValid) {
      return res.status(400).json({ error: 'Invalid Stripe API keys' });
    }

    raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['stripe']) as PaymentMethodSettings;
    raw.logo = getUrl('/methods/stripe.png');
    raw.features = {
      recurring: true,
      refund: true,
      dispute: true,
    };
    raw.confirmation = {
      type: 'callback',
    };

    const method = await PaymentMethod.create(raw as TPaymentMethod);

    // create default currency
    // FIXME: make this configurable
    const currency = await PaymentCurrency.create({
      livemode: method.livemode,
      active: method.active,
      locked: true,
      is_base_currency: false,
      payment_method_id: method.id,
      type: 'standard',

      name: 'Dollar',
      description: 'US Dollar',
      logo: getUrl('/currencies/dollar.png'),
      symbol: 'USD', // same currency code as stripe
      decimal: 2,
      maximum_precision: 2,

      minimum_payment_amount: '1', // cent
      maximum_payment_amount: '100000000000', // billion

      contract: '',
      metadata: {},
    });

    await method.update({ default_currency_id: currency.id });

    ensureWebhookRegistered().catch(console.error);

    return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
  }

  if (EVM_CHAIN_TYPES.includes(raw.type as string)) {
    const paymentType = raw.type as EVMChainType;
    if (!raw.settings[paymentType]?.api_host) {
      return res.status(400).json({ error: `${paymentType} api_host is required` });
    }
    if (!raw.settings[paymentType]?.explorer_host) {
      return res.status(400).json({ error: `${paymentType} explorer_host is required` });
    }
    if (!raw.settings[paymentType]?.native_symbol) {
      return res.status(400).json({ error: `${paymentType} native_symbol is required` });
    }

    try {
      const provider = new ethers.JsonRpcProvider(raw.settings[paymentType]?.api_host);
      const [network, blockNumber] = await Promise.all([provider.getNetwork(), provider.getBlockNumber()]);
      raw.settings[paymentType]!.chain_id = network.chainId.toString();
      logger.info(`${paymentType} api endpoint verified`, {
        settings: raw.settings[paymentType],
        network,
        blockNumber,
      });
    } catch (err) {
      logger.error(`verify ${paymentType} api endpoint failed`, err);
      return res.status(400).json({ error: err.message });
    }

    const exist = await PaymentMethod.findOne({
      where: { type: paymentType, [`settings.${paymentType}.chain_id`]: raw.settings[paymentType]?.chain_id },
    });
    if (exist) {
      return res.status(400).json({ error: `${paymentType} payment method already exist` });
    }

    raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), [paymentType]) as PaymentMethodSettings;
    raw.logo = raw.logo || getUrl(`/methods/${paymentType}.png`);
    raw.features = {
      recurring: true,
      refund: true,
      dispute: false,
    };
    raw.confirmation = {
      type: 'block',
      block: raw.settings[paymentType]?.confirmation || 1,
    };

    const method = await PaymentMethod.create(raw as TPaymentMethod);

    // create currency for native currency
    const symbol = raw.settings[paymentType]?.native_symbol as string;
    const currency = await PaymentCurrency.create({
      livemode: method.livemode,
      active: method.active,
      locked: true,
      is_base_currency: false,
      payment_method_id: method.id,
      type: 'standard',

      name: symbol,
      description: symbol,
      logo: raw.logo,
      symbol,
      decimal: 18,

      minimum_payment_amount: fromTokenToUnit(0.000001, 18).toString(),
      maximum_precision: 6,
      maximum_payment_amount: fromTokenToUnit(100000000, 18).toString(),

      contract: '',
      metadata: {},
    });
    await method.update({ default_currency_id: currency.id });
    return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
  }

  if (raw.type === 'google_play') {
    if (!raw.settings.google_play?.package_name) {
      return res.status(400).json({ error: 'google_play package_name is required' });
    }
    if (!raw.settings.google_play?.service_account_json) {
      return res.status(400).json({ error: 'google_play service_account_json is required' });
    }
    try {
      const parsed = JSON.parse(raw.settings.google_play.service_account_json);
      if (!parsed.client_email || !parsed.private_key) {
        return res.status(400).json({ error: 'service_account_json missing client_email or private_key' });
      }
    } catch {
      return res.status(400).json({ error: 'service_account_json is not valid JSON' });
    }

    const exist = await PaymentMethod.findOne({
      where: { type: 'google_play', livemode: raw.livemode },
    });
    if (exist) {
      return res.status(400).json({ error: 'google_play payment method already exists for this livemode' });
    }

    raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['google_play']) as PaymentMethodSettings;
    raw.logo = raw.logo || getUrl('/methods/google-play.png');
    raw.features = { recurring: true, refund: true, dispute: false };
    raw.confirmation = { type: 'callback' };

    const method = await PaymentMethod.create(raw as TPaymentMethod);

    // Create a default USD currency for the PaymentMethod (mirrors stripe path).
    const currency = await PaymentCurrency.create({
      livemode: method.livemode,
      active: method.active,
      locked: true,
      is_base_currency: false,
      payment_method_id: method.id,
      type: 'standard',
      name: 'Dollar',
      description: 'US Dollar (Google Play)',
      logo: getUrl('/currencies/dollar.png'),
      symbol: 'USD',
      decimal: 2,
      maximum_precision: 2,
      minimum_payment_amount: '1',
      maximum_payment_amount: '100000000000',
      contract: '',
      metadata: {},
    });
    await method.update({ default_currency_id: currency.id });

    return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
  }

  if (raw.type === 'app_store') {
    if (!raw.settings.app_store?.bundle_id) {
      return res.status(400).json({ error: 'app_store bundle_id is required' });
    }
    const env = raw.settings.app_store?.environment;
    if (env !== 'production' && env !== 'sandbox') {
      return res.status(400).json({ error: 'app_store environment must be production or sandbox' });
    }
    // Server API credentials are optional — StoreKit 2 JWS verify doesn't need them.
    // But if any of the three is set, all three must be set together.
    const hasAnyServerCred = !!(
      raw.settings.app_store?.issuer_id ||
      raw.settings.app_store?.key_id ||
      raw.settings.app_store?.private_key_pem
    );
    if (hasAnyServerCred) {
      if (
        !raw.settings.app_store?.issuer_id ||
        !raw.settings.app_store?.key_id ||
        !raw.settings.app_store?.private_key_pem
      ) {
        return res.status(400).json({
          error: 'app_store Server API credentials must include all of issuer_id, key_id, private_key_pem',
        });
      }
    }

    const exist = await PaymentMethod.findOne({
      where: { type: 'app_store', livemode: raw.livemode },
    });
    if (exist) {
      return res.status(400).json({ error: 'app_store payment method already exists for this livemode' });
    }

    raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['app_store']) as PaymentMethodSettings;
    raw.logo = raw.logo || getUrl('/methods/app-store.png');
    raw.features = { recurring: true, refund: false, dispute: false };
    raw.confirmation = { type: 'callback' };

    const method = await PaymentMethod.create(raw as TPaymentMethod);

    const currency = await PaymentCurrency.create({
      livemode: method.livemode,
      active: method.active,
      locked: true,
      is_base_currency: false,
      payment_method_id: method.id,
      type: 'standard',
      name: 'Dollar',
      description: 'US Dollar (App Store)',
      logo: getUrl('/currencies/dollar.png'),
      symbol: 'USD',
      decimal: 2,
      maximum_precision: 2,
      minimum_payment_amount: '1',
      maximum_payment_amount: '100000000000',
      contract: '',
      metadata: {},
    });
    await method.update({ default_currency_id: currency.id });

    return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
  }

  // FIXME: support bitcoin payment methods

  return res.status(400).json({ error: 'payment method type is not supported' });
});

router.get('/', auth, async (req, res) => {
  const { query } = req;
  const where: WhereOptions<InferAttributes<PaymentMethod>> = {};

  if (typeof query.active === 'string') {
    where.active = JSON.parse(query.active);
  }
  if (typeof query.livemode === 'string') {
    where.livemode = JSON.parse(query.livemode);
  }
  try {
    const list = await PaymentMethod.findAll({
      where,
      order: [['created_at', 'ASC']],
      include: [{ model: PaymentCurrency, as: 'payment_currencies', order: [['created_at', 'ASC']] }],
    });
    if (query.addresses === 'true') {
      try {
        const [arcblock, ethereum] = await Promise.all([
          getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
          getTokenSummaryByDid(ethWallet.address, !!req.livemode, EVM_CHAIN_TYPES),
        ]);
        res.json({
          list,
          addresses: { arcblock: wallet.address, ethereum: ethWallet.address },
          balances: { ...arcblock, ...ethereum },
        });
      } catch (err) {
        logger.error('get token summary failed', err.message);
        res.json({
          list,
          addresses: { arcblock: wallet.address, ethereum: ethWallet.address },
          balances: {},
          error: `get token summary failed: ${err.message}`,
        });
      }
    } else {
      res.json(list);
    }
  } catch (err) {
    logger.error('get payment methods failed', err);
    res.status(400).json({ error: err.message });
  }
});

router.get('/types', auth, (_, res) => {
  res.json([
    {
      type: 'arcblock',
      name: 'ArcBlock',
      description: 'Instant payment with ArcBlock chain',
      logo: getUrl('/methods/arcblock.png'),
    },
    {
      type: 'stripe',
      name: 'Stripe',
      description: 'Pay with credit card or bank account',
      logo: getUrl('/methods/stripe.png'),
    },
    {
      type: 'ethereum',
      name: 'Ethereum',
      description: 'Pay with ethereum compatible chains',
      logo: getUrl('/methods/ethereum.png'),
    },
    {
      type: 'base',
      name: 'Base',
      description: 'Pay with base compatible chains',
      logo: getUrl('/methods/base.png'),
    },
    {
      type: 'google_play',
      name: 'Google Play',
      description: 'Subscriptions purchased via Google Play in-app billing',
      logo: getUrl('/methods/google-play.png'),
    },
    {
      type: 'app_store',
      name: 'App Store',
      description: 'Subscriptions purchased via Apple App Store StoreKit',
      logo: getUrl('/methods/app-store.png'),
    },
  ]);
});

router.get('/:id', auth, async (req, res) => {
  const doc = await PaymentMethod.findOne({
    where: { [Op.or]: [{ id: req.params.id }, { name: req.params.id }] },
  });

  if (doc) {
    res.json(doc);
  } else {
    res.status(404).json(null);
  }
});

router.put('/:id/settings', auth, async (req, res) => {
  const { id } = req.params;
  const settings = req.body;
  try {
    const paymentMethod = await PaymentMethod.findOne({ where: { id } });
    if (!paymentMethod) {
      return res.status(400).json({ error: 'payment method not found' });
    }
    if (paymentMethod.type !== 'arcblock') {
      return res
        .status(400)
        .json({ error: `Updating settings is not supported for the ${paymentMethod.type} payment method` });
    }
    const updateSettings = merge(cloneDeep(paymentMethod.settings), settings);
    const doc = await paymentMethod.update({
      settings: updateSettings,
    });
    return res.json(doc);
  } catch (err) {
    return res.status(400).json({ error: err.message });
  }
});

const updateMethodSchema = Joi.object({
  name: Joi.string().empty('').max(32).optional(),
  description: Joi.string().empty('').max(255).optional(),
  logo: Joi.string().empty('').optional(),
}).unknown(true);
router.put('/:id', auth, async (req, res) => {
  const { id } = req.params;
  const { error, value: raw } = updateMethodSchema.validate(pick(req.body, ['name', 'description', 'logo']));
  if (error) {
    return res.status(400).json({ error: error.message });
  }

  try {
    const method = await PaymentMethod.findByPk(id);
    if (!method) {
      return res.status(404).json({ error: 'Payment method not found' });
    }
    const updateData: Partial<TPaymentMethod> = {
      name: raw.name ?? method.name,
      description: raw.description ?? method.description,
    };

    if ('logo' in method.dataValues || raw.logo !== undefined) {
      updateData.logo = raw.logo ?? method.logo;
    }
    let updateSettings = 'settings' in req.body ? req.body.settings : null;
    if (EVM_CHAIN_TYPES.includes(method.type as string) && updateSettings) {
      const paymentType = method.type as EVMChainType;
      if (!updateSettings[paymentType]?.api_host) {
        return res.status(400).json({ error: `${paymentType} api_host is required` });
      }
      if (!updateSettings[paymentType]?.explorer_host) {
        return res.status(400).json({ error: `${paymentType} explorer_host is required` });
      }
      if (!updateSettings[paymentType]?.native_symbol) {
        return res.status(400).json({ error: `${paymentType} native_symbol is required` });
      }
      try {
        const provider = new ethers.JsonRpcProvider(updateSettings[paymentType]?.api_host);
        const [network, blockNumber] = await Promise.all([provider.getNetwork(), provider.getBlockNumber()]);
        updateSettings[paymentType]!.chain_id = network.chainId.toString();
        logger.info(`${paymentType} api endpoint verified`, {
          settings: updateSettings[paymentType],
          network,
          blockNumber,
        });
      } catch (err) {
        logger.error(`verify ${paymentType} api endpoint failed`, err);
        return res.status(400).json({ error: err.message });
      }
      updateData.settings = pick(PaymentMethod.encryptSettings(updateSettings), [paymentType]) as PaymentMethodSettings;
    }
    if (method.type === 'stripe') {
      if (!updateSettings.stripe?.publishable_key) {
        return res.status(400).json({ error: 'stripe publishable key is required' });
      }
      if (!updateSettings.stripe?.secret_key) {
        return res.status(400).json({ error: 'stripe secret key is required' });
      }
      if (method.settings?.stripe?.secret_key !== updateSettings.stripe.secret_key) {
        const isValid = await validateStripeKeys(updateSettings.stripe.secret_key);
        if (!isValid) {
          return res.status(400).json({ error: 'Invalid Stripe API keys' });
        }
        updateSettings.stripe.webhook_signing_secret = null;
        updateSettings = PaymentMethod.encryptSettings(updateSettings);
        cleanupStripeWebhook(method.getStripeClient());
      }
      updateData.settings = pick(updateSettings, ['stripe']) as PaymentMethodSettings;
    }

    const updatedMethod = await method.update(updateData);
    if (method.type === 'stripe') {
      ensureWebhookRegistered().catch(console.error);
    }
    return res.json(updatedMethod);
  } catch (err) {
    return res.status(400).json({ error: err.message });
  }
});

export default router;
