import bodyParser from 'body-parser';
import dotenv from 'dotenv';
import normalizeEmail from 'normalize-email';
import Stripe from 'stripe';
import { sendDiscordPaymentErrorMessage } from '../cron/notification.js';
import query, { transaction } from '../server-utils/query.js';
import { declareHandler } from '../server-utils/routesHandler.js';
import trackServerEvent, { ActivityType } from '../utils/trackServerEvent.js';
import { isEmailable } from './emailSender.js';
import {
  getUserFirstName,
  sendAbandonedCartReengagementEmailToUser,
  sendIndieWelcomeEmailToUserId,
  sendProWelcomeEmailToUserId,
} from './sendgrid.js';
import Sentry from './sentry.js';

dotenv.config();

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2022-11-15' });

const canSubscribeWithStripe = (email: string) => isEmailable(email);

const endpointSecret = process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET;

export const resolvePlanId = async (metadataPlanId: string, trialEnd: number | null, priceId: string) => {
  const isTrial = !!(trialEnd && trialEnd > Date.now() / 1000);
  if (priceId === process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID) {
    return await query(`SELECT id FROM plans WHERE price_id_env_var = $1 AND is_trial = $2`, [
      'STRIPE_PRO_SUBSCRIPTION_PRICE_ID',
      isTrial,
    ]).then((res) => res.rows[0].id);
  } else if (priceId === process.env.STRIPE_INDIE_SUBSCRIPTION_PRICE_ID) {
    return await query(`SELECT id FROM plans WHERE price_id_env_var = $1`, ['STRIPE_INDIE_SUBSCRIPTION_PRICE_ID']).then(
      (res) => res.rows[0].id
    );
  } else if (metadataPlanId) {
    return Number(metadataPlanId);
  } else {
    throw new Error(`Could not resolve plan id for price id ${priceId}`);
  }
};

