import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
import { Router } from 'express';
import Joi from 'joi';
import pick from 'lodash/pick';
import isEmail from 'validator/es/lib/isEmail';

import { Op, type WhereOptions } from 'sequelize';
import { BN } from '@ocap/util';
import { getStakeSummaryByDid, getTokenSummaryByDid, getTokenByAddress } from '../integrations/arcblock/stake';
import { createListParamSchema, getOrder, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
import { authenticate } from '../libs/security';
import { formatMetadata } from '../libs/util';
import { Customer } from '../store/models/customer';
import { blocklet } from '../libs/auth';
import logger from '../libs/logger';
import {
  Invoice,
  PaymentCurrency,
  PaymentMethod,
  Price,
  Product,
  Subscription,
  SubscriptionItem,
} from '../store/models';
import { getSubscriptionPaymentAddress, calculateRecommendedRechargeAmount } from '../libs/subscription';
import { expandLineItems } from '../libs/session';
import { handleNotificationPreferenceChange } from '../queues/notification';
import { getEndpointAndSpaceDid } from '../libs/did-space';
import { spaceQueue } from '../queues/space';

const router = Router();
const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
const authPortal = authenticate<Customer>({
  component: true,
  roles: ['owner', 'admin'],
  record: {
    // @ts-ignore
    model: Customer,
    field: 'id',
    findById: (id: string) => Customer.findByPkOrDid(id),
  },
});

const schema = createListParamSchema<{ did?: string }>({ did: Joi.string().empty('') });
router.get('/', auth, async (req, res) => {
  const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
  const where = getWhereFromKvQuery(query.q);

  if (query.did) {
    where.did = query.did;
  }

  try {
    const { rows: list, count } = await Customer.findAndCountAll({
      where,
      order: getOrder(query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
      offset: (page - 1) * pageSize,
      limit: pageSize,
      include: [],
    });

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

// search customers
const searchSchema = createListParamSchema<{
  query: string;
}>({
  query: Joi.string(),
});
router.get('/search', auth, async (req, res) => {
  const { page, pageSize, query, livemode, q, o } = await searchSchema.validateAsync(req.query, {
    stripUnknown: false,
    allowUnknown: true,
  });

  const where = q != null ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
  if (typeof livemode === 'boolean') {
    where.livemode = livemode;
  }
  const { rows: list, count } = await Customer.findAndCountAll({
    where,
    order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
    offset: (page - 1) * pageSize,
    limit: pageSize,
    include: [],
  });

  res.json({ count, list, paging: { page, pageSize } });
});

// eslint-disable-next-line consistent-return
router.get('/me', sessionMiddleware({ accessKey: true }), async (req, res) => {
  if (!req.user) {
    return res.status(403).json({ error: 'Unauthorized' });
  }

  try {
    let doc = await Customer.findByPkOrDid(req.user.did as string);
    const livemode = req.query.livemode ? !!req?.livemode : !!doc?.livemode;
    if (!doc) {
      if (req.query.fallback) {
        const result = await blocklet.getUser(req.user.did);
        return res.json({ ...result.user, address: Customer.formatAddressFromUser(result.user), livemode });
      }
      if (req.query.create) {
        // create customer
        const { user } = await blocklet.getUser(req.user.did);
        const customer = await Customer.create({
          livemode: true,
          did: req.user.did,
          name: user.fullName,
          email: user.email,
          phone: user.phone,
          address: Customer.formatAddressFromUser(user),
          description: user.remark,
          metadata: {},
          balance: '0',
          next_invoice_sequence: 1,
          delinquent: false,
          invoice_prefix: Customer.getInvoicePrefix(),
        });
        logger.info('customer created', {
          customerId: customer.id,
          did: customer.did,
        });
        doc = customer;
      } else {
        return res.json({ error: 'Customer not found' });
      }
    }
    if (req.query.skipSummary) {
      return res.json({ ...doc.toJSON(), livemode });
    }
    try {
      const [summary, stake, token] = await Promise.all([
        doc.getSummary(livemode),
        getStakeSummaryByDid(doc.did, livemode),
        getTokenSummaryByDid(doc.did, livemode),
      ]);
      res.json({ ...doc.toJSON(), summary: { ...summary, stake, token }, livemode });
    } catch (summaryErr) {
      logger.error('get customer summary failed', summaryErr);
      if (req.query.skipError) {
        return res.json({
          ...doc.toJSON(),
          summary: { stake: {}, token: {} },
          livemode,
          summaryError: summaryErr.message,
          error: `Failed to get customer: ${summaryErr.message}`,
        });
      }
      throw summaryErr;
    }
  } catch (err) {
    logger.error('get customer failed', err);
    if (req.query.skipError) {
      return res.json({
        error: `Failed to get customer: ${err.message}`,
        did: req.user?.did,
        name: req.user?.fullName,
        address: {},
        livemode: !!req.query.livemode,
        summary: { stake: {}, token: {} },
      });
    }
    return res.status(500).json({ error: `Failed to get customer: ${err.message}` });
  }
});

router.post('/sync-to-space', sessionMiddleware(), async (req, res) => {
  if (!req.user) {
    return res.status(403).json({ error: 'Unauthorized' });
  }
  try {
    const userDid = req.user.did;
    const jobId = `space-${userDid}`;
    const { endpoint } = await getEndpointAndSpaceDid(userDid);
    if (endpoint) {
      const mainTask = await spaceQueue.get(jobId);
      if (mainTask) {
        return res.json({
          success: true,
          message: 'Billing data sync already in progress',
        });
      }
      spaceQueue.push({
        id: jobId,
        job: {
          type: 'customer',
          data: {
            id: userDid,
          },
        },
        delay: 60, // delay 1min
      });
      logger.info('Queued billing sync to DID Space for user:', { did: req.user.did });

      return res.json({
        success: true,
        message: 'Billing data sync will start soon',
      });
    }
    return res.json({
      success: false,
      message: 'No endpoint found for the user',
    });
  } catch (error) {
    return res.json({
      success: false,
      message: error.message,
    });
  }
});

// get overdue invoices
router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
  try {
    const doc = await Customer.findByPkOrDid(req.params.id as string);
    if (!doc) {
      return res.status(404).json({ error: 'Customer not found' });
    }
    const where: WhereOptions<Invoice> = {
      customer_id: doc.id,
      status: ['uncollectible'],
      amount_remaining: { [Op.gt]: '0' },
    };
    if (typeof req.livemode === 'boolean') {
      where.livemode = !!req.livemode;
    }
    const { rows: invoices, count } = await Invoice.findAndCountAll({
      where,
      include: [
        { model: PaymentCurrency, as: 'paymentCurrency' },
        { model: PaymentMethod, as: 'paymentMethod' },
      ],
    });
    if (count === 0) {
      return res.json({
        summary: null,
        invoices: [],
        subscriptionCount: 0,
      });
    }
    const summary: Record<string, { amount: string; currency: PaymentCurrency; method: PaymentMethod }> = {};
    invoices.forEach((invoice) => {
      const key = invoice.currency_id;
      if (!summary[key]) {
        summary[key] = {
          amount: '0',
          // @ts-ignore
          currency: invoice.paymentCurrency,
          // @ts-ignore
          method: invoice.paymentMethod,
        };
      }
      if (invoice && summary[key]) {
        // @ts-ignore
        summary[key].amount = new BN(summary[key]?.amount || '0')
          .add(new BN(invoice.amount_remaining || '0'))
          .toString();
      }
    });
    const subscriptionCount = new Set(invoices.map((x) => x.subscription_id)).size;
    return res.json({
      summary,
      invoices,
      subscriptionCount,
      customer: doc,
    });
  } catch (err) {
    logger.error(err);
    return res.status(500).json({ error: `Failed to get overdue invoices: ${err.message}` });
  }
});

router.get('/recharge', sessionMiddleware({ accessKey: true }), async (req, res) => {
  if (!req.user) {
    return res.status(403).json({ error: 'Unauthorized' });
  }
  if (!req.query.currencyId) {
    return res.status(400).json({ error: 'Currency ID is required' });
  }
  try {
    const customer = await Customer.findByPkOrDid(req.user.did as string);
    if (!customer) {
      return res.status(404).json({ error: 'Customer not found' });
    }

    const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
    if (!paymentCurrency) {
      return res.status(404).json({ error: 'Currency not found' });
    }

    const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
    if (!paymentMethod) {
      return res.status(404).json({ error: 'Payment method not found' });
    }

    let subscriptions = await Subscription.findAll({
      where: {
        customer_id: customer.id,
        currency_id: paymentCurrency.id,
        status: {
          [Op.in]: ['active', 'trialing', 'past_due'],
        },
      },
      include: [{ model: SubscriptionItem, as: 'items' }],
    });

    subscriptions = subscriptions.map((x) => x.toJSON());
    // S1 optimization: Load only referenced prices (with product) instead of full table scans
    const priceIds = [
      ...new Set(subscriptions.flatMap((x: any) => (x.items || []).map((i: any) => i.price_id)).filter(Boolean)),
    ];
    const pricesWithProduct =
      priceIds.length > 0
        ? await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] })
        : [];
    const pricesJson = pricesWithProduct.map((x) => x.toJSON());
    const productMap = new Map<string, any>();
    pricesJson.forEach((p: any) => {
      if (p.product) productMap.set(p.product.id, p.product);
    });
    const products = Array.from(productMap.values());
    // @ts-ignore
    subscriptions.forEach((x) => expandLineItems(x.items, products, pricesJson));

    const relatedSubscriptions = subscriptions.filter((sub) => {
      const payerAddress = getSubscriptionPaymentAddress(sub, paymentMethod.type);
      return payerAddress === customer.did;
    });

    // Calculate recommended recharge cycle and amount
    const recommendedRecharge = calculateRecommendedRechargeAmount(relatedSubscriptions, paymentCurrency.id);

    return res.json({
      currency: {
        ...paymentCurrency.toJSON(),
        paymentMethod,
      },
      relatedSubscriptions,
      recommendedRecharge,
    });
  } catch (err) {
    logger.error('Error getting balance recharge info', err);
    return res.status(500).json({ error: err.message });
  }
});

