import { middleware } from '@blocklet/payment-vendor';
import { getUrl } from '@blocklet/sdk/lib/component';
import { Router } from 'express';
import Joi from 'joi';

// eslint-disable-next-line import/no-extraneous-dependencies
import { gte } from 'semver';
import { Op } from 'sequelize';
import { MetadataSchema } from '../libs/api';
import { wallet } from '../libs/auth';
import dayjs from '../libs/dayjs';
import env from '../libs/env';
import logger from '../libs/logger';
import { authenticate } from '../libs/security';
import { formatToShortUrl } from '../libs/url';
import { getBlockletJson } from '../libs/util';
import { VendorFulfillmentService } from '../libs/vendor-util/fulfillment';
import { buildRefundInfoFromPayout } from '../libs/vendor-util/tool';
import { CheckoutSession, Invoice, Payout, PaymentCurrency, Subscription } from '../store/models';
import { ProductVendor } from '../store/models/product-vendor';

const VENDOR_DID = {
  launcher: 'z8iZkFBbrVQxZHvcWWB3Sa2TrfGmSeFz9MSU7',
  didnames: 'z2qaGosS3rZ7m5ttP3Nd4V4qczR9TryTcRV4p',
};

const authAdmin = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
const loginAuth = authenticate<CheckoutSession>({ component: true, ensureLogin: true, mine: true });

const createVendorSchema = Joi.object({
  vendor_key: Joi.string().max(50).required(),
  vendor_type: Joi.string().valid('launcher', 'didnames').default('launcher'),
  name: Joi.string().max(255).required(),
  description: Joi.string().max(1000).allow('').optional(),
  app_url: Joi.string().uri().max(512).required(),
  status: Joi.string().valid('active', 'inactive').default('active'),
  metadata: MetadataSchema,
}).unknown(true);

const updateVendorSchema = Joi.object({
  vendor_type: Joi.string().valid('launcher', 'didnames').optional(),
  name: Joi.string().max(255).optional(),
  description: Joi.string().max(1000).allow('').optional(),
  app_url: Joi.string().uri().max(512).optional(),
  status: Joi.string().valid('active', 'inactive').optional(),
  metadata: MetadataSchema,
}).unknown(true);

const vendorIdParamSchema = Joi.object({
  id: Joi.string().max(100).required(),
});

const sessionIdParamSchema = Joi.object({
  sessionId: Joi.string().max(100).required(),
});

const sessionIdsParamSchema = Joi.object({
  sessionIds: Joi.array().items(Joi.string().max(100)).required(),
});

const subscriptionIdParamSchema = Joi.object({
  subscriptionId: Joi.string().max(100).required(),
});

const vendorRedirectQuerySchema = Joi.object({
  vendorId: Joi.string().max(100).required(),
  target: Joi.string().valid('home', 'dashboard').default('home').allow(''),
});

function validateParams(schema: Joi.ObjectSchema) {
  return (req: any, res: any, next: any) => {
    const { error } = schema.validate(req.params);
    if (error) {
      logger.error('Invalid parameters', {
        error,
        params: req.params,
      });
      return res.status(400).json({
        error: 'Invalid parameters',
        message: error.message,
      });
    }
    return next();
  };
}

function validateQuery(schema: Joi.ObjectSchema) {
  return (req: any, res: any, next: any) => {
    const { error, value } = schema.validate(req.query);
    if (error) {
      logger.error('Invalid query parameters', {
        error,
        query: req.query,
      });
      return res.redirect('/404');
    }
    req.query = value;
    return next();
  };
}

async function getAllVendors(_req: any, res: any) {
  try {
    const vendors = await ProductVendor.findAll({
      order: [['created_at', 'DESC']],
    });

    return res.json({
      data: vendors,
      pk: wallet.publicKey,
      total: vendors.length,
    });
  } catch (error: any) {
    logger.error('Failed to get vendors', {
      error,
      stack: error.stack,
    });
    return res.status(500).json({ error: 'Internal server error. Failed to get vendors' });
  }
}

async function getVendorInfo(req: any, res: any) {
  try {
    const vendor = await ProductVendor.findByPk(req.params.id);
    if (!vendor) {
      return res.status(404).json({ error: 'Vendor not found' });
    }

    return res.json({ data: vendor });
  } catch (error: any) {
    logger.error('Failed to get vendor', {
      error,
      id: req.params.id,
    });
    return res.status(500).json({ error: 'Internal server error. Failed to get vendor' });
  }
}

