import React, { useRef, useState } from "react";
import {
  CardComponent,
  CardCVV,
  CardExpiry,
  CardNumber
} from "@chargebee/chargebee-js-react-wrapper";
import {
  PaymentElement,
  useStripe,
  useElements,
  Elements
} from "@stripe/react-stripe-js";
import { Form, FormGroup, Spinner } from "react-bootstrap";
import { useForm } from "react-hook-form";
import * as Sentry from "@sentry/react";
import { ContinueButton, FormItem } from "../components/atoms";
import { StagerComponent } from "../components/stager";
import { Summary } from "../components/summary";
import { sortedCountries } from "../constants";
import { useCSSStyle } from "../hooks/util";
import ddLogo from "../images/dd_logo_landscape.png";
import { PaymentMethodDDSchema, FormSchema, formatPhoneE164, validate } from "../schema";

import { get as getEnv, getStr as getEnvStr, getPaymentProviders, resolveStripePaymentMethodTypes, PaymentMethod, PaymentProvider } from "../env";
import { loadStripe } from "@stripe/stripe-js";
import type { StripeError } from "@stripe/stripe-js";
import { usePostResource } from "../services/rest-resource.service";
import { SAVED_STATE_KEY } from "../services/router.service";

const STRIPE_CODE_MESSAGES: Partial<Record<string, string>> = {
  payment_intent_authentication_failure:
    "We were unable to authenticate your payment. Please try a different payment method.",
  payment_intent_timeout: "Payment timed out. Please try again.",
};

const TECHNICAL_ERROR_TYPES = new Set([
  "api_error",
  "invalid_request_error",
  "authentication_error",
  "rate_limit_error",
  "idempotency_error",
  "stripe_invalid_response_error",
]);

const GENERIC_PAYMENT_ERROR =
  "An error occurred processing your payment. Please try again.";

function formatStripeError(error: StripeError): string {
  if (error.code && STRIPE_CODE_MESSAGES[error.code]) {
    return STRIPE_CODE_MESSAGES[error.code]!;
  }
  if (!TECHNICAL_ERROR_TYPES.has(error.type)) {
    return error.message ?? GENERIC_PAYMENT_ERROR;
  }
  return GENERIC_PAYMENT_ERROR;
}

export const PaymentDetailsPage: StagerComponent<FormSchema> = ({
  data,
  setData,
  onCompleted
}) => {
  const renderForm = () => {
    // Get first provider that matches selected payment method
    const paymentProviders = getPaymentProviders();
    let paymentProvider: PaymentProvider | null = null;
    for (const provider of Object.keys(paymentProviders)) {
      const methods = paymentProviders[provider as PaymentProvider];
      if (methods?.includes(data.paymentMethod as PaymentMethod)) {
        paymentProvider = provider as PaymentProvider;
        break;
      }
    }

    if (paymentProvider === "stripe") {
      return <StripePaymentPage data={data} setData={setData} onCompleted={onCompleted} />;
    }

    if (paymentProvider === "gocardless") {
      return <GocardlessPaymentPage data={data} setData={setData} onCompleted={onCompleted} />;
    }

    if (paymentProvider === "chargebee") {
      return <ChargebeePaymentPage data={data} setData={setData} onCompleted={onCompleted} />;
    }

    return <p>Error: no payment providers available. Please contact us.</p>;
  };

  return renderForm();
};

