import { getUrl } from '@blocklet/sdk';
import { literal, Op } from 'sequelize';
import { events } from '../libs/event';
import logger from '../libs/logger';
import createQueue from '../libs/queue';
import { uploadBillingInfo, BillingInfo, getEndpointAndSpaceDid } from '../libs/did-space';
import {
  Customer,
  EVMChainType,
  Invoice,
  Job,
  PaymentCurrency,
  PaymentIntent,
  PaymentMethod,
  PaymentMethodSettings,
  Refund,
  Subscription,
  TInvoiceExpanded,
  TRefundExpanded,
} from '../store/models';
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
import env from '../libs/env';
import dayjs from '../libs/dayjs';
import { getExplorerTxUrl } from '../libs/util';

// Types
export type SpaceUploadType = 'invoice' | 'refund' | 'customer' | 'customerInvoiceChunk' | 'customerRefundChunk';
export type SpaceUploadData = {
  invoice: { id: string };
  refund: { id: string };
  customer: { id: string };
  customerInvoiceChunk: {
    customerId: string;
    chunkIndex: number;
    totalChunks: number;
    ids: string[];
    processTime: number;
  };
  customerRefundChunk: {
    customerId: string;
    chunkIndex: number;
    totalChunks: number;
    ids: string[];
    processTime: number;
  };
};

export type SpaceUploadJob = {
  type: SpaceUploadType;
  data: SpaceUploadData[SpaceUploadType];
};

// Utility functions
const categoryMap = {
  stake: 'stake',
  slash_stake: 'slashStake',
  overdraft_protection: 'fee',
  stake_overdraft_protection: 'stake',
  recharge: 'recharge',
  return_stake: 'returnStake',
  auto_recharge: 'recharge',
} as const;

const getBillingCategory = (billingReason: string): string => {
  if (
    billingReason.includes('stake') ||
    billingReason.includes('recharge') ||
    billingReason === 'overdraft_protection'
  ) {
    return categoryMap[billingReason as keyof typeof categoryMap] || 'payment';
  }
  return 'payment';
};

const createSubscriptionInfo = (subscription: Subscription) => ({
  type: 'subscription' as const,
  id: subscription.id,
  name: subscription.description || subscription.id,
  status: subscription.status,
  period_start: subscription.current_period_start,
  period_end: subscription.current_period_end,
  link: getUrl(`customer/subscription/${subscription.id}`),
});