async function prepareVendorData(appUrlInput: string, vendorType: 'launcher' | 'didnames', metadata: any = {}) {
  let appUrl = '';
  let blockletJson = null;
  try {
    appUrl = new URL(appUrlInput).origin;
    blockletJson = await getBlockletJson(appUrl);
  } catch (error) {
    logger.error('Failed to get blocklet json', {
      appUrlInput,
      error,
    });
    return { error: `Invalid app URL: ${appUrlInput}, get blocklet json failed` as const };
  }

  if (!blockletJson?.appId || !blockletJson?.appPk) {
    return { error: `Invalid app URL: ${appUrl}, the appId or appPk is required in the target app` as const };
  }

  const vendorDid = VENDOR_DID[vendorType];
  const component = blockletJson?.componentMountPoints?.find((item: any) => item.did === vendorDid);

  if (!component) {
    return { error: `Invalid app URL: ${appUrl}, the ${vendorType} did is not found in the target server` as const };
  }

  const mountPoint = component.mountPoint || '/';
  return {
    vendor_did: vendorDid,
    app_url: appUrl,
    // Both appPid and appId can be used here for transfer purposes, with did being recommended.
    // Keeping appPid for now due to extensive changes required
    app_pid: blockletJson.appId,
    app_logo: blockletJson.appLogo,
    metadata: {
      ...metadata,
      mountPoint,
    },
    extends: {
      mountPoint,
      appId: blockletJson.appId,
      appPk: blockletJson.appPk,
    },
  };
}

async function createVendor(req: any, res: any) {
  try {
    const { error, value } = createVendorSchema.validate(req.body);
    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        message: error.message,
      });
    }

    const { vendor_key: vendorKey, vendor_type: type, name, description, metadata, status } = value;

    const vendorType = (type || 'launcher') as 'launcher' | 'didnames';

    const existingVendor = await ProductVendor.findOne({
      where: { vendor_key: vendorKey },
    });
    if (existingVendor) {
      return res.status(400).json({ error: 'Vendor key already exists' });
    }

    const preparedData = await prepareVendorData(value.app_url, vendorType, metadata);
    if ('error' in preparedData) {
      return res.status(400).json({ error: preparedData.error });
    }

    const vendor = await ProductVendor.create({
      ...preparedData,
      vendor_key: vendorKey,
      vendor_type: vendorType,
      name,
      description,
      status: status || 'active',
      created_by: req.user?.did || 'admin',
    });

    logger.info('Vendor created', { vendorId: vendor.id, vendorKey: vendor.vendor_key });
    return res.status(201).json({ data: vendor });
  } catch (error: any) {
    logger.error('Failed to create vendor', {
      error,
    });
    return res.status(500).json({ error: 'Internal server error' });
  }
}

async function updateVendor(req: any, res: any) {
  try {
    const vendor = await ProductVendor.findByPk(req.params.id);
    if (!vendor) {
      return res.status(404).json({ error: 'Vendor not found' });
    }

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

    const { vendor_type: type, vendor_key: vendorKey, name, description, status, metadata } = value;

    if (vendorKey && vendorKey !== vendor.vendor_key) {
      const existingVendor = await ProductVendor.findOne({
        where: { vendor_key: vendorKey },
      });
      if (existingVendor) {
        return res.status(400).json({ error: 'Vendor key already exists' });
      }
    }

    const vendorType = (type || 'launcher') as 'launcher' | 'didnames';
    const preparedData = await prepareVendorData(value.app_url, vendorType, metadata);
    if ('error' in preparedData) {
      return res.status(400).json({ error: preparedData.error });
    }

    const updates = {
      ...preparedData,
      vendor_type: vendorType,
      vendor_key: vendorKey,
      name,
      description,
      status,
    };

    await vendor.update(Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined)));

    logger.info('Vendor updated', { vendorId: vendor.id });
    return res.json({ data: vendor });
  } catch (error: any) {
    logger.error('Failed to update vendor', { error, id: req.params.id });
    return res.status(500).json({ error: 'Internal server error' });
  }
}

async function deleteVendor(req: any, res: any) {
  try {
    const vendor = await ProductVendor.findByPk(req.params.id);
    if (!vendor) {
      return res.status(404).json({ error: 'Vendor not found' });
    }

    await vendor.destroy();

    logger.info('Vendor deleted', { vendorId: vendor.id });
    return res.json({ success: true });
  } catch (error: any) {
    logger.error('Failed to delete vendor', { error, id: req.params.id });
    return res.status(500).json({ error: 'Internal server error' });
  }
}

