import clsx from "clsx";
import { FormEvent, useState } from "react";
import { redirect, useNavigate } from "react-router";
import { PAYSITE_LIST, SITE_NAME } from "#env/env-vars";
import { fetchCreateImport } from "#api/imports";
import { PageSkeleton } from "#components/pages";

import * as styles from "./importer_list.module.scss";

const MAX_LENGTH = 1024;

function titlize(s: string) {
  return s.replace("_", " ").replace(/\b\w+/g, text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase());
}

interface Input {
  name: string;
  label?: string;
  hint?: string;
  default?: () => string;
}

interface PaysiteForm {
  name?: string;
  inputs: Array<Input>;
  // Returns an error message, or undefined if no errors.
  validate: (...args: Array<any>) => string | undefined;
  includeDMs?: boolean;
}

const PAYSITES: { [name: string]: PaysiteForm } = {
  patreon: {
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (session_key.length != 43)
        return `Invalid key: Expected 43 characters, got ${session_key.length}`;
    },
    includeDMs: true,
  },
  fanbox: {
    name: "Pixiv Fanbox",
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (!session_key.match(/^\d+_\w+$/) || session_key.length > MAX_LENGTH) {
        return "Invalid key.";
      }
    },
  },
  afdian: {
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (session_key.length > MAX_LENGTH) {
        return "Key is too long.";
      }
    },
  },
  boosty: {
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      try {
        JSON.parse(decodeURIComponent(session_key));
      } catch {
        return "Invalid key: Expected valid JSON.";
      }
    },
  },
  discord: {
    inputs: [
      { name: "session_key", label: "Token" },
      { name: "channel_ids", label: "Channel IDs", hint: "Separate with commas." },
    ],
    validate({ session_key, channel_ids }) {
      if (!session_key.match(/^(mfa.[a-z0-9_-]{20,})|([a-z0-9_-]{23,28}.[a-z0-9_-]{6,7}.[a-z0-9_-]{27})$/)) {
        return "Invalid token format.";
      }
      for (const id of channel_ids.split(/\s*,\s*/)) {
        if (id && !parseInt(id)) { //
          return `${id} is not a valid channel ID.`;
        }
      }
    },
  },
  dlsite: {
    name: "DLsite",
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (session_key.length > MAX_LENGTH) {
        return "Key is too long.";
      }
    },
  },
  fantia: {
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (![32, 64].includes(session_key.length)) {
        return "Invalid key: Expected 32 or 64 characters.";
      }
    },
  },
  gumroad: {
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (session_key.length < 200 || session_key.length > MAX_LENGTH) {
        return `Invalid key: Expected 200 to ${MAX_LENGTH} characters.`;
      }
    },
  },
  subscribestar: {
    name: "SubscribeStar",
    inputs: [
      { name: "session_key" },
    ],
    validate({ session_key }) {
      if (session_key.length > MAX_LENGTH) {
        return "Key is too long.";
      }
    },
  },
  onlyfans: {
    name: "OnlyFans",
    inputs: [
      { name: "session_key", hint: "Can be found in cookies -> sess." },
      { name: "auth_id", label: "User ID", hint: "Can be found in cookies -> auth_id." },
      { name: "x-bc", label: "BC Token", hint: "Can be found in local storage -> bcTokenSha. Paste <code style=\"user-select: all\">localStorage.bcTokenSha</code> into the console for easy access." },
      {
        name: "user_agent",
        label: "User-Agent",
        hint: "This needs to be set to the User-Agent of the last device that logged into your OnlyFans account; leave it as the default value if you are on it right now.",
        default: () => navigator.userAgent,
      },
    ],
    validate({ session_key, auth_id: user_id, "x-bc": bc_token, user_agent }) {
      if (session_key.length > MAX_LENGTH) {
        return "Key is too long.";
      }
      if (!parseInt(user_id)) {
        return "User ID must consist of only digits.";
      }
      if (!bc_token.match(/^[a-f0-9]{40}$/)) {
        return "Invalid BC token (expected 40 hexadecimal characters).";
      }
      if (!/^[\x00-\x7F]*$/.test(user_agent)) {
        return "Invalid User-Agent (contains non-ASCII characters).";
      }
    },
  },
  fansly: {
    inputs: [
      {
        name: "session_key",
        hint: `
          Copy the following string and enter it into the browser Console,
          accessible by pressing F12.
          <core style="user-select: all">btoa(JSON.stringify({...JSON.parse(localStorage?.session_active_session||'{}'),device:localStorage?.device_device_id}))</code>
        `
      },
    ],
    validate({ session_key }) {
      if (session_key.length == 71 && !/^[A-Za-z0-9]{71}$/.test(session_key)) {
        return "The key doesn't match the required pattern.";
      }
      try {
        if (!JSON.parse(atob(session_key))?.token) {
          return "Token not found in JSON.";
        }
      } catch {
        return "Key is not valid JSON."
      }
    },
    includeDMs: true,
  },
  candfans: {
    name: "CandFans",
    inputs: [
      { name: "session_key", hint: "On CandFans page, Press F12 -> \"Application\" tab (check >> if its hidden) -> Storage: Cookies -> candfans.jp -> secure_candfans_session value." },
    ],
    validate({ session_key }) {
      try {
        let keys = Object.keys(JSON.parse(atob(decodeURIComponent(session_key))));
        if (!["mac", "iv", "tag", "value"].every(key => keys.includes(key))) {
          return "The key does not contain the appropriate values.";
        }
      } catch {
        return "The key was not decodable.";
      }
    },
  }
}