const GocardlessPaymentPage: StagerComponent<FormSchema> = ({
  data,
  onCompleted
}) => {
  const form = useForm({
    defaultValues: {
      ddAccountHolderName: [data.firstName, data.lastName].join(" "),
      ...data
    },
    resolver: validate(PaymentMethodDDSchema)
  });
  const organisation = getEnv("ORGANISATION_NAME");

  return (
    <form
      className="form-content"
      noValidate
      onSubmit={form.handleSubmit(onCompleted)}
    >
      <div className="mb-4">
        <Summary data={data} />
      </div>
      <section className="form-section">
        <h2>Your bank details</h2>
        <FormItem form={form} label="Account Name" name="ddAccountHolderName">
          <Form.Control />
        </FormItem>
        <FormItem form={form} label="Account Number" name="ddAccountNumber">
          <Form.Control />
        </FormItem>
        <FormItem form={form} label="Sort Code" name="ddSortCode">
          <Form.Control />
        </FormItem>
        <FormItem form={form} name="ddConfirmAccountHolder">
          <Form.Check label="I confirm that I am the account holder and am authorised to set up Direct Debit payments on this account." />
        </FormItem>
        {getEnv("IS_UPDATE_FLOW") ? (
          <FormItem label="Country" form={form} name="addressCountry">
            <Form.Control
              autoComplete="country"
              as="select"
              className="form-control"
            >
              {sortedCountries.map((c) => (
                <option key={c.numeric} value={c.alpha2}>
                  {c.name}
                </option>
              ))}
            </Form.Control>
          </FormItem>
        ) : null}
      </section>

      <section className="form-section">
        <img
          className="img-blend"
          alt="Direct Debit logo"
          width={200}
          src={ddLogo}
        />
        <h2 className="mt-2">The Direct Debit Guarantee</h2>
        <ul className="fineprint">
          <li>
            The Guarantee is offered by all banks and building societies that
            accept instructions to pay Direct Debits
          </li>
          <li>
            If there are any changes to the amount, date or frequency of your
            Direct Debit {organisation} will notify you (normally 3 working
            days) in advance of your account being debited or as otherwise
            agreed. If you request {organisation} to collect a payment,
            confirmation of the amount and date will be given to you at the time
            of the request
          </li>
          <li>
            If an error is made in the payment of your Direct Debit, by the
            organisation or your bank or building society, you are entitled to a
            full and immediate refund of the amount paid from your bank or
            building society
          </li>
          <li>
            If you receive a refund you are not entitled to, you must pay it
            back when {organisation} asks you to
          </li>
          <li>
            You can cancel a Direct Debit at any time by simply contacting your
            bank or building society. Written confirmation may be required.
            Please also notify the organisation.
          </li>
        </ul>

        <p className="fineprint">
          Direct debit payments are processed by GoCardless. Read the{" "}
          <a
            href="https://gocardless.com/legal/privacy/"
            rel="noopener noreferrer"
            target="_blank"
          >
            GoCardless privacy notice
          </a>{" "}
          for more information.
        </p>
      </section>

      <ContinueButton />
    </form>
  );
};

const ChargebeePaymentPage: StagerComponent<FormSchema> = ({
  onCompleted,
  data
}) => {
  const organisationName = getEnv("ORGANISATION_NAME");

  const cardRef = useRef<any>();
  const form = useForm();

  const chargebeeStylePropsList = [
    "color",
    "letterSpacing",
    "textAlign",
    "textTransform",
    "textDecoration",
    "textShadow",
    "fontFamily",
    "fontWeight",
    "fontSize",
    "fontSmoothing",
    "fontSmoothing",
    "fontSmoothing",
    "fontStyle",
    "fontVariant"
  ];

  const inputStyle = useCSSStyle(
    "form-control",
    "input",
    chargebeeStylePropsList
  );

  const handleCompleted = async () => {
    const { token } = await cardRef.current.tokenize();
    onCompleted({
      paymentToken: token
    });
  };

  return (
    <form
      className="form-content"
      noValidate
      onSubmit={form.handleSubmit(handleCompleted)}
    >
      <div className="mb-4">
        <Summary data={data} />
      </div>
      <CardComponent
        className="form-section"
        styles={{ base: inputStyle }}
        classes={{
          invalid: "is-invalid"
        }}
        ref={cardRef}
      >
        <h2>Card details</h2>
        <p className="text-secondary mb-5">
          You've chosen to join {organisationName} by paying by card.
        </p>
        <FormGroup className="mb-5">
          <Form.Label>Card Number</Form.Label>
          <p className="text-secondary">
            The long number on the front of your card.
          </p>
          <CardNumber className="form-control" />
          <Form.Control.Feedback type="invalid">
            This doesn't look like a valid credit or debit card number.
          </Form.Control.Feedback>
        </FormGroup>
        <FormGroup className="mb-5">
          <Form.Label>Card Expiry</Form.Label>
          <p className="text-secondary">Should be on the front of your card.</p>
          <CardExpiry className="form-control" />
          <Form.Control.Feedback type="invalid">
            This doesn't look like a valid credit or debit card expiry date. It
            should a date in the future.
          </Form.Control.Feedback>
        </FormGroup>
        <FormGroup className="mb-5">
          <Form.Label>CVV</Form.Label>
          <p className="text-secondary">
            The three number security code on the back of your card.
          </p>
          <CardCVV className="form-control" />
          <Form.Control.Feedback type="invalid">
            This doesn't look like a valid credit or debit card CVV - it's
            normally three numbers on the back of your card.
          </Form.Control.Feedback>
        </FormGroup>
      </CardComponent>

      <ContinueButton />
    </form>
  );
};