async function testVendorConnection(req: any, res: any) {
  try {
    const vendor = await ProductVendor.findByPk(req.params.id);
    if (!vendor) {
      return res.status(404).json({
        code: 'VENDOR_NOT_FOUND',
        error: 'Vendor not found',
      });
    }

    const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
    const result = await vendorAdapter.connectTest();

    if (result.connected) {
      if (env.appId !== result.did) {
        return res.status(400).json({
          code: 'VENDOR_CONNECT_TEST_FAILED',
          error: 'Vendor connection test failed! Please check the DID in the vendor preferences config',
        });
      }
      return res.status(200).json({ ...result });
    }

    if (result.error) {
      return res.status(400).json({
        code: 'VENDOR_CONNECT_TEST_FAILED',
        error: result.error,
      });
    }

    if (!result.sdkVersion) {
      return res.status(400).json({
        code: 'VENDOR_CONNECT_TEST_FAILED',
        error: 'Vendor SDK version is too low. Please upgrade to the latest version (>=1.21.4)',
      });
    }

    return res.status(400).json({ ...result });
  } catch (error: any) {
    logger.error('Failed to test vendor connection', { error, id: req.params.id });
    return res.status(500).json({ error: 'Internal server error' });
  }
}

async function executeVendorOperation({
  vendorId,
  orderId,
  operation,
  shortUrl,
  appUrl,
  ownerDid,
  appDid,
}: {
  vendorId: string;
  orderId: string;
  operation: 'getOrder' | 'getOrderStatus';
  shortUrl: boolean;
  appUrl?: string;
  ownerDid?: string;
  appDid?: string;
}) {
  if (!vendorId || !orderId) {
    return {
      error: 'Bad Request',
      message: `vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`,
      code: 400,
    };
  }

  const vendor = await ProductVendor.findByPk(vendorId);
  if (!vendor) {
    return {
      error: 'Not Found',
      message: `vendor not found, vendorId: ${vendorId}`,
      code: 404,
    };
  }

  try {
    const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
    const data = await vendorAdapter[operation](vendor, orderId, { shortUrl, appUrl, ownerDid, appDid });

    return { data: { ...data, vendorType: vendor.vendor_type }, code: 200 };
  } catch (error: any) {
    const operationName = operation === 'getOrder' ? 'order' : 'order status';
    logger.error(`Failed to get vendor ${operationName}`, {
      error,
      vendorId,
      orderId,
      vendorKey: vendor.vendor_key,
    });
    return {
      error: 'Service Unavailable',
      message: `Failed to get vendor ${operationName}`,
      code: 503,
    };
  }
}

async function processVendorOrders(
  sessionId: string,
  operation: 'getOrder' | 'getOrderStatus',
  shortUrl: boolean,
  ownerDid?: string,
  appDid?: string
) {
  const doc = await CheckoutSession.findByPk(sessionId);

  if (!doc) {
    return {
      code: 404,
      error: 'CheckoutSession not found',
      vendors: [],
    };
  }

  // FIXME: will remove payment_status @pengfei
  const paymentStatus = doc.status === 'complete' ? 'paid' : 'unpaid';

  if (doc.status !== 'complete') {
    return {
      payment_status: paymentStatus,
      session_status: doc.status,
      error: 'CheckoutSession not complete',
      vendors: [],
    };
  }
  if (!doc.vendor_info) {
    return {
      payment_status: paymentStatus,
      session_status: doc.status,
      error: 'Vendor info not found',
      vendors: [],
    };
  }

  const vendors = doc.vendor_info.map(async (item) => {
    if (!item.order_id) {
      return {
        key: item.vendor_key,
        progress: 0,
        status: 'pending',
        vendorType: item.vendor_type,
        appUrl: item.app_url,
      };
    }

    const result = await executeVendorOperation({
      vendorId: item.vendor_id,
      orderId: item.order_id,
      operation,
      shortUrl,
      appUrl: item.app_url,
      ownerDid,
      appDid,
    });

    // Handle error responses from vendor functions
    if (result.error) {
      logger.warn('Vendor operation returned error', {
        vendorId: item.vendor_id,
        orderId: item.order_id,
        operation,
        error: result.error,
        message: result.message,
      });
      return {
        key: item.vendor_key,
        error: result.error,
        message: result.message,
        status: 'error',
        vendorType: item.vendor_type,
        appUrl: item.app_url,
      };
    }

    return result.data;
  });

  return {
    payment_status: doc.payment_status,
    session_status: doc.status,
    preferred_locale: doc.metadata?.preferred_locale,
    vendors: await Promise.all(vendors),
    subscriptionId: doc.subscription_id,
    data: {
      preferredLocale: doc.metadata?.preferred_locale,
    },
    error: null,
  };
}

