import clsx from "clsx";
import * as React from "react";
import { Link, UNSAFE_FrameworkContext, useNavigate } from "react-router";
import { PasskeyLogo } from "..";
import {
  AUTH_HOST,
  browserSupportsWebAuthn,
  checkResponse,
  initRegistration,
} from "../../utils/webauthn";
import { resolveRoute } from "../Link";
import * as styles from "./styles.module.css";

enum SignupState {
  RegisterUser,
  OTP,
  RegisterPasskey,
  Exists,
}

const titles: { [key in SignupState]: string } = {
  [SignupState.RegisterUser]: "Register",
  [SignupState.OTP]: "Confirm code",
  [SignupState.RegisterPasskey]: "Save a passkey",
  [SignupState.Exists]: "Already registered",
};

const subtitles: { [key in SignupState]: (email: string | null) => string } = {
  [SignupState.RegisterUser]: (email) => "Enter your email to continue.",
  [SignupState.OTP]: (email) => `Please enter the code sent to "${email}".`,
  [SignupState.RegisterPasskey]: (email) =>
    "Sign in to your account easily and securely with a passkey. Note: Your biometric data is only stored on your devices and never shared.",
  [SignupState.Exists]: (email) =>
    `A user with email "${email}" as already been registered. Login?`,
};

type Classes = {
  headline?: string;
  subtitle?: string;
  error?: string;
  container?: string;
  input?: string;
  primaryButton?: string;
  secondaryButton?: string;
  divider?: string;
  footer?: string;
  link?: string;
};

export function Signup({
  callbackUrl: _callbackUrl,
  loginUrl: _loginUrl,
  className,
  classes = {},
  query = [],
  ...props
}: {
  callbackUrl?: any;
  loginUrl?: any;
  query: { key: string; value: string }[];
  className?: string;
  classes?: Classes;
} & React.HTMLAttributes<HTMLDivElement>) {
  const [error, setError] = React.useState<string | null>(null);
  const [step, setStep] = React.useState(SignupState.RegisterUser);
  const [email, setEmail] = React.useState<string | null>("");
  const framework = React.use(UNSAFE_FrameworkContext);
  if (!framework) {
    throw new Error(
      "Signup component must be used within a FrameworkContext provider.",
    );
  }
  const callbackUrl = React.useMemo(() => {
    return resolveRoute(_callbackUrl, framework.manifest);
  }, [_callbackUrl, framework.manifest]);
  const loginUrl = React.useMemo(() => {
    return resolveRoute(_loginUrl, framework.manifest);
  }, [_loginUrl, framework.manifest]);
  const queryStr = React.useMemo(
    () =>
      query?.length
        ? `?${new URLSearchParams(query.map(({ key, value }) => [key, value]))}`
        : "",
    [query],
  );
  const renderStep = React.useCallback(
    (step: SignupState) => {
      switch (step) {
        case SignupState.RegisterUser:
          return (
            <RegisterUser
              setEmail={setEmail}
              loginUrl={loginUrl ?? "/"}
              setStep={setStep}
              callbackUrl={callbackUrl + queryStr}
              setError={setError}
              classes={classes}
            />
          );
        case SignupState.OTP:
          return (
            <ConfirmCode
              email={email}
              callbackUrl={callbackUrl + queryStr}
              setError={setError}
              setStep={setStep}
              classes={classes}
            />
          );
        case SignupState.RegisterPasskey:
          return (
            <RegisterPasskey
              callbackUrl={callbackUrl + queryStr}
              setError={setError}
              setStep={setStep}
              classes={classes}
            />
          );
        case SignupState.Exists:
          return (
            <GoToLogin
              loginUrl={`${loginUrl ?? "/"}?email=${email}`}
              classes={classes}
              setStep={setStep}
            />
          );
        default:
          // nothing to render for this route
          return null;
      }
    },
    [email, callbackUrl, classes, queryStr],
  );

  return (
    <section className={clsx("__login", className)} {...props}>
      <div className={clsx("__login-container", classes?.container)}>
        <h1 className={clsx("__login-headline", classes?.headline)}>
          {titles[step] ?? ""}
        </h1>
        <h2
          className={clsx(
            "__login-subtitle",
            styles.subtitle,
            classes?.subtitle,
          )}
        >
          {subtitles[step]?.(email) ?? ""}
        </h2>
      </div>
      {error ? (
        <section className={clsx("__login-error", classes?.error)}>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="18"
            height="18"
            viewBox="0 0 24 24"
            className={clsx("__login-errorIcon", styles.errorIcon)}
          >
            <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path>
          </svg>
          <span className={"__login-errorText"}>{error}</span>
        </section>
      ) : null}
      {renderStep(step)}
    </section>
  );
}

