/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

#include "common.h"
#include "config.h"
#include "timing.h"
#include "jwt.h"

#include <cjose/cjose.h>
#include <jansson.h>

#include <string.h>
#include <search.h>
#include <errno.h>

#define JSONError(err) PluginError("json-err: %s:%d:%d: %s", (err).source, (err).line, (err).column, (err).text)

#define AUTH_DENY 0
#define AUTH_ALLOW 1
struct auth_directive {
  char auth;
  char *container;
};

struct config {
  struct hsearch_data *issuers;
  cjose_jwk_t ***jwkis;
  char **issuer_names;
  struct signer signer;
  struct auth_directive *auth_directives;
  char *id;
  bool strip_token;
};

cjose_jwk_t **
find_keys(struct config *cfg, const char *issuer)
{
  ENTRY *entry;
  if (!hsearch_r((ENTRY){.key = (char *)issuer}, FIND, &entry, cfg->issuers) || !entry) {
    PluginDebug("Unable to locate any keys at %p for issuer %s in %p->%p", entry, issuer, cfg, cfg->issuers);
    return NULL;
  }

  int n = 0;
  for (cjose_jwk_t **jwks = entry->data; *jwks; ++jwks, ++n) {
    ;
  }
  PluginDebug("Located %d keys for issuer %s in %p->%p", n, issuer, cfg, cfg->issuers);
  return entry->data;
}

cjose_jwk_t *
find_key_by_kid(struct config *cfg, const char *issuer, const char *kid)
{
  const char *this_kid;
  cjose_jwk_t **jwkis = find_keys(cfg, issuer);
  if (!jwkis) {
    return NULL;
  }
  for (cjose_jwk_t **jwks = jwkis; *jwks; ++jwks) {
    if ((this_kid = cjose_jwk_get_kid(*jwks, NULL)) && !strcmp(this_kid, kid)) {
      return *jwks;
    }
  }
  return NULL;
}

const char *
config_get_id(struct config *cfg)
{
  return cfg->id;
}

bool
config_strip_token(struct config *cfg)
{
  return cfg->strip_token;
}

struct config *
config_new(size_t n)
{
  PluginDebug("Creating new config object with size %ld", n);
  struct config *cfg = malloc(sizeof *cfg);

  cfg->issuers = calloc(1, sizeof *cfg->issuers);
  if (!hcreate_r(n * 2, cfg->issuers)) {
    PluginError("Unable to create config table (%d)!", errno);
    free(cfg);
    return NULL;
  }
  PluginDebug("Created table with size %d", cfg->issuers->size);

  cfg->jwkis    = malloc((n + 1) * sizeof *cfg->jwkis);
  cfg->jwkis[n] = NULL;

  cfg->issuer_names    = malloc((n + 1) * sizeof *cfg->issuer_names);
  cfg->issuer_names[n] = NULL;

  cfg->signer.issuer = NULL;
  cfg->signer.jwk    = NULL;
  cfg->signer.alg    = NULL;

  cfg->auth_directives = NULL;
  cfg->id              = NULL;

  cfg->strip_token = false;

  PluginDebug("New config object created at %p", cfg);
  return cfg;
}

void
config_delete(struct config *cfg)
{
  if (!cfg) {
    return;
  }
  hdestroy_r(cfg->issuers);
  free(cfg->issuers);

  for (cjose_jwk_t ***jwkis = cfg->jwkis; *jwkis; ++jwkis) {
    for (cjose_jwk_t **jwks = *jwkis; *jwks; ++jwks) {
      cjose_jwk_release(*jwks);
    }
    free(*jwkis);
  }
  free(cfg->jwkis);

  if (cfg->id) {
    free(cfg->id);
  }

  for (char **name = cfg->issuer_names; *name; ++name) {
    free(*name);
  }
  free(cfg->issuer_names);

  if (cfg->signer.alg) {
    free(cfg->signer.alg);
  }

  if (cfg->auth_directives) {
    for (struct auth_directive *ad = cfg->auth_directives; ad->container; ++ad) {
      free(ad->container);
    }
    free(cfg->auth_directives);
  }
  free(cfg);
}