async function getVendorStatus(sessionId: string) {
  const result: any = await processVendorOrders(sessionId, 'getOrderStatus', false);

  if (result.subscriptionId) {
    const subscriptionUrl = getUrl(`/customer/subscription/${result.subscriptionId}`);

    result.subscriptionUrl = await formatToShortUrl({
      url: subscriptionUrl,
      maxVisits: 5,
      validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
    });
  }

  return result;
}

async function getVendorFulfillmentStatus(req: any, res: any) {
  const { sessionId } = req.params;

  try {
    const status = await getVendorStatus(sessionId);

    if (status.code) {
      return res.status(status.code).json({ error: status.error });
    }
    return res.json(status);
  } catch (error: any) {
    return res.status(500).json({ error: error.message });
  }
}

async function getVendorFulfillmentDetail(req: any, res: any) {
  const { sessionId } = req.params;
  const { shortUrl, ownerDid, appDid } = req.query;

  try {
    const detail = await processVendorOrders(sessionId, 'getOrder', shortUrl === 'true', ownerDid, appDid);
    if (detail.code) {
      return res.status(detail.code).json({ error: detail.error });
    }
    return res.json(detail);
  } catch (error: any) {
    return res.status(500).json({ error: error.message });
  }
}

async function redirectToVendor(req: any, res: any) {
  const { subscriptionId } = req.params;
  const { vendorId, target } = req.query;

  try {
    const checkoutSession = await CheckoutSession.findBySubscriptionId(subscriptionId);
    if (!checkoutSession) {
      logger.warn('CheckoutSession not found for subscription[redirect to vendor]', { subscriptionId });
      return res.redirect('/404');
    }

    const order = checkoutSession?.vendor_info?.find((item) => item.vendor_id === vendorId);
    if (!order) {
      logger.warn('Vendor order not found in checkout session[redirect to vendor]', {
        subscriptionId,
        vendorId,
        availableVendors: checkoutSession.vendor_info?.map((v) => v.vendor_key) || [],
      });
      return res.redirect('/404');
    }

    const isOwner = req.user.did === checkoutSession.customer_did;

    if (!isOwner) {
      if (order.app_url) {
        return res.redirect(order.app_url);
      }
      return res.redirect('/404');
    }

    const result = await executeVendorOperation({
      vendorId,
      orderId: order.order_id || '',
      operation: 'getOrder',
      shortUrl: false,
      appUrl: order.app_url,
    });
    if (result.error || !result.data) {
      logger.warn('Vendor status detail not found', {
        subscriptionId,
        vendorId,
        orderId: order.order_id,
        error: result.error,
        message: result.message,
      });
      return res.redirect('/404');
    }

    const redirectUrl = target === 'dashboard' ? result.data.dashboardUrl : result.data.homeUrl;
    return res.redirect(redirectUrl);
  } catch (error: any) {
    logger.error('Failed to redirect to vendor service', {
      error,
      subscriptionId,
      vendorId,
      target,
    });
    return res.redirect('/404');
  }
}

async function getCancelledSessions(req: any, res: any) {
  const { error } = sessionIdsParamSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.message });
  }

  const { sessionIds = [] } = req.body;

  const allCheckoutSessions = await CheckoutSession.findAll({
    where: { id: { [Op.in]: sessionIds } },
    attributes: ['id', 'subscription_id', 'fulfillment_status', 'vendor_info'],
  });

  const subscriptionIds = allCheckoutSessions.map((item) => item.subscription_id!).filter((item) => !!item);

  const cancelledSubscriptions = await Subscription.findAll({
    where: {
      id: { [Op.in]: subscriptionIds },
      status: 'canceled',
    },
    attributes: ['id'],
  });

  const cancelledSubIds = cancelledSubscriptions.map((item) => item.id);
  const cancelledSessions = allCheckoutSessions.filter(
    (item) => item.subscription_id && cancelledSubIds.includes(item.subscription_id)
  );
  return res.json({ cancelledSessions });
}

async function getVendorSubscription(req: any, res: any) {
  const { sessionId } = req.params;

  const checkoutSession = await CheckoutSession.findByPk(sessionId);

  if (!checkoutSession) {
    return res.status(404).json({ error: 'Checkout session not found' });
  }

  const subscription = await Subscription.findByPk(checkoutSession.subscription_id);

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

  const invoices = await Invoice.findAll({
    where: { subscription_id: subscription.id },
    order: [['created_at', 'DESC']],
    attributes: [
      'id',
      'amount_due',
      'amount_paid',
      'amount_remaining',
      'status',
      'currency_id',
      'period_start',
      'period_end',
      'created_at',
      'due_date',
      'description',
      'invoice_pdf',
    ],
    include: [
      {
        model: PaymentCurrency,
        as: 'paymentCurrency',
        attributes: ['id', 'symbol', 'decimal', 'name', 'logo'],
        required: false,
      },
    ],
    limit: 20,
  });

  return res.json({
    subscription: subscription.toJSON(),
    billing_history: invoices,
  });
}