function RegisterUser({
  setEmail,
  callbackUrl,
  loginUrl,
  setError,
  setStep,
  classes = {},
}: {
  setEmail: (email: string) => void;
  loginUrl?: string;
  callbackUrl?: string;
  setError: (error: string) => void;
  setStep: (step: SignupState) => void;
  classes?: Classes;
}) {
  const navigate = useNavigate();
  return (
    <>
      <form
        className={clsx("__login-container", classes.container)}
        onSubmit={(e) => {
          e.preventDefault();
          const formData = new FormData(e.target as HTMLFormElement);
          setEmail(formData.get("email") as string);
          fetch(`${AUTH_HOST}/available`, {
            credentials: "include",
            method: "POST",
            body: formData,
          })
            .then(checkResponse)
            .then((data) => {
              if (data.exists) {
                setStep(SignupState.Exists);
              } else {
                return fetch(`${AUTH_HOST}/magic-link/login`, {
                  credentials: "include",
                  method: "POST",
                  body: formData,
                })
                  .then(checkResponse)
                  .then((data) => {
                    if (data.success) {
                      setStep(SignupState.OTP);
                    } else {
                      throw new Error(data.error);
                    }
                  })
                  .catch((err) => {
                    setError(`${err}`);
                  });
              }
            })
            .catch((err) => {
              setError(`${err}`);
            });
        }}
      >
        {callbackUrl ? (
          <input type="hidden" name="callbackUrl" value={callbackUrl} />
        ) : null}
        <input
          className={clsx(
            "__login-input",
            "__login-input-email",
            styles.input,
            classes.input,
          )}
          type="email"
          name="email"
          inputMode="email"
          placeholder="Enter your email"
          autoComplete="username webauthn"
          autoFocus={!process.env.PREVIEW}
          pattern="^.*[^0-9]+$"
        />
        <button
          className={clsx(
            "__login-button",
            "__login-button-primary",
            classes.primaryButton,
          )}
          type="submit"
        >
          Continue
        </button>
      </form>
      <section className={clsx("__login-footer", classes.footer)}>
        <span
          className={clsx("__login-link", styles.link, classes.link)}
          onClick={() => {
            navigate(loginUrl || "/");
          }}
        >
          Login?
        </span>
      </section>
    </>
  );
}

function ConfirmCode({
  email,
  callbackUrl,
  setError,
  setStep,
  classes = {},
}: {
  email: string | null;
  callbackUrl?: string;
  setError: (error: string) => void;
  setStep: (step: SignupState) => void;
  classes?: Classes;
}) {
  const navigate = useNavigate();
  return (
    <form
      className={clsx("__login-container", classes.container)}
      onSubmit={(e) => {
        e.preventDefault();
        const formData = new FormData(e.target as HTMLFormElement);
        fetch(`${AUTH_HOST}/magic-link/verify`, {
          method: "POST",
          body: formData,
          credentials: "include",
        })
          .then(checkResponse)
          .then((data) => {
            if (data.success) {
              // done!
              return fetch(`${AUTH_HOST}/me`, {
                credentials: "include",
              });
            } else {
              throw new Error(data.error);
            }
          })
          .then(checkResponse)
          .then((data) => {
            if (
              !process.env.PREVIEW &&
              browserSupportsWebAuthn() &&
              shouldRegister(data?.authenticators)
            ) {
              setStep(SignupState.RegisterPasskey);
            } else {
              // done, redirect
              navigate(callbackUrl || "/");
            }
          })
          .catch((err) => {
            setError(`${err}`);
          });
      }}
    >
      <input type="hidden" name="callbackUrl" value={callbackUrl} />
      <input type="hidden" name="email" value={email ?? ""} />
      <input
        className={clsx(
          "__login-input",
          "__login-input-token",
          styles.input,
          classes.input,
        )}
        type="text"
        name="token"
        inputMode="numeric"
        placeholder="Enter your code"
        autoComplete="one-time-code"
        autoCorrect="off"
        autoFocus={!process.env.PREVIEW}
        maxLength={6}
        minLength={6}
        pattern="[0-9]*"
      />
      <button
        className={clsx(
          "__login-button",
          "__login-button-primary",
          classes.primaryButton,
        )}
        type="submit"
      >
        Validate Code
      </button>
      <section className={clsx("__login-footer", classes.footer)}>
        <span
          className={clsx("__login-link", styles.link, classes.link)}
          onClick={() => setStep(SignupState.RegisterUser)}
        >
          Back
        </span>
      </section>
    </form>
  );
}

