import { Router } from 'express';
import Joi from 'joi';
import { Op, QueryTypes, Sequelize } from 'sequelize';

import { fromTokenToUnit } from '@ocap/util';
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { formatMetadata } from '../libs/util';
import { trimDecimals } from '../libs/math-utils';
import { Customer, Meter, MeterEvent, MeterEventStatus, PaymentCurrency, Subscription } from '../store/models';

import { getCachedMeter, getCachedCurrency } from '../libs/reference-cache';

const router = Router();
const auth = authenticate<MeterEvent>({ component: true, roles: ['owner', 'admin'] });
const authMine = authenticate<MeterEvent>({ component: true, roles: ['owner', 'admin'], mine: true });

const SourceDataSchema = Joi.alternatives()
  .try(
    Joi.object().pattern(Joi.string().max(40), Joi.string().max(256).allow('')).min(0),
    Joi.array()
      .items(
        Joi.object({
          key: Joi.string().max(40).required(),
          label: Joi.alternatives()
            .try(
              Joi.string().max(100),
              Joi.object({
                zh: Joi.string().max(100).optional(),
                en: Joi.string().max(100).optional(),
              })
            )
            .required(),
          value: Joi.string().max(256).allow('').optional(),
          type: Joi.string().valid('text', 'image', 'url').optional(),
          url: Joi.string().uri().optional(),
          group: Joi.string().max(40).optional(),
        })
      )
      .min(0)
  )
  .optional();

const meterEventSchema = Joi.object({
  event_name: Joi.string().max(128).required(),
  payload: Joi.object({
    customer_id: Joi.string().required(),
    value: Joi.number().greater(0).required(),
    subscription_id: Joi.string().max(128).optional(),
  }).required(),
  timestamp: Joi.number().integer().optional(),
  identifier: Joi.string().max(255).required(),
  metadata: MetadataSchema,
  source_data: SourceDataSchema,
});

const listSchema = createListParamSchema<{
  event_name?: string;
  meter_id?: string;
  customer_id?: string;
  start?: number;
  end?: number;
  status?: string;
}>({
  event_name: Joi.string().empty(''),
  meter_id: Joi.string().empty(''),
  customer_id: Joi.string().empty(''),
  start: Joi.number().integer().optional(),
  end: Joi.number().integer().optional(),
  status: Joi.string().empty(''),
});

const statsSchema = Joi.object({
  meter_id: Joi.string().required(),
  start: Joi.number().integer().required(),
  end: Joi.number().integer().required(),
  customer_id: Joi.string().optional(),
  granularity: Joi.string().valid('minute', 'hour', 'day').default('day'),
});

