import { useEffect, useMemo, useState } from "react";
import { ApiError } from "../../api/client";
import type { Session, ViewerTokenResponse } from "../../api/types";
import { Badge } from "../../components/ui/Badge";
import { Button } from "../../components/ui/Button";
import { Modal } from "../../components/ui/Modal";
import { Spinner } from "../../components/ui/Spinner";
import { ErrorState } from "../../components/ui/States";
import { CopyIcon } from "../../components/layout/icons";
import { useClipboard } from "../../lib/useClipboard";
import { useMintViewerToken } from "./hooks";
import "./sessions.css";

interface JoinSessionModalProps {
  /** The session to join, or null when the modal is closed. */
  session: Session | null;
  /** Whether the caller may request a control token (admin or `control`). */
  canControl: boolean;
  onClose: () => void;
}

/**
 * Build the exact `/ws/viewer` URL the relay expects for a minted token. The
 * relay reads `session_id`, `viewer_name`, and `token` query params
 * (server/src/relay/mod.rs::ViewerParams) and binds the token to this session.
 * Same-origin in production; the dashboard is served behind the same host as
 * the relay.
 */
function buildViewerUrl(token: ViewerTokenResponse, viewerName: string): string {
  const wsProto = window.location.protocol === "https:" ? "wss:" : "ws:";
  const qs = new URLSearchParams({
    session_id: token.session_id,
    viewer_name: viewerName,
    token: token.token,
  });
  return `${wsProto}//${window.location.host}/ws/viewer?${qs.toString()}`;
}

/**
 * Join action. The in-dashboard web viewer is not built yet (a later pass), and
 * the existing static `viewer.html` is incompatible with v2: it reads the token
 * from localStorage and sends the raw login JWT, which the v2 relay rejects on
 * the viewer plane (it requires a session-scoped viewer token). So instead of
 * opening a viewer that would be refused, we mint the token here and reveal it
 * with the exact relay URL and copy buttons. This is the honest, working bridge
 * until the web viewer ships.
 *
 * The access mode (Control vs View) is decided SERVER-SIDE from the caller's
 * permissions; we only request and reflect it. `canControl` gates the label and
 * intent so we never present a control affordance the server would downgrade.
 */
export function JoinSessionModal({
  session,
  canControl,
  onClose,
}: JoinSessionModalProps) {
  const open = session != null;
  const mint = useMintViewerToken();
  const tokenCopy = useClipboard();
  const urlCopy = useClipboard();
  const [result, setResult] = useState<ViewerTokenResponse | null>(null);

  const viewerName = canControl ? "Operator" : "Observer";

  // Mint once when the modal opens for a session; reset on close. We mint via an
  // effect (not on the click) so the modal can own the loading/error/success
  // states cleanly and a re-open always re-mints a fresh short-lived token.
  useEffect(() => {
    if (!session) {
      setResult(null);
      mint.reset();
      return;
    }
    let cancelled = false;
    mint
      .mutateAsync(session.id)
      .then((res) => {
        if (!cancelled) setResult(res);
      })
      .catch(() => {
        // Error surfaced via mint.isError below; nothing to do here.
      });
    return () => {
      cancelled = true;
    };
    // Mint is keyed to the session id only; the mutation object is stable.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [session?.id]);

  const viewerUrl = useMemo(
    () => (result ? buildViewerUrl(result, viewerName) : ""),
    [result, viewerName],
  );

  if (!open) return null;

  const grantedControl = result?.access === "control";
  const ttlMins = result ? Math.round(result.expires_in_secs / 60) : 0;

  return (
    <Modal
      open={open}
      title={canControl ? "Control session" : "View session"}
      onClose={onClose}
      wide
      footer={<Button variant="primary" onClick={onClose}>Done</Button>}
    >
      <p className="joinmodal__lede">
        A short-lived viewer token for{" "}
        <span className="dt__strong">{session.agent_name}</span> scoped to this
        session only. The in-dashboard web viewer ships in a later pass; until
        then, use this token with the relay URL below to connect a viewer.
      </p>

      {mint.isPending && !result ? (
        <div className="joinmodal__loading">
          <Spinner label="Minting viewer token…" />
        </div>
      ) : mint.isError ? (
        <ErrorState
          title="Could not mint a viewer token"
          message={
            mint.error instanceof ApiError
              ? mint.error.message
              : "The relay did not return a token. The session may have just ended."
          }
          action={
            <Button
              variant="primary"
              onClick={() => {
                if (session)
                  void mint.mutateAsync(session.id).then(setResult).catch(() => {});
              }}
            >
              Retry
            </Button>
          }
        />
      ) : result ? (
        <>
          <div className="joinmodal__accessrow">
            <span className="joinmodal__accesslabel">Access granted</span>
            {grantedControl ? (
              <Badge tone="ok" dot>
                Full control
              </Badge>
            ) : (
              <Badge tone="neutral" dot>
                View only
              </Badge>
            )}
            {canControl && !grantedControl && (
              <span className="joinmodal__downgrade">
                The relay granted view-only for your account.
              </span>
            )}
          </div>

          <FieldReveal
            label="Viewer token"
            value={result.token}
            copied={tokenCopy.copied}
            onCopy={() => void tokenCopy.copy(result.token)}
            mono
          />

          <FieldReveal
            label="Relay viewer URL"
            value={viewerUrl}
            copied={urlCopy.copied}
            onCopy={() => void urlCopy.copy(viewerUrl)}
          />

          <p className="joinmodal__hint">
            Token expires in about {ttlMins} minute{ttlMins === 1 ? "" : "s"} and
            works only for this session. It carries the access mode in its signed
            claims, so it cannot be upgraded to control after the fact.
          </p>
        </>
      ) : null}
    </Modal>
  );
}

interface FieldRevealProps {
  label: string;
  value: string;
  copied: boolean;
  onCopy: () => void;
  mono?: boolean;
}

/** A labeled read-only value with a copy button (token / URL reveal). */
function FieldReveal({ label, value, copied, onCopy, mono }: FieldRevealProps) {
  return (
    <div className="joinmodal__field">
      <span className="joinmodal__fieldlabel">{label}</span>
      <div className="joinmodal__fieldvalue">
        <code className={mono ? "joinmodal__code joinmodal__code--mono" : "joinmodal__code"}>
          {value}
        </code>
        <Button
          variant="ghost"
          size="sm"
          onClick={onCopy}
          aria-label={copied ? `${label} copied to clipboard` : `Copy ${label.toLowerCase()}`}
        >
          <CopyIcon width={14} height={14} />
          {copied ? "Copied" : "Copy"}
        </Button>
      </div>
    </div>
  );
}