export const handleStripeWebhook = declareHandler({
  middleware: bodyParser.raw({ type: '*/*' }),
  func: async (req, res) => {
    const sig = req.headers['stripe-signature'];

    let event;

    try {
      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    } catch (err) {
      console.error(err);
      Sentry.captureException(err);
      res.status(400).send(`Webhook Error: ${err.message}`);
      return;
    }

    console.log('webhook:', event.type);

    // Handle the event
    if (event.type === 'invoice.payment_succeeded') {
      const invoice = event.data.object as Stripe.Invoice;
      const items = invoice.lines.data as Stripe.InvoiceLineItem[];
      const subscription = items.find((item) => item.type === 'subscription') as Stripe.InvoiceLineItem & {
        trial_end: number;
        subscription: string;
      };

      // This will happen if the user updated a subscription, but did not create one.
      if (!subscription) {
        res.send();
        return;
      }

      const subscribedUntil = new Date(subscription.period.end * 1000);

      await transaction(async (query) => {
        try {
          if (subscription.metadata.user_id) {
            await query(`UPDATE users SET stripe_customer_id = $1 WHERE id = $2`, [
              invoice.customer as string,
              subscription.metadata.user_id,
            ]);
          } else {
            await query(`UPDATE users SET stripe_customer_id = $1 WHERE normalized_email = $2`, [
              invoice.customer as string,
              normalizeEmail(invoice.customer_email),
            ]);
          }

          const trialEnd =
            invoice.billing_reason === 'subscription_create' && subscription.metadata.plan_id === '2'
              ? subscription.period?.end
              : null;

          const insertResult = await query(
            `INSERT INTO subscription_updates (user_id, subscribed_until, amount_paid_cents_usd, will_renew, plan_id) VALUES ((SELECT id FROM users WHERE stripe_customer_id = $1), $2, $3, true, $4) RETURNING id`,
            [
              invoice.customer as string,
              subscribedUntil,
              invoice.amount_paid,
              await resolvePlanId(subscription.metadata.plan_id, trialEnd, subscription.price.id),
            ]
          );

          if (!insertResult.rows?.[0]?.id) {
            console.log(
              `Subscription update failed for stripe customer ${invoice.customer as string} at ${new Date()}}`
            );
            Sentry.captureMessage(`Subscription update not inserted.`);
            throw new Error('Subscription update not inserted');
          } else {
            console.log(`Subscription update inserted with id ${insertResult.rows[0].id} at ${new Date()}}`);
          }

          let user;
          if (subscription.metadata.user_id) {
            const userResult = await query(`SELECT * FROM users WHERE id = $1`, [subscription.metadata.user_id]);
            user = userResult?.rows?.[0];
          } else {
            const userResult = await query(`SELECT * FROM users WHERE normalized_email = $1`, [
              normalizeEmail(invoice.customer_email),
            ]);
            user = userResult?.rows?.[0];
          }

          if (!user) {
            throw new Error(`User with ${subscription.metadata.user_id || invoice.customer_email} not found.`);
          }

          if (!canSubscribeWithStripe(user.email)) {
            Sentry.captureMessage(
              `Stripe invoice.payment_succeeded webhook triggered for user ${user.id}, who is not stripe-eligible.`
            );
          }

          const subscriptionActivityBefore1HrAgo = await query(
            `SELECT * FROM subscription_updates WHERE user_id = $1 AND created_at < NOW() - INTERVAL '1 hour'`,
            [user.id]
          );
          if (subscriptionActivityBefore1HrAgo.rows.length === 0) {
            if (subscription.plan.id === process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID) {
              await sendProWelcomeEmailToUserId(user.email, user.display_name);
            } else if (subscription.plan.id === process.env.STRIPE_INDIE_SUBSCRIPTION_PRICE_ID) {
              await sendIndieWelcomeEmailToUserId(user.email, user.display_name);
            }
          }

          trackServerEvent(user.id, {
            name: ActivityType.StripePaymentSucceeded,
            properties: {
              amount: invoice.amount_paid,
              subscription: subscription.subscription,
              plan: subscription.plan.id,
            },
          });
        } catch (e) {
          Sentry.captureException(e);
          console.error(e);
          res.status(500).send(`Webhook Error: ${e.message}`);
          return;
        }
      });
    } else if (event.type === 'customer.subscription.updated') {
      try {
        const subscription = event.data.object;
        const subscribedUntil = new Date(subscription.current_period_end * 1000);

        const userId =
          subscription.metadata.user_id ||
          (await query(`SELECT id FROM users WHERE stripe_customer_id = $1`, [subscription.customer]).then(
            (res) => res.rows[0].id
          ));

        if (!userId) {
          throw new Error(
            `User with metadata user id "${subscription.metadata.user_id}" or customer id "${subscription.customer}" not found.`
          );
        }

        // note: subscription.plan.id is the price id
        const planId = await resolvePlanId(subscription.metadata.plan_id, subscription.trial_end, subscription.plan.id);

        const existingIdenticalRow = await query(
          `
            SELECT * FROM subscription_updates
            WHERE user_id = $1
            AND subscribed_until = $2
            AND amount_paid_cents_usd = 0
            AND will_renew = $3
            AND plan_id = $4
            AND created_at = (SELECT MAX(created_at) FROM subscription_updates WHERE user_id = $1)
          `,
          [userId, subscribedUntil, !subscription.cancel_at_period_end, planId]
        );

        if (existingIdenticalRow.rows.length === 0) {
          await query(
            `
              INSERT INTO subscription_updates
              (user_id, subscribed_until, amount_paid_cents_usd, will_renew, plan_id)
              VALUES
              ($1, $2, 0, $3, $4)
            `,
            [userId, subscribedUntil, !subscription.cancel_at_period_end, planId]
          );
        }

        // If the subscription was cancelled, send a cancellation confirmation email.
        if (event.data.object.cancel_at_period_end && !event.data.previous_attributes.cancel_at_period_end) {
          const userResult = await query(
            `
              SELECT
                u.email,
                u.display_name,
                su.subscribed_until
              FROM users u
              JOIN subscription_updates su ON u.id = su.user_id
              WHERE u.id = $1
              ORDER BY su.created_at DESC LIMIT 1
            `,
            [userId]
          );

          const email = userResult?.rows?.[0].email;
          const subscribedUntil = userResult?.rows?.[0].subscribed_until;
          const displayName = userResult?.rows?.[0].display_name;
          const firstName = await getUserFirstName(userId);

          if (!canSubscribeWithStripe(email)) {
            Sentry.captureMessage(
              `Stripe customer.subscription.updated webhook triggered for user ${userId}, who is not stripe-eligible.`
            );
          }

          const tier =
            subscription.plan.id === process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID
              ? 'Pro'
              : subscription.plan.id === process.env.STRIPE_INDIE_SUBSCRIPTION_PRICE_ID
              ? 'Indie'
              : undefined;

          // sendCancellationConfirmationEmailToUser(email, firstName || displayName, subscribedUntil, tier);
          // sendFoundersCancellationOutreachEmailToUser(email, firstName || displayName, userId);

          // create a cancellation details record for cancellation request - a separate row for finalisation will be created on delete event
          await query(
            `
              INSERT INTO subscription_cancellation_details
              (user_id, reason, feedback, comment)
              VALUES
              ($1, $2, $3, $4)
            `,
            [
              userId,
              subscription.cancellation_details.reason,
              subscription.cancellation_details.feedback,
              subscription.cancellation_details.comment,
            ]
          );
        }

        // If the subscription was reactivated, create an appropriate cancellation details record.
        if (event.data.previous_attributes.cancel_at_period_end && !event.data.object.cancel_at_period_end) {
          await query(
            `
              INSERT INTO subscription_cancellation_details
              (user_id, reason, feedback, comment, status)
              VALUES
              ($1, $2, $3, $4, 'reactivated')
            `,
            [
              userId,
              subscription.cancellation_details.reason,
              subscription.cancellation_details.feedback,
              subscription.cancellation_details.comment,
            ]
          );
        }
      } catch (e) {
        Sentry.captureException(e);
        console.error(e);
        res.status(500).send(`Webhook Error: ${e.message}`);
        return;
      }
    } else if (event.type === 'customer.subscription.deleted') {
      try {
        const subscription = event.data.object;

        const userResult = await query(`SELECT email, display_name, id FROM users WHERE stripe_customer_id = $1`, [
          subscription.customer,
        ]);

        const userId = subscription.metadata.user_id || userResult?.rows?.[0].id;
        const email = userResult?.rows?.[0].email;
        const displayName = userResult?.rows?.[0].display_name;
        const firstName = await getUserFirstName(userId);

        if (!userId) {
          throw new Error(
            `User with metadata user id "${subscription.metadata.user_id}" or customer id "${subscription.customer}" not found.`
          );
        }

        const tier =
          subscription.plan.id === process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID
            ? 'Pro'
            : subscription.plan.id === process.env.STRIPE_INDIE_SUBSCRIPTION_PRICE_ID
            ? 'Indie'
            : undefined;

        await query(
          `
            INSERT INTO subscription_cancellation_details
            (user_id, reason, feedback, comment, status)
            VALUES
            ($1, $2, $3, $4, 'finalised')
          `,
          [
            userId,
            subscription.cancellation_details.reason,
            subscription.cancellation_details.feedback,
            subscription.cancellation_details.comment,
          ]
        );

        if (!canSubscribeWithStripe(email)) {
          Sentry.captureMessage(
            `Stripe customer.subscription.deleted webhook triggered for user ${userId}, who is not stripe-eligible.`
          );
        }

        // sendSubscriptionEndedEmailToUser(email, firstName || displayName, tier);
      } catch (e) {
        Sentry.captureException(e);
        console.error(e);
        res.status(500).send(`Webhook Error: ${e.message}`);
        return;
      }
    } else if (event.type === 'checkout.session.expired') {
      try {
        const session = event.data.object;
        const email = session.customer_email;

        const userResult = await query(`SELECT display_name, id FROM users WHERE email = $1`, [
          email, // Use customer_email instead of customer since the customer might not exist on Stripe yet
        ]);

        if (userResult.rows.length === 0) {
          await sendDiscordPaymentErrorMessage(`
            Error: No user in DB has the following customer email: "${email}".\n
            Event type: "${event.type}"\n
            Event object:\n
              \`\`\`\n
                ${JSON.stringify(event.data.object)}\n
              \`\`\`
          `);

          throw new Error(`No user in DB has the following customer email: "${email}".`);
        }

        const displayName = userResult?.rows?.[0]?.display_name;
        const firstName = await getUserFirstName(userResult?.rows?.[0].id);
        const userId = session.metadata.user_id || userResult?.rows?.[0].id;

        if (!userId) {
          throw new Error(
            `User with metadata user id "${session.metadata.user_id}" or customer email "${email}" not found.`
          );
        }

        if (!canSubscribeWithStripe(email)) {
          Sentry.captureMessage(
            `Stripe checkout.session.expired webhook triggered for user ${userId}, who is not stripe-eligible.`
          );
        }

        sendAbandonedCartReengagementEmailToUser(email, firstName || displayName);
      } catch (e) {
        Sentry.captureException(e);
        console.error(e);
        res.status(500).send(`Webhook Error: ${e.message}`);
        return;
      }
    } else {
      console.log(`Unhandled event type ${event.type}`);
    }

    // Return a 200 response to acknowledge receipt of the event
    res.send();
  },
});

