// ---------------------------------------------------------------------------
// shell/ShareDialog.tsx — WAVE 20 items 18 / 23 / 26 (R10, contract C-SHARE):
// ONE dialog that shares a view, a folder or a database, and edits who already
// has it.
//
// ONE dialog for three kinds is the ruling, not a convenience: R10 says folders
// and databases share "with the SAME two-role vocabulary views use". Three
// dialogs would drift into three vocabularies within a wave.
//
// It renders over the ALREADY-LOADED state and decides nothing itself — every
// rule (what a junk entry means, who may administer, what the PUT carries) is in
// `shareModel.ts`, where the access gate can run it.
// ---------------------------------------------------------------------------

import { useCallback, useEffect, useState } from "react";
import { API_V1, CREDENTIALS } from "../apiContract";
import {
  SHARE_ROLES,
  addablePeople,
  parseShare,
  sharePutBody,
  shareSummary,
  withEntry,
  withoutEntry,
} from "./shareModel";
import type { ShareEntry, ShareKind, ShareRole, ShareState } from "./shareModel";

const KIND_WORD: Record<ShareKind, string> = {
  view: "view",
  folder: "folder",
  database: "database",
};

/** What each role MEANS on each kind, in the reader's own terms. A role picker
 *  whose options are two nouns makes the reader guess; these are the sentences
 *  the view rail already uses ("Anyone who can see this table can change it"),
 *  extended to the two new kinds rather than re-invented for them. */
const ROLE_BLURB: Record<ShareKind, Record<ShareRole, string>> = {
  view: {
    view: "Can open this view. Cannot rename, refilter or delete it.",
    edit: "Can change this view's filters, sorts and columns.",
  },
  folder: {
    view: "Can open the folder and the views inside it.",
    edit: "Can rename the folder and move views in and out of it.",
  },
  database: {
    view: "Can open this database and read its records.",
    edit: "Can add, edit and delete its records.",
  },
};

