/* P.E.S.T Web App — root: async store provider over nc-proxy.php, routing, login gate */

const EMPTY_STATE = {
  projects: [],
  tokens: [],
  users: [],
  referralsByMember: {},
  votes: [],
  myVotes: {},
  user: { nick: "", username: "", isAdmin: false, pinned: [], watchTokens: [] },
  loggedIn: false,
  bootstrapped: false,
};

const ERR_MSG = {
  invalid_credentials: "Wrong username or password.",
  missing_fields:      "Fill in all fields.",
  username_taken:      "That username is taken.",
  username_too_short:  "Username is too short.",
  password_too_short:  "Use at least 6 characters.",
  wrong_password:      "Current password is wrong.",
  pin_cap:             "Dashboard is full (8 max).",
  watch_cap:           "Watchlist is full (8 max).",
  ref_cap:             "You can only share 3 referral links.",
  unknown_user:        "That user no longer exists.",
  nick_taken:          "That nickname is already taken.",
  cannot_delete_self:  "You can't delete your own account.",
  cannot_demote_self:  "You can't remove your own admin rights.",
  invalid_user:        "Fill in nick, username and a password (6+ chars).",
  invalid_url:         "Enter a valid URL (https://…).",
  unknown_project:     "That project no longer exists.",
  unknown_token:       "That token no longer exists.",
  duplicate_id:        "An item with that id already exists.",
  duplicate_token:     "A token with that symbol already exists.",
  already_voted:       "You already voted.",
  vote_closed:         "Voting is closed for this proposal.",
  admin_only:          "Admin only.",
  unauthenticated:     "Session expired. Please log in again.",
  file_too_large:      "Image is too large (max 2 MB).",
  not_an_image:        "That file isn't a valid image.",
  unsupported_image_type: "Use PNG, JPG, GIF or WEBP.",
};
function errMsg(e) {
  const k = (e && e.message) || "";
  return ERR_MSG[k] || ("Something went wrong (" + (k || "unknown") + ")");
}

// Translate server response { db, nick } into local state slice.
function deriveState(resp) {
  const db = resp.db || {};
  const meNick = resp.nick || "";
  const me = (db.users || []).find((u) => u.nick === meNick) || {};
  return {
    projects: db.projects || [],
    tokens:   db.tokens   || [],
    users:    db.users    || [],
    referralsByMember: db.referralsByMember || {},
    votes:    db.votes    || [],
    myVotes:  (db.myVotes && typeof db.myVotes === "object") ? db.myVotes : {},
    user: {
      nick:        me.nick     || meNick,
      username:    me.username || "",
      isAdmin:     !!me.isAdmin,
      pinned:      me.pinned      || [],
      watchTokens: me.watchTokens || [],
    },
    loggedIn: true,
    bootstrapped: true,
  };
}