cjose_jwk_t *
load_jwk(json_t *obj, cjose_err *err)
{
  char *s = json_dumps(obj, JSON_COMPACT);
  if (!s) {
    PluginError("Failed to re-serialize JSON sub-object.");
    return NULL;
  }

  cjose_jwk_t *jwk = cjose_jwk_import(s, strlen(s), err);
  free(s);
  return jwk;
}

struct config *
read_config(const char *path)
{
  json_error_t err    = {0};
  json_t *issuer_json = json_load_file(path, 0, &err);
  if (!issuer_json) {
    JSONError(err);
    goto fail;
  }

  if (!json_is_object(issuer_json)) {
    PluginError("Config file is not a valid JSON object");
    goto issuer_fail;
  }

  size_t issuers_ct = json_object_size(issuer_json);
  if (!issuers_ct) {
    PluginError("Config file contains no issuers.");
    goto issuer_fail;
  }

  struct config *cfg = config_new(issuers_ct);
  if (!cfg) {
    PluginError("Unable to allocate config.");
    goto issuer_fail;
  }

  cjose_jwk_t ***jwkis = cfg->jwkis;
  char **issuer        = cfg->issuer_names;
  const char *json_issuer;
  json_t *jwks;
  json_object_foreach(issuer_json, json_issuer, jwks)
  {
    *issuer = strdup(json_issuer);

    json_t *ad_json = json_object_get(jwks, "auth_directives");
    if (ad_json) {
      PluginDebug("Loading auth_directives.");
      size_t ad_ct = json_array_size(ad_json);
      if (ad_ct) {
        PluginDebug("Loading %d new auth_directives.", (int)ad_ct);
        struct auth_directive *ad = cfg->auth_directives;
        if (cfg->auth_directives) {
          /* We've already got directives, so extend them. */
          PluginDebug("Extending existing auth_directives.");
          size_t ad_old_ct = 0;
          while (ad->container) {
            ++ad;
            ++ad_old_ct;
          }
          cfg->auth_directives = realloc(cfg->auth_directives, (ad_ct + ad_old_ct + 1) * sizeof *cfg->auth_directives);
          ad                   = cfg->auth_directives + ad_old_ct;
        } else {
          ad = cfg->auth_directives = malloc((ad_ct + 1) * sizeof *cfg->auth_directives);
        }
        json_t *ad_obj;
        for (size_t idx = 0; (idx < ad_ct) && (ad_obj = json_array_get(ad_json, idx)); ++idx, ++ad) {
          json_t *uri_json  = json_object_get(ad_obj, "uri");
          json_t *auth_json = json_object_get(ad_obj, "auth");
          if (uri_json) {
            const char *uri = json_string_value(uri_json);
            ad->container   = strdup(uri ? uri : "");
            ad->auth        = AUTH_DENY;
            if (auth_json) {
              const char *auth = json_string_value(auth_json);
              if (!auth) {
                auth = "";
              }
              if (!strcmp(auth, "allow")) {
                ad->auth = AUTH_ALLOW;
              } else if (!strcmp(auth, "deny")) {
                ad->auth = AUTH_DENY;
              } else {
                PluginError("auth_directive has unknown auth parameter '%s', defaulting to deny: %s", auth, uri);
              }
            } else {
              PluginError("auth_directive is missing auth parameter, defaulting to deny: %s", uri);
            }
            PluginDebug("Adding auth_directive %d for %s.", (int)ad->auth, ad->container);
          }
        }
        ad->container = NULL;
      }
    } else {
      PluginDebug("No auth_directives to load for %s.", *issuer);
    }

    json_t *key_ary = json_object_get(jwks, "keys");
    if (!key_ary) {
      PluginError("Failed to get keys member from jwk for issuer %s", *issuer);
      *jwkis = NULL;
      goto cfg_fail;
    }
    PluginDebug("Created table with size %d", cfg->issuers->size);

    const char *renewal_kid  = NULL;
    json_t *renewal_kid_json = json_object_get(jwks, "renewal_kid");
    if (renewal_kid_json) {
      renewal_kid = json_string_value(renewal_kid_json);
    }

    json_t *id_json = json_object_get(jwks, "id");
    const char *id;
    if (id_json) {
      id = json_string_value(id_json);
      if (id) {
        cfg->id = malloc(strlen(id) + 1);
        strcpy(cfg->id, id);
        PluginDebug("Found Id in the config: %s", cfg->id);
      }
    }

    json_t *strip_json = json_object_get(jwks, "strip_token");
    if (strip_json) {
      cfg->strip_token = json_boolean_value(strip_json);
    }

    size_t jwks_ct     = json_array_size(key_ary);
    cjose_jwk_t **jwks = (*jwkis++ = malloc((jwks_ct + 1) * sizeof *jwks));
    PluginDebug("Created table with size %d", cfg->issuers->size);
    if (!hsearch_r(((ENTRY){*issuer, jwks}), ENTER, &(ENTRY *){0}, cfg->issuers)) {
      PluginDebug("Failed to store keys for issuer %s", *issuer);
    } else {
      PluginDebug("Stored keys for %s at %16p", *issuer, jwks);
    }

    json_t *jwk_obj;
    cjose_err jwk_err = {0};
    for (size_t idx = 0; (idx < jwks_ct) && (jwk_obj = json_array_get(key_ary, idx)); ++idx, ++jwks) {
      if ((*jwks = load_jwk(jwk_obj, &jwk_err))) {
        const char *kid = cjose_jwk_get_kid(*jwks, NULL);
        PluginDebug("Stored jwk %ld for issuer %s, kid %s, cfg %p->%p", idx, *issuer, kid ? kid : "<no kid>", cfg, cfg->issuers);
        if (renewal_kid && kid && !strcmp(kid, renewal_kid)) {
          if (cfg->signer.issuer) {
            PluginError("Cannot load multiple renewal keys for a single remap. iss:\"%s\", kid:\"%s\"; iss:\"%s\", kid:\"%s\"",
                        cfg->signer.issuer, cjose_jwk_get_kid(cfg->signer.jwk, NULL), *issuer, kid);
            goto cfg_fail;
          } else {
            cfg->signer.issuer = *issuer;
            cfg->signer.jwk    = *jwks;

            const char *jwk_alg = json_string_value(json_object_get(jwk_obj, "alg"));
            if (!jwk_alg) {
              PluginError("Cannot load JWK algorithm for renewal key.");
              goto cfg_fail;
            }
            cfg->signer.alg = strdup(jwk_alg);
          }
        }
      } else {
        PluginError("Failed to load jwk %ld for issuer %s: %s", idx, *issuer, jwk_err.message);
        goto cfg_fail;
      }
    }
    *jwks = NULL;
    ++issuer;
  }
  if (!cfg->signer.issuer) {
    PluginError("Cannot load remap without signing key.");
    goto cfg_fail;
  }
  json_decref(issuer_json);
  PluginDebug("Loaded config file successfully.");
  return cfg;
cfg_fail:
  config_delete(cfg);
issuer_fail:
  json_decref(issuer_json);
fail:
  return NULL;
}

struct signer *
config_signer(struct config *cfg)
{
  if (!cfg) {
    return NULL;
  }
  return &cfg->signer;
}

bool
uri_matches_auth_directive(struct config *cfg, const char *uri, size_t uri_ct)
{
  if (!cfg || !cfg->auth_directives || !uri) {
    return false;
  }

  char *uri_s = malloc(uri_ct + 1);
  memcpy(uri_s, uri, uri_ct);
  uri_s[uri_ct] = 0;
  for (const struct auth_directive *ad = cfg->auth_directives; ad->container; ++ad) {
    if (jwt_check_uri(ad->container, uri_s)) {
      free(uri_s);
      return (ad->auth == AUTH_ALLOW);
    }
  }
  free(uri_s);
  return false;
}