export default function ShareDialog({
  kind,
  id,
  label,
  me,
  onClose,
  onToast,
}: {
  kind: ShareKind;
  id: string;
  label: string;
  /** The signed-in account's username, so the reader recognises themselves in the
   *  list. `people` is deliberately the OTHER accounts (it is the add-picker's
   *  source), so without this the owner row prints a raw login where every other
   *  row prints a name. */
  me: string;
  onClose: () => void;
  onToast: (message: string) => void;
}) {
  const [state, setState] = useState<ShareState | null>(null);
  const [entries, setEntries] = useState<ShareEntry[]>([]);
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);
  const [pick, setPick] = useState("");
  const [pickRole, setPickRole] = useState<ShareRole>("view");

  const path = `${API_V1}/share/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`;

  useEffect(() => {
    let dead = false;
    setError("");
    void (async () => {
      try {
        const res = await fetch(path, { credentials: CREDENTIALS });
        if (!res.ok) {
          // 4xx text is policy the reader needs; a 5xx's internals are not theirs.
          if (!dead) setError(res.status >= 500
            ? "Something went wrong on our side. Try again in a moment."
            : `The server answered ${res.status}.`);
          return;
        }
        const body = (await res.json().catch(() => null)) as unknown;
        if (dead) return;
        const parsed = parseShare(body);
        setState(parsed);
        setEntries(parsed.entries);
      } catch {
        if (!dead) setError("Cannot reach the server.");
      }
    })();
    return () => {
      dead = true;
    };
  }, [path]);

  // ⛔ ESCAPE CLOSES A MODAL, or its scrim becomes a trap — the wave-18 lesson this
  // shell already carries at its other two dialogs.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape" && !busy) onClose();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [busy, onClose]);

  const save = useCallback(
    async (next: ShareEntry[]) => {
      setBusy(true);
      setError("");
      try {
        const res = await fetch(path, {
          method: "PUT",
          credentials: CREDENTIALS,
          headers: { "Content-Type": "application/json" },
          // The WHOLE list, every time: the PUT replaces, so a body assembled from
          // a delta would revoke everyone it failed to mention.
          body: JSON.stringify(sharePutBody(next)),
        });
        if (!res.ok) {
          const body = (await res.json().catch(() => null)) as
            | { error?: { message?: string } }
            | null;
          setError(
            res.status >= 500
              ? "Something went wrong on our side. Try again in a moment."
              : body?.error?.message || `The server answered ${res.status}.`
          );
          return false;
        }
        const body = (await res.json().catch(() => null)) as unknown;
        // Re-read the SERVER's copy rather than trusting the draft: `_clean_entries`
        // drops what it will not store, and an editor that kept showing a row the
        // store rejected would be the "shared, silently inert" failure this feature
        // exists to avoid.
        // ⚠ Only `.entries` is consumed here — the PUT answers with the stored record
        // (`{owner, entries}`) and says nothing about this session's standing, so the
        // literal below feeds the parser's required field and is never read back. The
        // editor's `mayAdminister` stays the one the GET established; a save cannot
        // promote anybody, and this line must never be the reason it looks like it can.
        const saved = parseShare({ ...(body as object), mayAdminister: true });
        setEntries(saved.entries);
        return true;
      } catch {
        setError("Cannot reach the server.");
        return false;
      } finally {
        setBusy(false);
      }
    },
    [path]
  );

  const mayAdminister = !!state?.mayAdminister;
  const options = state ? addablePeople(state.people, entries) : [];
  const nameOf = (user: string) =>
    user === "*"
      ? "Everyone"
      : user && user === me.trim().toLowerCase()
        ? "You"
        : state?.people.find((p) => p.user === user)?.name ?? user;

  return (
    <div className="shell-newdb-scrim" onClick={() => (busy ? null : onClose())}>
      <div
        className="shell-newdb shell-share"
        role="dialog"
        aria-label={`Share ${label}`}
        onClick={(e) => e.stopPropagation()}
      >
        <h2>Share {KIND_WORD[kind]}</h2>
        <p className="shell-newdb-sub">
          <strong>{label}</strong> — who can reach it, and what they can do with it. Sharing
          never widens past this workspace: everyone here can already open the surface it
          lives on.
        </p>

        {!state && !error ? (
          <div className="shell-share-wait">
            <span className="lp-spin" role="status" aria-label="Loading" />
          </div>
        ) : null}

        {state ? (
          <>
            {/* THE MANAGE-ACCESS EDITOR (item 23): the list first, because the
                question people open this for is "who has this already" — with the
                one-line answer above it, since the fact that changes everything
                ("Everyone can edit") is the one a list of rows buries. */}
            <p className="shell-share-summary">{shareSummary(entries)}</p>
            <div className="shell-share-list">
              {state.owner ? (
                <div className="shell-share-row is-owner">
                  <span className="shell-share-who">{nameOf(state.owner)}</span>
                  <span className="shell-share-role">Owner</span>
                </div>
              ) : null}
              {entries.length === 0 ? (
                <div className="shell-share-empty">
                  Not shared with anyone yet.
                </div>
              ) : null}
              {entries.map((e) => (
                <div className="shell-share-row" key={e.user}>
                  <span className="shell-share-who">{nameOf(e.user)}</span>
                  <select
                    className="shell-share-select"
                    value={e.role}
                    disabled={!mayAdminister || busy}
                    aria-label={`Role for ${nameOf(e.user)}`}
                    onChange={(ev) => {
                      const next = withEntry(entries, e.user, ev.target.value as ShareRole);
                      setEntries(next);
                      void save(next);
                    }}
                  >
                    {SHARE_ROLES.map((r) => (
                      <option key={r} value={r}>
                        {r === "edit" ? "Can edit" : "Can view"}
                      </option>
                    ))}
                  </select>
                  <button
                    type="button"
                    className="shell-share-revoke"
                    disabled={!mayAdminister || busy}
                    onClick={() => {
                      const next = withoutEntry(entries, e.user);
                      setEntries(next);
                      void save(next).then((ok) => {
                        if (ok) onToast(`${nameOf(e.user)} no longer has this ${KIND_WORD[kind]}.`);
                      });
                    }}
                  >
                    Remove
                  </button>
                </div>
              ))}
            </div>

            {mayAdminister ? (
              <div className="shell-share-add">
                <select
                  className="shell-share-select"
                  value={pick}
                  disabled={busy}
                  aria-label="Who to share with"
                  onChange={(e) => setPick(e.target.value)}
                >
                  {/* ⚠ An explicit placeholder OPTION, not a blank first row: a
                      <select> whose value matches nothing renders its first option
                      while holding "", so the box would read as a chosen person and
                      the button beside it would grant somebody nobody picked
                      ([[cg-condition-builder-items]]). */}
                  <option value="">Choose a person…</option>
                  <option value="*">Everyone in this workspace</option>
                  {options.map((p) => (
                    <option key={p.user} value={p.user}>
                      {p.name}
                    </option>
                  ))}
                </select>
                <select
                  className="shell-share-select"
                  value={pickRole}
                  disabled={busy}
                  aria-label="Role for the person being added"
                  onChange={(e) => setPickRole(e.target.value as ShareRole)}
                >
                  {SHARE_ROLES.map((r) => (
                    <option key={r} value={r}>
                      {r === "edit" ? "Can edit" : "Can view"}
                    </option>
                  ))}
                </select>
                <button
                  type="button"
                  className="login-submit shell-share-grant"
                  disabled={busy || !pick}
                  onClick={() => {
                    const next = withEntry(entries, pick, pickRole);
                    setEntries(next);
                    void save(next).then((ok) => {
                      if (ok) {
                        onToast(`${nameOf(pick)} can now ${pickRole} this ${KIND_WORD[kind]}.`);
                        setPick("");
                      }
                    });
                  }}
                >
                  Share
                </button>
                <p className="shell-share-blurb">{ROLE_BLURB[kind][pickRole]}</p>
              </div>
            ) : (
              // ⛔ NOT A HIDDEN EDITOR — a stated refusal. A collaborator who can
              // change this object's CONTENT still cannot change who else reaches
              // it (the server's rule; this is the courtesy half). Saying why beats
              // greying three controls and letting the reader guess.
              <p className="shell-share-note">
                Only the owner of this {KIND_WORD[kind]} — or an administrator — can change who
                it is shared with. You can still use it as your role allows.
              </p>
            )}
          </>
        ) : null}

        {error ? <p className="shell-newdb-err">{error}</p> : null}
        <div className="shell-newdb-actions">
          <button type="button" className="login-submit" disabled={busy} onClick={onClose}>
            Done
          </button>
        </div>
      </div>
    </div>
  );
}
