import { Router } from 'express';
import Joi from 'joi';
import { Op, type WhereOptions } from 'sequelize';
import { joinURL } from 'ufo';

import { BN } from '@ocap/util';
import { getPaymentStat } from '../crons/payment-stat';
import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
import { createListParamSchema, getOrder } from '../libs/api';
import { ethWallet, wallet } from '../libs/auth';
import dayjs from '../libs/dayjs';
import { authenticate } from '../libs/security';
import {
  CreditGrant,
  EVMChainType,
  Invoice,
  InvoiceItem,
  PaymentCurrency,
  PaymentIntent,
  PaymentMethod,
  PaymentStat,
  Payout,
  Refund,
  RevenueSnapshot,
  Subscription,
} from '../store/models';
import { EVM_CHAIN_TYPES } from '../libs/constants';
import logger from '../libs/logger';

const router = Router();
const auth = authenticate<PaymentStat>({ component: true, roles: ['owner', 'admin'] });
const BILLING_REASON_EXCLUSIONS = ['stake', 'stake_overdraft_protection', 'recharge'];

const schema = createListParamSchema<{ currency_id?: string; start?: number; end?: number }>({
  currency_id: Joi.string().optional().empty(''),
  start: Joi.number().positive().optional().empty(''),
  end: Joi.number().positive().optional().empty(''),
});

const summaryQuerySchema = Joi.object({
  currency_id: Joi.string().optional().empty(''),
  start: Joi.number().positive().optional().empty(''),
  end: Joi.number().positive().optional().empty(''),
});
router.get('/', auth, async (req, res) => {
  const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
  const where: WhereOptions = {};

  if (typeof query.livemode === 'boolean') {
    where.livemode = query.livemode;
  }
  if (query.currency_id) {
    where.currency_id = query.currency_id;
  }

  const startTime = query.start || dayjs().subtract(30, 'days').startOf('day').unix();
  const endTime = query.end || dayjs().endOf('day').unix();
  const todayStart = dayjs().startOf('day').unix();
  const todayEnd = dayjs().endOf('day').unix();

  where.timestamp = {
    [Op.gte]: startTime,
    [Op.lte]: endTime,
  };

  try {
    const { rows: list, count } = await PaymentStat.findAndCountAll({
      where,
      order: getOrder(req.query, [['created_at', 'ASC']]),
      include: [],
    });

    const includesToday = startTime <= todayEnd && endTime >= todayStart;
    if (includesToday) {
      const { stats, timestamp, currencies } = await getPaymentStat(dayjs().toDate().toString());
      list.push(
        // @ts-ignore
        ...currencies
          .filter((x) => x.livemode === !!req.livemode)
          .map((currency) => ({
            livemode: currency.livemode,
            timestamp,
            currency_id: currency.id,
            amount_paid: stats.payment![currency.id] || '0',
            amount_payout: stats.payout![currency.id] || '0',
            amount_refund: stats.refund![currency.id] || '0',
          }))
      );
    }

    res.json({ count, list });
  } catch (err) {
    logger.error(err);
    res.json({ count: 0, list: [] });
  }
});

async function getCurrencyLinks(livemode: boolean) {
  const items = await PaymentCurrency.findAll({
    where: { livemode },
    include: [{ model: PaymentMethod, as: 'payment_method' }],
  });

  return items.reduce((acc, item: any) => {
    if (item.payment_method?.type === 'arcblock') {
      acc[item.id] = joinURL(
        item.payment_method.settings.arcblock?.explorer_host,
        'accounts',
        wallet.address,
        'tokens'
      );
    }
    if (EVM_CHAIN_TYPES.includes(item.payment_method?.type as string)) {
      acc[item.id] = joinURL(
        item.payment_method.settings[item.payment_method.type as EVMChainType]?.explorer_host,
        'address',
        ethWallet.address
      );
    }
    return acc;
  }, {} as any);
}

type RevenueStats = {
  totalRevenue: string;
  refundAmount: string;
  promotionCost: string;
  creditGrantCost: string;
  vendorCost: string;
  taxedRevenue: string;
  netRevenue: string;
};

const ZERO_REVENUE_STATS: RevenueStats = {
  totalRevenue: '0',
  refundAmount: '0',
  promotionCost: '0',
  creditGrantCost: '0',
  vendorCost: '0',
  taxedRevenue: '0',
  netRevenue: '0',
};