const handleInvoicePaid = async (invoiceId: string) => {
  const invoice = (await Invoice.findByPk(invoiceId, {
    include: [
      {
        model: Customer,
        as: 'customer',
        attributes: ['did'],
      },
      {
        model: PaymentCurrency,
        as: 'paymentCurrency',
        attributes: ['symbol', 'decimal'],
      },
      {
        model: PaymentMethod,
        as: 'paymentMethod',
        attributes: ['type', 'settings'],
      },
      {
        model: Subscription,
        as: 'subscription',
        attributes: ['id', 'description', 'status', 'current_period_start', 'current_period_end'],
      },
      {
        model: PaymentIntent,
        as: 'paymentIntent',
      },
    ],
  })) as TInvoiceExpanded | null;

  // Validation
  if (!invoice) {
    logger.info('Upload invoice skipped because invoice not found:', { invoiceId });
    return;
  }

  if (invoice.metadata?.did_space_uploaded) {
    logger.info('Upload invoice skipped because invoice already uploaded:', { invoiceId });
    return;
  }

  const paymentDetails = invoice.paymentIntent?.payment_details || invoice.metadata?.payment_details;
  if (!paymentDetails) {
    logger.info('Upload invoice skipped because payment details not found:', { invoiceId });
    return;
  }

  if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(invoice?.paymentMethod?.type)) {
    logger.info('Upload invoice skipped because payment method is not supported:', {
      invoiceId,
      paymentMethod: invoice?.paymentMethod?.type,
    });
    return;
  }

  const txHash = paymentDetails[invoice.paymentMethod?.type].tx_hash;
  if (!txHash) {
    logger.info('Upload invoice skipped because tx hash not found:', { invoiceId });
    return;
  }

  let spaceDid = null;
  let endpoint = null;
  try {
    const result = await getEndpointAndSpaceDid(invoice.customer?.did);
    spaceDid = result.spaceDid;
    endpoint = result.endpoint;
  } catch (error) {
    logger.info('Customer space endpoint not available:', {
      invoiceId,
      did: invoice.customer?.did,
      error: (error as Error).message,
    });
    return;
  }

  const methodInfo = (invoice.paymentMethod?.settings as PaymentMethodSettings)?.[
    invoice.paymentMethod?.type as 'arcblock' | EVMChainType
  ];
  // Create billing info
  const billInfo: BillingInfo = {
    tx_hash: txHash,
    timestamp: dayjs(invoice.updated_at).unix(),
    invoice_id: invoice.id,
    category: getBillingCategory(invoice.billing_reason),
    description: invoice.description || 'Payment received',
    amount: invoice.amount_paid,
    currency: {
      symbol: invoice.paymentCurrency?.symbol,
      decimal: invoice.paymentCurrency?.decimal || 18,
      type: invoice.paymentMethod?.type || '',
      chain_id: methodInfo?.chain_id || '',
      explorer_host: methodInfo?.explorer_host || '',
      explorer_tx_url: methodInfo?.explorer_host
        ? getExplorerTxUrl({
            explorerHost: methodInfo?.explorer_host,
            txHash,
            type: invoice.paymentMethod?.type || '',
          })
        : '',
    },
    customer_did: invoice.customer?.did,
    link: getUrl(`customer/invoice/${invoice.id}`),
    app_pid: env.appPid,
  };

  if (invoice.subscription_id && invoice.subscription) {
    billInfo.related = createSubscriptionInfo(invoice.subscription as Subscription);
  }

  // Upload billing info
  const result = await uploadBillingInfo(invoice.customer?.did, billInfo, endpoint);
  if (result) {
    // @ts-ignore
    await invoice.update({
      metadata: {
        ...invoice.metadata,
        did_space_uploaded: true,
        did_space: spaceDid,
      },
    });
    logger.info('Successfully uploaded paid invoice:', {
      invoiceId: invoice.id,
      txHash,
      customerDid: invoice.customer?.did,
    });
  } else {
    logger.error('Failed to upload paid invoice:', {
      invoiceId: invoice.id,
      txHash,
      customerDid: invoice.customer?.did,
    });
  }
};