// get address token
router.get('/payer-token', sessionMiddleware({ accessKey: true }), async (req, res) => {
  if (!req.user) {
    return res.status(403).json({ error: 'Unauthorized' });
  }
  if (!req.query.currencyId) {
    return res.status(400).json({ error: 'Currency ID is required' });
  }
  try {
    let paymentAddress = req.query.payerAddress as string;
    let customer: Customer | null = null;
    if (!paymentAddress) {
      customer = await Customer.findByPkOrDid(req.user.did as string);
      if (!customer) {
        return res.status(404).json({ error: 'Customer not found' });
      }
      paymentAddress = customer.did;
    }

    const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
    if (!paymentCurrency) {
      return res.status(404).json({ error: 'Currency not found' });
    }

    const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
    if (!paymentMethod) {
      return res.status(404).json({ error: 'Payment method not found' });
    }

    if (!['arcblock', 'ethereum', 'base'].includes(paymentMethod.type)) {
      return res.status(400).json({ error: `Payment method not supported: ${paymentMethod.type}` });
    }

    if (!paymentAddress) {
      return res.status(400).json({ error: `Payment address not found for customer: ${customer?.id}` });
    }

    const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
    return res.json({ token, paymentAddress });
  } catch (err) {
    logger.error('Error getting customer payer token', err);
    return res.status(500).json({ error: err.message });
  }
});

