/*******************************************************************************
 * Copyright 2018 The MIT Internet Trust Consortium
 *
 * Portions copyright 2011-2013 The MITRE Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
var ClientModel = Backbone.Model.extend({
  idAttribute: "id",

  initialize: function () {
    // bind validation errors to dom elements
    // this will display form elements in red if they are not valid
    this.bind("error", function (model, errs) {
      _.map(errs, function (val, elID) {
        $("#" + elID).addClass("error");
      });
    });
  },

  // We can pass it default values.
  defaults: {
    id: null,

    clientId: null,
    clientSecret: null,
    redirectUris: [],
    clientName: null,
    clientUri: null,
    contacts: [],
    tosUri: null,
    tokenEndpointAuthMethod: null,
    scope: [],
    grantTypes: [],
    responseTypes: [],
    policyUri: null,

    jwksUri: null,
    jwks: null,
    jwksType: "URI",

    applicationType: null,
    sectorIdentifierUri: null,
    subjectType: null,

    requestObjectSigningAlg: null,

    userInfoSignedResponseAlg: null,
    userInfoEncryptedResponseAlg: null,
    userInfoEncryptedResponseEnc: null,

    idTokenSignedResponseAlg: null,
    idTokenEncryptedResponseAlg: null,
    idTokenEncryptedResponseEnc: null,

    tokenEndpointAuthSigningAlg: null,

    defaultMaxAge: null,
    requireAuthTime: false,
    defaultACRvalues: null,

    initiateLoginUri: null,
    postLogoutRedirectUris: [],

    requestUris: [],

    softwareStatement: null,
    softwareId: null,
    softwareVersion: null,

    codeChallengeMethod: null,

    authorities: [],
    accessTokenValiditySeconds: null,
    refreshTokenValiditySeconds: null,
    resourceIds: [],
    // additionalInformation?

    claimsRedirectUris: [],

    clientDescription: null,
    reuseRefreshToken: true,
    clearAccessTokensOnRefresh: true,
    dynamicallyRegistered: false,
    allowIntrospection: false,
    idTokenValiditySeconds: null,
    deviceCodeValiditySeconds: null,
    createdAt: null,

    allowRefresh: false,
    displayClientSecret: false,
    generateClientSecret: false,
  },

  urlRoot: "api/clients",

  matches: function (term) {
    var matches = [];

    if (term) {
      if (
        this.get("clientId").toLowerCase().indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.id"));
      }
      if (
        this.get("clientName") != null &&
        this.get("clientName").toLowerCase().indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.name"));
      }
      if (
        this.get("clientDescription") != null &&
        this.get("clientDescription")
          .toLowerCase()
          .indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.description"));
      }
      if (
        this.get("clientUri") != null &&
        this.get("clientUri").toLowerCase().indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.homepage"));
      }
      if (
        this.get("policyUri") != null &&
        this.get("policyUri").toLowerCase().indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.policy"));
      }
      if (
        this.get("tosUri") != null &&
        this.get("tosUri").toLowerCase().indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.terms"));
      }
      if (this.get("contacts") != null) {
        var f = _.filter(this.get("contacts"), function (item) {
          return item.toLowerCase().indexOf(term.toLowerCase()) != -1;
        });
        if (f.length > 0) {
          matches.push($.t("client.client-table.match.contacts"));
        }
      }
      if (this.get("redirectUris") != null) {
        var f = _.filter(this.get("redirectUris"), function (item) {
          return item.toLowerCase().indexOf(term.toLowerCase()) != -1;
        });
        if (f.length > 0) {
          matches.push($.t("client.client-table.match.redirect"));
        }
      }
      if (this.get("scope") != null) {
        var f = _.filter(this.get("scope"), function (item) {
          return item.toLowerCase().indexOf(term.toLowerCase()) != -1;
        });
        if (f.length > 0) {
          matches.push($.t("client.client-table.match.scope"));
        }
      }
      if (
        this.get("softwareId") != null &&
        this.get("softwareId").toLowerCase().indexOf(term.toLowerCase()) != -1
      ) {
        matches.push($.t("client.client-table.match.software-id"));
      }
      if (
        this.get("softwareVersion") != null &&
        this.get("softwareVersion").toLowerCase().indexOf(term.toLowerCase()) !=
          -1
      ) {
        matches.push($.t("client.client-table.match.software-version"));
      }
    } else {
      // there's no search term, we always match

      this.unset("matches", {
        silent: true,
      });
      // console.log('no term');
      return true;
    }

    var matchString = matches.join(" | ");

    if (matches.length > 0) {
      this.set(
        {
          matches: matchString,
        },
        {
          silent: true,
        },
      );

      return true;
    } else {
      this.unset("matches", {
        silent: true,
      });

      return false;
    }
  },
});

var RegistrationTokenModel = Backbone.Model.extend({
  idAttribute: "clientId",
  urlRoot: "api/tokens/registration",
});

var ClientCollection = Backbone.Collection.extend({
  initialize: function () {
    // this.fetch();
  },

  model: ClientModel,
  url: "api/clients",

  getByClientId: function (clientId) {
    var clients = this.where({
      clientId: clientId,
    });
    if (clients.length == 1) {
      return clients[0];
    } else {
      return null;
    }
  },
});

var ClientView = Backbone.View.extend({
  tagName: "tr",

  isRendered: false,

  initialize: function (options) {
    this.options = options;

    if (!this.template) {
      this.template = _.template($("#tmpl-client-table-item").html());
    }

    if (!this.scopeTemplate) {
      this.scopeTemplate = _.template($("#tmpl-scope-list").html());
    }

    if (!this.moreInfoTemplate) {
      this.moreInfoTemplate = _.template(
        $("#tmpl-client-more-info-block").html(),
      );
    }

    if (!this.registrationTokenTemplate) {
      this.registrationTokenTemplate = _.template(
        $("#tmpl-client-registration-token").html(),
      );
    }

    if (!this.countTemplate) {
      this.countTemplate = _.template($("#tmpl-client-count").html());
    }

    this.model.bind("change", this.render, this);
  },

  render: function (eventName) {
    var creationDate = this.model.get("createdAt");
    var displayCreationDate = $.t("client.client-table.unknown");
    var hoverCreationDate = "";
    if (creationDate == null || !moment(creationDate).isValid()) {
      displayCreationDate = $.t("client.client-table.unknown");
      hoverCreationDate = "";
    } else {
      creationDate = moment(creationDate);
      if (moment().diff(creationDate, "months") < 6) {
        displayCreationDate = creationDate.fromNow();
      } else {
        displayCreationDate = "on " + creationDate.format("LL");
      }
      hoverCreationDate = creationDate.format("LLL");
    }

    var json = {
      client: this.model.toJSON(),
      whiteList: this.options.whiteList,
      displayCreationDate: displayCreationDate,
      hoverCreationDate: hoverCreationDate,
    };
    this.$el.html(this.template(json));

    $(".scope-list", this.el).html(
      this.scopeTemplate({
        scopes: this.model.get("scope"),
        systemScopes: this.options.systemScopeList,
      }),
    );

    $(".client-more-info-block", this.el).html(
      this.moreInfoTemplate({
        client: this.model.toJSON(),
      }),
    );

    $(".clientid-full", this.el).hide();

    this.$(".dynamically-registered").tooltip({
      title: $.t("client.client-table.dynamically-registered-tooltip"),
    });
    this.$(".allow-introspection").tooltip({
      title: $.t("client.client-table.allow-introspection-tooltip"),
    });

    this.updateMatched();

    $(this.el).i18n();

    this.isRendered = true;

    return this;
  },

  showRegistrationToken: function (e) {
    e.preventDefault();

    $("#modalAlertLabel").html(
      $.t("client.client-form.registration-access-token"),
    );

    var token = new RegistrationTokenModel({
      clientId: this.model.get("clientId"),
    });

    var _self = this;
    token.fetch({
      success: function () {
        var savedModel = {
          clientId: _self.model.get("clientId"),
          registrationToken: token.get("value"),
        };

        $("#modalAlert .modal-body").html(
          _self.registrationTokenTemplate(savedModel),
        );

        $("#modalAlert .modal-body #rotate-token").click(function (e) {
          if (
            confirm($.t("client.client-form.rotate-registration-token-confirm"))
          ) {
            token.save(null, {
              success: function () {
                console.log("token:" + token.get("value"));
                $("#modalAlert .modal-body #registrationToken").val(
                  token.get("value"),
                );
              },
              error: app.errorHandlerView.handleError({
                message: $.t(
                  "client.client-form.rotate-registration-token-error",
                ),
              }),
            });
          }
        });

        $("#modalAlert").i18n();
        $("#modalAlert").modal({
          backdrop: "static",
          keyboard: true,
          show: true,
        });
      },
      error: app.errorHandlerView.handleError({
        log: "An error occurred when fetching the registration token",
        message: $.t("client.client-form.registration-token-error"),
      }),
    });
  },

  updateMatched: function () {
    // console.log(this.model.get('matches'));

    if (this.model.get("matches")) {
      $(".matched", this.el).show();
      $(".matched span", this.el).html(this.model.get("matches"));
    } else {
      $(".matched", this.el).hide();
    }
  },

  events: {
    "click .btn-edit": "editClient",
    "click .btn-delete": "deleteClient",
    "click .btn-whitelist": "whiteListClient",
    "click .toggleMoreInformation": "toggleMoreInformation",
    "click .clientid-substring": "showClientId",
    "click .dynamically-registered": "showRegistrationToken",
  },

  editClient: function (e) {
    e.preventDefault();
    app.navigate("admin/client/" + this.model.id, {
      trigger: true,
    });
  },

  whiteListClient: function (e) {
    e.preventDefault();
    if (this.options.whiteList == null) {
      // create a new one
      app.navigate("admin/whitelist/new/" + this.model.get("id"), {
        trigger: true,
      });
    } else {
      // edit the existing one
      app.navigate("admin/whitelist/" + this.options.whiteList.get("id"), {
        trigger: true,
      });
    }
  },

  deleteClient: function (e) {
    e.preventDefault();

    if (confirm($.t("client.client-table.confirm"))) {
      var _self = this;

      this.model.destroy({
        dataType: false,
        processData: false,
        success: function () {
          _self.$el.fadeTo("fast", 0.0, function () {
            // fade
            $(this).slideUp("fast", function () {
              // slide up
              $(this).remove(); // then remove from the DOM
              _self.parentView.togglePlaceholder();
            });
          });
        },
        error: app.errorHandlerView.handleError({
          log: "An error occurred when deleting a client",
        }),
      });

      _self.parentView.delegateEvents();
    }

    return false;
  },

  toggleMoreInformation: function (e) {
    e.preventDefault();
    if ($(".moreInformation", this.el).is(":visible")) {
      // hide it
      $(".moreInformationContainer", this.el)
        .removeClass("alert")
        .removeClass("alert-info")
        .addClass("muted");
      $(".moreInformation", this.el).hide("fast");
      $(".toggleMoreInformation i", this.el).attr(
        "class",
        "icon-chevron-right",
      );
    } else {
      // show it
      $(".moreInformationContainer", this.el)
        .addClass("alert")
        .addClass("alert-info")
        .removeClass("muted");
      $(".moreInformation", this.el).show("fast");
      $(".toggleMoreInformation i", this.el).attr("class", "icon-chevron-down");
    }
  },

  showClientId: function (e) {
    e.preventDefault();

    $(".clientid-full", this.el).show();
  },

  close: function () {
    $(this.el).unbind();
    $(this.el).empty();
  },
});

var ClientListView = Backbone.View.extend({
  tagName: "span",

  initialize: function (options) {
    this.options = options;
    this.filteredModel = this.model;
  },

  load: function (callback) {
    if (
      this.model.isFetched &&
      this.options.whiteListList.isFetched &&
      this.options.systemScopeList.isFetched
    ) {
      callback();
      return;
    }

    $("#loadingbox").sheet("show");
    $("#loading").html(
      '<span class="label" id="loading-clients">' +
        $.t("common.clients") +
        "</span> " +
        '<span class="label" id="loading-whitelist">' +
        $.t("whitelist.whitelist") +
        "</span> " +
        '<span class="label" id="loading-scopes">' +
        $.t("common.scopes") +
        "</span> ",
    );

    $.when(
      this.model.fetchIfNeeded({
        success: function (e) {
          $("#loading-clients").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
      this.options.whiteListList.fetchIfNeeded({
        success: function (e) {
          $("#loading-whitelist").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
      this.options.systemScopeList.fetchIfNeeded({
        success: function (e) {
          $("#loading-scopes").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
    ).done(function () {
      $("#loadingbox").sheet("hide");
      callback();
    });
  },

  events: {
    "click .new-client": "newClient",
    "click .refresh-table": "refreshTable",
    "keyup .search-query": "searchTable",
    "click .form-search button": "clearSearch",
    "page .paginator": "changePage",
  },

  newClient: function (e) {
    e.preventDefault();
    this.remove();
    app.navigate("admin/client/new", {
      trigger: true,
    });
  },

  render: function (eventName) {
    // append and render table structure
    $(this.el).html($("#tmpl-client-table").html());

    this.renderInner();
    $(this.el).i18n();
    return this;
  },

  renderInner: function (eventName) {
    // set up the rows to render
    // (note that this doesn't render until visibility is determined in
    // togglePlaceholder)

    _.each(
      this.filteredModel.models,
      function (client, index) {
        var view = new ClientView({
          model: client,
          systemScopeList: this.options.systemScopeList,
          whiteList: this.options.whiteListList.getByClientId(
            client.get("clientId"),
          ),
        });
        view.parentView = this;
        // var element = view.render().el;
        var element = view.el;
        $("#client-table", this.el).append(element);
        this.addView(client.get("id"), view);
      },
      this,
    );

    this.togglePlaceholder();
  },

  views: {},

  addView: function (index, view) {
    this.views[index] = view;
  },

  getView: function (index) {
    return this.views[index];
  },

  togglePlaceholder: function () {
    // set up pagination
    var numPages = Math.ceil(this.filteredModel.length / 10);
    if (numPages > 1) {
      $(".paginator", this.el).show();
      $(".paginator", this.el).bootpag({
        total: numPages,
        maxVisible: 10,
        leaps: false,
        page: 1,
      });
    } else {
      $(".paginator", this.el).hide();
    }

    if (this.filteredModel.length > 0) {
      this.changePage(undefined, 1);
      $("#client-table", this.el).show();
      $("#client-table-empty", this.el).hide();
      $("#client-table-search-empty", this.el).hide();
    } else {
      if (this.model.length > 0) {
        // there's stuff in the model but it's been filtered out
        $("#client-table", this.el).hide();
        $("#client-table-empty", this.el).hide();
        $("#client-table-search-empty", this.el).show();
      } else {
        // we're empty
        $("#client-table", this.el).hide();
        $("#client-table-empty", this.el).show();
        $("#client-table-search-empty", this.el).hide();
      }
    }
  },

  changePage: function (event, num) {
    console.log("Page changed: " + num);

    $(".paginator", this.el).bootpag({
      page: num,
    });
    var _self = this;

    _.each(this.filteredModel.models, function (client, index) {
      var view = _self.getView(client.get("id"));
      if (!view) {
        console.log("Error: no view for client " + client.get("id"));
        return;
      }

      // only show/render clients on the current page

      console.log(
        ":: " + index + " " + num + " " + Math.ceil((index + 1) / 10) != num,
      );

      if (Math.ceil((index + 1) / 10) != num) {
        $(view.el).hide();
      } else {
        if (!view.isRendered) {
          view.render();
        }
        $(view.el).show();
      }
    });

    /*
     * $('#client-table tbody tr', this.el).each(function(index, element) {
     * if (Math.ceil((index + 1) / 10) != num) { // hide the element
     * $(element).hide(); } else { // show the element $(element).show(); }
     * });
     */
  },

  refreshTable: function (e) {
    e.preventDefault();
    $("#loadingbox").sheet("show");
    $("#loading").html(
      '<span class="label" id="loading-clients">' +
        $.t("common.clients") +
        "</span> " +
        '<span class="label" id="loading-whitelist">' +
        $.t("whitelist.whitelist") +
        "</span> " +
        '<span class="label" id="loading-scopes">' +
        $.t("common.scopes") +
        "</span> ",
    );

    var _self = this;
    $.when(
      this.model.fetch({
        success: function (e) {
          $("#loading-clients").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
      this.options.whiteListList.fetch({
        success: function (e) {
          $("#loading-whitelist").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
      this.options.systemScopeList.fetch({
        success: function (e) {
          $("#loading-scopes").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
    ).done(function () {
      $("#loadingbox").sheet("hide");
      _self.render();
    });
  },

  searchTable: function (e) {
    var term = $(".search-query", this.el).val();

    this.filteredModel = new ClientCollection(
      this.model.filter(function (client) {
        return client.matches(term);
      }),
    );

    // clear out the table
    $("tbody", this.el).html("");

    // re-render the table
    this.renderInner();
  },

  clearSearch: function (e) {
    $(".search-query", this.el).val("");
    this.searchTable();
  },
});

var ClientFormView = Backbone.View.extend({
  tagName: "span",

  initialize: function (options) {
    this.options = options;

    if (!this.template) {
      this.template = _.template($("#tmpl-client-form").html());
    }

    if (!this.clientSavedTemplate) {
      this.clientSavedTemplate = _.template($("#tmpl-client-saved").html());
    }

    this.redirectUrisCollection = new Backbone.Collection();
    this.scopeCollection = new Backbone.Collection();
    this.contactsCollection = new Backbone.Collection();
    this.defaultACRvaluesCollection = new Backbone.Collection();
    this.requestUrisCollection = new Backbone.Collection();
    this.resourceIdsCollection = new Backbone.Collection();
    this.postLogoutRedirectUrisCollection = new Backbone.Collection();
    this.claimsRedirectUrisCollection = new Backbone.Collection();
    // TODO: add Spring authorities collection and resource IDs collection?

    // collection of sub-views that need to be sync'd on save
    this.listWidgetViews = [];
  },

  events: {
    "click .btn-save": "saveClient",
    "click #allowRefresh": "toggleRefreshTokenTimeout",
    "click #disableAccessTokenTimeout": function () {
      $("#access-token-timeout-time", this.$el).prop(
        "disabled",
        !$("#access-token-timeout-time", this.$el).prop("disabled"),
      );
      $("#access-token-timeout-unit", this.$el).prop(
        "disabled",
        !$("#access-token-timeout-unit", this.$el).prop("disabled"),
      );
      document.getElementById("access-token-timeout-time").value = "";
    },
    "click #disableRefreshTokenTimeout": function () {
      $("#refresh-token-timeout-time", this.$el).prop(
        "disabled",
        !$("#refresh-token-timeout-time", this.$el).prop("disabled"),
      );
      $("#refresh-token-timeout-unit", this.$el).prop(
        "disabled",
        !$("#refresh-token-timeout-unit", this.$el).prop("disabled"),
      );
      document.getElementById("refresh-token-timeout-time").value = "";
    },
    "click .btn-cancel": "cancel",
    "change #tokenEndpointAuthMethod input:radio": "toggleClientCredentials",
    "change #displayClientSecret": "toggleDisplayClientSecret",
    "change #generateClientSecret": "toggleGenerateClientSecret",
    "change #jwkSelector input:radio": "toggleJWKSetType",
  },

  cancel: function (e) {
    e.preventDefault();
    app.navigate("admin/clients", {
      trigger: true,
    });
  },

  load: function (callback) {
    if (this.model.isFetched && this.options.systemScopeList.isFetched) {
      callback();
      return;
    }

    $("#loadingbox").sheet("show");
    $("#loading").html(
      '<span class="label" id="loading-clients">' +
        $.t("common.clients") +
        "</span> " +
        '<span class="label" id="loading-scopes">' +
        $.t("common.scopes") +
        "</span> ",
    );

    $.when(
      this.options.systemScopeList.fetchIfNeeded({
        success: function (e) {
          $("#loading-scopes").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
      this.model.fetchIfNeeded({
        success: function (e) {
          $("#loading-clients").addClass("label-success");
        },
        error: app.errorHandlerView.handleError(),
      }),
    ).done(function () {
      $("#loadingbox").sheet("hide");
      callback();
    });
  },

  toggleRefreshTokenTimeout: function () {
    $("#refreshTokenValidityTime", this.$el).toggle();
  },

  /**
   * Set up the form based on the current state of the tokenEndpointAuthMethod
   * parameter
   *
   * @param event
   */
  toggleClientCredentials: function () {
    var tokenEndpointAuthMethod = $("#tokenEndpointAuthMethod input", this.el)
      .filter(":checked")
      .val();

    if (
      tokenEndpointAuthMethod == "SECRET_BASIC" ||
      tokenEndpointAuthMethod == "SECRET_POST" ||
      tokenEndpointAuthMethod == "SECRET_JWT"
    ) {
      // client secret is required, show all the bits
      $("#clientSecretPanel", this.el).show();
      // this function sets up the display portions
      this.toggleGenerateClientSecret();
    } else {
      // no client secret, hide all the bits
      $("#clientSecretPanel", this.el).hide();
    }

    // show or hide the signing algorithm method depending on what's
    // selected
    if (
      tokenEndpointAuthMethod == "PRIVATE_KEY" ||
      tokenEndpointAuthMethod == "SECRET_JWT"
    ) {
      $("#tokenEndpointAuthSigningAlg", this.el).show();
    } else {
      $("#tokenEndpointAuthSigningAlg", this.el).hide();
    }
  },

  /**
   * Set up the form based on the JWK Set selector
   */
  toggleJWKSetType: function () {
    var jwkSelector = $("#jwkSelector input:radio", this.el)
      .filter(":checked")
      .val();

    if (jwkSelector == "URI") {
      $("#jwksUri", this.el).show();
      $("#jwks", this.el).hide();
    } else if (jwkSelector == "VAL") {
      $("#jwksUri", this.el).hide();
      $("#jwks", this.el).show();
    } else {
      $("#jwksUri", this.el).hide();
      $("#jwks", this.el).hide();
    }
  },

  /**
   * Set up the form based on the "Generate" checkbox
   *
   * @param event
   */
  toggleGenerateClientSecret: function () {
    if ($("#generateClientSecret input", this.el).is(":checked")) {
      // show the "generated" block, hide the "display" checkbox
      $("#displayClientSecret", this.el).hide();
      $("#clientSecret", this.el).hide();
      $("#clientSecretGenerated", this.el).show();
      $("#clientSecretHidden", this.el).hide();
    } else {
      // show the display checkbox, fall back to the "display" logic
      $("#displayClientSecret", this.el).show();
      this.toggleDisplayClientSecret();
    }
  },

  /**
   * Handle whether or not to display the client secret
   *
   * @param event
   */
  toggleDisplayClientSecret: function () {
    if ($("#displayClientSecret input").is(":checked")) {
      // want to display it
      $("#clientSecret", this.el).show();
      $("#clientSecretHidden", this.el).hide();
      $("#clientSecretGenerated", this.el).hide();
    } else {
      // want to hide it
      $("#clientSecret", this.el).hide();
      $("#clientSecretHidden", this.el).show();
      $("#clientSecretGenerated", this.el).hide();
    }
  },

  // rounds down to the nearest integer value in seconds.
  getFormTokenNumberValue: function (value, timeUnit) {
    if (value == "") {
      return null;
    } else if (timeUnit == "hours") {
      return parseInt(parseFloat(value) * 3600);
    } else if (timeUnit == "minutes") {
      return parseInt(parseFloat(value) * 60);
    } else {
      // seconds
      return parseInt(value);
    }
  },

  // returns "null" if given the value "default" as a string, otherwise
  // returns input value. useful for parsing the JOSE algorithm dropdowns
  defaultToNull: function (value) {
    if (value == "default") {
      return null;
    } else {
      return value;
    }
  },

  // returns "null" if the given value is falsy
  emptyToNull: function (value) {
    if (value) {
      return value;
    } else {
      return null;
    }
  },

  disableUnsupportedJOSEItems: function (serverSupported, query) {
    var supported = ["default"];
    if (serverSupported) {
      supported = _.union(supported, serverSupported);
    }
    $(query, this.$el).each(function (idx) {
      if (_.contains(supported, $(this).val())) {
        $(this).prop("disabled", false);
      } else {
        $(this).prop("disabled", true);
      }
    });
  },

  // maps from a form-friendly name to the real grant parameter name
  grantMap: {
    authorization_code: "authorization_code",
    password: "password",
    implicit: "implicit",
    client_credentials: "client_credentials",
    redelegate: "urn:ietf:params:oauth:grant_type:redelegate",
    refresh_token: "refresh_token",
    device: "urn:ietf:params:oauth:grant-type:device_code",
    "token-exchange": "urn:ietf:params:oauth:grant-type:token-exchange",
  },

  // maps from a form-friendly name to the real response type parameter name
  responseMap: {
    code: "code",
    token: "token",
    idtoken: "id_token",
    "token-idtoken": "token id_token",
    "code-idtoken": "code id_token",
    "code-token": "code token",
    "code-token-idtoken": "code token id_token",
  },

  saveClient: function (event) {
    $(".control-group").removeClass("error");

    // sync any leftover collection items
    _.each(this.listWidgetViews, function (v) {
      v.addItem($.Event("click"));
    });

    // build the scope object
    var scopes = this.scopeCollection.pluck("item");

    // build the grant type object
    var grantTypes = [];
    $.each(this.grantMap, function (index, type) {
      if ($("#grantTypes-" + index).is(":checked")) {
        grantTypes.push(type);
      }
    });

    // build the response type object
    var responseTypes = [];
    $.each(this.responseMap, function (index, type) {
      if ($("#responseTypes-" + index).is(":checked")) {
        responseTypes.push(type);
      }
    });

    var generateClientSecret = $("#generateClientSecret input").is(":checked");
    var clientSecret = null;

    var tokenEndpointAuthMethod = $("#tokenEndpointAuthMethod input")
      .filter(":checked")
      .val();

    // whether or not the client secret changed
    var secretChanged = false;

    if (
      tokenEndpointAuthMethod == "SECRET_BASIC" ||
      tokenEndpointAuthMethod == "SECRET_POST" ||
      tokenEndpointAuthMethod == "SECRET_JWT"
    ) {
      if (!generateClientSecret) {
        // if it's required but we're not generating it, send the value
        // to preserve it
        clientSecret = $("#clientSecret input").val();

        // if it's not the same as before, offer to display it
        if (clientSecret != this.model.get("clientSecret")) {
          secretChanged = true;
        }
      } else {
        // it's being generated anew
        secretChanged = true;
      }
    }

    var accessTokenValiditySeconds = null;
    if (!$("disableAccessTokenTimeout").is(":checked")) {
      accessTokenValiditySeconds = this.getFormTokenNumberValue(
        $("#accessTokenValidityTime input[type=text]").val(),
        $("#accessTokenValidityTime select").val(),
      );
    }

    var idTokenValiditySeconds = this.getFormTokenNumberValue(
      $("#idTokenValidityTime input[type=text]").val(),
      $("#idTokenValidityTime select").val(),
    );

    var deviceCodeValiditySeconds = this.getFormTokenNumberValue(
      $("#deviceCodeValidityTime input[type=text]").val(),
      $("#deviceCodeValidityTime select").val(),
    );

    var refreshTokenValiditySeconds = null;
    if ($("#allowRefresh").is(":checked")) {
      if ($.inArray("refresh_token", grantTypes) == -1) {
        grantTypes.push("refresh_token");
      }

      if ($.inArray("offline_access", scopes) == -1) {
        scopes.push("offline_access");
      }

      if (!$("disableRefreshTokenTimeout").is(":checked")) {
        refreshTokenValiditySeconds = this.getFormTokenNumberValue(
          $("#refreshTokenValidityTime input[type=text]").val(),
          $("#refreshTokenValidityTime select").val(),
        );
      }
    }

    // make sure that the subject identifier is consistent with the redirect
    // URIs
    var subjectType = $("#subjectType input").filter(":checked").val();
    var redirectUris = this.redirectUrisCollection.pluck("item");
    var sectorIdentifierUri = $("#sectorIdentifierUri input").val();
    if (
      subjectType == "PAIRWISE" &&
      redirectUris.length > 1 &&
      sectorIdentifierUri == ""
    ) {
      // Display an alert with an error message
      app.errorHandlerView.showErrorMessage(
        $.t("client.client-form.error.consistency"),
        $.t("client.client-form.error.pairwise-sector"),
      );
      return false;
    }

    // process the JWKS
    var jwksUri = null;
    var jwks = null;
    var jwkSelector = $("#jwkSelector input:radio", this.el)
      .filter(":checked")
      .val();

    if (jwkSelector == "URI") {
      jwksUri = $("#jwksUri input").val();
      jwks = null;
    } else if (jwkSelector == "VAL") {
      jwksUri = null;
      try {
        jwks = JSON.parse($("#jwks textarea").val());
      } catch (e) {
        console.log("An error occurred when parsing the JWK Set");
        app.errorHandlerView.showErrorMessage(
          $.t("client.client-form.error.jwk-set"),
          $.t("client.client-form.error.jwk-set-parse"),
        );
        return false;
      }
    } else {
      jwksUri = null;
      jwks = null;
    }

    var attrs = {
      clientName: this.emptyToNull($("#clientName input").val()),
      clientId: this.emptyToNull($("#clientId input").val()),
      clientSecret: clientSecret,
      generateClientSecret: generateClientSecret,
      redirectUris: redirectUris,
      clientDescription: this.emptyToNull(
        $("#clientDescription textarea").val(),
      ),
      grantTypes: grantTypes,
      accessTokenValiditySeconds: accessTokenValiditySeconds,
      refreshTokenValiditySeconds: refreshTokenValiditySeconds,
      idTokenValiditySeconds: idTokenValiditySeconds,
      deviceCodeValiditySeconds: deviceCodeValiditySeconds,
      allowRefresh: $("#allowRefresh").is(":checked"),
      allowIntrospection: $("#allowIntrospection input").is(":checked"),
      scope: scopes,
      tosUri: this.emptyToNull($("#tosUri input").val()),
      policyUri: this.emptyToNull($("#policyUri input").val()),
      clientUri: this.emptyToNull($("#clientUri input").val()),
      applicationType: $("#applicationType input").filter(":checked").val(),
      jwksUri: jwksUri,
      jwks: jwks,
      subjectType: subjectType,
      softwareStatement: this.emptyToNull(
        $("#softwareStatement textarea").val(),
      ),
      softwareId: this.emptyToNull($("#softwareId input").val()),
      softwareVersion: this.emptyToNull($("#softwareVersion input").val()),
      tokenEndpointAuthMethod: tokenEndpointAuthMethod,
      responseTypes: responseTypes,
      sectorIdentifierUri: sectorIdentifierUri,
      initiateLoginUri: this.emptyToNull($("#initiateLoginUri input").val()),
      postLogoutRedirectUris:
        this.postLogoutRedirectUrisCollection.pluck("item"),
      claimsRedirectUris: this.claimsRedirectUrisCollection.pluck("item"),
      reuseRefreshToken: $("#reuseRefreshToken").is(":checked"),
      clearAccessTokensOnRefresh: $("#clearAccessTokensOnRefresh").is(
        ":checked",
      ),
      requireAuthTime: $("#requireAuthTime input").is(":checked"),
      defaultMaxAge: parseInt($("#defaultMaxAge input").val()),
      contacts: this.contactsCollection.pluck("item"),
      requestUris: this.requestUrisCollection.pluck("item"),
      resourceIds: this.resourceIdsCollection.pluck("item"),
      defaultACRvalues: this.defaultACRvaluesCollection.pluck("item"),
      requestObjectSigningAlg: this.defaultToNull(
        $("#requestObjectSigningAlg select").val(),
      ),
      userInfoSignedResponseAlg: this.defaultToNull(
        $("#userInfoSignedResponseAlg select").val(),
      ),
      userInfoEncryptedResponseAlg: this.defaultToNull(
        $("#userInfoEncryptedResponseAlg select").val(),
      ),
      userInfoEncryptedResponseEnc: this.defaultToNull(
        $("#userInfoEncryptedResponseEnc select").val(),
      ),
      idTokenSignedResponseAlg: this.defaultToNull(
        $("#idTokenSignedResponseAlg select").val(),
      ),
      idTokenEncryptedResponseAlg: this.defaultToNull(
        $("#idTokenEncryptedResponseAlg select").val(),
      ),
      idTokenEncryptedResponseEnc: this.defaultToNull(
        $("#idTokenEncryptedResponseEnc select").val(),
      ),
      tokenEndpointAuthSigningAlg: this.defaultToNull(
        $("#tokenEndpointAuthSigningAlg select").val(),
      ),
      codeChallengeMethod: this.defaultToNull(
        $("#codeChallengeMethod select").val(),
      ),
    };

    // post-validate
    if (attrs["allowRefresh"] == false) {
      attrs["refreshTokenValiditySeconds"] = null;
    }

    if ($("#disableAccessTokenTimeout").is(":checked")) {
      attrs["accessTokenValiditySeconds"] = null;
    }

    if ($("#disableRefreshTokenTimeout").is(":checked")) {
      attrs["refreshTokenValiditySeconds"] = null;
    }

    // set all empty strings to nulls
    for (var key in attrs) {
      if (attrs[key] === "") {
        attrs[key] = null;
      }
    }

    var _self = this;
    this.model.save(attrs, {
      success: function () {
        $("#modalAlertLabel").html($.t("client.client-form.saved.saved"));

        var savedModel = {
          clientId: _self.model.get("clientId"),
          clientSecret: _self.model.get("clientSecret"),
          secretChanged: secretChanged,
        };

        $("#modalAlert div.modal-header").html(
          $.t("client.client-form.saved.saved"),
        );

        $("#modalAlert .modal-body").html(
          _self.clientSavedTemplate(savedModel),
        );

        $("#modalAlert .modal-body #savedClientSecret").hide();

        $("#modalAlert").on("click", "#clientSaveShow", function (event) {
          event.preventDefault();
          $("#clientSaveShow").hide();
          $("#savedClientSecret").show();
        });

        $("#modalAlert").i18n();
        $("#modalAlert").modal({
          backdrop: "static",
          keyboard: true,
          show: true,
        });

        app.clientList.add(_self.model);
        app.navigate("admin/clients", {
          trigger: true,
        });
      },
      error: app.errorHandlerView.handleError({
        log: "An error occurred when saving a client",
      }),
    });

    return false;
  },

  render: function (eventName) {
    var data = {
      client: this.model.toJSON(),
      heartMode: heartMode,
    };
    $(this.el).html(this.template(data));

    var _self = this;

    // clear the sub-view collection
    this.listWidgetViews = [];

    // build and bind registered redirect URI collection and view
    _.each(this.model.get("redirectUris"), function (redirectUri) {
      _self.redirectUrisCollection.add(
        new URIModel({
          item: redirectUri,
        }),
      );
    });

    var redirUriView = new ListWidgetView({
      type: "uri",
      placeholder: "https://",
      helpBlockText: $.t("client.client-form.redirect-uris-help"),
      collection: this.redirectUrisCollection,
    });
    $("#redirectUris .controls", this.el).html(redirUriView.render().el);
    this.listWidgetViews.push(redirUriView);

    // build and bind scopes
    _.each(this.model.get("scope"), function (scope) {
      _self.scopeCollection.add(
        new Backbone.Model({
          item: scope,
        }),
      );
    });

    var scopeView = new ListWidgetView({
      placeholder: $.t("client.client-form.scope-placeholder"),
      autocomplete: _.uniq(
        _.flatten(this.options.systemScopeList.pluck("value")),
      ),
      helpBlockText: $.t("client.client-form.scope-help"),
      collection: this.scopeCollection,
    });
    $("#scope .controls", this.el).html(scopeView.render().el);
    this.listWidgetViews.push(scopeView);

    // build and bind contacts
    _.each(this.model.get("contacts"), function (contact) {
      _self.contactsCollection.add(
        new Backbone.Model({
          item: contact,
        }),
      );
    });

    var contactsView = new ListWidgetView({
      placeholder: $.t("client.client-form.contacts-placeholder"),
      helpBlockText: $.t("client.client-form.contacts-help"),
      collection: this.contactsCollection,
    });
    $("#contacts .controls", this.el).html(contactsView.render().el);
    this.listWidgetViews.push(contactsView);

    // build and bind post-logout redirect URIs
    _.each(
      this.model.get("postLogoutRedirectUris"),
      function (postLogoutRedirectUri) {
        _self.postLogoutRedirectUrisCollection.add(
          new URIModel({
            item: postLogoutRedirectUri,
          }),
        );
      },
    );

    var postLogoutRedirectUrisView = new ListWidgetView({
      type: "uri",
      placeholder: "https://",
      helpBlockText: $.t("client.client-form.post-logout-help"),
      collection: this.postLogoutRedirectUrisCollection,
    });
    $("#postLogoutRedirectUris .controls", this.el).html(
      postLogoutRedirectUrisView.render().el,
    );
    this.listWidgetViews.push(postLogoutRedirectUrisView);

    // build and bind claims redirect URIs
    _.each(this.model.get("claimsRedirectUris"), function (claimsRedirectUri) {
      _self.claimsRedirectUrisCollection.add(
        new URIModel({
          item: claimsRedirectUri,
        }),
      );
    });

    var claimsRedirectUrisView = new ListWidgetView({
      type: "uri",
      placeholder: "https://",
      helpBlockText: $.t("client.client-form.claims-redirect-uris-help"),
      collection: this.claimsRedirectUrisCollection,
    });
    $("#claimsRedirectUris .controls", this.el).html(
      claimsRedirectUrisView.render().el,
    );
    this.listWidgetViews.push(claimsRedirectUrisView);

    // build and bind request URIs
    _.each(this.model.get("requestUris"), function (requestUri) {
      _self.requestUrisCollection.add(
        new URIModel({
          item: requestUri,
        }),
      );
    });

    var requestUriView = new ListWidgetView({
      type: "uri",
      placeholder: "https://",
      helpBlockText: $.t("client.client-form.request-uri-help"),
      collection: this.requestUrisCollection,
    });
    $("#requestUris .controls", this.el).html(requestUriView.render().el);
    this.listWidgetViews.push(requestUriView);

    // build and bind contacts
    _.each(this.model.get("resourceIds"), function (resource) {
      _self.resourceIdsCollection.add(
        new Backbone.Model({
          item: resource,
        }),
      );
    });

    var resourceIdsView = new ListWidgetView({
      placeholder: $.t("client.client-form.resource-ids-placeholder"),
      helpBlockText: $.t("client.client-form.resource-ids-help"),
      collection: this.resourceIdsCollection,
    });
    $("#resourceIds .controls", this.el).html(resourceIdsView.render().el);
    this.listWidgetViews.push(resourceIdsView);

    // build and bind default ACR values
    _.each(this.model.get("defaultACRvalues"), function (defaultACRvalue) {
      _self.defaultACRvaluesCollection.add(
        new Backbone.Model({
          item: defaultACRvalue,
        }),
      );
    });

    var defaultAcrView = new ListWidgetView({
      placeholder: $.t("client.client-form.acr-values-placeholder"),
      // TODO: autocomplete from spec
      helpBlockText: $.t("client.client-form.acr-values-help"),
      collection: this.defaultACRvaluesCollection,
    });
    $("#defaultAcrValues .controls", this.el).html(defaultAcrView.render().el);
    this.listWidgetViews.push(defaultAcrView);

    // build and bind

    // set up token fields
    if (!this.model.get("allowRefresh")) {
      $("#refreshTokenValidityTime", this.$el).hide();
    }

    if (this.model.get("accessTokenValiditySeconds") == null) {
      $("#access-token-timeout-time", this.$el).prop("disabled", true);
      $("#access-token-timeout-unit", this.$el).prop("disabled", true);
    }

    if (this.model.get("refreshTokenValiditySeconds") == null) {
      $("#refresh-token-timeout-time", this.$el).prop("disabled", true);
      $("#refresh-token-timeout-unit", this.$el).prop("disabled", true);
    }

    // toggle other dynamic fields
    this.toggleClientCredentials();
    this.toggleJWKSetType();

    // disable unsupported JOSE algorithms
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.request_object_signing_alg_values_supported,
      "#requestObjectSigningAlg option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.userinfo_signing_alg_values_supported,
      "#userInfoSignedResponseAlg option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.userinfo_encryption_alg_values_supported,
      "#userInfoEncryptedResponseAlg option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.userinfo_encryption_enc_values_supported,
      "#userInfoEncryptedResponseEnc option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.id_token_signing_alg_values_supported,
      "#idTokenSignedResponseAlg option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.id_token_encryption_alg_values_supported,
      "#idTokenEncryptedResponseAlg option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.id_token_encryption_enc_values_supported,
      "#idTokenEncryptedResponseEnc option",
    );
    this.disableUnsupportedJOSEItems(
      app.serverConfiguration.token_endpoint_auth_signing_alg_values_supported,
      "#tokenEndpointAuthSigningAlg option",
    );

    this.$(".nyi").clickover({
      placement: "right",
      title: $.t("common.not-yet-implemented"),
      content: $.t("common.not-yet-implemented-content"),
    });

    $(this.el).i18n();
    return this;
  },
});

ui.routes.push({
  path: "admin/clients",
  name: "listClients",
  callback: function () {
    if (!isAdmin()) {
      this.root();
      return;
    }

    this.breadCrumbView.collection.reset();
    this.breadCrumbView.collection.add([
      {
        text: $.t("admin.home"),
        href: "",
      },
      {
        text: $.t("client.manage"),
        href: "manage/#admin/clients",
      },
    ]);

    this.updateSidebar("admin/clients");

    var view = new ClientListView({
      model: this.clientList,
      systemScopeList: this.systemScopeList,
      whiteListList: this.whiteListList,
    });
    view.load(function () {
      $("#content").html(view.render().el);
      view.delegateEvents();
      setPageTitle($.t("client.manage"));
    });
  },
});

ui.routes.push({
  path: "admin/client/new",
  name: "newClient",
  callback: function () {
    console.log("newClient");
    if (!isAdmin()) {
      this.root();
      return;
    }

    this.breadCrumbView.collection.reset();
    this.breadCrumbView.collection.add([
      {
        text: $.t("admin.home"),
        href: "",
      },
      {
        text: $.t("client.manage"),
        href: "manage/#admin/clients",
      },
      {
        text: $.t("client.client-form.new"),
        href: "",
      },
    ]);

    this.updateSidebar("admin/clients");

    var client = new ClientModel();

    var view = new ClientFormView({
      model: client,
      systemScopeList: this.systemScopeList,
    });
    view.load(function () {
      var userInfo = getUserInfo();
      var contacts = [];
      if (userInfo != null && userInfo.email != null) {
        contacts.push(userInfo.email);
      }

      // use a different set of defaults based on heart mode flag
      if (heartMode) {
        client.set(
          {
            tokenEndpointAuthMethod: "PRIVATE_KEY",
            generateClientSecret: false,
            displayClientSecret: false,
            requireAuthTime: true,
            defaultMaxAge: 60000,
            scope: _.uniq(
              _.flatten(app.systemScopeList.defaultScopes().pluck("value")),
            ),
            accessTokenValiditySeconds: 3600,
            refreshTokenValiditySeconds: 24 * 3600,
            idTokenValiditySeconds: 300,
            deviceCodeValiditySeconds: 30 * 60,
            grantTypes: ["authorization_code"],
            responseTypes: ["code"],
            subjectType: "PUBLIC",
            jwksType: "URI",
            contacts: contacts,
          },
          {
            silent: true,
          },
        );
      } else {
        // set up this new client to require a secret and have us
        // autogenerate one
        client.set(
          {
            tokenEndpointAuthMethod: "SECRET_BASIC",
            generateClientSecret: true,
            displayClientSecret: false,
            requireAuthTime: true,
            defaultMaxAge: 60000,
            scope: _.uniq(
              _.flatten(app.systemScopeList.defaultScopes().pluck("value")),
            ),
            accessTokenValiditySeconds: 3600,
            idTokenValiditySeconds: 600,
            deviceCodeValiditySeconds: 30 * 60,
            grantTypes: ["authorization_code"],
            responseTypes: ["code"],
            subjectType: "PUBLIC",
            jwksType: "URI",
            contacts: contacts,
          },
          {
            silent: true,
          },
        );
      }

      $("#content").html(view.render().el);
      setPageTitle($.t("client.client-form.new"));
    });
  },
});

ui.routes.push({
  path: "admin/client/:id",
  name: "editClient",
  callback: function (id) {
    console.log("editClient " + id);
    if (!isAdmin()) {
      this.root();
      return;
    }

    this.breadCrumbView.collection.reset();
    this.breadCrumbView.collection.add([
      {
        text: $.t("admin.home"),
        href: "",
      },
      {
        text: $.t("client.manage"),
        href: "manage/#admin/clients",
      },
      {
        text: $.t("client.client-form.edit"),
        href: "manage/#admin/client/" + id,
      },
    ]);

    this.updateSidebar("admin/clients");

    var client = this.clientList.get(id);
    if (!client) {
      client = new ClientModel({
        id: id,
      });
    }

    var view = new ClientFormView({
      model: client,
      systemScopeList: app.systemScopeList,
    });
    view.load(function () {
      if ($.inArray("refresh_token", client.get("grantTypes")) != -1) {
        client.set(
          {
            allowRefresh: true,
          },
          {
            silent: true,
          },
        );
      }

      if (client.get("jwks")) {
        client.set(
          {
            jwksType: "VAL",
          },
          {
            silent: true,
          },
        );
      } else {
        client.set(
          {
            jwksType: "URI",
          },
          {
            silent: true,
          },
        );
      }

      client.set(
        {
          generateClientSecret: false,
          displayClientSecret: false,
        },
        {
          silent: true,
        },
      );

      $("#content").html(view.render().el);
      setPageTitle($.t("client.client-form.edit"));
    });
  },
});

ui.templates.push("resources/template/client.html");

ui.init.push(function (app) {
  app.clientList = new ClientCollection();
});
