/* contact.jsx — the contact intake form, as a modal.
 *
 * Opened from anywhere by dispatching a window event:
 *
 *     window.dispatchEvent(new CustomEvent("vastola.contact.open"))
 *
 * The masthead's "Let's connect" and the Partnering page's "Let's talk" both
 * do that. The component mounts its own React root on <body> rather than
 * living in a page's tree, so it is available on every page that loads this
 * file — including A Creative Life, which renders a different app.
 */
/* global React, ReactDOM */

const { useState: useStateCF, useEffect: useEffectCF, useRef: useRefCF } = React;

/* Apps Script web apps do not answer a CORS preflight, so the request has to
   stay "simple": no-cors mode and a text/plain content type. The trade is that
   the response is opaque — see onSubmit. */
const CONTACT_ENDPOINT =
  "https://script.google.com/macros/s/AKfycbwrDAyXDiD7R15pvhJvAESID2Ts24qxY0nvvwIP9VsiExZ_dCjpYh581jwy9CwTXNqc/exec";

/* `key` is the payload key the sheet expects. Two-up in this order, so Name
   and Email share the first row. `optional` fields are still sent — they are
   just not held against anyone. */
const CONTACT_FIELDS = [
  { key: "name", label: "Name", type: "text", autoComplete: "name" },
  { key: "email", label: "Email address", type: "email", autoComplete: "email" },
  { key: "linkedin", label: "LinkedIn URL", type: "text", placeholder: "linkedin.com/in/…" },
  { key: "companyName", label: "Company name", type: "text", autoComplete: "organization" },
  { key: "title", label: "Title", type: "text", autoComplete: "organization-title", optional: true },
  { key: "companySize", label: "Company size", type: "select", optional: true,
    options: ["0-100", "100-500", "500-1000", "1000+"] },
  { key: "reason", label: "Reason for reaching out", type: "textarea" },
];

const EMPTY = CONTACT_FIELDS.reduce((o, f) => { o[f.key] = ""; return o; }, {});

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
/* Deliberately loose: a bare linkedin.com/in/name is what people paste, so the
   protocol is optional and the host just has to look like a host. */
const URL_RE = /^(https?:\/\/)?([\w-]+\.)+[a-z]{2,}(\/[^\s]*)?$/i;

function validate(values) {
  const errors = {};
  CONTACT_FIELDS.forEach((f) => {
    if (f.optional) return;
    if (!String(values[f.key] || "").trim()) errors[f.key] = "This one is required.";
  });
  if (!errors.email && !EMAIL_RE.test(values.email.trim())) {
    errors.email = "That does not look like an email address.";
  }
  if (!errors.linkedin && !URL_RE.test(values.linkedin.trim())) {
    errors.linkedin = "That does not look like a URL.";
  }
  return errors;
}

