import React from "react";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { Auth0Provider } from "@auth0/auth0-react";
import { ChakraProvider, Box, useColorModeValue } from "@chakra-ui/react";
import { ApolloProvider } from "@apollo/client";
import { useAuth0 } from "@auth0/auth0-react";
import { BrowserRouter } from "react-router-dom";
import { Global } from "@emotion/react";

import buildApolloClient from "./apolloClient";

export default function AppProvider({ children }) {
  React.useEffect(() => setupLogging(), []);

  return (
    <BrowserRouter>
      <Auth0Provider
        domain="openneo.us.auth0.com"
        clientId="8LjFauVox7shDxVufQqnviUIywMuuC4r"
        redirectUri={
          process.env.NODE_ENV === "development"
            ? "http://localhost:3000"
            : "https://impress-2020.openneo.net"
        }
        audience="https://impress-2020.openneo.net/api"
        scope=""
      >
        <DTIApolloProvider>
          <ChakraProvider resetCSS={false}>
            <ScopedCSSReset>{children}</ScopedCSSReset>
          </ChakraProvider>
        </DTIApolloProvider>
      </Auth0Provider>
    </BrowserRouter>
  );
}

function DTIApolloProvider({ children, additionalCacheState = {} }) {
  const auth0 = useAuth0();
  const auth0Ref = React.useRef(auth0);

  React.useEffect(() => {
    auth0Ref.current = auth0;
  }, [auth0]);

  // Save the first `additionalCacheState` we get as our `initialCacheState`,
  // which we'll use to initialize the client without having to wait a tick.
  const [initialCacheState, unusedSetInitialCacheState] =
    React.useState(additionalCacheState);

  const client = React.useMemo(
    () =>
      buildApolloClient({
        getAuth0: () => auth0Ref.current,
        initialCacheState,
      }),
    [initialCacheState]
  );

  // When we get a new `additionalCacheState` object, merge it into the cache:
  // copy the previous cache state, merge the new cache state's entries in,
  // and "restore" the new merged cache state.
  //
  // HACK: Using `useMemo` for this is a dastardly trick!! What we want is the
  //       semantics of `useEffect` kinda, but we need to ensure it happens
  //       *before* all the children below get rendered, so they don't fire off
  //       unnecessary network requests. Using `useMemo` but throwing away the
  //       result kinda does that. It's evil! It's nasty! It's... perfect?
  //       (This operation is safe to run multiple times too, in case memo
  //       re-runs it. It's just, y'know, a performance loss. Maybe it's
  //       actually kinda perfect lol)
  //
  //       I feel like there's probably a better way to do this... like, I want
  //       the semantic of replacing this client with an updated client - but I
  //       don't want to actually replace the client, because that'll break
  //       other kinds of state, like requests loading in the shared layout.
  //       Idk! I'll see how it goes!
  React.useMemo(() => {
    const previousCacheState = client.cache.extract();
    const mergedCacheState = { ...previousCacheState };
    for (const key of Object.keys(additionalCacheState)) {
      mergedCacheState[key] = {
        ...mergedCacheState[key],
        ...additionalCacheState[key],
      };
    }
    console.debug(
      "Merging Apollo cache:",
      additionalCacheState,
      mergedCacheState
    );
    client.cache.restore(mergedCacheState);
  }, [client, additionalCacheState]);

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

function setupLogging() {
  Sentry.init({
    dsn: "https://c55875c3b0904264a1a99e5b741a221e@o506079.ingest.sentry.io/5595379",
    autoSessionTracking: true,
    integrations: [
      new Integrations.BrowserTracing({
        beforeNavigate: (context) => ({
          ...context,
          // Assume any path segment starting with a digit is an ID, and replace
          // it with `:id`. This will help group related routes in Sentry stats.
          // NOTE: I'm a bit uncertain about the timing on this for tracking
          //       client-side navs... but we now only track first-time
          //       pageloads, and it definitely works correctly for them!
          name: window.location.pathname.replaceAll(/\/[0-9][^/]*/g, "/:id"),
        }),

        // We have a _lot_ of location changes that don't actually signify useful
        // navigations, like in the wardrobe page. It could be useful to trace
        // them with better filtering someday, but frankly we don't use the perf
        // features besides Web Vitals right now, and those only get tracked on
        // first-time pageloads, anyway. So, don't track client-side navs!
        startTransactionOnLocationChange: false,
      }),
    ],
    denyUrls: [
      // Don't log errors that were probably triggered by extensions and not by
      // our own app. (Apparently Sentry's setting to ignore browser extension
      // errors doesn't do this anywhere near as consistently as I'd expect?)
      //
      // Adapted from https://gist.github.com/impressiver/5092952, as linked in
      // https://docs.sentry.io/platforms/javascript/configuration/filtering/.
      /^chrome-extension:\/\//,
      /^moz-extension:\/\//,
    ],

    // Since we're only tracking first-page loads and not navigations, 100%
    // sampling isn't actually so much! Tune down if it becomes a problem, tho.
    tracesSampleRate: 1.0,
  });
}

/**
 * ScopedCSSReset applies a copy of Chakra UI's CSS reset, but only to its
 * children (or, well, any element with the chakra-css-reset class).
 *
 * We also apply some base styles, like the default text color.
 *
 * TODO: What about Chakra's portal elements like toast messages, which are
 * intentionally mounted elsewhere in the document?
 *
 * NOTE: We use the `:where` CSS selector, instead of the .chakra-css-reset
 * selector directly, to avoid specificity conflicts. e.g. the selector
 * `.chakra-css-reset h1` is considered MORE specific than `.my-h1`, whereas
 * the selector `:where(.chakra-css-reset) h1` is lower specificity.
 */
function ScopedCSSReset({ children }) {
  const baseTextColor = useColorModeValue("green.800", "green.50");

  return (
    <>
      <Box className="chakra-css-reset" color={baseTextColor}>
        {children}
      </Box>
      <Global
        styles={`
          :where(.chakra-css-reset) {
            *,
            *::before,
            *::after {
              border-width: 0;
              border-style: solid;
              box-sizing: border-box;
            }

            main {
              display: block;
            }

            hr {
              border-top-width: 1px;
              box-sizing: content-box;
              height: 0;
              overflow: visible;
            }

            pre,
            code,
            kbd,
            samp {
              font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
              font-size: 1em;
            }

            a {
              background-color: transparent;
              color: inherit;
              text-decoration: inherit;
            }

            abbr[title] {
              border-bottom: none;
              text-decoration: underline;
              -webkit-text-decoration: underline dotted;
              text-decoration: underline dotted;
            }

            b,
            strong {
              font-weight: bold;
            }

            small {
              font-size: 80%;
            }

            sub,
            sup {
              font-size: 75%;
              line-height: 0;
              position: relative;
              vertical-align: baseline;
            }

            sub {
              bottom: -0.25em;
            }

            sup {
              top: -0.5em;
            }

            img {
              border-style: none;
            }

            button,
            input,
            optgroup,
            select,
            textarea {
              font-family: inherit;
              font-size: 100%;
              line-height: 1.15;
              margin: 0;
            }

            button,
            input {
              overflow: visible;
            }

            button,
            select {
              text-transform: none;
            }

            button::-moz-focus-inner,
            [type="button"]::-moz-focus-inner,
            [type="reset"]::-moz-focus-inner,
            [type="submit"]::-moz-focus-inner {
              border-style: none;
              padding: 0;
            }

            fieldset {
              padding: 0.35em 0.75em 0.625em;
            }

            legend {
              box-sizing: border-box;
              color: inherit;
              display: table;
              max-width: 100%;
              padding: 0;
              white-space: normal;
            }

            progress {
              vertical-align: baseline;
            }

            textarea {
              overflow: auto;
            }

            [type="checkbox"],
            [type="radio"] {
              box-sizing: border-box;
              padding: 0;
            }

            [type="number"]::-webkit-inner-spin-button,
            [type="number"]::-webkit-outer-spin-button {
              -webkit-appearance: none !important;
            }

            input[type="number"] {
              -moz-appearance: textfield;
            }

            [type="search"] {
              -webkit-appearance: textfield;
              outline-offset: -2px;
            }

            [type="search"]::-webkit-search-decoration {
              -webkit-appearance: none !important;
            }

            ::-webkit-file-upload-button {
              -webkit-appearance: button;
              font: inherit;
            }

            details {
              display: block;
            }

            summary {
              display: list-item;
            }

            template {
              display: none;
            }

            [hidden] {
              display: none !important;
            }

            body,
            blockquote,
            dl,
            dd,
            h1,
            h2,
            h3,
            h4,
            h5,
            h6,
            hr,
            figure,
            p,
            pre {
              margin: 0;
            }

            button {
              background: transparent;
              padding: 0;
            }

            fieldset {
              margin: 0;
              padding: 0;
            }

            ol,
            ul {
              margin: 0;
              padding: 0;
            }

            textarea {
              resize: vertical;
            }

            button,
            [role="button"] {
              cursor: pointer;
            }

            button::-moz-focus-inner {
              border: 0 !important;
            }

            table {
              border-collapse: collapse;
            }

            h1,
            h2,
            h3,
            h4,
            h5,
            h6 {
              font-size: inherit;
              font-weight: inherit;
            }

            button,
            input,
            optgroup,
            select,
            textarea {
              padding: 0;
              line-height: inherit;
              color: inherit;
            }

            img,
            svg,
            video,
            canvas,
            audio,
            iframe,
            embed,
            object {
              display: block;
            }

            img,
            video {
              max-width: 100%;
              height: auto;
            }

            [data-js-focus-visible] :focus:not([data-focus-visible-added]) {
              outline: none;
              box-shadow: none;
            }

            select::-ms-expand {
              display: none;
            }
          }
        `}
      />
    </>
  );
}