const buildCurrencyFilter = (currencyIds?: string[]) => {
  if (!currencyIds || currencyIds.length === 0) {
    return undefined;
  }
  if (currencyIds.length === 1) {
    return currencyIds[0];
  }
  return { [Op.in]: currencyIds };
};

const addToMap = (map: Record<string, string>, key: string | null | undefined, value: string | number) => {
  if (!key) {
    return;
  }
  const previous = map[key] || '0';
  map[key] = new BN(previous).add(new BN(value || '0')).toString();
};

const hasNonZeroValue = (values: string[]) => values.some((value) => !new BN(value || '0').isZero());

async function getTaxedInvoiceIds(
  livemode: boolean,
  currencyIds?: string[],
  start?: number,
  end?: number
): Promise<Set<string>> {
  const invoiceWhere: WhereOptions = {
    livemode,
    status: 'paid',
    billing_reason: { [Op.notIn]: BILLING_REASON_EXCLUSIONS },
  };

  const currencyFilter = buildCurrencyFilter(currencyIds);
  if (currencyFilter) {
    invoiceWhere.currency_id = currencyFilter;
  }

  if (start || end) {
    invoiceWhere.created_at = {};
    if (start) {
      invoiceWhere.created_at[Op.gte] = new Date(start * 1000);
    }
    if (end) {
      invoiceWhere.created_at[Op.lt] = new Date(end * 1000);
    }
  }

  const invoiceItems = await InvoiceItem.findAll({
    where: {
      tax_rate_id: { [Op.ne]: null },
    } as any,
    include: [
      {
        model: Invoice,
        as: 'invoice',
        where: invoiceWhere,
        attributes: ['id'],
      },
    ],
    attributes: ['invoice_id'],
  });

  return new Set(invoiceItems.map((item: any) => item.invoice_id).filter(Boolean));
}