/**
 * TODO: split into separate pages per service
 */
export function ImporterPage() {
  const [selectedService, changeSelectedService] = useState(PAYSITE_LIST[0]);
  const [error, setError] = useState<string | undefined>(undefined);
  const navigate = useNavigate();
  const title = "Import paywall posts/comments/DMs";
  const heading = "Import from Paysite";

  async function onSubmit(event: FormEvent) {
    event.preventDefault();
    setError(undefined);
    let form = event.target as HTMLFormElement;
    let inputs = form.querySelectorAll("input");
    let args: {[key: string]: string} = { service: selectedService };
    inputs.forEach(el => {
      if (el.type == "checkbox") {
        args[el.name] = el.checked ? "1" : "0";
      } else {
        args[el.name] = el.value.trim();
      }
    });
    let error = PAYSITES[selectedService].validate(args);
    if (error) {
      setError(error);
    } else {
      try {
        let { import_id } = await fetchCreateImport(args as any);
        await navigate(`/importer/status/${import_id}`);
      } catch (resp: any) {
        setError(resp.message)
      }
    }
  }

  return (
    <PageSkeleton name="importer" title={title} heading={heading}>
      <form id="import-list" className="form form--bigger" onSubmit={onSubmit}>
        <div className="form__section">
          <label htmlFor="service" className="form__label">
            Paysite:
          </label>
          <select
            id="service"
            className="form__select"
            name="service"
            defaultValue={PAYSITE_LIST[0]}
            onChange={(event) => changeSelectedService(event.target.value)}
          >
            {PAYSITE_LIST.map((entry, index) => {
              const paysite = PAYSITES[entry];

              if (!paysite) {
                console.error(`Requested paysite '${entry}' does not exist in the table`);
                return;
              }

              return (
                <option key={index} className="form__option" value={entry}>
                  {paysite.name ?? titlize(entry)}
                </option>
              );
            })}
          </select>
        </div>

        {PAYSITES[selectedService].inputs.map((input, index) => {
          return (
            <div className="form__section" key={input.name}>
              <label className="form__label" htmlFor={input.name}>{input.label || titlize(input.name)}:</label>
              <input
                id={input.name}
                type="text"
                name={input.name}
                className="form__input"
                autoComplete="off"
                autoCorrect="off"
                autoCapitalize="off"
                spellCheck="false"
                maxLength={1024}
                required
                value={input.default?.()}
              />
              {index === 0 && (
                <small className="form__subtitle other__notes">
                  <a href="/importer/tutorial">Learn how to get your session key.</a>
                </small>
              )}
              {input.hint && (
                <small className="form__subtitle other__notes" dangerouslySetInnerHTML={{__html: input.hint}} />
              )}
            </div>
          )
        })}

        {PAYSITES[selectedService].includeDMs && (
          <div
            id="dm-consent"
            className="form__section form__section--checkbox"
          >
            <input
              className="form__input"
              type="checkbox"
              id="save-dms"
              name="save_dms"
              defaultChecked={true}
            />
            <label className="form__label" htmlFor="save-dms">
              Allow the importer to access your direct messages
              <br />
              <small className="form__subtitle">
                You will be able to manually approve or discard messages before
                they are publicly displayed.
              </small>
            </label>
          </div>
        )}

        <div id="consent" className="form__section form__section--checkbox">
          <input
            className="form__input"
            type="checkbox"
            defaultChecked={true}
            id="save-session-key"
            name="save_session_key"
          />
          <label className="form__label" htmlFor="save-session-key">
            Allow administrator to use my session for debugging
            <br />
            <small className="form__subtitle">
              Contributed debugging keys are encrypted using a strong RSA 4096
              key that only the administrator can decipher.
            </small>
          </label>
        </div>

        <div
          id="auto-import-consent"
          className="form__section form__section--checkbox"
        >
          <input
            className="form__input"
            type="checkbox"
            defaultChecked={true}
            id="auto_import"
            name="auto_import"
          />
          <label className="form__label" htmlFor="auto_import">
            Allow the importer to save my session key for auto-import
            <br />
            <small className="form__subtitle">
              If enabled, new posts will automatically be imported every 24
              hours without manual intervention. Direct message importing still
              requires manual import. See notes below for security information.
            </small>
          </label>
        </div>

        {selectedService == "fanbox" && (
          <div
            id="fanbox-test-consent"
            className={"form__section form__section--checkbox"}
          >
            <input
              className="form__input"
              type="checkbox"
              id="fanbox-test-consent"
              name="fanbox-test-consent"
              required
            />
            <label className="form__label" htmlFor="fanbox-test-consent">
              I agree that this importer is in its testing phase, and that there
              may be risks involved.
              <br />
              <small className="form__subtitle">
                <a href="/fanboximports">Check details here</a>
              </small>
            </label>
          </div>
        )}

        <div className={styles.error} hidden={!error}>
          Error: {error}
        </div>

        <button className="button form__button form__button--submit">Submit Key</button>
      </form>

      <h2 className="site-section__subheading">Important information</h2>
      <p>
        Your session key is used to scrape paid posts from your feed. After
        downloading missing posts, the key is immediately discarded and never
        stored without permission.
      </p>

      {!PAYSITE_LIST.includes("fantia") ? undefined : (
        <>
          <h3>Fantia</h3>
          <ul>
            <li>
              At least one paid content must be unlocked for the post to be
              imported. <i>Free posts cannot be archived at this time.</i>
            </li>
            <li>
              In order to download post contents accurately, the importer will
              automatically enable adult-viewing mode for duration of the import
              if you have it turned off.{" "}
              <b>Do not change back to general-viewing during imports.</b>
            </li>
          </ul>
        </>
      )}

      <h3>Auto-import</h3>
      <p>
        The auto-import feature allows users to give {SITE_NAME} permission to
        automatically detect and retrieve new posts and creators by storing
        session keys long-term, without need for manual key submission. All keys
        are encrypted using a strong RSA 4096 key. When the administrators start
        a new autoimport round, a computer outside of {SITE_NAME}'s
        infrastucture sends the private key to the backend, allowing it to
        decrypt all working keys and start import tasks. Even if {SITE_NAME}'s
        private database were to somehow be compromised, your tokens would
        remain anonymous and secure.
        <br />
        If you are logged into {SITE_NAME}, any key you submit with autoimport
        enabled can be managed under the <b>Keys</b> section of your{" "}
        <b>[Account]</b> tab in the header. There, you will be able to view
        import logs or revoke access.{" "}
        <i>Please note that anonymously-submitted keys cannot be managed.</i>
      </p>
    </PageSkeleton>
  );
}
