// ---------------------------------------------------------------------------
// shell/LoginPage.tsx — X5: the branded door.
//
// THE CARD LANGUAGE IS `gate()`'s, DELIBERATELY (app.py:1228). Same lockup
// (mark + "Loopable" wordmark, side by side — wave-9 I5), same 410px card with
// ONE hairline closing all four sides (wave-10 I25a removed the gold top-rule:
// "there is an orange line at the top of the sign-in place… it's ugly" — do not
// reintroduce an accent edge here), same two fields and one primary button.
// Two doors into one product must not look like two products, and this is the
// door that outlives the other: `gate()` dies with app.py at EXIT-6.
//
// WHAT IT DOES NOT INHERIT: `gate()`'s `st.empty()` slot dance is a workaround
// for Streamlit replacing elements POSITIONALLY, labelled in its own docstring
// as something "the successor shell deletes". This is that successor. React
// unmounts what it unmounts; there is no ghost card to chase.
//
// NO CLIENT-SIDE BACKOFF (X5: the server owns it). The only local rule is
// single-flight — a second submit while one is in the air is dropped, which is
// double-click protection, not rate limiting.
// ---------------------------------------------------------------------------

import { useState } from "react";
import type { FormEvent } from "react";
import { Brand } from "./Brand";
import { login } from "./session";
import type { SessionUser } from "./session";

/** The reveal toggle the host's password field ships natively (BaseWeb's eye).
 *  Two doors, one affordance. */
function EyeIcon({ off }: { off: boolean }) {
  return (
    <svg viewBox="0 0 20 20" aria-hidden="true">
      <path d="M2.2 10s2.8-4.6 7.8-4.6S17.8 10 17.8 10 15 14.6 10 14.6 2.2 10 2.2 10z" />
      <circle cx="10" cy="10" r="2.3" />
      {off ? <path d="M4 16 16 4" /> : null}
    </svg>
  );
}

export default function LoginPage({ onSignedIn }: { onSignedIn: (user: SessionUser) => void }) {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);
  const [reveal, setReveal] = useState(false);

  async function submit(e: FormEvent) {
    e.preventDefault();
    if (busy) return;
    setBusy(true);
    setError("");
    const result = await login(username, password);
    if (result.ok) {
      // No setBusy(false): the parent unmounts this tree on success, and
      // flipping state on the way out is how "update on an unmounted
      // component" warnings are born.
      onSignedIn(result.user);
      return;
    }
    setBusy(false);
    setError(result.message);
    setPassword("");
  }

  // An error that survives the correction is an error that reads as sticky.
  function edit(set: (v: string) => void) {
    return (e: { target: { value: string } }) => {
      if (error) setError("");
      set(e.target.value);
    };
  }

  return (
    <div className="login-root">
      <div className="login-card">
        <Brand size={44} className="login-brand" />

        <form className="login-form" onSubmit={submit} noValidate>
          {/* Labels are present and hidden, not absent. `gate()` collapses them
              because Streamlit gives no other way to get a placeholder-only
              field; here the label exists for assistive tech and the visual is
              identical. */}
          {/* No autoFocus: the other door cannot autofocus (Streamlit), and the
              focus ring it would paint on load reads as a highlighted error on
              an untouched form. */}
          <label className="lp-sr-only" htmlFor="login-username">Username</label>
          <input
            id="login-username"
            className="login-input"
            name="username"
            type="text"
            autoComplete="username"
            placeholder="Username"
            value={username}
            onChange={edit(setUsername)}
            disabled={busy}
            aria-invalid={error ? true : undefined}
          />

          <label className="lp-sr-only" htmlFor="login-password">Password</label>
          <div className="login-field">
            <input
              id="login-password"
              className="login-input"
              name="password"
              type={reveal ? "text" : "password"}
              autoComplete="current-password"
              placeholder="Password"
              value={password}
              onChange={edit(setPassword)}
              disabled={busy}
              aria-invalid={error ? true : undefined}
            />
            {/* tabIndex −1: Tab goes Username → Password → Log in; the reveal is
                a mouse affordance, same as the host's. */}
            <button
              type="button"
              className="login-eye"
              aria-label={reveal ? "Hide password" : "Show password"}
              onClick={() => setReveal((v) => !v)}
              disabled={busy}
              tabIndex={-1}
            >
              <EyeIcon off={reveal} />
            </button>
          </div>

          {/* Disabled through THREE channels (the real attribute, the cursor and
              the opacity), never opacity alone — a control that only looks dead
              is a control that still takes the click. */}
          <button className="login-submit" type="submit" disabled={busy}>
            {busy ? "Signing in…" : "Log in"}
          </button>
        </form>

        {/* The message line is ALWAYS in the layout (min-height: 1lh) so an
            arriving error does not shove the card. `role="status"` rather than
            "alert": polite announcement, no interruption. */}
        <p className="login-msg" role="status" aria-live="polite">
          {error}
        </p>
      </div>
    </div>
  );
}