function ContactForm({ onClose }) {
  const [values, setValues] = useStateCF(EMPTY);
  const [errors, setErrors] = useStateCF({});
  const [state, setState] = useStateCF("editing");   // editing | sending | sent | failed
  const firstRef = useRefCF(null);

  useEffectCF(() => {
    const t = setTimeout(() => { if (firstRef.current) firstRef.current.focus(); }, 60);
    return () => clearTimeout(t);
  }, []);

  function set(key, v) {
    setValues((prev) => ({ ...prev, [key]: v }));
    /* Clear a field's error as soon as it is touched — re-validating the whole
       form on every keystroke would flag fields nobody has reached yet. */
    setErrors((prev) => (prev[key] ? { ...prev, [key]: undefined } : prev));
  }

  async function onSubmit(e) {
    e.preventDefault();
    const found = validate(values);
    setErrors(found);
    if (Object.keys(found).length) {
      const first = CONTACT_FIELDS.find((f) => found[f.key]);
      const el = document.getElementById("cf-" + first.key);
      if (el) el.focus();
      return;
    }

    setState("sending");
    const payload = CONTACT_FIELDS.reduce((o, f) => {
      o[f.key] = String(values[f.key]).trim();
      return o;
    }, {});

    try {
      await fetch(CONTACT_ENDPOINT, {
        method: "POST",
        mode: "no-cors",
        headers: { "Content-Type": "text/plain;charset=utf-8" },
        body: JSON.stringify(payload),
      });
      /* no-cors returns an opaque response: there is no status to read, so a
         request that left without throwing is as much as the browser will tell
         us. Anything network-level still lands in the catch. */
      setState("sent");
    } catch (err) {
      setState("failed");
    }
  }

  if (state === "sent") {
    return (
      <div className="cform__done" role="status">
        <p className="eyebrow cform__eyebrow"><span>Sent</span></p>
        <h2 className="cform__title">Thank you &mdash; it&rsquo;s on its way.</h2>
        <p className="cform__lede">
          I read everything myself and reply personally, usually within a couple of
          working days.
        </p>
        <button type="button" className="cform__submit" onClick={onClose}>Close</button>
      </div>
    );
  }

  return (
    <form className="cform__form" onSubmit={onSubmit} noValidate>
      <p className="eyebrow cform__eyebrow"><span>Start a conversation</span></p>
      <h2 className="cform__title" id="contact-title">Let&rsquo;s talk</h2>
      <p className="cform__lede">
        I like to start with a conversation. Tell me what&rsquo;s going on and
        I&rsquo;ll come back to you with some times we can connect and discuss how I
        may be able to help.
      </p>

      <div className="cform__grid">
        {CONTACT_FIELDS.map((f, i) => {
          const id = "cf-" + f.key;
          const bad = errors[f.key];
          const common = {
            id,
            name: f.key,
            value: values[f.key],
            onChange: (e) => set(f.key, e.target.value),
            className: "cform__input" + (bad ? " is-invalid" : ""),
            "aria-invalid": bad ? "true" : undefined,
            "aria-describedby": bad ? id + "-err" : undefined,
            placeholder: f.placeholder,
            disabled: state === "sending",
            ref: i === 0 ? firstRef : undefined,
          };
          return (
            <p className={"cform__field" + (f.type === "textarea" ? " cform__field--wide" : "")}
               key={f.key}>
              <label className="cform__label" htmlFor={id}>
                {f.label}
                {f.optional && <span className="cform__optional"> optional</span>}
              </label>
              {f.type === "textarea" ? (
                <textarea {...common} rows={5} />
              ) : f.type === "select" ? (
                <select {...common}>
                  {/* Empty and disabled, so "required" still means something —
                      the browser cannot preselect a real answer for them. */}
                  <option value="" disabled>Choose one</option>
                  {f.options.map((o) => <option key={o} value={o}>{o}</option>)}
                </select>
              ) : (
                <input {...common} type={f.type === "email" ? "email" : "text"}
                       autoComplete={f.autoComplete} />
              )}
              {bad && <span className="cform__err" id={id + "-err"}>{bad}</span>}
            </p>
          );
        })}
      </div>

      {state === "failed" && (
        <p className="cform__err cform__err--form" role="alert">
          That didn&rsquo;t send &mdash; check your connection and try again, or reach me
          on <a href="https://www.linkedin.com/in/avastola/" target="_blank" rel="noopener noreferrer">LinkedIn</a>.
        </p>
      )}

      <div className="cform__actions">
        <button type="submit" className="cform__submit" disabled={state === "sending"}>
          {state === "sending" ? "Sending…" : "Send"}
        </button>
        <button type="button" className="cform__cancel" onClick={onClose}
                disabled={state === "sending"}>Cancel</button>
      </div>
    </form>
  );
}

function ContactModal({ onClose }) {
  useEffectCF(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    function onKey(e) { if (e.key === "Escape") onClose(); }
    document.addEventListener("keydown", onKey);
    return () => {
      document.body.style.overflow = prev;
      document.removeEventListener("keydown", onKey);
    };
  }, [onClose]);

  return (
    <div className="cform" role="dialog" aria-modal="true" aria-labelledby="contact-title"
         onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="cform__card">
        <button type="button" className="cform__close" onClick={onClose} aria-label="Close">&times;</button>
        <ContactForm onClose={onClose} />
      </div>
    </div>
  );
}

/* The host: listens for the open event and owns nothing else. */
function ContactHost() {
  const [open, setOpen] = useStateCF(false);
  useEffectCF(() => {
    function onOpen() { setOpen(true); }
    window.addEventListener("vastola.contact.open", onOpen);
    return () => window.removeEventListener("vastola.contact.open", onOpen);
  }, []);
  return open ? <ContactModal onClose={() => setOpen(false)} /> : null;
}

Object.assign(window, { ContactForm, ContactModal, ContactHost });

/* Its own root, appended to <body>. Two reasons: the form has to open from
   pages that render different apps, and .page-host carries a CSS filter, which
   would trap a position:fixed overlay inside it. */
(function mountContact() {
  function go() {
    if (document.getElementById("contact-root")) return;
    const host = document.createElement("div");
    host.id = "contact-root";
    document.body.appendChild(host);
    ReactDOM.createRoot(host).render(<ContactHost />);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", go);
  } else {
    go();
  }
})();