const handleRefundPaid = async (refundId: string) => {
  const refund = (await Refund.findByPk(refundId, {
    include: [
      { model: Customer, as: 'customer', attributes: ['did'] },
      {
        model: PaymentMethod,
        as: 'paymentMethod',
        attributes: ['type', 'settings'],
      },
      {
        model: PaymentIntent,
        as: 'paymentIntent',
      },
      {
        model: PaymentCurrency,
        as: 'paymentCurrency',
        attributes: ['symbol', 'decimal'],
      },
    ],
  })) as TRefundExpanded | null;
  if (!refund) {
    logger.info('Upload refund skipped because refund not found:', { refundId });
    return;
  }

  if (refund.metadata?.did_space_uploaded) {
    logger.info('Upload refund skipped because refund already uploaded:', { refundId });
    return;
  }

  if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(refund.paymentMethod?.type)) {
    logger.info('Upload refund skipped because payment method is not supported:', {
      refundId,
      paymentMethod: refund.paymentMethod?.type,
    });
    return;
  }
  // @ts-ignore
  const txHash = refund?.payment_details?.[refund.paymentMethod?.type]?.tx_hash;
  if (!txHash) {
    logger.info('Upload refund skipped because tx hash not found:', { refundId });
    return;
  }

  let spaceDid = null;
  let endpoint = null;
  try {
    const result = await getEndpointAndSpaceDid(refund.customer?.did);
    spaceDid = result.spaceDid;
    endpoint = result.endpoint;
  } catch (error) {
    logger.info('Customer space endpoint not available:', {
      refundId,
      did: refund.customer?.did,
      error: (error as Error).message,
    });
    return;
  }

  let invoice = null;
  if (refund.invoice_id) {
    invoice = await Invoice.findByPk(refund.invoice_id, {
      include: [{ model: Customer, as: 'customer', attributes: ['did'] }],
    });
  }
  const methodInfo = (refund.paymentMethod?.settings as PaymentMethodSettings)?.[
    refund.paymentMethod?.type as 'arcblock' | EVMChainType
  ];
  const billInfo: BillingInfo = {
    tx_hash: txHash,
    timestamp: dayjs(refund.updated_at).unix(),
    invoice_id: refund.id,
    category: 'refund',
    description: refund.description || 'Refund',
    amount: refund.amount,
    currency: {
      symbol: refund.paymentCurrency?.symbol,
      decimal: refund.paymentCurrency?.decimal || 18,
      type: refund.paymentMethod?.type || '',
      chain_id: methodInfo?.chain_id || '',
      explorer_host: methodInfo?.explorer_host || '',
      explorer_tx_url: methodInfo?.explorer_host
        ? getExplorerTxUrl({
            explorerHost: methodInfo?.explorer_host,
            txHash,
            type: refund.paymentMethod?.type || '',
          })
        : '',
    },
    customer_did: refund.customer?.did,
    link: getUrl(`customer/invoice/${refund.invoice_id}`),
    app_pid: env.appPid,
  };
  if (invoice) {
    billInfo.related = {
      type: 'invoice',
      id: invoice.id,
      name: invoice.description || invoice.id,
      link: getUrl(`customer/invoice/${invoice.id}`),
    };
  }

  const result = await uploadBillingInfo(refund.customer?.did, billInfo, endpoint);
  if (result) {
    // @ts-ignore
    await refund.update({
      metadata: {
        ...refund.metadata,
        did_space_uploaded: true,
        did_space: spaceDid,
      },
    });
    logger.info('Successfully uploaded paid refund:', {
      refundId: refund.id,
      txHash,
      customerDid: refund.customer?.did,
    });
  } else {
    logger.error('Failed to upload paid refund:', { refundId: refund.id, txHash, customerDid: refund.customer?.did });
  }
};

/**
 * create batch tasks for invoice or refund
 * @param type 'invoice' | 'refund'
 * @param customerId customer id
 * @param records records list
 * @param now current timestamp
 * @returns number of new tasks created
 */
const createBatchTasks = (
  type: 'invoice' | 'refund',
  customerId: string,
  records: { id: string; created_at?: Date }[],
  now: number
): { tasksCreated: number; totalRecords: number } => {
  if (records.length === 0) {
    return { tasksCreated: 0, totalRecords: 0 };
  }

  const recordIds = records.map((record) => record.id);
  const recordLength = recordIds.length;
  const batchSize = 30;
  const chunks = Math.ceil(recordLength / batchSize);
  let tasksCreated = 0;

  for (let i = 0; i < chunks; i++) {
    const start = i * batchSize;
    const end = Math.min(start + batchSize, recordLength);
    const chunkRecordIds = recordIds.slice(start, end);
    const processTime = new Date(records?.[start]?.created_at || now).getTime();
    const chunkId = `space-${customerId}-${type}-chunk-${i}-${processTime}`;

    spaceQueue.push({
      id: chunkId,
      job: {
        type: type === 'invoice' ? 'customerInvoiceChunk' : 'customerRefundChunk',
        data: {
          customerId,
          chunkIndex: i,
          totalChunks: chunks,
          ids: chunkRecordIds,
          processTime,
        },
      },
      delay: 60 * (i + 1), // add 1 minute delay for each chunk
    });

    tasksCreated++;
  }

  logger.info(`Created ${type} chunk tasks`, {
    customerId,
    chunks,
    totalRecords: recordLength,
  });

  return {
    tasksCreated,
    totalRecords: recordLength,
  };
};