export const getPlanSettings = async (name: string) => {
  // Get the newest matching plan that hasn't expired yet.
  const result = await query(
    `
      SELECT id, price_id_env_var, is_trial, trial_days
      FROM plans
      WHERE name = $1
      AND (active_until IS NULL OR active_until > NOW())
      ORDER BY created_at DESC LIMIT 1
    `,
    [name]
  );
  if (!result?.rows?.[0]) {
    return null;
  }
  const { id, price_id_env_var, is_trial, trial_days } = result.rows[0];
  return {
    planId: id,
    priceIdEnvVar: price_id_env_var,
    isTrial: is_trial,
    trialDays: trial_days,
  };
};

const getActiveSubscription = async (userId: string) => {
  const result = await query(
    `
      SELECT name, price_id_env_var
      FROM plans
      INNER JOIN subscription_updates
      ON plans.id = subscription_updates.plan_id
      WHERE user_id = $1
      AND subscribed_until > NOW()
      ORDER BY subscription_updates.created_at DESC LIMIT 1
    `,
    [userId]
  );
  return result?.rows?.[0] as { name: string; price_id_env_var: string } | undefined;
};

export const checkoutSessionFunction = declareHandler({
  func: async (req, res, data) => {
    const { appUri } = data;
    if (!req.user || !req.user.email) {
      return res.status(401).send({ error: 'Unauthorized - you may need to log in again!' });
    }
    if (!canSubscribeWithStripe(req.user.email)) {
      return res
        .status(400)
        .send({ error: 'Please subscribe through third-party marketplace that you used to install WavTool.' });
    }
    const plan = req.body.plan;
    const newPlanSettings = await getPlanSettings(plan);
    if (!newPlanSettings) {
      return res.status(400).send({ error: 'Invalid plan' });
    }
    const { planId, priceIdEnvVar, isTrial, trialDays } = newPlanSettings;
    const priceId = process.env[priceIdEnvVar];
    const activeSubscription = await getActiveSubscription(req.user.id);

    if (activeSubscription?.name === plan) {
      res.status(400).send({ error: 'You already have this plan!' });
    } else if (activeSubscription && isTrial) {
      res.status(400).send({ error: 'Trials are not available for users with an active subscription.' });
    } else if (activeSubscription) {
      // user is upgrading or downgrading. rather than creating a new subscription, we have to update the existing one.
      const customerId = await query(`SELECT stripe_customer_id FROM users WHERE id = $1`, [req.user.id]).then(
        (res) => res.rows[0].stripe_customer_id
      );

      const subscriptions = await stripe.subscriptions.list({ customer: customerId }).then((result) => result.data);

      let subscriptionToUpdate = subscriptions[0];
      if (subscriptions.length > 1) {
        // If we somehow have more than one subscription, update the one that corresponds to the row in the DB.
        Sentry.captureMessage(`User ${req.user.id} has more than one active subscription!`);
        subscriptionToUpdate =
          subscriptions.find((s) => s.items?.data?.[0].price?.id === priceId) || subscriptionToUpdate;
      }

      if (!subscriptionToUpdate) {
        throw new Error(`Active subscription could not be found on Stripe!`);
      }

      // This should trigger the appropriate stripe webhooks, which will in turn update the DB.
      await stripe.subscriptions.update(subscriptionToUpdate.id, {
        proration_behavior: 'always_invoice',
        cancel_at_period_end: false,
        metadata: {
          user_id: req.user.id,
          plan_id: planId,
        },
        items: [
          {
            id: subscriptionToUpdate.items.data[0].id,
            price: priceId,
          },
        ],
        discounts:
          priceId === process.env.STRIPE_INDIE_SUBSCRIPTION_PRICE_ID
            ? [
                {
                  coupon: 'indiepromo2024',
                },
              ]
            : [],
      } as Stripe.SubscriptionUpdateParams);

      const portalSession = await stripe.billingPortal.sessions.create({
        customer: customerId,
        return_url: appUri,
      });

      res.status(200).send({ url: portalSession.url });
    } else {
      try {
        const session = await stripe.checkout.sessions.create({
          billing_address_collection: 'auto',
          customer_email: req.user.email,
          automatic_tax: {
            enabled: true,
          },
          line_items: [
            {
              price: priceId,
              quantity: 1,
            },
          ],
          subscription_data: {
            trial_settings: isTrial ? { end_behavior: { missing_payment_method: 'cancel' } } : undefined,
            trial_period_days: isTrial ? trialDays : undefined,
            metadata: {
              user_id: req.user.id,
              plan_id: planId,
            },
          },
          mode: 'subscription',
          success_url: `${appUri}/upgrade-return`,
          cancel_url: `${appUri}`,
          expires_at: Math.floor(Date.now() / 1000) + 2 * 60 * 60, // Add 2 hours to the current time
          consent_collection: {
            promotions: 'auto',
          },
          after_expiration: {
            recovery: {
              enabled: true,
            },
          },
          // Stripe does not allow us to include both parameters in the same session
          ...(priceId === process.env.STRIPE_INDIE_SUBSCRIPTION_PRICE_ID
            ? {
                discounts: [{ coupon: 'indiepromo2024' }],
              }
            : {
                allow_promotion_codes: true,
              }),
        });

        res.status(200).send({ url: session.url });
      } catch (e) {
        // Errors raised by Stripe's library have a strange effect on Express CORS headers
        throw new Error(`Failed to create stripe checkout session: ${e.message}`);
      }
    }
  },
});
export const portalSessionFunction = declareHandler({
  func: async (req, res, data) => {
    const { appUri } = data;
    if (!req.user || !req.user.email) {
      return res.status(401).send({ error: 'Unauthorized - you may need to log in again!' });
    }

    if (!canSubscribeWithStripe(req.user.email)) {
      return res
        .status(400)
        .send({ error: 'Please subscribe through third-party marketplace that you used to install WavTool.' });
    }

    const returnUrl = appUri;

    const customer_id = await query(`SELECT stripe_customer_id FROM users WHERE id = $1`, [req.user.id]).then(
      (res) => res.rows[0].stripe_customer_id
    );

    const portalSession = await stripe.billingPortal.sessions.create({
      customer: customer_id,
      return_url: returnUrl,
    });

    res.status(200).send({ url: portalSession.url });
  },
});