router.get('/', authMine, async (req, res) => {
  try {
    const { page, pageSize, ...query } = await listSchema.validateAsync(req.query, { stripUnknown: true });
    const where = getWhereFromKvQuery(query.q);

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

    if (query.meter_id) {
      const meter = await Meter.findByPk(query.meter_id);
      if (meter) {
        where.event_name = meter.event_name;
      }
    }

    if (query.customer_id) {
      where[Op.and] = where[Op.and] || [];
      where[Op.and].push(
        Sequelize.where(Sequelize.literal("json_extract(payload, '$.customer_id')"), query.customer_id)
      );
    }

    if (query.start || query.end) {
      where.timestamp = {};
      if (query.start) {
        where.timestamp[Op.gte] = Number(query.start);
      }
      if (query.end) {
        where.timestamp[Op.lte] = Number(query.end);
      }
    }

    if (query.status) {
      where.status = {
        [Op.in]: query.status
          ?.split(',')
          .map((x) => x.trim())
          .filter(Boolean),
      };
    }

    const { rows: list, count } = await MeterEvent.findAndCountAll({
      where,
      order: getOrder(query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
      offset: (page - 1) * pageSize,
      limit: pageSize,
    });
    const expanded = await MeterEvent.expand(list, !!req.livemode, {
      customer: true,
      subscription: true,
      meter: !!query.meter_id,
    });

    res.json({ count, list: expanded, paging: { page, pageSize } });
  } catch (err) {
    logger.error('Error listing meter events', err);
    res.status(400).json({ error: err?.message });
  }
});

router.get('/stats', authMine, async (req, res) => {
  try {
    const {
      meter_id: meterId,
      start,
      end,
      customer_id: customerId,
      granularity,
    } = await statsSchema.validateAsync(req.query, { stripUnknown: true });

    const meter = await Meter.findByPk(meterId);
    if (!meter) {
      return res.status(404).json({ error: 'Meter not found' });
    }

    const startDate = new Date(start * 1000);
    const endDate = new Date(end * 1000);

    // 根据granularity选择聚合方式
    let dateFormat;
    let groupBy;
    if (granularity === 'minute') {
      // 分钟级别聚合（实际按小时聚合，前端处理更细粒度显示）
      dateFormat = "DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00')";
      groupBy = "DATE_FORMAT(created_at, '%Y-%m-%d %H')";
    } else if (granularity === 'hour') {
      dateFormat = "DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00')";
      groupBy = "DATE_FORMAT(created_at, '%Y-%m-%d %H')";
    } else {
      dateFormat = 'DATE(created_at)';
      groupBy = 'DATE(created_at)';
    }

    // 构建查询条件
    let whereClause =
      'event_name = :eventName AND livemode = :livemode AND created_at >= :startDate AND created_at <= :endDate';
    const replacements: any = {
      eventName: meter.event_name,
      livemode: !!req.livemode,
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
    };

    if (customerId) {
      whereClause += " AND json_extract(payload, '$.customer_id') = :customerId";
      replacements.customerId = customerId;
    }

    const statsQuery = `
      SELECT 
        ${dateFormat} as date,
        ${dateFormat} as timestamp,
        COUNT(*) as event_count,
        COALESCE(SUM(CAST(payload->>'value' AS DECIMAL)), 0) as total_value
      FROM meter_events 
      WHERE ${whereClause}
      GROUP BY ${groupBy}
      ORDER BY ${groupBy}
    `;

    const { sequelize } = MeterEvent;
    if (!sequelize) {
      return res.status(500).json({ error: 'Database connection not available' });
    }

    const stats = await sequelize.query(statsQuery, {
      replacements,
      type: QueryTypes.SELECT,
    });

    return res.json({
      count: stats.length,
      list: stats.map((item: any) => ({
        ...item,
        timestamp: new Date(item.date).toISOString(),
      })),
    });
  } catch (err) {
    logger.error('Error getting meter event stats', err);
    return res.status(400).json({ error: err?.message });
  }
});

router.post('/', auth, async (req, res) => {
  const t0 = performance.now();
  try {
    const { error } = meterEventSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: `Meter event create request invalid: ${error.message}` });
    }

    // Phase 1: dedupe + meter + customer lookups in parallel (all independent)
    let t1 = performance.now();
    const [existing, meter, customer] = await Promise.all([
      MeterEvent.isEventExists(req.body.identifier),
      getCachedMeter(req.body.event_name),
      Customer.findByPkOrDid(req.body.payload.customer_id),
    ]);
    const tPhase1 = performance.now() - t1;

    if (existing) {
      return res.status(400).json({ error: `Event with identifier "${req.body.identifier}" already exists` });
    }
    if (!meter) {
      return res
        .status(400)
        .json({ error: `Meter not found for event name "${req.body.event_name}"`, code: 'METER_NOT_FOUND' });
    }
    if (meter.status !== 'active') {
      return res
        .status(400)
        .json({ error: 'Meter is not active, please activate it first.', code: 'METER_NOT_ACTIVE' });
    }
    if (!customer) {
      return res.status(400).json({ error: 'Customer not found' });
    }

    // Phase 2: currency + subscription in parallel (currency depends on meter)
    t1 = performance.now();
    const [paymentCurrency, subscription] = await Promise.all([
      getCachedCurrency(meter.currency_id),
      req.body.payload.subscription_id
        ? Subscription.findByPk(req.body.payload.subscription_id)
        : Promise.resolve(null),
    ]);
    const tPhase2 = performance.now() - t1;

    if (!paymentCurrency) {
      return res.status(400).json({ error: `Payment currency not found for meter "${meter.id}"` });
    }

    if (subscription) {
      if (subscription.currency_id !== paymentCurrency.id) {
        return res.status(400).json({ error: 'Subscription currency does not match meter currency' });
      }
      if (!subscription.isConsumesCredit()) {
        return res.status(400).json({ error: 'Subscription does not consume credit' });
      }
      if (subscription.customer_id !== req.body.payload.customer_id) {
        return res.status(400).json({ error: 'This is not your subscription' });
      }
      if (subscription.isImmutable()) {
        return res.status(400).json({ error: 'Subscription is immutable' });
      }
    } else if (req.body.payload.subscription_id) {
      return res.status(400).json({ error: `Subscription not found for meter event "${req.body.event_name}"` });
    }

    const timestamp = req.body.timestamp || req.body.payload.timestamp || Math.floor(Date.now() / 1000);

    const value = trimDecimals(req.body.payload.value, paymentCurrency.decimal);
    const eventData = {
      event_name: req.body.event_name,
      payload: {
        customer_id: customer.id,
        currency_id: paymentCurrency.id,
        decimal: paymentCurrency.decimal,
        unit: paymentCurrency.name,
        subscription_id: req.body.payload.subscription_id,
        value: fromTokenToUnit(value, paymentCurrency.decimal).toString(),
      },
      identifier: req.body.identifier,
      livemode: meter.livemode,
      processed: false,
      status: 'pending' as MeterEventStatus,
      attempt_count: 0,
      credit_consumed: '0',
      credit_pending: fromTokenToUnit(value, paymentCurrency.decimal).toString(),
      created_via: req.user?.via || 'api',
      metadata: formatMetadata(req.body.metadata),
      source_data: req.body.source_data,
      timestamp,
    };

    t1 = performance.now();
    const event = await MeterEvent.create(eventData);
    const tCreate = performance.now() - t1;

    const tTotal = performance.now() - t0;

    logger.info('Meter event created and will be queued for processing via afterCreate hook', {
      eventId: event.id,
      eventName: event.event_name,
    });

    // Server-Timing header for benchmark observability
    const timings = [
      `phase1;dur=${tPhase1.toFixed(1)}`,
      `phase2;dur=${tPhase2.toFixed(1)}`,
      `create;dur=${tCreate.toFixed(1)}`,
      `total;dur=${tTotal.toFixed(1)}`,
    ];
    res.set('Server-Timing', timings.join(', '));

    return res.json({
      ...event.toJSON(),
      processing: {
        queued: true,
        message: 'Credit consumption will be processed asynchronously',
      },
    });
  } catch (err) {
    logger.error('create meter event failed', { error: err?.message, request: req.body });
    return res.status(400).json({ error: err?.message });
  }
});