router.get('/:id', auth, async (req, res) => {
  if (!req.params.id) {
    return res.status(400).json({ error: 'Customer ID is required' });
  }
  try {
    const doc = await Customer.findByPkOrDid(req.params.id as string);
    if (doc) {
      return res.json(doc);
    }
    if (req.body.create) {
      if (!req.user) {
        return res.status(403).json({ error: 'Unauthorized' });
      }
      if (req.params.id.startsWith('cus_')) {
        return res.status(404).json({ error: 'Customer not found' });
      }
      const { user } = await blocklet.getUser(req.params.id);
      if (!user) {
        return res.status(404).json({ error: 'User not found' });
      }
      const customer = await Customer.create({
        livemode: true,
        did: user?.did || req.params.id,
        name: user?.fullName ?? req.params.id,
        email: user?.email ?? '',
        phone: user?.phone ?? '',
        address: Customer.formatAddressFromUser(user),
        description: user?.remark ?? '',
        metadata: {},
        balance: '0',
        next_invoice_sequence: 1,
        delinquent: false,
        invoice_prefix: Customer.getInvoicePrefix(),
      });
      logger.info('customer created', {
        customerId: customer.id,
        did: customer.did,
      });
      return res.json(customer);
    }
    return res.status(404).json(null);
  } catch (err) {
    logger.error(err);
    return res.status(500).json({ error: `Failed to get customer: ${err.message}` });
  }
});