const convertCurrencyFromMajorToMinorUnits = (amount: number) => amount * 100;

const StripePaymentPage: StagerComponent<FormSchema> = ({
  data,
  setData
}) => {
  const stripePromise = loadStripe(getEnv("STRIPE_PUBLISHABLE_KEY") as string);
  const plan = (getEnv("MEMBERSHIP_PLANS") as any[]).find(
    (plan) => plan.value === data.membership
  );
  const isOneOffDonation = Boolean(getEnv("DONATION_SUPPORTER_MODE")) && !data.recurDonation;
  // For one-off donations the amount comes from donationAmount (set by donation.page).
  // For subscriptions the amount is only used by Stripe Elements for display; the
  // actual charge is determined server-side from the plan / customMembershipAmount.
  const amountForElements = isOneOffDonation
    ? convertCurrencyFromMajorToMinorUnits(data.donationAmount as number)
    : (plan.amount ? convertCurrencyFromMajorToMinorUnits(plan.amount) : 100);
  const currency = plan.currency.toLowerCase() || "gbp";
  const paymentMethodTypes = resolveStripePaymentMethodTypes(isOneOffDonation, currency);

  return (
    <Elements
      stripe={stripePromise}
      options={{
        paymentMethodCreation: "manual",
        mode: isOneOffDonation ? "payment" : "subscription",
        amount: amountForElements,
        currency,
        paymentMethodTypes,
      }}
    >
      <StripeForm data={data} setData={setData} plan={plan} isOneOffDonation={isOneOffDonation} />
    </Elements>
  );
};