router.get('/pending-amount', authMine, async (req, res) => {
  try {
    const params: any = {
      livemode: !!req.livemode,
    };
    if (req.query.subscription_id) {
      params.subscriptionId = req.query.subscription_id;
    }
    if (req.query.currency_id) {
      params.currencyId = req.query.currency_id;
    }
    if (req.query.customer_id) {
      if (typeof req.query.customer_id !== 'string') {
        return res.status(400).json({ error: 'Customer ID must be a string' });
      }
      const customer = await Customer.findByPkOrDid(req.query.customer_id);
      if (!customer) {
        return res.status(404).json({ error: 'Customer not found' });
      }
      params.customerId = customer.id;
    }
    const [summary] = await MeterEvent.getPendingAmounts(params);
    return res.json(summary);
  } catch (err) {
    logger.error('Error getting meter event pending amount', err);
    return res.status(400).json({ error: err?.message });
  }
});

// Get overdue summary by credit currency (action-required amounts grouped by currency)
router.get('/overdue-summary', auth, async (req, res) => {
  try {
    // Query to aggregate credit_pending by currency, showing overdue amounts per credit currency
    const results = await MeterEvent.sequelize!.query<{
      currency_id: string;
      total_pending: string;
      customer_count: number;
      event_count: number;
    }>(
      `SELECT 
        m.currency_id,
        SUM(CAST(me.credit_pending AS DECIMAL(40,0))) as total_pending,
        COUNT(DISTINCT json_extract(me.payload, '$.customer_id')) as customer_count,
        COUNT(*) as event_count
      FROM meter_events me
      JOIN meters m ON me.event_name = m.event_name
      WHERE me.livemode = :livemode 
        AND me.status IN ('requires_capture', 'requires_action')
        AND m.status = 'active'
      GROUP BY m.currency_id
      HAVING total_pending > 0
      ORDER BY total_pending DESC`,
      {
        replacements: { livemode: req.livemode ? 1 : 0 },
        type: QueryTypes.SELECT,
      }
    );

    // Fetch currency details
    const currencyIds = results.map((r) => r.currency_id);
    const currencies =
      currencyIds.length > 0 ? await PaymentCurrency.findAll({ where: { id: { [Op.in]: currencyIds } } }) : [];
    const currencyMap = new Map(currencies.map((c) => [c.id, c]));

    const list = results.map((r) => ({
      currency: currencyMap.get(r.currency_id),
      total_pending: r.total_pending,
      customer_count: r.customer_count,
      event_count: r.event_count,
    }));

    return res.json({ list });
  } catch (err) {
    logger.error('Error getting overdue summary', err);
    return res.status(400).json({ error: err?.message });
  }
});