function shouldRegister(authenticators?: any[]) {
  if (!authenticators || !authenticators.length) {
    return true;
  }

  let saved: string[] = [];
  try {
    saved = JSON.parse(localStorage.getItem("__brevity_passkeys") || "[]");
  } catch (err) {
    // ignore
  }

  if (saved.length === 0) {
    return true;
  }

  return !authenticators.some((authenticator) => {
    return saved.includes(authenticator?.id as string);
  });
}

function RegisterPasskey({
  callbackUrl,
  setError,
  setStep,
  classes = {},
}: {
  callbackUrl?: string;
  setError: (error: string) => void;
  setStep: (step: SignupState) => void;
  classes?: Classes;
}) {
  const navigate = useNavigate();
  const [supportsWebauthn, setSupportsWebauthn] = React.useState(true);
  React.useEffect(() => {
    setSupportsWebauthn(browserSupportsWebAuthn());
  }, []);
  return (
    <div className={clsx("__login-container", classes.container)}>
      <button
        className={clsx(
          "__login-button",
          "__login-button-primary",
          classes.primaryButton,
        )}
        disabled={!supportsWebauthn}
        onClick={() => {
          if (process.env.PREVIEW) {
            navigate(callbackUrl || "/");
            return;
          }
          initRegistration()
            .then(({ verified, credentialID }) => {
              if (verified) {
                if (credentialID) {
                  // append to list in local storage
                  try {
                    const list = JSON.parse(
                      localStorage.getItem("__brevity_passkeys") || "[]",
                    );
                    list.push(credentialID);
                    localStorage.setItem(
                      "__brevity_passkeys",
                      JSON.stringify(list),
                    );
                  } catch (err) {
                    // ignore
                  }
                }
                navigate(callbackUrl || "/");
              } else {
                setError("Failed to save passkey");
              }
            })
            .catch((err) => {
              if (err.name === "AbortError") {
                // This error is thrown when the user cancels the operation in the browser
                return;
              } else if (err.name === "InvalidStateError") {
                setError("This device has already been registered.");
              } else {
                setError(`${err}`);
              }
            });
        }}
      >
        <PasskeyLogo />
        Save Passkey
      </button>
      <section className={clsx("__login-footer", classes.footer)}>
        <a
          className={clsx("__login-link", styles.link, classes.link)}
          href="https://fidoalliance.org/passkeys/#faq"
          rel="noopener noreferrer"
          target="_blank"
        >
          What are passkeys?
        </a>
        <Link
          className={clsx("__login-link", styles.link, classes.link)}
          to={callbackUrl || "/"}
        >
          Skip
        </Link>
      </section>
    </div>
  );
}

function GoToLogin({
  setStep,
  loginUrl,
  classes = {},
}: {
  setStep: (step: SignupState) => void;
  loginUrl?: string;
  classes?: Classes;
}) {
  const navigate = useNavigate();
  return (
    <div className={clsx("__login-container", classes.container)}>
      <button
        className={clsx(
          "__login-button",
          "__login-button-primary",
          classes.primaryButton,
        )}
        onClick={() => {
          navigate(loginUrl || "/");
        }}
      >
        Go to Login
      </button>
      <section className={clsx("__login-footer", classes.footer)}>
        <span
          className={clsx("__login-link", styles.link, classes.link)}
          onClick={() => setStep(SignupState.RegisterUser)}
        >
          Back
        </span>
      </section>
    </div>
  );
}