const StripeForm = ({
  data,
  setData,
  plan,
  isOneOffDonation
}: {
  data: FormSchema;
  setData: (d: FormSchema) => void;
  plan: { frequency: string };
  isOneOffDonation: boolean;
}) => {
  const stripe = useStripe();
  const elements = useElements();
  const createSubscription = usePostResource<
    FormSchema,
    {
      id: string;
      customer: string;
      latest_invoice: { payment_intent: { id: string; client_secret: string } };
    }
  >("/stripe/create-subscription");
  const createPaymentIntent = usePostResource<
    FormSchema,
    { id: string; client_secret: string; customer: string }
  >("/stripe/create-payment-intent");

  const [errorMessage, setErrorMessage] = useState<string | undefined>();
  const [errorDetails, setErrorDetails] = useState<string | undefined>();
  const [showErrorDetails, setShowErrorDetails] = useState(false);
  const [loading, setLoading] = useState<boolean>(false);

  // Use ref for paymentMethodOrder so that it is fixed when the component is first rendered
  // Otherwise the Stripe form switches the order whenever the payment method is changed
  const paymentMethodOrder = useRef<string[]>(data.paymentMethod === "directDebit" ? ["bacs_debit", "card"] : ["card", "bacs_debit"]);

  const handleError = (error: { message: string }) => {
    setLoading(false);
    setErrorMessage(error.message);
    Sentry.captureMessage(error.message, 'error')
  };

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (!stripe || !elements) {
      return;
    }

    setLoading(true);

    const { error: submitError } = await elements.submit();
    if (submitError) {
      handleError(submitError as { message: string });
      return;
    }

    try {
      let clientSecret: string;

      if (isOneOffDonation) {
        const paymentIntent = await createPaymentIntent({ ...data });
        clientSecret = paymentIntent.client_secret;

        sessionStorage.setItem(
          SAVED_STATE_KEY,
          JSON.stringify({
            ...data,
            stripeCustomerId: paymentIntent.customer,
            stripePaymentIntentId: paymentIntent.id
          })
        );
      } else {
        const subscription = await createSubscription({ ...data });
        const paymentIntent = subscription.latest_invoice.payment_intent;
        clientSecret = paymentIntent.client_secret;

        sessionStorage.setItem(
          SAVED_STATE_KEY,
          JSON.stringify({
            ...data,
            stripeCustomerId: subscription.customer,
            stripeSubscriptionId: subscription.id,
            stripePaymentIntentId: paymentIntent.id
          })
        );

        // Resubmitting within one session returns the subscription Stripe
        // already created (see buildSubscriptionIdempotencyKey server-side).
        // If that payment has already gone through, confirming it again would
        // error — and taking a second payment would be worse. Carry straight
        // on to the confirm step instead.
        if (paymentIntent.status === "succeeded") {
          const succeededUrl = new URL(window.location.href);
          succeededUrl.searchParams.set("stripe_success", "true");
          window.location.assign(succeededUrl.toString());
          return;
        }
      }

      const returnUrl = new URL(window.location.href);
      returnUrl.searchParams.set("stripe_success", "true");

      const { error } = await stripe.confirmPayment({
        elements,
        clientSecret,
        confirmParams: {
          return_url: returnUrl.toString()
        }
      });

      if (error) {
        Sentry.captureMessage("Stripe confirmPayment error", {
          level: "error",
          extra: { stripeError: error },
        });
        setLoading(false);
        setErrorMessage(formatStripeError(error));
        setErrorDetails(JSON.stringify(error, null, 2));
        setShowErrorDetails(false);
        return;
      }
    } catch (e: any) {
      console.error("Create payment error", e);
      Sentry.captureException(e);
      setLoading(false);
      setErrorMessage(GENERIC_PAYMENT_ERROR);
      setErrorDetails(JSON.stringify(e, null, 2));
      setShowErrorDetails(false);
    }
  };

  // Pre-fill billing details from the form data
  // This allows users to verify their details are correct when setting up Direct Debit
  const defaultValues = {
    billingDetails: {
      name: data.firstName && data.lastName
        ? `${data.firstName} ${data.lastName}`
        : undefined,
      email: data.email || undefined,
      phone: formatPhoneE164(data.phoneNumber, data.phoneCountry),
      address: {
        line1: data.addressLine1 || undefined,
        line2: data.addressLine2 || undefined,
        city: data.addressCity || undefined,
        postal_code: data.addressPostcode || undefined,
        country: data.addressCountry || undefined,
      }
    }
  };

  return (
    <form className="form-content" onSubmit={handleSubmit}>
      <div className="mb-4">
        <Summary data={data} />
      </div>
      <div className="mb-4">
        <PaymentElement options={{defaultValues, paymentMethodOrder: paymentMethodOrder.current}} onChange={(e) => {
          const stripePaymentMethod = e.value.type;
          setData({ ...data, paymentMethod: stripePaymentMethod === "bacs_debit" ? "directDebit" : "creditCard" });
        }}/>
        <ContinueButton disabled={loading} text={loading ? "Loading..." : ""} />
        {errorMessage && (
          <div className="invalid-feedback d-block mt-3">
            {errorMessage}
            {errorDetails && (
              <>
                {" "}
                <button
                  type="button"
                  onClick={() => setShowErrorDetails((v) => !v)}
                  style={{ background: "none", border: "none", padding: 0, color: "inherit", textDecoration: "underline", cursor: "pointer", font: "inherit" }}
                >
                  {showErrorDetails ? "Hide details" : "Show details"}
                </button>
                {showErrorDetails && (
                  <pre style={{ marginTop: "0.5rem", whiteSpace: "pre-wrap", wordBreak: "break-all", fontSize: "0.8em" }}>
                    {errorDetails}
                  </pre>
                )}
              </>
            )}
          </div>
        )}
      </div>
    </form>
  );
};