const syncCustomerBillingToSpace = async (customerId: string) => {
  try {
    const now = Date.now();
    const customer = await Customer.findByPkOrDid(customerId);
    if (!customer) {
      logger.info('Customer not found:', { customerId });
      return;
    }

    try {
      await getEndpointAndSpaceDid(customer.did);
    } catch (error) {
      logger.info('Customer space endpoint not available:', {
        customerId,
        did: customer.did,
        error: (error as Error).message,
      });
      return;
    }

    const validMethods = await PaymentMethod.findAll({
      where: {
        type: { [Op.in]: CHARGE_SUPPORTED_CHAIN_TYPES },
      },
      attributes: ['id'],
    });

    if (!validMethods.length) {
      logger.info('No valid payment methods found', { customerId });
      return;
    }

    const validMethodIds = validMethods.map((method) => method.id);

    let invoiceProcessTime = 0;
    let refundProcessTime = 0;
    try {
      // get the latest process time from the queue
      const [invoiceProcessQueue, refundProcessQueue] = await Promise.all([
        Job.findOne({
          where: {
            queue: 'did-space',
            id: { [Op.like]: `space-${customerId}-invoice-chunk-%` },
            cancelled: false,
          },
          order: [['created_at', 'DESC']],
        }),
        Job.findOne({
          where: {
            queue: 'did-space',
            id: { [Op.like]: `space-${customerId}-refund-chunk-%` },
            cancelled: false,
          },
          order: [['created_at', 'DESC']],
        }),
      ]);

      invoiceProcessTime = invoiceProcessQueue?.job?.data?.processTime ?? 0;
      refundProcessTime = refundProcessQueue?.job?.data?.processTime ?? 0;
      logger.info('Invoice and refund process time', {
        customerId,
        invoiceProcessTime,
        refundProcessTime,
      });
    } catch (error) {
      logger.error('Failed to get invoice and refund process time', { customerId, error });
    }

    let [invoices, refunds] = await Promise.all([
      Invoice.findAll({
        where: {
          status: 'paid',
          'metadata.did_space_uploaded': { [Op.not]: true },
          customer_id: customer.id,
          default_payment_method_id: { [Op.in]: validMethodIds },
          // only process invoices after the latest process time
          created_at: { [Op.gte]: invoiceProcessTime },
        },
        attributes: ['id', 'metadata', 'payment_intent_id', 'created_at'],
        order: [['created_at', 'DESC']],
      }),
      Refund.findAll({
        where: {
          status: 'succeeded',
          'metadata.did_space_uploaded': { [Op.not]: true },
          customer_id: customer.id,
          payment_method_id: { [Op.in]: validMethodIds },
          [Op.and]: [literal('payment_details IS NOT NULL')],
          // only process refunds after the latest process time
          created_at: { [Op.gte]: refundProcessTime },
        },
        attributes: ['id', 'metadata', 'payment_details', 'created_at'],
        order: [['created_at', 'DESC']],
      }),
    ]);

    invoices = invoices.filter((x) => x.metadata?.payment_details || x.payment_intent_id);
    refunds = refunds.filter((x) => x.payment_details);

    const invoicesLength = invoices.length;
    const refundsLength = refunds.length;
    logger.info('Found records to upload:', {
      customerId,
      did: customer.did,
      invoiceCount: invoicesLength,
      refundCount: refundsLength,
    });

    if (invoicesLength === 0 && refundsLength === 0) {
      logger.info('No records to process', { customerId });
      return;
    }

    const invoiceChunks = createBatchTasks('invoice', customerId, invoices, now);

    const refundChunks = createBatchTasks('refund', customerId, refunds, now);

    logger.info('Completed creating all chunk tasks', {
      customerId,
      invoiceChunks: invoiceChunks.tasksCreated,
      refundChunks: refundChunks.tasksCreated,
      totalRecords: invoicesLength + refundsLength,
    });
  } catch (error) {
    logger.error('Failed to create chunk tasks:', {
      customerId,
      error: error instanceof Error ? error.message : String(error),
    });
  }
};

