/**
 * D4Science boot component that handles keykloak authz
 * <d4s-boot-2
 *  /@url: keycloak url, for example: https://accounts.d4science.org/auth]
 *  /@realm: is the realm [defaults to: d4science]
 *  /@gateway: is the client id, for example: "pre.d4science.org"
 *  /@context: is the audience, for example: "%2Fpred4s%2Fpreprod%2FpreVRE"
 *  /@redirect-url: where to go on logout
 */
window.customElements.define('d4s-boot-2', class extends HTMLElement {

  #keycloak = null
  #authorization = null
  #url = null
  #realm = "d4science"
  #clientId = null
  #redirectUrl = null
  #audience = null
  #authenticated = false
  #locked = true
  #queue = []
  #interval = null
  #config = null
  #rpt = null

  constructor() {
    super()
  }
   
  unlock() {
    this.#locked = false
  }

  fire(etype) {
    const tp = this.#keycloak.tokenParsed
    var outtp = {
      aud: tp.aud,
      azp: tp.azp,
      email: tp.email,
      email_verified: tp.email_verified,
      exp: tp.exp,
      family_name: tp.family_name,
      given_name: tp.given_name,
      locale: tp.locale,
      name: tp.name,
      preferred_username: tp.preferred_username
    }
    const evt = new CustomEvent(etype, { detail : outtp })
    document.dispatchEvent(evt)
  }

  connectedCallback() {
    this.startStateChecker()

    this.loadKeycloak().then(() => {
      console.log("Keycloak loaded")
      return this.initKeycloak()

    }).then(authenticated => {
      if (!authenticated) {
        throw "Failed to authenticate"
      }
      console.log("Keycloak initialized and user authenticated")
      //console.log("Token exp: " + this.expirationDate(this.#keycloak.tokenParsed.exp))

      //if an audience is provided then perform also authorization
      if (this.#audience) {
        return this.loadConfig()
      } else {
        Promise.resolve()
      }

    }).then(() => {
      this.#authenticated = true
      this.unlock()
      this.fire("authenticated")

    }).catch(err => {
      console.error("Unable to initialize Keycloak", err)
    })
  }

  loadKeycloak() {
    return new Promise((resolve, reject) => {
      if (typeof Keycloak === 'undefined') {
        const script = document.createElement('script')
        script.src = this.#url + '/js/keycloak.js'
        script.type = 'text/javascript'
        script.addEventListener('load', resolve)
        document.head.appendChild(script)
      } else {
        resolve()
      }
    })
  }

  initKeycloak() {
    this.#keycloak = new Keycloak({
      url: this.#url,
      realm: this.#realm,
      clientId: this.#clientId
    })

    return this.#keycloak.init({onLoad: 'login-required', checkLoginIframe: false })
  }

  startStateChecker() {
    this.#interval = window.setInterval(() => {
      if (this.#locked) {
        console.log("Still locked. Currently has " + this.#queue.length + " pending requests.")
      } else if (!this.authenticated) {
        window.alert("Not authorized!")
      } else {
        if (this.#queue.length > 0) {
          this.#keycloak.updateToken(30).then(() => {
            if (this.#audience) {
              console.log("Checking entitlement for audience", this.#audience)
              const audience = encodeURIComponent(this.#audience)
              return this.entitlement(audience)
            } else {
              return Promise.resolve(this.#keycloak.token)
            }

          }).then(token => {
            console.log("Authorized. Token exp: " + this.expirationDate(this.parseJwt(token).exp))
            //transform all queued requests to fetches
            console.log("All pending requests to promises")
            let promises = this.#queue.map(r => {
              r.request.headers["Authorization"] = "Bearer " + token
              return r.resolve( fetch(r.url, r.request) )
            })
            //clear queue
            this.#queue = []
            console.log("Resolving all fetches")
            return Promise.all(promises)

          }).catch(err => console.error("Unable to make calls: " + err)) // Sometimes throws: Unable to make calls: TypeError: Cannot read properties of undefined (reading 'split')
        }
      }
    }, 300)
  }

  parseJwt(token) {
    var base64Url = token.split('.')[1]
    var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
    var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    }).join(''))

    return JSON.parse(jsonPayload);
  }

  expirationDate(utc) {
    let d = new Date(0)
    d.setUTCSeconds(utc)
    return d
  }

  checkContext() {
    const parseJwt = this.parseJwt
    const expDt = this.expirationDate
    const audience = encodeURIComponent(this.#audience)
    this.entitlement(audience).then(function (rpt) {
      // onGrant callback function.
      // If authorization was successful you'll receive an RPT
      // with the necessary permissions to access the resource server
      //console.log(rpt)
      //console.log("rpt expires: " + expDt(parseJwt(rpt).exp))
    })
  }

  secureFetch(url, request) {
    const p = new Promise((resolve, reject) => {
      let req = request ? request : {}
      if ("headers" in req) {
        req.headers["Authorization"] = null
      } else {
        req.headers = { "Authorization" : null}
      }
      console.log("Queued request to url ", url)
      this.#queue.push({ url : url, request : req, resolve : resolve, reject : reject})
    })
    return p
  }

  logout() {
    if (this.#keycloak) {
      if (!this.#redirectUrl) {
        console.error("Missing required @redirectUrl attribute in d4s-boot")
      } else {
        this.#keycloak.logout({
          redirectUri: this.#redirectUrl
        })
      }
    }
  }

  loadConfig() {
    const kc = this.#keycloak
    return new Promise((resolve, reject) => {
      fetch(kc.authServerUrl + '/realms/' + kc.realm + '/.well-known/uma2-configuration')
        .then(response => response.json())
        .then(json => {
          this.#config = json
          console.log("Keycloak uma2 configuration loaded")
          resolve(true)
        })
        .catch(err => reject("Failed to fetch uma2-configuration from server: " + err))
    })
  }

  /**
   * Refactor of entitlement in '/js/keycloak-authz.js'
   * (dependency no longer needed)
   */
  entitlement(resourceServerId, authorizationRequest) {
    return new Promise((resolve, reject) => {
      const keycloak = this.#keycloak
      const config = this.#config
      if (!keycloak || !config) {
        reject("Keycloak not initialized")

      } else {
        if (!authorizationRequest) {
          authorizationRequest = {}
        }
        var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId
        if (authorizationRequest.claimToken) {
          params += "&claim_token=" + authorizationRequest.claimToken
          if (authorizationRequest.claimTokenFormat) {
            params += "&claim_token_format=" + authorizationRequest.claimTokenFormat
          }
        }

        params += "&audience=" + resourceServerId

        var permissions = authorizationRequest.permissions

        if (!permissions) {
            permissions = []
        }

        for (var i = 0; i < permissions.length; i++) {
          var resource = permissions[i]
          var permission = resource.id

          if (resource.scopes && resource.scopes.length > 0) {
            permission += "#"
            for (j = 0; j < resource.scopes.length; j++) {
              var scope = resource.scopes[j]
              if (permission.indexOf('#') != permission.length - 1) {
                permission += ","
              }
              permission += scope
            }
          }

          params += "&permission=" + permission
        }

        var metadata = authorizationRequest.metadata
        if (metadata) {
          if (metadata.responseIncludeResourceName) {
            params += "&response_include_resource_name=" + metadata.responseIncludeResourceName
          }
          if (metadata.responsePermissionsLimit) {
            params += "&response_permissions_limit=" + metadata.responsePermissionsLimit
          }
        }

        if (this.#rpt) {
          params += "&rpt=" + this.#rpt
        }

        fetch(config.token_endpoint, {
          method: 'POST',
          headers: {
            "Content-type" : "application/x-www-form-urlencoded",
            "Authorization" : "Bearer " + this.#keycloak.token
          },
          body: params
        })
        .then(response => response.json())
        .then(json => {
          this.#rpt = json.access_token
          resolve(this.#rpt)
        })
        .catch(err => reject(err))
      }
    })
  }

  static get observedAttributes() {
    return ["url", "realm", "gateway", "redirect-url", "context"];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      switch (name) {
        case "url":
          this.#url = newValue
          break
        case "realm":
          this.#realm = newValue
          break
        case "gateway":
          this.#clientId = newValue
          break
        case "redirect-url":
          this.#redirectUrl = newValue
          break
        case "context":
          this.#audience = newValue
          break
      }
    }
  }

  get authenticated(){
    return this.#authenticated
  }

  get subject(){
  	return this.#keycloak.subject
  }
  
  get loginToken(){
  	return this.#keycloak.tokenParsed
  }

  get url() {
    return this.#url
  }

  set url(url) {
    this.#url = url
    this.setAttribute("url", url)
  }

  get realm() {
    return this.#realm
  }

  set realm(realm) {
    this.#realm = realm
    this.setAttribute("realm", realm)
  }

  get clientId() {
    return this.#clientId
  }

  set clientId(clientId) {
    this.#clientId = clientId
    this.setAttribute("gateway", clientId)
  }

  get redirectUrl() {
    return this.#redirectUrl
  }

  set redirectUrl(redirectUrl) {
    this.#redirectUrl = redirectUrl
    this.setAttribute("redirect-url", redirectUrl)
  }

  get context() {
    return this.#audience
  }

  set context(context) {
    this.#audience = context
    this.setAttribute("context", context)
  }
})

