// ---------------------------------------------------------------------------
// customer-grid / FormInterface.tsx
// ⭐⭐ WAVE-29 R7 (owner item 10, contracts C3 / C4) — THE FORM INTERFACE.
//
// The owner's ask, verbatim: "a user fills in information; the form has a shareable link that
// accepts submissions either from authorized people (by email) or from the public. Google Forms
// is the reference."
//
// This file is the BUILDER — the in-app surface a `form`-kind view renders. The page a stranger
// fills in is `FormPublic.tsx` and it already shipped in wave 23, along with the whole public
// door (`api/routes_forms.py`). What had never existed was any way to CREATE the thing they
// serve: `form` was not a legal display mode and `_clean_display` dropped `display.form`, so the
// key the public door reads could not be written by anything. That is D-90, and it is why this
// component is mostly plumbing rather than a new idea — the door was built first and the handle
// never fitted.
//
// THE TWO HALVES, and they are stored in two different places on purpose:
//   * the SPEC (questions, title, who may submit) lives on the view, at `config.display.form`,
//     written through the host's ordinary view-save path like every other mode's configuration;
//   * the TOKEN lives in a server-owned index a browser cannot write, because a share token that
//     rides the wire is a token a browser can CHOOSE, and the public resolver answers with the
//     first tenant that matches. See `routes_forms.py`'s note for the whole argument.
// ---------------------------------------------------------------------------

import { useCallback, useEffect, useMemo, useState } from "react";
import { API_V1, CREDENTIALS } from "../apiContract";
import type { Field, FieldType, FormSpec } from "./types";
import { TYPE_LABELS } from "./iconShapes";
import "./FormInterface.css";

/**
 * The field types a form may COLLECT. **Mirrors `platform/aios_grid.py::FORM_FIELD_TYPES` and
 * `verify_forms.py` compares the two files name-for-name** — a type offered here and refused
 * there is a question that silently vanishes from the published form, which is the failure a
 * one-sided list produces every time.
 *
 * An ALLOW-LIST, not a list of exclusions: a type missing here is a question the builder cannot
 * ask yet, while a type wrongly present is a public door writing values its column cannot mean.
 * The two mistakes do not cost the same, so the list fails closed.
 */
export const FORM_FIELD_TYPES: ReadonlySet<FieldType> = new Set<FieldType>([
  "text", "select", "multiselect", "int", "currency", "pct", "date",
  "checkbox", "phone", "email", "url", "rating",
]);

/**
 * The stored bag at `view.config.display.form` is `FormSpec`, and it is DECLARED IN `./types`
 * (imported above), not here. ⛔ Declaring it in this file made a type-only circular import —
 * `types.ts` needs it for `DisplaySpec.form` while this file needs `Field` from `types.ts` — which
 * vite erased and `tsc --ignoreConfig` could not, killing `verify_optimism` and
 * `verify_live_workspace` with TS6142. Re-exported here so this file still names the contract it
 * edits; the single declaration stays in the type module. ⛔ No `token` — see the header.
 */
export type { FormSpec } from "./types";

export interface FormInterfaceProps {
  /** The database this form collects into (`topicForScope(scope)`), e.g. `ut_leads`. */
  topic: string;
  /** The view holding the spec. `null` while no saved view is active — a form has nowhere to live. */
  viewId: string | null;
  /** Every column of the table; this component decides which can be questions. */
  fields: Field[];
  /**
   * The stored spec, or `null` when this view has no form yet.
   * ⚠ REQUIRED and NULLABLE, never optional: the host passes `displaySpec?.form ?? null`, and the
   * `?? null` is the point — an optional prop degrades to "the feature does not exist", which is
   * indistinguishable from never having been built.
   */
  spec: FormSpec | null;
  /** Writes the spec back into `config.display.form`. `null` clears it. */
  onSpec: (next: FormSpec | null) => void;
  /** May this viewer change the form's configuration (the view-config grant)? */
  canEdit: boolean;
  /** Why not, in one sentence, when `canEdit` is false. `null` when it is true. */
  readOnlyReason: string | null;
  /** Does this database accept new records at all? A locked one can never be a form's target. */
  canCollect: boolean;
}

interface LinkState {
  token: string | null;
  url: string | null;
  stored: { fields: number; access: string; title: string } | null;
}