async function getRevenueStats(livemode: boolean, currencyId?: string, start?: number, end?: number) {
  const currencyFilter = currencyId ? [currencyId] : undefined;
  const byCurrency: Record<string, RevenueStats> = {};

  // Step 1: Query snapshots within the date range
  const snapshotWhere: WhereOptions = { livemode };
  if (currencyId) {
    snapshotWhere.currency_id = currencyId;
  }
  if (start || end) {
    snapshotWhere.timestamp = {};
    if (start) {
      (snapshotWhere.timestamp as any)[Op.gte] = start;
    }
    if (end) {
      (snapshotWhere.timestamp as any)[Op.lt] = end;
    }
  }

  const snapshots = await RevenueSnapshot.findAll({ where: snapshotWhere });

  // Track the latest snapshot end timestamp to avoid double counting
  // Each snapshot's timestamp is month start, so the data covers [timestamp, timestamp + 1 month)
  let latestSnapshotEnd = 0;

  for (const snapshot of snapshots) {
    const id = snapshot.currency_id;
    if (!byCurrency[id]) {
      byCurrency[id] = { ...ZERO_REVENUE_STATS };
    }
    const entry = byCurrency[id]!;
    entry.totalRevenue = new BN(entry.totalRevenue).add(new BN(snapshot.total_revenue || '0')).toString();
    entry.refundAmount = new BN(entry.refundAmount).add(new BN(snapshot.refund_amount || '0')).toString();
    entry.promotionCost = new BN(entry.promotionCost).add(new BN(snapshot.promotion_cost || '0')).toString();
    entry.creditGrantCost = new BN(entry.creditGrantCost).add(new BN(snapshot.credit_grant_cost || '0')).toString();
    entry.vendorCost = new BN(entry.vendorCost).add(new BN(snapshot.vendor_cost || '0')).toString();
    entry.taxedRevenue = new BN(entry.taxedRevenue).add(new BN(snapshot.taxed_revenue || '0')).toString();
    entry.netRevenue = new BN(entry.netRevenue).add(new BN(snapshot.net_revenue || '0')).toString();

    // Calculate the end of this snapshot's coverage (start of next month)
    const snapshotEndTimestamp = dayjs.unix(snapshot.timestamp).add(1, 'month').startOf('month').unix();
    if (snapshotEndTimestamp > latestSnapshotEnd) {
      latestSnapshotEnd = snapshotEndTimestamp;
    }
  }

  // Step 2: Query live data only for time AFTER the latest snapshot
  // This prevents double counting data that's already in snapshots
  const liveDataStart = latestSnapshotEnd > 0 ? Math.max(latestSnapshotEnd, start || 0) : start;
  const liveDataEnd = end;

  // Skip live data query if the requested range is entirely covered by snapshots
  if (liveDataStart && liveDataEnd && liveDataStart >= liveDataEnd) {
    if (Object.keys(byCurrency).length === 0 && currencyId) {
      byCurrency[currencyId] = { ...ZERO_REVENUE_STATS };
    }
    return byCurrency;
  }

  const invoiceWhere: WhereOptions = {
    livemode,
    status: 'paid',
    billing_reason: { [Op.notIn]: BILLING_REASON_EXCLUSIONS },
  };

  const invoiceCurrencyFilter = buildCurrencyFilter(currencyFilter);
  if (invoiceCurrencyFilter) {
    invoiceWhere.currency_id = invoiceCurrencyFilter;
  }

  if (liveDataStart || liveDataEnd) {
    invoiceWhere.created_at = {};
    if (liveDataStart) {
      invoiceWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
    }
    if (liveDataEnd) {
      invoiceWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
    }
  }

  const [invoices, taxedInvoiceIds] = await Promise.all([
    Invoice.findAll({
      where: invoiceWhere,
      attributes: ['id', 'total', 'total_discount_amounts', 'currency_id'],
    }),
    getTaxedInvoiceIds(livemode, currencyFilter, liveDataStart, liveDataEnd),
  ]);

  const totalRevenueByCurrency: Record<string, string> = {};
  const promotionCostByCurrency: Record<string, string> = {};
  const taxedRevenueByCurrency: Record<string, string> = {};

  invoices.forEach((inv) => {
    addToMap(totalRevenueByCurrency, inv.currency_id, inv.total);

    const discounts = inv.total_discount_amounts || [];
    if (discounts.length > 0) {
      const invoiceDiscount = discounts.reduce((sum: BN, discount: any) => {
        const amount = discount.amount || 0;
        return sum.add(new BN(amount));
      }, new BN('0'));
      if (!invoiceDiscount.isZero()) {
        addToMap(promotionCostByCurrency, inv.currency_id, invoiceDiscount.toString());
      }
    }

    if (taxedInvoiceIds.has(inv.id)) {
      addToMap(taxedRevenueByCurrency, inv.currency_id, inv.total);
    }
  });

  // Query payouts for live data period
  const payoutWhere: WhereOptions = {
    livemode,
    status: { [Op.in]: ['paid', 'deferred'] },
    vendor_info: { [Op.ne]: null },
  };

  const payoutCurrencyFilter = buildCurrencyFilter(currencyFilter);
  if (payoutCurrencyFilter) {
    payoutWhere.currency_id = payoutCurrencyFilter;
  }

  if (liveDataStart || liveDataEnd) {
    payoutWhere.created_at = {};
    if (liveDataStart) {
      payoutWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
    }
    if (liveDataEnd) {
      payoutWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
    }
  }

  const payouts = await Payout.findAll({
    where: payoutWhere,
    attributes: ['amount', 'currency_id'],
  });

  const vendorCostByCurrency: Record<string, string> = {};
  payouts.forEach((payout) => {
    addToMap(vendorCostByCurrency, payout.currency_id, payout.amount || '0');
  });

  // Query refunds for live data period
  const refundWhere: WhereOptions = {
    livemode,
    status: 'succeeded',
  };
  const refundCurrencyFilter = buildCurrencyFilter(currencyFilter);
  if (refundCurrencyFilter) {
    refundWhere.currency_id = refundCurrencyFilter;
  }
  if (liveDataStart || liveDataEnd) {
    refundWhere.created_at = {};
    if (liveDataStart) {
      refundWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
    }
    if (liveDataEnd) {
      refundWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
    }
  }

  const refunds = await Refund.findAll({
    where: refundWhere,
    attributes: ['amount', 'currency_id'],
  });

  const refundAmountByCurrency: Record<string, string> = {};
  refunds.forEach((refund) => {
    addToMap(refundAmountByCurrency, refund.currency_id, refund.amount || '0');
  });

  // Query credit grants for live data period
  // Include granted (active), depleted, and expired - all have consumed portions
  // Exclude: pending (not yet active), voided (revoked, not a cost)
  // Cost = amount - remaining_amount (the actually consumed portion)
  const creditGrantWhere: WhereOptions = {
    livemode,
    status: { [Op.in]: ['granted', 'depleted', 'expired'] },
  };
  const creditGrantCurrencyFilter = buildCurrencyFilter(currencyFilter);
  if (creditGrantCurrencyFilter) {
    creditGrantWhere.currency_id = creditGrantCurrencyFilter;
  }
  if (liveDataStart || liveDataEnd) {
    creditGrantWhere.created_at = {};
    if (liveDataStart) {
      creditGrantWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
    }
    if (liveDataEnd) {
      creditGrantWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
    }
  }

  const creditGrants = await CreditGrant.findAll({
    where: creditGrantWhere,
    attributes: ['amount', 'remaining_amount', 'currency_id'],
  });

  const creditGrantCostByCurrency: Record<string, string> = {};
  creditGrants.forEach((grant) => {
    // Cost = consumed amount = total amount - remaining amount
    const consumed = new BN(grant.amount || '0').sub(new BN(grant.remaining_amount || '0'));
    addToMap(creditGrantCostByCurrency, grant.currency_id, consumed.toString());
  });

  // Merge live data with snapshot data
  const currencySet = new Set<string>(currencyFilter || []);
  [
    totalRevenueByCurrency,
    promotionCostByCurrency,
    taxedRevenueByCurrency,
    vendorCostByCurrency,
    refundAmountByCurrency,
    creditGrantCostByCurrency,
  ].forEach((map) => {
    Object.keys(map).forEach((id) => currencySet.add(id));
  });
  Object.keys(byCurrency).forEach((id) => currencySet.add(id));

  const currencyList = Array.from(currencySet);

  currencyList.forEach((id) => {
    const snapshotData = byCurrency[id] || ZERO_REVENUE_STATS;
    const liveTotal = totalRevenueByCurrency[id] || '0';
    const liveRefund = refundAmountByCurrency[id] || '0';
    const livePromotion = promotionCostByCurrency[id] || '0';
    const liveCreditGrant = creditGrantCostByCurrency[id] || '0';
    const liveTaxed = taxedRevenueByCurrency[id] || '0';
    const liveVendor = vendorCostByCurrency[id] || '0';

    const totalRevenue = new BN(snapshotData.totalRevenue).add(new BN(liveTotal)).toString();
    const refundAmount = new BN(snapshotData.refundAmount).add(new BN(liveRefund)).toString();
    const promotionCost = new BN(snapshotData.promotionCost).add(new BN(livePromotion)).toString();
    const creditGrantCost = new BN(snapshotData.creditGrantCost).add(new BN(liveCreditGrant)).toString();
    const taxedRevenue = new BN(snapshotData.taxedRevenue).add(new BN(liveTaxed)).toString();
    const vendorCost = new BN(snapshotData.vendorCost).add(new BN(liveVendor)).toString();
    // Net Revenue = Total Revenue - Refunds
    const netRevenue = new BN(totalRevenue).sub(new BN(refundAmount)).toString();

    const includeEntry =
      hasNonZeroValue([totalRevenue, refundAmount, promotionCost, creditGrantCost, taxedRevenue, vendorCost]) ||
      Boolean(currencyFilter);

    if (!includeEntry) {
      return;
    }

    byCurrency[id] = {
      totalRevenue,
      refundAmount,
      promotionCost,
      creditGrantCost,
      vendorCost,
      taxedRevenue,
      netRevenue,
    };
  });

  if (currencyList.length === 0 && currencyId) {
    byCurrency[currencyId] = { ...ZERO_REVENUE_STATS };
  }

  return byCurrency;
}

router.get('/summary', auth, async (req, res) => {
  try {
    const {
      currency_id: currencyId,
      start,
      end,
    } = await summaryQuerySchema.validateAsync(req.query, {
      stripUnknown: true,
    });
    const livemode = !!req.livemode;
    const [arcblock, ethereum, links, revenueByCurrency] = await Promise.all([
      getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
      getTokenSummaryByDid(ethWallet.address, !!req.livemode, EVM_CHAIN_TYPES),
      getCurrencyLinks(livemode),
      getRevenueStats(livemode, currencyId, start, end),
    ]);
    res.json({
      links,
      balances: { ...arcblock, ...ethereum },
      addresses: { arcblock: wallet.address, ethereum: ethWallet.address },
      summary: {
        subscription: await Subscription.getSummary(livemode),
        invoice: await Invoice.getSummary(livemode),
        payment: await PaymentIntent.getSummary(livemode),
        payout: await Payout.getSummary(livemode),
        refund: await Refund.getSummary(livemode),
      },
      revenueByCurrency,
    });
  } catch (err) {
    logger.error(err);
    res.json(null);
  }
});

export default router;