const handleCustomerInvoiceChunk = async (data: SpaceUploadData['customerInvoiceChunk']) => {
  const { customerId, chunkIndex, totalChunks, ids: invoiceIds } = data;

  logger.info('Processing invoice chunk', {
    customerId,
    chunkIndex: chunkIndex + 1,
    totalChunks,
    invoiceCount: invoiceIds.length,
  });

  try {
    await Promise.all(
      invoiceIds.map((invoiceId) =>
        handleInvoicePaid(invoiceId).catch((error) => {
          logger.error('Failed to process invoice:', {
            customerId,
            invoiceId,
            error: error.message,
          });
        })
      )
    );

    logger.info('Completed invoice chunk processing', {
      customerId,
      chunkIndex: chunkIndex + 1,
      totalChunks,
      processedInvoices: invoiceIds.length,
    });
  } catch (error) {
    logger.error('Invoice chunk processing failed:', {
      customerId,
      chunkIndex,
      error: error instanceof Error ? error.message : String(error),
    });
  }
};

const handleCustomerRefundChunk = async (data: SpaceUploadData['customerRefundChunk']) => {
  const { customerId, chunkIndex, totalChunks, ids: refundIds } = data;

  logger.info('Processing refund chunk', {
    customerId,
    chunkIndex: chunkIndex + 1,
    totalChunks,
    refundCount: refundIds.length,
  });

  try {
    await Promise.all(
      refundIds.map((refundId) =>
        handleRefundPaid(refundId).catch((error) => {
          logger.error('Failed to process refund:', {
            customerId,
            refundId,
            error: error.message,
          });
        })
      )
    );

    logger.info('Completed refund chunk processing', {
      customerId,
      chunkIndex: chunkIndex + 1,
      totalChunks,
      processedRefunds: refundIds.length,
    });
  } catch (error) {
    logger.error('Refund chunk processing failed:', {
      customerId,
      chunkIndex,
      error: error instanceof Error ? error.message : String(error),
    });
  }
};

const handlers = {
  invoice: (data: SpaceUploadData['invoice']) => handleInvoicePaid(data.id),
  refund: (data: SpaceUploadData['refund']) => handleRefundPaid(data.id),
  customer: (data: SpaceUploadData['customer']) => syncCustomerBillingToSpace(data.id),
  customerInvoiceChunk: (data: SpaceUploadData['customerInvoiceChunk']) => handleCustomerInvoiceChunk(data),
  customerRefundChunk: (data: SpaceUploadData['customerRefundChunk']) => handleCustomerRefundChunk(data),
};

export const handleSpaceUpload = async (job: SpaceUploadJob) => {
  logger.info('Starting to handle space upload', job);
  const handler = handlers[job.type];
  if (!handler) {
    logger.error('No handler found for job type', { job });
    return;
  }
  // @ts-ignore
  await handler(job.data);
};

export const spaceQueue = createQueue<SpaceUploadJob>({
  name: 'did-space',
  onJob: handleSpaceUpload,
  options: {
    concurrency: 5,
    maxRetries: 3,
    enableScheduledJob: true,
  },
});

spaceQueue.on('failed', ({ id, job, error }) => {
  logger.error('Space upload job failed', { id, job, error });
});

export const startUploadBillingInfoListener = () => {
  events.on('invoice.paid', (invoice) => {
    spaceQueue.push({
      id: `space-${invoice.id}`,
      job: {
        type: 'invoice',
        data: { id: invoice.id },
      },
    });
  });

  events.on('refund.succeeded', (refund) => {
    spaceQueue.push({
      id: `space-${refund.id}`,
      job: { type: 'refund', data: { id: refund.id } },
    });
  });

  logger.info('Space upload listeners started');
};