// Get all customers with credit overdue (action-required amount > 0), grouped by customer + currency
router.get('/overdue-customers', auth, async (req, res) => {
  try {
    const page = Math.max(1, parseInt(String(req.query.page || '1'), 10));
    const pageSize = Math.min(100, Math.max(1, parseInt(String(req.query.pageSize || '20'), 10)));
    const offset = (page - 1) * pageSize;
    const currencyId = req.query.currency_id as string | undefined;
    const searchQuery = req.query.q as string | undefined;

    // If search query provided, first find matching customer IDs
    let customerFilter = '';
    const replacements: any = { livemode: req.livemode ? 1 : 0, limit: pageSize, offset };
    const countReplacements: any = { livemode: req.livemode ? 1 : 0 };

    // Build currency filter
    let currencyFilter = '';
    if (currencyId) {
      currencyFilter = 'AND m.currency_id = :currencyId';
      replacements.currencyId = currencyId;
      countReplacements.currencyId = currencyId;
    }

    if (searchQuery) {
      // Escape LIKE wildcards to prevent unintended matches
      const escapedQuery = searchQuery.replace(/[%_]/g, '\\$&');
      const searchCustomers = await Customer.findAll({
        where: {
          [Op.or]: [
            { name: { [Op.like]: `%${escapedQuery}%` } },
            { email: { [Op.like]: `%${escapedQuery}%` } },
            { did: { [Op.like]: `%${escapedQuery}%` } },
          ],
        },
        attributes: ['id'],
      });
      const searchCustomerIds = searchCustomers.map((c) => c.id);
      if (searchCustomerIds.length === 0) {
        return res.json({ list: [], count: 0, paging: { page, pageSize } });
      }
      // Generate parameterized placeholders for customer IDs
      const placeholders = searchCustomerIds.map((_, i) => `:searchId${i}`);
      customerFilter = `AND json_extract(me.payload, '$.customer_id') IN (${placeholders.join(',')})`;
      searchCustomerIds.forEach((id, i) => {
        replacements[`searchId${i}`] = id;
        countReplacements[`searchId${i}`] = id;
      });
    }

    // Query to aggregate credit_pending by customer + currency, order by event_count DESC
    const results = await MeterEvent.sequelize!.query<{
      customer_id: string;
      currency_id: string;
      total_pending: string;
      event_count: number;
    }>(
      `SELECT 
        json_extract(me.payload, '$.customer_id') as customer_id,
        m.currency_id as currency_id,
        SUM(CAST(me.credit_pending AS DECIMAL(40,0))) as total_pending,
        COUNT(*) as event_count
      FROM meter_events me
      JOIN meters m ON me.event_name = m.event_name
      WHERE me.livemode = :livemode 
        AND me.status IN ('requires_capture', 'requires_action')
        AND m.status = 'active'
        ${currencyFilter}
        ${customerFilter}
      GROUP BY json_extract(me.payload, '$.customer_id'), m.currency_id
      HAVING total_pending > 0
      ORDER BY event_count DESC
      LIMIT :limit OFFSET :offset`,
      {
        replacements,
        type: QueryTypes.SELECT,
      }
    );

    // Get total count
    const countResult = await MeterEvent.sequelize!.query<{ count: number }>(
      `SELECT COUNT(*) as count FROM (
        SELECT json_extract(me.payload, '$.customer_id') as customer_id, m.currency_id
        FROM meter_events me
        JOIN meters m ON me.event_name = m.event_name
        WHERE me.livemode = :livemode 
          AND me.status IN ('requires_capture', 'requires_action')
          AND m.status = 'active'
          ${currencyFilter}
          ${customerFilter}
        GROUP BY json_extract(me.payload, '$.customer_id'), m.currency_id
        HAVING SUM(CAST(me.credit_pending AS DECIMAL(40,0))) > 0
      )`,
      {
        replacements: countReplacements,
        type: QueryTypes.SELECT,
      }
    );

    const count = countResult[0]?.count || 0;
    const customerIds = [...new Set(results.map((r) => r.customer_id))];
    const currencyIds = [...new Set(results.map((r) => r.currency_id))];

    // Fetch customer and currency details
    const customers = customerIds.length > 0 ? await Customer.findAll({ where: { id: { [Op.in]: customerIds } } }) : [];
    const customerMap = new Map(customers.map((c) => [c.id, c]));

    const currencies =
      currencyIds.length > 0 ? await PaymentCurrency.findAll({ where: { id: { [Op.in]: currencyIds } } }) : [];
    const currencyMap = new Map(currencies.map((c) => [c.id, c]));

    const list = results.map((r) => ({
      customer: customerMap.get(r.customer_id),
      currency: currencyMap.get(r.currency_id),
      total_pending: r.total_pending,
      event_count: r.event_count,
    }));

    return res.json({ list, count, paging: { page, pageSize } });
  } catch (err) {
    logger.error('Error getting overdue customers', err);
    return res.status(400).json({ error: err?.message });
  }
});
router.get('/:id', authMine, async (req, res) => {
  try {
    logger.info('get meter event', { id: req.params.id });
    const event = await MeterEvent.findByPk(req.params.id);

    if (!event) {
      return res.status(404).json({ error: 'Meter event not found' });
    }

    const customerId = event.getCustomerId();
    const subscriptionId = event.getSubscriptionId();
    const subscription = subscriptionId ? await Subscription.findByPk(subscriptionId) : null;
    const customer = customerId ? await Customer.findByPk(customerId) : null;
    if (!customer) {
      return res.status(404).json({ error: 'Customer not found' });
    }
    if (customer.did !== req.user?.did && !['owner', 'admin'].includes(req.user?.role || '')) {
      return res.status(403).json({ error: 'You are not allowed to access this resource' });
    }
    const meter = await Meter.getMeterByEventName(event.event_name);
    let paymentCurrency = null;
    if (meter) {
      paymentCurrency = await PaymentCurrency.findByPk(meter.currency_id);
    }

    const result = {
      ...event.toJSON(),
      customer,
      subscription,
      meter,
      paymentCurrency,
    };
    return res.json(result);
  } catch (err) {
    logger.error('Error getting meter event', err);
    return res.status(400).json({ error: err?.message });
  }
});

export default router;