const EMPTY_LINK: LinkState = { token: null, url: null, stored: null };

/** The share URL the page shows. The server returns an origin-qualified one when it knows the
 *  public base; otherwise it returns a path, and only the browser knows where it is. */
function absolute(url: string): string {
  return url.startsWith("http") ? url : `${window.location.origin}${url}`;
}

export function FormInterface({
  topic,
  viewId,
  fields,
  spec,
  onSpec,
  canEdit,
  readOnlyReason,
  canCollect,
}: FormInterfaceProps) {
  const [link, setLink] = useState<LinkState>(EMPTY_LINK);
  const [busy, setBusy] = useState(false);
  const [problem, setProblem] = useState("");
  const [copied, setCopied] = useState(false);
  const [emailDraft, setEmailDraft] = useState("");

  /**
   * The columns that can be questions.
   *
   * ⚠ The server enforces this too, and both walls are needed rather than one: a column can
   * BECOME computed (a formula, a rollup, a metric binding) long after it was added to a live
   * form, and this component is not running when that happens. Here it is a courtesy that keeps
   * the builder honest; `routes_forms._public_form` is the wall.
   */
  const askable = useMemo(
    () =>
      fields.filter(
        (f) =>
          FORM_FIELD_TYPES.has(f.type) &&
          f.source !== "odoo" &&
          !f.automation &&
          !f.rollup &&
          !f.metric
      ),
    [fields]
  );
  const byKey = useMemo(() => new Map(fields.map((f) => [f.key, f])), [fields]);

  const chosen = spec?.fields ?? [];
  const required = useMemo(() => new Set(spec?.required ?? []), [spec]);
  const emails = spec?.emails ?? [];
  const restricted = spec?.access === "emails";

  /**
   * ⭐ THE DEAD-STATE DETECTOR, and it earns its four lines.
   *
   * The spec reaches this panel through the host's `cleanDisplay`, which builds its output key by
   * key — so a host that does not carry `display.form` hands back `null` forever. The panel would
   * then look exactly like a form nobody has built yet: every keystroke accepted, nothing stored,
   * no error anywhere. That is [W-07]'s failure verbatim — "the feature does not exist" and "the
   * feature was never wired" are indistinguishable from the outside, and four consecutive waves
   * lost a hand-off to it.
   *
   * One write plus one render answers it: if this panel has SENT a spec and the next render still
   * has none, the carry is missing. It cannot false-positive on a slow save — `onSpec` is
   * synchronous host state, not a request.
   */
  const [wrote, setWrote] = useState(false);
  const carryBroken = wrote && spec === null;

  const patch = useCallback(
    (next: Partial<FormSpec>) => {
      if (!canEdit) return;
      setWrote(true);
      onSpec({ ...(spec ?? {}), ...next });
    },
    [canEdit, onSpec, spec]
  );

  // ---- the share link, read from the server (never minted on read) -------------------------
  const url = `${API_V1}/form-link?topic=${encodeURIComponent(topic)}&view=${encodeURIComponent(
    viewId ?? ""
  )}`;

  useEffect(() => {
    if (!viewId) {
      setLink(EMPTY_LINK);
      return;
    }
    let live = true;
    fetch(url, { credentials: CREDENTIALS })
      .then((r) => (r.ok ? r.json() : EMPTY_LINK))
      .then((body: LinkState) => {
        if (live) setLink(body ?? EMPTY_LINK);
      })
      .catch(() => {
        if (live) setLink(EMPTY_LINK);
      });
    return () => {
      live = false;
    };
  }, [url, viewId]);

  const call = useCallback(
    async (method: "POST" | "DELETE", body?: unknown) => {
      setBusy(true);
      setProblem("");
      try {
        const response = await fetch(method === "POST" ? `${API_V1}/form-link` : url, {
          method,
          credentials: CREDENTIALS,
          ...(body
            ? { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
            : {}),
        });
        const payload = await response.json().catch(() => null);
        if (!response.ok) {
          setProblem(
            (payload as { error?: { message?: string } } | null)?.error?.message ??
              `The server answered ${response.status}.`
          );
          return;
        }
        setLink(
          method === "DELETE"
            ? { ...EMPTY_LINK, stored: link.stored }
            : { ...(payload as LinkState), stored: link.stored }
        );
      } finally {
        setBusy(false);
      }
    },
    [link.stored, url]
  );

  const copy = useCallback(() => {
    if (!link.url) return;
    void navigator.clipboard?.writeText(absolute(link.url));
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1600);
  }, [link.url]);

  // ---- the states that are not a form -------------------------------------------------------
  if (!viewId) {
    return (
      <div className="cg-form-empty">
        A form belongs to a saved view. Save this view first, then build the form on it.
      </div>
    );
  }
  if (!canCollect) {
    // ⚠ NOT `readOnlyReason`, and the two must not be merged: this is a fact about the DATABASE
    // (a locked one accepts no new records, so a form could only ever collect refusals), while
    // `readOnlyReason` is about the VIEWER's grant.
    return (
      <div className="cg-form-empty">
        This database does not accept new records, so a form has nowhere to put an answer.
      </div>
    );
  }

  const diverged = spec === null && (link.stored?.fields ?? 0) > 0;

  return (
    <div className="cg-form-build">
      <div className="cg-form-pane">
        <h2 className="cg-form-h">Form</h2>
        {!canEdit && readOnlyReason && <p className="cg-form-note">{readOnlyReason}</p>}
        {/* ⚠ The divergence banner. The `spec` prop comes from the host's `cleanDisplay`, and if
            that ever stops carrying `display.form` the panel would render an empty form over a
            stored one — and the next save would erase real questions. Saying so beats losing
            them quietly. */}
        {diverged && (
          <p className="cg-form-warn">
            The server holds {link.stored?.fields} question
            {link.stored?.fields === 1 ? "" : "s"} for this view that this page is not carrying.
            Reload before editing — saving now would replace them.
          </p>
        )}
        {/* ⚠ The banner above needs a STORED spec to compare against, so it can only speak once a
            form has been saved successfully at least once. This one covers the state before
            that: nothing has ever been stored, so there is nothing to diverge from. */}
        {carryBroken && (
          <p className="cg-form-warn">
            Nothing is being saved: this view gave back no form after the last change. Reload the
            page — if it repeats, this build cannot store forms and an administrator should be
            told, because everything typed here is being discarded.
          </p>
        )}

        <label className="cg-form-row">
          <span>Title</span>
          <input
            type="text"
            value={spec?.title ?? ""}
            maxLength={120}
            disabled={!canEdit}
            placeholder="What this form is for"
            onChange={(e) => patch({ title: e.target.value })}
          />
        </label>
        <label className="cg-form-row">
          <span>Description</span>
          <textarea
            rows={2}
            value={spec?.desc ?? ""}
            maxLength={1000}
            disabled={!canEdit}
            placeholder="Anything the person filling it in should know"
            onChange={(e) => patch({ desc: e.target.value })}
          />
        </label>
        <label className="cg-form-row">
          <span>Submit button</span>
          <input
            type="text"
            value={spec?.submitLabel ?? ""}
            maxLength={60}
            disabled={!canEdit}
            placeholder="Submit"
            onChange={(e) => patch({ submitLabel: e.target.value })}
          />
        </label>

        <h3 className="cg-form-sub">Questions</h3>
        {askable.length === 0 ? (
          <p className="cg-form-note">
            No column on this database can be a form question yet. A form can ask for text,
            numbers, dates, choices and yes/no — it cannot ask for a value the app computes.
          </p>
        ) : (
          <ul className="cg-form-fields">
            {askable.map((f) => {
              const on = chosen.includes(f.key);
              return (
                <li key={f.key} className={on ? "cg-form-field on" : "cg-form-field"}>
                  <label className="cg-form-pick">
                    <input
                      type="checkbox"
                      checked={on}
                      disabled={!canEdit}
                      onChange={() =>
                        patch({
                          // Order is the FORM's, so a newly ticked question joins the END rather
                          // than being sorted into the column order — the same rule the server
                          // preserves and the public page renders by.
                          fields: on
                            ? chosen.filter((k) => k !== f.key)
                            : [...chosen, f.key],
                          required: on
                            ? (spec?.required ?? []).filter((k) => k !== f.key)
                            : spec?.required,
                        })
                      }
                    />
                    <span className="cg-form-label">{f.label}</span>
                    <span className="cg-form-type">{TYPE_LABELS[f.type]}</span>
                  </label>
                  {on && (
                    <label className="cg-form-req">
                      <input
                        type="checkbox"
                        checked={required.has(f.key)}
                        disabled={!canEdit}
                        onChange={() =>
                          patch({
                            required: required.has(f.key)
                              ? (spec?.required ?? []).filter((k) => k !== f.key)
                              : [...(spec?.required ?? []), f.key],
                          })
                        }
                      />
                      <span>Required</span>
                    </label>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <div className="cg-form-pane">
        <h3 className="cg-form-sub">Who can submit</h3>
        <label className="cg-form-radio">
          <input
            type="radio"
            name="cg-form-access"
            checked={!restricted}
            disabled={!canEdit}
            onChange={() => patch({ access: "public" })}
          />
          <span>
            <strong>Anyone with the link</strong>
            <em>No sign-in. Rate-limited, and capped per day.</em>
          </span>
        </label>
        <label className="cg-form-radio">
          <input
            type="radio"
            name="cg-form-access"
            checked={restricted}
            disabled={!canEdit}
            onChange={() => patch({ access: "emails" })}
          />
          <span>
            <strong>Only the people I list</strong>
            {/* ⚠ The honest boundary, in the UI and not only in the docstring: this asks the
                submitter to name themselves and refuses an address that is not on the list. It
                does not PROVE the address is theirs. */}
            <em>
              They enter their email address and it must match the list. This identifies them; it
              does not verify them — anyone with the link who knows a listed address can submit.
            </em>
          </span>
        </label>

        {restricted && (
          <div className="cg-form-emails">
            <ul>
              {emails.map((e) => (
                <li key={e}>
                  <span>{e}</span>
                  {canEdit && (
                    <button
                      type="button"
                      onClick={() => patch({ emails: emails.filter((x) => x !== e) })}
                      aria-label={`Remove ${e}`}
                    >
                      Remove
                    </button>
                  )}
                </li>
              ))}
              {emails.length === 0 && (
                <li className="cg-form-note">
                  Nobody is listed yet, so nobody can submit this form.
                </li>
              )}
            </ul>
            {canEdit && (
              <form
                className="cg-form-addmail"
                onSubmit={(e) => {
                  e.preventDefault();
                  const next = emailDraft.trim().toLowerCase();
                  if (!next || emails.includes(next)) return;
                  patch({ emails: [...emails, next] });
                  setEmailDraft("");
                }}
              >
                <input
                  type="email"
                  value={emailDraft}
                  placeholder="name@company.com"
                  onChange={(e) => setEmailDraft(e.target.value)}
                />
                <button type="submit">Add</button>
              </form>
            )}
          </div>
        )}

        <h3 className="cg-form-sub">Share link</h3>
        {problem && <p className="cg-form-warn">{problem}</p>}
        {link.url ? (
          <div className="cg-form-link">
            <code>{absolute(link.url)}</code>
            <div className="cg-form-actions">
              <button type="button" onClick={copy}>
                {copied ? "Copied" : "Copy link"}
              </button>
              {canEdit && (
                <>
                  <button
                    type="button"
                    disabled={busy}
                    onClick={() => void call("POST", { topic, view: viewId, regenerate: true })}
                    title="Mints a new link. The old one stops working immediately."
                  >
                    Regenerate
                  </button>
                  <button type="button" disabled={busy} onClick={() => void call("DELETE")}>
                    Turn off
                  </button>
                </>
              )}
            </div>
          </div>
        ) : (
          <div className="cg-form-link">
            <p className="cg-form-note">
              This form has no link yet, so nobody outside the app can reach it.
            </p>
            {canEdit && (
              <button
                type="button"
                disabled={busy || chosen.length === 0}
                onClick={() => void call("POST", { topic, view: viewId })}
              >
                Create link
              </button>
            )}
            {chosen.length === 0 && (
              <p className="cg-form-note">Pick at least one question first.</p>
            )}
          </div>
        )}
        {chosen.length > 0 && (
          <p className="cg-form-note">
            Answers land as new records in this database
            {chosen.length === 1 ? ", one column" : `, ${chosen.length} columns`}:{" "}
            {chosen.map((k) => byKey.get(k)?.label ?? k).join(", ")}.
          </p>
        )}
      </div>
    </div>
  );
}

export default FormInterface;