router.get('/:id/summary', auth, async (req, res) => {
  try {
    const doc = await Customer.findByPkOrDid(req.params.id as string);
    if (!doc) {
      res.status(404).json({ error: 'Customer not found' });
      return;
    }

    const [summary, stake, token] = await Promise.all([
      doc.getSummary(doc.livemode),
      getStakeSummaryByDid(doc.did, doc.livemode),
      getTokenSummaryByDid(doc.did, doc.livemode),
    ]);
    res.json({ ...summary, stake, token });
  } catch (err) {
    logger.error(err);
    res.json(null);
  }
});

const updatePreferenceSchema = Joi.object({
  notification: Joi.object({
    frequency: Joi.string().valid('default', 'daily', 'weekly', 'monthly').required(),
    schedule: Joi.object({
      time: Joi.string().pattern(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
      date: Joi.alternatives().conditional('..frequency', {
        switch: [
          { is: 'weekly', then: Joi.number().min(0).max(6).required() },
          { is: 'monthly', then: Joi.number().min(1).max(31).required() },
          { is: Joi.valid('daily', 'default'), then: Joi.number().optional() },
        ],
      }),
    }).when('frequency', {
      is: 'default',
      then: Joi.optional(),
      otherwise: Joi.required(),
    }),
  }).optional(),
}).unknown(false);

router.put('/preference', sessionMiddleware({ accessKey: true }), async (req, res) => {
  try {
    if (!req.user) {
      return res.status(403).json({ error: 'Unauthorized' });
    }

    const doc = await Customer.findByPkOrDid(req.user.did as string);
    if (!doc) {
      return res.status(404).json({ error: 'Customer not found' });
    }

    const { error, value } = updatePreferenceSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.message });
    }

    // Get old preference before update
    const oldPreference = doc.preference?.notification;

    await doc.update({ preference: value });

    // Handle notification queue updates if notification preference changed
    if (
      oldPreference?.frequency !== value.notification?.frequency ||
      oldPreference?.schedule?.time !== value.notification?.schedule?.time ||
      oldPreference?.schedule?.date !== value.notification?.schedule?.date
    ) {
      await handleNotificationPreferenceChange(doc.id, value.notification);
    }

    return res.json(doc);
  } catch (err) {
    logger.error('Failed to update customer preference', err);
    return res.status(400).json({ error: `Failed to update preference: ${err.message}` });
  }
});

const updateCustomerSchema = Joi.object({
  metadata: MetadataSchema,
  name: Joi.string().min(2).max(30).empty(''),
  email: Joi.string()
    .custom((value, helpers) => {
      if (!isEmail(value)) {
        return helpers.error('any.invalid');
      }
      return value;
    })
    .empty(''),
  phone: Joi.string().empty(''),
  address: Joi.object({
    country: Joi.string().empty(''),
    state: Joi.string().max(50).empty(''),
    city: Joi.string().max(50).empty(''),
    line1: Joi.string().max(100).empty(''),
    line2: Joi.string().max(100).empty(''),
    postal_code: Joi.string().max(20).empty(''),
  }).empty(''),
}).unknown(true);
// eslint-disable-next-line consistent-return
router.put('/:id', authPortal, async (req, res) => {
  try {
    const doc = await Customer.findByPkOrDid(req.params.id as string);
    if (!doc) {
      return res.status(404).json({ error: 'Customer not found' });
    }

    const raw = pick(req.body, ['metadata', 'name', 'email', 'phone', 'address']);
    const { error } = updateCustomerSchema.validate(raw);
    if (error) {
      return res.status(400).json({ error: error.message });
    }
    if (raw.metadata) {
      raw.metadata = formatMetadata(raw.metadata);
    }

    await doc.update(raw);
    res.json(doc);
  } catch (err) {
    logger.error(err);
    res.json(null);
  }
});

export default router;