async function handleSubscriptionRedirect(req: any, res: any) {
  const { sessionId } = req.params;

  const checkoutSession = await CheckoutSession.findByPk(sessionId);
  if (!checkoutSession) {
    return res.status(404).json({ error: 'Checkout session not found' });
  }

  return res.redirect(getUrl(`/customer/subscription/${checkoutSession.subscription_id}`));
}

async function vendorRefund(req: any, res: any) {
  const { id } = req.params;
  const { reason } = req.body;
  try {
    const payout = await Payout.findByPk(id);

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

    if (payout.status !== 'paid') {
      return res.status(400).json({ error: 'Only paid payouts can be refunded' });
    }

    if (!payout.vendor_info?.vendor_id || !payout.vendor_info?.order_id) {
      return res.status(400).json({ error: 'Payout has no vendor information' });
    }

    const vendor = await ProductVendor.findByPk(payout.vendor_info.vendor_id);
    if (!vendor) {
      return res.status(404).json({ error: 'Vendor not found' });
    }

    let refundInfo;
    try {
      refundInfo = await buildRefundInfoFromPayout(payout, reason);
    } catch (err: any) {
      return res.status(400).json({ error: err.message });
    }

    const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
    if (!vendorAdapter) {
      return res.status(404).json({ error: `No adapter found for vendor: ${vendor.vendor_key}` });
    }

    logger.info('Requesting refund from vendor', {
      payoutId: payout.id,
      vendorId: vendor.id,
      orderId: payout.vendor_info.order_id,
      refundInfo,
      requestedBy: req.user?.did,
    });

    const result = await vendorAdapter.requestRefund({
      ...refundInfo,
      orderId: payout.vendor_info.order_id,
    });

    if (result.data?.refundResult?.status === 'succeeded') {
      logger.info('Vendor payout refund successful', {
        payoutId: payout.id,
        orderId: payout.vendor_info.order_id,
        refundResult: result.data?.refundResult,
      });
      await payout.update({ status: 'reverted', description: reason || 'Refund requested by admin' });
      return res.json({ data: result.data?.refundResult });
    }
    logger.error('Vendor payout refund failed', {
      payoutId: payout.id,
      orderId: payout.vendor_info.order_id,
      refundInfo,
      refundResult: result.data?.refundResult,
    });
    return res.status(400).json({
      error: result.message || 'Vendor payout refund failed',
      data: result.data?.refundResult,
      requestedBy: req.user?.did,
    });
  } catch (err: any) {
    logger.error('Request refund failed', {
      error: err,
      payoutId: req.params.id,
      requestedBy: req.user?.did,
    });
    return res.status(500).json({ error: err.message || 'Failed to request refund' });
  }
}

function getVendorConnectTest(req: any, res: any) {
  const sdkVersion = req.headers['x-broker-vendor-version'];
  if (sdkVersion && gte(sdkVersion, '1.21.4')) {
    return res.json({ connected: true, sdkVersion });
  }
  return res.json({
    connected: false,
    sdkVersion,
    error: 'Vendor SDK version is too low, please upgrade to the latest version (>=1.21.4)',
  });
}

const router = Router();

const ensureVendorAuth = middleware.ensureVendorAuth((vendorPk: string) =>
  ProductVendor.findOne({ where: { 'extends.appPk': vendorPk } }).then((v) => v as any)
);

router.get('/order/:sessionId/status', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
router.get('/order/:sessionId/detail', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);

// Those for Vendor Call
router.get('/connectTest', ensureVendorAuth, getVendorConnectTest);
router.post('/subscription/cancelled', ensureVendorAuth, getCancelledSessions);
router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);

router.get(
  '/open/:subscriptionId',
  loginAuth,
  validateParams(subscriptionIdParamSchema),
  validateQuery(vendorRedirectQuerySchema),
  redirectToVendor
);

router.post('/refund/:id', authAdmin, vendorRefund);

router.get('/', getAllVendors);
router.get('/:id', authAdmin, validateParams(vendorIdParamSchema), getVendorInfo);
router.post('/', authAdmin, createVendor);
router.put('/:id', authAdmin, validateParams(vendorIdParamSchema), updateVendor);
router.delete('/:id', authAdmin, validateParams(vendorIdParamSchema), deleteVendor);

router.post('/:id/test-connection', authAdmin, validateParams(vendorIdParamSchema), testVendorConnection);

export default router;