function useStoreState() {
  const [state, setState] = useState(EMPTY_STATE);
  const members = useMemo(() => window.PEST_APP.getMembers(), []);

  // Initial: try to resume session from cookie.
  useEffect(() => {
    let alive = true;
    window.PEST_APP.api.fetchDb()
      .then((resp) => { if (alive) setState(deriveState(resp)); })
      .catch((e) => {
        if (!alive) return;
        if (e.status === 401) setState((s) => ({ ...s, bootstrapped: true, loggedIn: false }));
        else { toast(errMsg(e), "err"); setState((s) => ({ ...s, bootstrapped: true, loggedIn: false })); }
      });
    return () => { alive = false; };
  }, []);

  const member = useMemo(
    () => members.find((m) => m.nick === state.user.nick) || members[0],
    [members, state.user.nick]
  );

  const runOp = useCallback(async (type, payload, okMsg) => {
    try {
      const resp = await window.PEST_APP.api.op(type, payload);
      setState(deriveState(resp));
      if (okMsg) toast(okMsg);
      return true;
    } catch (e) {
      if (e.status === 401) { setState({ ...EMPTY_STATE, bootstrapped: true }); toast("Session expired.", "err"); }
      else { toast(errMsg(e), "err"); }
      return false;
    }
  }, []);

  const api = useMemo(() => ({
    projects: state.projects,
    tokens: state.tokens,
    users: state.users,
    referralsByMember: state.referralsByMember,
    votes: state.votes,
    myVotes: state.myVotes,
    user: state.user,
    members,
    member,
    loggedIn: state.loggedIn,
    bootstrapped: state.bootstrapped,

    login: async (username, password) => {
      try {
        const resp = await window.PEST_APP.api.login(username, password);
        setState(deriveState(resp));
        return { ok: true };
      } catch (e) { return { ok: false, error: errMsg(e) }; }
    },
    logout: async () => {
      try { await window.PEST_APP.api.logout(); } catch (_) {}
      setState({ ...EMPTY_STATE, bootstrapped: true });
    },

    pinProject:    (id) => runOp("pinProject",    { id }),
    unpinProject:  (id) => runOp("unpinProject",  { id }),
    toggleToken:   (id) => runOp("toggleToken",   { id }),
    setUsername:   (username) => runOp("setUsername",  { username }, "Username updated"),
    changePassword:(current, next) => runOp("changePassword", { current, new: next }, "Password changed"),
    addReferral:   (projectId, url) => runOp("addReferral",   { projectId, url }, "Referral link added"),
    removeReferral:(idx) => runOp("removeReferral", { idx }, "Referral removed"),
    castVote:      (voteId, optionId) => runOp("castVote", { voteId, optionId }, "Vote cast"),

    addProject:    (proj) => runOp("addProject",    proj, proj.name + " added"),
    updateProject: (proj) => runOp("updateProject", proj, proj.name + " updated"),
    deleteProject: (id, name) => runOp("deleteProject", { id }, (name || "Project") + " deleted"),

    addToken:      (tok) => runOp("addToken",    tok, tok.symbol + " added"),
    updateToken:   (tok) => runOp("updateToken", tok, tok.symbol + " updated"),
    deleteToken:   (id, symbol) => runOp("deleteToken", { id }, (symbol || "Token") + " deleted"),

    addVote:       (vote) => runOp("addVote",   vote, "Vote created"),
    closeVote:     (id)   => runOp("closeVote", { id }, "Vote closed"),
    deleteVote:    (id)   => runOp("deleteVote",{ id }, "Vote deleted"),

    createUser:     (u) => runOp("createUser", u, u.nick + " created"),
    adminUpdateUser:(patch, okMsg) => runOp("adminUpdateUser", patch, okMsg || "User updated"),
    deleteUser:     (nick) => runOp("deleteUser", { nick }, nick + " deleted"),
  }), [state, members, member, runOp]);

  return api;
}

function BootSplash() {
  return (
    <div className="login">
      <div className="app-bg"></div>
      <div className="app-grid"></div>
      <div className="login-card" style={{ display: "grid", placeItems: "center", padding: 40 }}>
        <div className="login-emblem"><img src="assets/pest-emblem.png" alt="P.E.S.T" />
          <span className="wm grad-text" data-text="P.E.S.T">P.E.S.T</span>
          <span className="sub">connecting…</span>
        </div>
      </div>
    </div>
  );
}

function App() {
  const store = useStoreState();
  const [route, setRoute] = useState("dashboard");
  const [guest, setGuest] = useState(false);

  if (!store.bootstrapped) {
    return <window.StoreCtx.Provider value={store}><BootSplash /><ToastHost /></window.StoreCtx.Provider>;
  }

  if (!store.loggedIn) {
    if (guest) {
      return (
        <window.StoreCtx.Provider value={store}>
          <window.Guest onExit={() => setGuest(false)} />
          <ToastHost />
        </window.StoreCtx.Provider>
      );
    }
    return (
      <window.StoreCtx.Provider value={store}>
        <window.Login onLoggedIn={() => setRoute("dashboard")} onGuest={() => setGuest(true)} />
        <ToastHost />
      </window.StoreCtx.Provider>
    );
  }

  let screen = null;
  if (route === "dashboard") screen = <window.Dashboard onNavigate={setRoute} />;
  else if (route === "members") screen = <window.Members />;
  else if (route === "projects") screen = <window.Projects onNavigate={setRoute} />;
  else if (route === "votes") screen = <window.Votes />;
  else if (route === "profile") screen = <window.Profile />;
  else if (route === "admin") screen = store.user.isAdmin ? <window.Admin /> : <window.Dashboard onNavigate={setRoute} />;

  return (
    <window.StoreCtx.Provider value={store}>
      <window.Shell route={route} setRoute={setRoute}>
        {screen}
      </window.Shell>
      <ToastHost />
    </window.StoreCtx.Provider>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
