[@@@ocaml.warning "-26-27"]

open Melange_json.Primitives
open React
module Aria = ReactAria

let cimd_suffix_len = String.length "/oauth-client-metadata.json"

type actor = AccountSwitcher.actor =
  {did: string; handle: string; avatar_data_uri: string option [@default None]}
[@@deriving json]

type permission_set_display =
  { nsid: string
  ; title: string option [@default None]
  ; detail: string option [@default None]
  ; expanded_scopes: string list }
[@@deriving json]

type props =
  { client_url: string * string (* (host, path) *)
  ; client_name: string option [@default None]
  ; logo_uri: string option [@default None]
  ; current_user: actor
  ; logged_in_users: actor list
  ; scopes: string list
  ; permission_sets: permission_set_display list [@default []]
  ; code: string
  ; request_uri: string
  ; csrf_token: string }
[@@deriving json]

module ScopesTable = struct
  type repo_action = Create | Update | Delete

  type parsed_repo_scope = {collections: string list; actions: repo_action list}

  type parsed_rpc_scope = {lxm: string; aud: string}

  type parsed_scope =
    | Email of [`Read | `Manage]
    | Identity of [`Handle | `Full]
    | Repo of parsed_repo_scope
    | Rpc of parsed_rpc_scope
    | Blob of string list (* mimetypes *)
    | Bluesky (* transition:generic or app.bsky.* *)
    | Chat (* transition:chat.bsky or chat.bsky.* *)
    | Atproto
    | PermissionSet of
        { nsid: string
        ; title: string option
        ; detail: string option
        ; expanded_scopes: string list (* raw scope strings for display *) }
    | Unknown of string

  let parse_scope scope =
    if scope = "atproto" then Atproto
    else if scope = "transition:generic" then Bluesky
    else if scope = "transition:chat.bsky" then Chat
    else if scope = "transition:email" then Email `Read
    else if
      String.starts_with ~prefix:"account:" scope
      || String.starts_with ~prefix:"account?" scope
    then
      let has_positional = String.starts_with ~prefix:"account:" scope in
      let rest =
        String.sub scope 8 (String.length scope - 8)
      in
      let parts = String.split_on_char '?' rest in
      let positional_attr =
        if has_positional then
          match parts with a :: _ when a <> "" -> Some a | _ -> None
        else None
      in
      let query_str =
        if has_positional then
          if List.length parts > 1 then Some (List.nth parts 1) else None
        else if rest <> "" then Some rest
        else None
      in
      let parse_query_params qs =
        String.split_on_char '&' qs
        |> List.filter_map (fun pair ->
            match String.split_on_char '=' pair with
            | [k; v] ->
                Some (k, v)
            | _ ->
                None )
      in
      let params =
        Option.map parse_query_params query_str |> Option.value ~default:[]
      in
      let attr =
        match positional_attr with
        | Some a ->
            a
        | None ->
            List.find_map
              (fun (k, v) -> if k = "attr" then Some v else None)
              params
            |> Option.value ~default:""
      in
      let action =
        List.find_map
          (fun (k, v) -> if k = "action" then Some v else None)
          params
        |> Option.value ~default:"read"
      in
      if attr = "email" then
        if action = "manage" then Email `Manage else Email `Read
      else Unknown scope (* repo and other attrs not displayed specially *)
    else if
      String.starts_with ~prefix:"identity:" scope
      || String.starts_with ~prefix:"identity?" scope
    then
      (* attrs are "handle" or "*" *)
      let has_positional = String.starts_with ~prefix:"identity:" scope in
      let rest = String.sub scope 9 (String.length scope - 9) in
      let parts = String.split_on_char '?' rest in
      let positional_attr =
        if has_positional then
          match parts with a :: _ when a <> "" -> Some a | _ -> None
        else None
      in
      let attr =
        match positional_attr with
        | Some a ->
            a
        | None ->
            let params =
              if has_positional then
                if List.length parts > 1 then List.nth parts 1 else ""
              else rest
            in
            String.split_on_char '&' params
            |> List.find_map (fun pair ->
                match String.split_on_char '=' pair with
                | [k; v] when k = "attr" ->
                    Some v
                | _ ->
                    None )
            |> Option.value ~default:"handle"
      in
      if attr = "*" then Identity `Full else Identity `Handle
    else if
      String.starts_with ~prefix:"repo:" scope
      || String.starts_with ~prefix:"repo?" scope
    then
      let has_positional = String.starts_with ~prefix:"repo:" scope in
      let rest = String.sub scope 5 (String.length scope - 5) in
      let parts = String.split_on_char '?' rest in
      let positional_coll =
        if has_positional then
          match parts with coll :: _ when coll <> "" -> Some coll | _ -> None
        else None
      in
      let query_str =
        if has_positional then
          if List.length parts > 1 then Some (List.nth parts 1) else None
        else if
          (* for repo?... format, rest starts with the query string *)
          rest <> ""
        then Some rest
        else None
      in
      let parse_query_params qs =
        String.split_on_char '&' qs
        |> List.filter_map (fun pair ->
            match String.split_on_char '=' pair with
            | [k; v] ->
                Some (k, v)
            | _ ->
                None )
      in
      let params =
        Option.map parse_query_params query_str |> Option.value ~default:[]
      in
      let collection =
        match positional_coll with
        | Some c ->
            [c]
        | None -> (
            List.filter_map
              (fun (k, v) -> if k = "collection" then Some v else None)
              params
            |> function [] -> ["*"] | cols -> cols )
      in
      let actions =
        let action_strs =
          List.filter_map
            (fun (k, v) -> if k = "action" then Some v else None)
            params
          |> List.concat_map (String.split_on_char ',')
        in
        if action_strs = [] then [Create; Update; Delete]
        else
          List.filter_map
            (fun a ->
              match a with
              | "create" ->
                  Some Create
              | "update" ->
                  Some Update
              | "delete" ->
                  Some Delete
              | _ ->
                  None )
            action_strs
          |> function [] -> [Create; Update; Delete] | l -> l
      in
      if
        List.exists
          (fun c ->
            String.starts_with ~prefix:"app.bsky." c
            || String.starts_with ~prefix:"chat.bsky." c )
          collection
      then
        if
          List.exists
            (fun c -> String.starts_with ~prefix:"chat.bsky." c)
            collection
        then Chat
        else Bluesky
      else Repo {collections= collection; actions}
    else if
      String.starts_with ~prefix:"rpc:" scope
      || String.starts_with ~prefix:"rpc?" scope
    then
      let has_positional = String.starts_with ~prefix:"rpc:" scope in
      let rest = String.sub scope 4 (String.length scope - 4) in
      let parts = String.split_on_char '?' rest in
      let positional_lxm =
        if has_positional then
          match parts with l :: _ when l <> "" -> Some l | _ -> None
        else None
      in
      let query_str =
        if has_positional then
          if List.length parts > 1 then Some (List.nth parts 1) else None
        else if rest <> "" then Some rest
        else None
      in
      let parse_query_params qs =
        String.split_on_char '&' qs
        |> List.filter_map (fun pair ->
            match String.split_on_char '=' pair with
            | [k; v] ->
                Some (k, v)
            | _ ->
                None )
      in
      let params =
        Option.map parse_query_params query_str |> Option.value ~default:[]
      in
      let lxm =
        match positional_lxm with
        | Some l ->
            l
        | None ->
            List.find_map
              (fun (k, v) -> if k = "lxm" then Some v else None)
              params
            |> Option.value ~default:"*"
      in
      let aud =
        List.find_map (fun (k, v) -> if k = "aud" then Some v else None) params
        |> Option.value ~default:"*"
      in
      if String.starts_with ~prefix:"app.bsky." lxm then Bluesky
      else if String.starts_with ~prefix:"chat.bsky." lxm then Chat
      else Rpc {lxm; aud}
    else if
      String.starts_with ~prefix:"blob:" scope
      || String.starts_with ~prefix:"blob?" scope
    then
      let has_positional = String.starts_with ~prefix:"blob:" scope in
      let rest = String.sub scope 5 (String.length scope - 5) in
      let mimetypes =
        if has_positional then [rest]
        else
          String.split_on_char '&' rest
          |> List.filter_map (fun pair ->
              match String.split_on_char '=' pair with
              | [k; v] when k = "accept" ->
                  Some v
              | _ ->
                  None )
      in
      Blob (if mimetypes = [] then ["*/*"] else mimetypes)
    else Unknown scope

  (* parse repo scope string without converting app.bsky/chat.bsky to Bluesky/Chat *)
  let parse_repo_scope_raw scope =
    if
      String.starts_with ~prefix:"repo:" scope
      || String.starts_with ~prefix:"repo?" scope
    then
      let has_positional = String.starts_with ~prefix:"repo:" scope in
      let rest = String.sub scope 5 (String.length scope - 5) in
      let parts = String.split_on_char '?' rest in
      let positional_coll =
        if has_positional then
          match parts with coll :: _ when coll <> "" -> Some coll | _ -> None
        else None
      in
      let query_str =
        if has_positional then
          if List.length parts > 1 then Some (List.nth parts 1) else None
        else if rest <> "" then Some rest
        else None
      in
      let parse_query_params qs =
        String.split_on_char '&' qs
        |> List.filter_map (fun pair ->
            match String.split_on_char '=' pair with
            | [k; v] ->
                Some (k, v)
            | _ ->
                None )
      in
      let params =
        Option.map parse_query_params query_str |> Option.value ~default:[]
      in
      let collection =
        match positional_coll with
        | Some c ->
            [c]
        | None -> (
            List.filter_map
              (fun (k, v) -> if k = "collection" then Some v else None)
              params
            |> function [] -> ["*"] | cols -> cols )
      in
      let actions =
        let action_strs =
          List.filter_map
            (fun (k, v) -> if k = "action" then Some v else None)
            params
          |> List.concat_map (String.split_on_char ',')
        in
        if action_strs = [] then [Create; Update; Delete]
        else
          List.filter_map
            (fun a ->
              match a with
              | "create" ->
                  Some Create
              | "update" ->
                  Some Update
              | "delete" ->
                  Some Delete
              | _ ->
                  None )
            action_strs
          |> function [] -> [Create; Update; Delete] | l -> l
      in
      Some {collections= collection; actions}
    else None

  type collection_actions = {create: bool; update: bool; delete: bool}

  module StringMap = Map.Make (String)

  let build_collection_actions_map repos =
    List.fold_left
      (fun acc r ->
        List.fold_left
          (fun acc coll ->
            let existing =
              StringMap.find_opt coll acc
              |> Option.value
                   ~default:{create= false; update= false; delete= false}
            in
            let updated =
              { create= existing.create || List.mem Create r.actions
              ; update= existing.update || List.mem Update r.actions
              ; delete= existing.delete || List.mem Delete r.actions }
            in
            StringMap.add coll updated acc )
          acc r.collections )
      StringMap.empty repos

  let build_aud_lxms_map rpcs =
    List.fold_left
      (fun acc r ->
        let existing =
          StringMap.find_opt r.aud acc |> Option.value ~default:[]
        in
        let lxms =
          if List.mem r.lxm existing then existing else r.lxm :: existing
        in
        StringMap.add r.aud lxms acc )
      StringMap.empty rpcs

  let merge_parsed_scopes scopes =
    let email = ref None in
    let identity = ref None in
    let repos = ref [] in
    let rpcs = ref [] in
    let blobs = ref [] in
    let has_bluesky = ref false in
    let has_chat = ref false in
    let unknowns = ref [] in
    List.iter
      (fun scope ->
        match parse_scope scope with
        | Email `Manage ->
            email := Some `Manage
        | Email `Read ->
            if !email = None then email := Some `Read
        | Identity `Full ->
            identity := Some `Full
        | Identity `Handle ->
            if !identity = None then identity := Some `Handle
        | Repo r ->
            repos := r :: !repos
        | Rpc r ->
            rpcs := r :: !rpcs
        | Blob mimes ->
            blobs := mimes @ !blobs
        | Bluesky ->
            has_bluesky := true
        | Chat ->
            has_chat := true
        | Atproto ->
            ()
        | PermissionSet _ ->
            ()
        | Unknown s ->
            unknowns := s :: !unknowns )
      scopes ;
    ( !email
    , !identity
    , !repos
    , !rpcs
    , !blobs
    , !has_bluesky
    , !has_chat
    , !unknowns )

  let[@react.component] make ~scopes ?(permission_sets = []) () =
    let email, identity, repos, rpcs, blobs, has_bluesky, has_chat, unknowns =
      merge_parsed_scopes scopes
    in
    let ps_displays =
      List.map
        (fun (ps : permission_set_display) ->
          PermissionSet
            { nsid= ps.nsid
            ; title= ps.title
            ; detail= ps.detail
            ; expanded_scopes= ps.expanded_scopes } )
        permission_sets
    in
    <div className="w-full mt-3 space-y-1">
      ( match email with
      | Some level ->
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <MailIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">(string "email")</div>
              <div className="text-sm text-mist-100">
                (string
                   ( if level = `Manage then
                       "Read and update your account's email address"
                     else "Read your account's email address" ) )
              </div>
            </div>
          </div>
      | None ->
          null )
      ( match identity with
      | Some level ->
          <div className="flex items-start gap-3 p-3">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <AtIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">
                (string "identity")
              </div>
              <div className="text-sm text-mist-100">
                (string
                   ( if level = `Full then
                       "Manage your full identity including your @handle, with \
                        the ability to move your account to another PDS or \
                        permanently lock you out of your account."
                     else "Change your @handle" ) )
              </div>
            </div>
          </div>
      | None ->
          null )
      ( if has_bluesky then
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <BlueskyIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">(string "bluesky")</div>
              <div className="text-sm text-mist-100">
                (string "Manage your profile, posts, likes and follows")
              </div>
            </div>
          </div>
        else null )
      ( if has_chat then
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <MessageIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">(string "chat")</div>
              <div className="text-sm text-mist-100">
                (string "Read and send messages")
              </div>
            </div>
          </div>
        else null )
      ( if List.length repos > 0 then
          let coll_actions_map = build_collection_actions_map repos in
          let coll_actions_list =
            StringMap.bindings coll_actions_map
            |> List.sort (fun (a, _) (b, _) -> String.compare a b)
          in
          let star_actions = StringMap.find_opt "*" coll_actions_map in
          let has_full_access =
            match star_actions with
            | Some a ->
                a.create && a.update && a.delete
            | None ->
                false
          in
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <FolderGitIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">
                (string "repository")
              </div>
              <div className="text-sm text-mist-100">
                (string
                   ( if has_full_access then
                       "Create, update, and delete any public record"
                     else "Publish changes to your repository" ) )
              </div>
              ( if not has_full_access then
                  <table className="w-full mt-2 text-xs">
                    <thead>
                      <tr className="text-mist-80">
                        <th className="text-left font-normal pb-1">
                          (string "Collection")
                        </th>
                        <th className="text-center font-normal pb-1 w-16">
                          (string "Create")
                        </th>
                        <th className="text-center font-normal pb-1 w-16">
                          (string "Update")
                        </th>
                        <th className="text-center font-normal pb-1 w-16">
                          (string "Delete")
                        </th>
                      </tr>
                    </thead>
                    <tbody>
                      ( coll_actions_list
                      |> List.map (fun (coll, actions) ->
                          let star_create =
                            Option.map (fun a -> a.create) star_actions
                            |> Option.value ~default:false
                          in
                          let star_update =
                            Option.map (fun a -> a.update) star_actions
                            |> Option.value ~default:false
                          in
                          let star_delete =
                            Option.map (fun a -> a.delete) star_actions
                            |> Option.value ~default:false
                          in
                          <tr key=coll className="text-mist-100">
                            <td className="py-0.5">
                              <span className="font-medium">
                                (string
                                   ( if coll = "*" then "Any collection"
                                     else coll ) )
                              </span>
                            </td>
                            <td className="text-center">
                              ( if star_create || actions.create then
                                  <span className="text-mana-100">
                                    (string {js|✓|js})
                                  </span>
                                else null )
                            </td>
                            <td className="text-center">
                              ( if star_update || actions.update then
                                  <span className="text-mana-100">
                                    (string {js|✓|js})
                                  </span>
                                else null )
                            </td>
                            <td className="text-center">
                              ( if star_delete || actions.delete then
                                  <span className="text-mana-100">
                                    (string {js|✓|js})
                                  </span>
                                else null )
                            </td>
                          </tr> )
                      |> Array.of_list |> array )
                    </tbody>
                  </table>
                else null )
            </div>
          </div>
        else null )
      ( if List.length rpcs > 0 then
          let aud_lxms_map = build_aud_lxms_map rpcs in
          let aud_lxms_list =
            StringMap.bindings aud_lxms_map
            |> List.map (fun (aud, lxms) ->
                let sorted_lxms =
                  if List.mem "*" lxms then ["*"]
                  else List.sort String.compare lxms
                in
                (aud, sorted_lxms) )
            |> List.sort (fun (a, _) (b, _) -> String.compare a b)
          in
          let has_full_access =
            List.exists
              (fun (aud, lxms) -> aud = "*" && List.mem "*" lxms)
              aud_lxms_list
          in
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <UsersRoundIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">
                (string "authenticate")
              </div>
              <div className="text-sm text-mist-100">
                (string
                   ( if has_full_access then
                       "Act on your behalf towards any service"
                     else "Perform actions on your behalf" ) )
              </div>
              ( if not has_full_access then
                  <table className="w-full mt-2 text-xs">
                    <thead>
                      <tr className="text-mist-80">
                        <th className="text-left font-normal pb-1">
                          (string "Method")
                        </th>
                        <th className="text-left font-normal pb-1">
                          (string "Service")
                        </th>
                      </tr>
                    </thead>
                    <tbody>
                      ( aud_lxms_list
                      |> List.concat_map (fun (aud, lxms) ->
                          let render_aud () =
                            if aud = "*" then
                              <span className="text-mist-100 font-medium">
                                (string "Any service")
                              </span>
                            else if
                              String.starts_with ~prefix:"did:web:api.bsky.app#"
                                aud
                            then
                              <span className="text-mist-100" title=aud>
                                (string "Bluesky services")
                              </span>
                            else if
                              String.starts_with
                                ~prefix:"did:web:api.bsky.chat#" aud
                            then
                              <span className="text-mist-100" title=aud>
                                (string "Bluesky chat services")
                              </span>
                            else if
                              String.starts_with ~prefix:"did:web:" aud
                              && String.contains aud '#'
                            then
                              let domain =
                                String.sub aud 8 (String.index aud '#' - 8)
                              in
                              <span className="text-mist-100" title=aud>
                                (string ("Service by " ^ domain))
                              </span>
                            else
                              <span className="text-mist-100">
                                (string aud)
                              </span>
                          in
                          List.map
                            (fun lxm ->
                              <tr key=(aud ^ lxm) className="text-mist-100">
                                <td className="py-0.5">
                                  <span className="text-mist-100 font-medium">
                                    (string
                                       (if lxm = "*" then "Any method" else lxm) )
                                  </span>
                                </td>
                                <td className="py-0.5">(render_aud ())</td>
                              </tr> )
                            lxms )
                      |> Array.of_list |> array )
                    </tbody>
                  </table>
                else null )
            </div>
          </div>
        else null )
      ( if List.length blobs > 0 then
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <UploadIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">
                (string "file upload")
              </div>
              <div className="text-sm text-mist-100">
                (string
                   ( if List.mem "*/*" blobs then "Upload any files"
                     else "Upload files to your repository" ) )
              </div>
            </div>
          </div>
        else null )
      ( if List.length unknowns > 0 then
          <div className="flex items-start gap-3 p-3 rounded-lg">
            <div
              className="flex-shrink-0 w-8 h-8 flex items-center \
                         justify-center rounded-full bg-mist-20/50 \
                         text-mist-80">
              <BoxesIcon className="w-4 h-4" />
            </div>
            <div className="flex-1 min-w-0">
              <div className="font-serif text-mana-100">
                (string "other permissions")
              </div>
              <div className="text-sm text-mist-100">
                ( unknowns
                |> List.map (fun s ->
                    <span key=s className="block">(string s)</span> )
                |> Array.of_list |> array )
              </div>
            </div>
          </div>
        else null )
      (* permission sets *)
      ( List.map
          (fun ps ->
            match ps with
            | PermissionSet {nsid; title; detail; expanded_scopes} ->
                let repos =
                  List.filter_map parse_repo_scope_raw expanded_scopes
                in
                let coll_actions_map = build_collection_actions_map repos in
                let coll_actions_list =
                  StringMap.bindings coll_actions_map
                  |> List.sort (fun (a, _) (b, _) -> String.compare a b)
                in
                <div key=nsid className="flex items-start gap-3 p-3 rounded-lg">
                  <div
                    className="flex-shrink-0 w-8 h-8 flex items-center \
                               justify-center rounded-full bg-mist-20/50 \
                               text-mist-80">
                    <BoxesIcon className="w-4 h-4" />
                  </div>
                  <div className="flex-1 min-w-0">
                    <div className="font-serif text-mana-100">
                      (string (Option.value title ~default:nsid))
                    </div>
                    ( match detail with
                    | Some d ->
                        <div className="text-sm text-mist-100">(string d)</div>
                    | None ->
                        null )
                    ( if List.length coll_actions_list > 0 then
                        <table className="w-full mt-2 text-xs">
                          <thead>
                            <tr className="text-mist-80">
                              <th className="text-left font-normal pb-1">
                                (string "Collection")
                              </th>
                              <th className="text-center font-normal pb-1 w-16">
                                (string "Create")
                              </th>
                              <th className="text-center font-normal pb-1 w-16">
                                (string "Update")
                              </th>
                              <th className="text-center font-normal pb-1 w-16">
                                (string "Delete")
                              </th>
                            </tr>
                          </thead>
                          <tbody>
                            ( coll_actions_list
                            |> List.map (fun (coll, actions) ->
                                <tr key=coll className="text-mist-100">
                                  <td className="py-0.5">
                                    <span className="font-medium">
                                      (string
                                         ( if coll = "*" then "Any collection"
                                           else coll ) )
                                    </span>
                                  </td>
                                  <td className="text-center">
                                    ( if actions.create then
                                        <span className="text-mana-100">
                                          (string {js|✓|js})
                                        </span>
                                      else null )
                                  </td>
                                  <td className="text-center">
                                    ( if actions.update then
                                        <span className="text-mana-100">
                                          (string {js|✓|js})
                                        </span>
                                      else null )
                                  </td>
                                  <td className="text-center">
                                    ( if actions.delete then
                                        <span className="text-mana-100">
                                          (string {js|✓|js})
                                        </span>
                                      else null )
                                  </td>
                                </tr> )
                            |> Array.of_list |> array )
                          </tbody>
                        </table>
                      else null )
                  </div>
                </div>
            | _ ->
                null )
          ps_displays
      |> Array.of_list |> React.array )
    </div>
end

let[@react.component] make
    ~props:
      ({ client_url
       ; client_name
       ; logo_uri
       ; current_user
       ; logged_in_users
       ; scopes
       ; permission_sets
       ; code
       ; request_uri
       ; csrf_token } :
        props ) () =
  let host, path = client_url in
  let rendered_name =
    match client_name with
    | Some client_name ->
        <span className="text-mana-100 font-serif">
          (string client_name)
          <span className="font-sans">(string (" (" ^ host ^ ")"))</span>
        </span>
    | None when String.length path = 0 ->
        <span className="text-mana-100 font-serif">(string host)</span>
    | None ->
        <span className="text-mana-100 font-serif">
          (string host) <span className="text-mana-40">(string path)</span>
        </span>
  in
  let query_string =
    match%platform Runtime.platform with
    | Server ->
        ""
    | Client ->
        Webapi.Dom.Location.search Webapi.Dom.location
  in
  let add_account_url = "/account/login" ^ query_string in
  let favicon_url, set_favicon_url =
    useState (fun () ->
        Option.value logo_uri ~default:("https://" ^ host ^ "/favicon.ico") )
  in
  <form className="w-full h-auto max-w-lg px-4 sm:px-0 py-16 my-auto">
    <h1 className="text-2xl font-serif text-mana-200 mb-2">
      (string ("authorizing " ^ host))
    </h1>
    <span className="w-full inline text-mist-100">
      (string "You're signing into ")
      ( if favicon_url <> "" then
          <img
            src=favicon_url
            className="w-4 h-4 inline ml-0.5 mr-1 -mt-0.5"
            onError=(fun _ -> set_favicon_url (fun _ -> "")) />
        else null )
      rendered_name
      (string " as ")
      <AccountSwitcher current_user logged_in_users add_account_url inline=true
      />
      (string " and granting it the following permissions:")
    </span>
    <ScopesTable scopes permission_sets />
    <div className="w-full flex flex-row items-center justify-between mt-6">
      <input type_="hidden" name="dream.csrf" value=csrf_token />
      <input type_="hidden" name="code" value=code />
      <input type_="hidden" name="request_uri" value=request_uri />
      <Button
        kind=`Secondary
        type_="submit"
        formMethod="post"
        name="action"
        value="deny"
        className="grow basis-1/3 min-w-0">
        (string "cancel")
      </Button>
      <Button
        kind=`Primary
        type_="submit"
        formMethod="post"
        name="action"
        value="allow"
        className="grow basis-2/3 min-w-0 max-w-2xs">
        (string "authorize")
      </Button>
    </div>
  </form>
