/*
 * Copyright (c) 2019 Clementine Computing LLC.
 *
 * This file is part of PopuFare.
 *
 * PopuFare is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PopuFare is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "ruleparam_db.h"

extern int commhub_fd;
struct message_record log_msg;

int ruleparam_db_init( ruleparam_db_ctx *ctx )
{
  ctx->n = 0;
  ctx->head = NULL;

  ctx->sync_counter = 0;
  ctx->sync_every_n = 100;

  ctx->n_filename_history = 4;
  ctx->pos_filename_history = 0;

  ctx->seq = 0;

  return 0;
}

static int _chomp(char *s)
{
  int n;

  if (!s) 
  {
    return -1;
  }

  n = strlen(s);

  if (n==0)
    return 0;

  if (s[n-1] == '\n')
  {
    s[n-1] = '\0';
    return n-1;
  }

  return n;
}

// In the consistency check, we allocate our own ruleparam_db, so we needed to expose
// this function.
// 
passdb_slim_ruleparam *_alloc_ruleparam_db_nod(int id, char *name, char *param)
{
  passdb_slim_ruleparam *nod;
  nod = (passdb_slim_ruleparam *)malloc(sizeof(passdb_slim_ruleparam));
  if (!nod)
  {
    fprintf(stderr, "_alloc_ruelparam_db_nod malloc failed: %s line %i\n", __FILE__, __LINE__ );
    return NULL;
  }

  nod->id = id;
  strncpy( nod->name, name, RULENAME_LEN);
  strncpy( nod->param, param, PARAM_LEN);
  nod->name[RULENAME_LEN-1] = '\0';
  nod->param[PARAM_LEN-1] = '\0';
  nod->reference_count = 0;
  nod->next = NULL;
  return nod;
}


ruleparam_db_ctx * ruleparam_db_alloc(void)
{
  ruleparam_db_ctx *ctx;
  ctx = (ruleparam_db_ctx *)malloc(sizeof(ruleparam_db_ctx));
  if (!ctx)
  {
    fprintf(stderr, "ruleparam_db_alloc: malloc failed: %s line %i\n", __FILE__, __LINE__ );
    return NULL;
  }

  ctx->n_filename_history = 4;
  ctx->pos_filename_history = 0;


  ruleparam_db_init(ctx);
  return ctx;
}


int ruleparam_db_load( ruleparam_db_ctx **ctx, char *db_fn )
{
  FILE *fp;
  passdb_slim_ruleparam *nod, *prev=NULL;
  ruleparam_db_ctx *tctx = NULL;


  FILE *seq_fp;
  char db_seq_fn[1024];

  char buf[1024];
  char name[RULENAME_LEN];
  char param[PARAM_LEN];
  char *chp, *chp_next;
  char *token;
  int r = -1;
  int id;

  int line_no = 0;
  int k;

  unsigned long long seq=0;
  long long int tseq;
  int rulecount=-1;


  tctx = (ruleparam_db_ctx *)malloc(sizeof(ruleparam_db_ctx));
  CHECK_ALLOC_FAIL( (tctx) );

  ruleparam_db_init( tctx );


  // Read in sequence number from file.
  // Keep going if file does not exist.
  //
  snprintf(db_seq_fn, 128, "%s.seq", db_fn );
  seq_fp = fopen( db_seq_fn, "r");
  if (!seq_fp)
  {
    perror( db_seq_fn );
  }
  else
  {
    fgets( buf, 1024, seq_fp );
    tseq = atoll( buf );
    if (tseq > 0)
    {
      seq = (unsigned long long)tseq;
    }
    fclose(seq_fp);
  }


  fp = fopen(db_fn, "r");
  if (!fp)
  {
    perror(db_fn);
    if (tctx) 
      free(tctx);
    return -1;
  }

  while (fgets(buf, 1024, fp ))
  {

    line_no++;

    chp = buf;
    if (!chp)
      break;

    if (!(*chp))
      continue;

    token = chp;

    // skip initial whitespace
    //
    while ( (*token) && (*token == ' ') )
      token++;

    // skip comment
    //
    if (token[0] == '#') 
    {

      /*
      if (strncmp( token, "#seq: ", strlen("#seq: ")) == 0)
      {
        tseq = atoll( token + strlen("#seq: ") );
        if (tseq > 0)
        {
          seq = (unsigned long long)tseq;
        }
      }
      else 
      */

      if (strncmp( token, "#rulecount: ", strlen("#rulecount: ")) == 0)
      {
        rulecount = atoi( token + strlen("#rulecount: ") );
      }
      continue;
    }

    // skip if empty line
    //
    if (*token == '\n') continue;

    // FIRST TOKEN (id)
    //find next token (tab)
    chp_next = strchr( chp, '\t' );
    if (!chp_next) { goto _cleanup; }
    *chp_next = '\0';

    // and process
    k = _chomp(token);
    if (k<0) { goto _cleanup; }
    if (k==0) continue;

    id = atoi(token);


    // SECOND TOKEN (rule)
    // find next token (tab)
    token = chp_next+1;
    chp_next = strchr( token, '\t' );
    if ( !chp_next ) { goto _cleanup; }
    *chp_next = '\0';
    _chomp(token);

    // and process
    strncpy(name, token, RULENAME_LEN);
    name[RULENAME_LEN-1] = '\0';


    // THIRD TOKEN (param)
    // NOTE: fourth token assummed to exist, even though we don't read it
    // in.  By convention, the fourth token is the reference count.
    //
    // find next token (tab)
    token = chp_next+1;
    chp_next = strchr( token, '\t' );
    if ( !chp_next ) { goto _cleanup; }
    *chp_next = '\0';
    _chomp(token);

    // and process
    strncpy(param, token, PARAM_LEN);
    param[PARAM_LEN-1] = '\0';

    nod = _alloc_ruleparam_db_nod( id, name, param );

    if (tctx->head) prev->next = nod;
    else            tctx->head = nod;
    prev = nod;

    tctx->n++;

  }

  if (tctx->n != rulecount)
  {
    fprintf(stderr, "WARNING: ruleparam_db_load: read rule count (%i) != reported rule count (%i)\n", tctx->n, rulecount);
  }

  r = 0;
  tctx->db_filename = strdup( db_fn );
  tctx->seq = seq;
  *ctx = tctx;

  return 0;

_cleanup:

  fprintf(stderr, "ERROR: ruleparam_db_load: FAIL on line %i\n", line_no);

  if (tctx) 
  {
    nod = tctx->head;
    while (nod)
    {
      prev = nod;
      nod = nod->next;
      free(prev);
    }
    free(tctx);
  }

  fclose(fp);
  return -1;

}

int _copy_file( char *src_fn, char *dst_fn )
{
  FILE *src_fp, *dst_fp;
  char buf[1024];
  int read_len=0, write_len=0;
  int ret=0;

  if ( !(src_fp = fopen(src_fn, "r")) )
  {
    perror(src_fn);
    return errno;
  }

  if ( !(dst_fp = fopen(dst_fn, "w")) )
  {

    fclose(src_fp);
    perror(dst_fn);
    return errno;
  }

  while (!feof(src_fp))
  {
    read_len = fread(buf, 1, sizeof(buf), src_fp);

    if (read_len <= 0) 
    {
      if (ferror(src_fp) != 0)
      {
        perror(src_fn);
        ret = errno;
        goto _copy_file_cleanup;
      }
      continue;
    }


    while ( (write_len = fwrite(buf, 1, read_len, dst_fp)) != read_len )
    {
      perror(dst_fn);
      ret = errno;
      goto _copy_file_cleanup;
    }

  }


_copy_file_cleanup:
  fclose(src_fp);
  fclose(dst_fp);

  return ret;
}

int ruleparam_db_seq_save( ruleparam_db_ctx *ctx )
{
  FILE *seq_fp;
  char tmp_fn[512];
  char seq_fn[512];


  // After ruleparam.db has been synchronized, finally
  // write out sequence number in 'ruleparam.db.seq' file.
  //
  snprintf(tmp_fn, 512, "%s.seq.tmp", ctx->db_filename );
  snprintf(seq_fn, 512, "%s.seq", ctx->db_filename );
  seq_fp = fopen( tmp_fn, "w" );
  if (seq_fp)
  {
    fprintf(seq_fp, "%llu", ctx->seq);
    fclose(seq_fp);
    sync();

    rename( tmp_fn, seq_fn );
    sync();

  }
  else
  {
    perror( tmp_fn );
    return -1;
  }

  return 0;
}

int ruleparam_db_save( ruleparam_db_ctx *ctx )
{
  FILE *fp;
  passdb_slim_ruleparam *nod;
  struct tm tm_tim;
  struct timeval timval;
  char str_tim[64];

  char tmp_fn[1024];
  char log_fn[1024];
  int fn_num = 0;

  int rule_count=0;

  if ( ctx->n_filename_history > 1 )
  {
    if (ctx->pos_filename_history < 0)
    {
      ctx->pos_filename_history = 0;
    }
    else
    {
      ctx->pos_filename_history++;
      ctx->pos_filename_history %= ctx->n_filename_history;
    }

    fn_num = ctx->pos_filename_history;

  }

  sprintf(tmp_fn, "%s.%i.tmp", ctx->db_filename, fn_num);
  sprintf(log_fn, "%s.%i", ctx->db_filename, fn_num);

  gettimeofday(&timval, NULL);
  localtime_r(&(timval.tv_sec), &tm_tim);
  asctime_r(&tm_tim, str_tim);


  fp = fopen(tmp_fn, "w");
  if (!fp)
  {
    perror(tmp_fn);

    format_log_message(&log_msg, LOGLEVEL_ERROR, "Ruleparam dabase open error (%s)", tmp_fn);
    if (commhub_fd >= 0)
    {
      send_message(commhub_fd, &log_msg);
    }

    return -1;
  }

  fprintf(fp, "# %s", str_tim);
  fprintf(fp, "# Tabs are a necessary field delimiter.\n");
  fprintf(fp, "#\n# InternalID,RuleName,RuleParam,ReferenceCount\n");
  fprintf(fp, "# Note: The reference count in this file is only used for debugging purposes.\n");
  fprintf(fp, "#seq: %llu\n", ctx->seq);
  fprintf(fp, "#\n#\n");

  nod = ctx->head;
  while (nod)
  {
    fprintf(fp, "%i\t%s\t%s\t%i\n", nod->id, nod->name, nod->param, nod->reference_count);
    nod = nod->next;

    rule_count++;
  }

  fprintf(fp, "#rulecount: %i\n", rule_count);

  fclose(fp);
  sync();

  if (_copy_file( tmp_fn, log_fn ) < 0)
  {
    format_log_message(&log_msg, LOGLEVEL_ERROR, "Ruleparam dabase copy error (%s -> %s)", tmp_fn, log_fn);
    if (commhub_fd >= 0)
    {
      send_message(commhub_fd, &log_msg);
    }

    return errno;
  }
  sync();

  rename( tmp_fn, ctx->db_filename );
  sync();

  ruleparam_db_seq_save( ctx );
  

  return 0;
}

int format_new_ruleparamdb( char *fn )
{
  ruleparam_db_ctx ctx;
  ruleparam_db_init(&ctx);

  //return ruleparam_db_save( &ctx, fn );
  return ruleparam_db_save( &ctx );
}


int ruleparam_db_add( ruleparam_db_ctx *ctx, char *name, char *param)
{
  int id;
  passdb_slim_ruleparam *prev = NULL, *nod ;

  if (ctx->n == RULEPARAM_DB_MAX)
    return RULEPARAM_DB_FULL;

  id = ruleparam_db_find( ctx, name, param );
  if (id >= 0)
    return RULEPARAM_DB_DUP;

  id = ruleparam_db_find_free_id( ctx );
  if (id < 0)
    return id;

  nod = _alloc_ruleparam_db_nod( id, name, param);

  ctx->n++;

  if (!(ctx->head))
  {
    ctx->head = nod;
    return 0;
  }

  prev = ctx->head;
  while(prev->next) prev = prev->next;

  prev->next = nod;
  return id;
}

int ruleparam_db_remove( ruleparam_db_ctx *ctx, int id)
{
  passdb_slim_ruleparam *prev = NULL, *nod ;

  nod = ctx->head;
  while (nod)
  {

    if (nod->id == id)
    {
      if (prev) prev->next = nod->next;
      else      ctx->head = nod->next;

      free(nod);
      ctx->n--;

      return 0;
    }

    prev = nod;
    nod = nod->next;
  }

  return RULEPARAM_DB_NOT_FOUND;

}


// Return id of found rule record.
// RULEPARAM_DB_NOT_FOUND if none found.
//
int ruleparam_db_find( ruleparam_db_ctx *ctx, char *name, char *param )
{
  passdb_slim_ruleparam *nod;

  nod = ctx->head;
  while (nod)
  {
    if ( (strncmp( nod->name, name, RULENAME_LEN ) == 0) &&
         (strncmp( nod->param, param, PARAM_LEN ) == 0) )
      return nod->id;
    nod = nod->next;
  }

  return RULEPARAM_DB_NOT_FOUND;
}

// Return reference count of rule give as (name,param) string pair
//
int ruleparam_db_reference_count( ruleparam_db_ctx *ctx, char *name, char *param )
{
  passdb_slim_ruleparam *nod;

  nod = ctx->head;
  while (nod)
  {
    if ( (strncmp( nod->name, name, RULENAME_LEN ) == 0) &&
         (strncmp( nod->param, param, PARAM_LEN ) == 0) )
      return nod->reference_count;
    nod = nod->next;
  }

  return RULEPARAM_DB_NOT_FOUND;
}


// Populate name and param from rule id, if found
//
int ruleparam_db_get( char *name, char *param, ruleparam_db_ctx *ctx, int id )
{
  passdb_slim_ruleparam *nod;

  nod = ctx->head;

  while (nod)
  {
    if (nod->id == id)
    {
      memcpy( name, nod->name, RULENAME_LEN );
      memcpy( param, nod->param, PARAM_LEN );
      name[RULENAME_LEN-1] = '\0';
      name[PARAM_LEN-1] = '\0';
      return 0;
    }

    nod = nod->next;
  }

  return RULEPARAM_DB_NOT_FOUND;

}


static int _icmp( const void *a, const void *b)
{
  return (*((int *)a)) - (*((int *)b)) ;
}

//  Take all id's, put them into an array.
//  Sort it.
//  Find the first non colliding entry starting
//  at 0.
//
int ruleparam_db_find_free_id( ruleparam_db_ctx *ctx )
{
  int i, n, pos=0;
  int *p;
  passdb_slim_ruleparam *nod;

  n = ctx->n;

  if (n == RULEPARAM_DB_MAX)
    return RULEPARAM_DB_FULL;

  if (n == 0)
    return 0;

  p = (int *)malloc(sizeof(int)*n);
  CHECK_ALLOC_FAIL( (p) );

  nod = ctx->head;
  while (nod)
  {
    p[pos++] = nod->id;
    nod = nod->next;
  }

  qsort(p, n, sizeof(int), _icmp);

  i=0;
  pos=0;
  while ( (i<n) && (pos < RULEPARAM_DB_MAX) )
  {
    if (p[i] == pos)
    {
      i++;
      pos++;
    }
    else break;
  }

  free(p);

  return pos;
}

void ruleparam_db_dump(ruleparam_db_ctx *ctx)
{
  passdb_slim_ruleparam *nod;
  printf("n %i\n", ctx->n);

  nod = ctx->head;
  while (nod)
  {
    printf(" (%i) '%s' '%s'\n", nod->id, nod->name, nod->param );
    nod = nod->next;
  }

  printf("\n");

}

passdb_slim_ruleparam *ruleparam_db_get_node( ruleparam_db_ctx *ctx, int id )
{
  passdb_slim_ruleparam *nod;
  nod = ctx->head;
  while (nod)
  {
    if (nod->id == id )
      return nod;
    nod = nod->next;
  }
  return NULL;
}

int ruleparam_db_update( ruleparam_db_ctx *ctx, char *name, char *param, int delta_ref_count )
{
  int id;
  int r;
  passdb_slim_ruleparam *nod = NULL;

  id = ruleparam_db_find( ctx, name, param );

  // If it's not found, create it and update reference count
  //
  if ( id == RULEPARAM_DB_NOT_FOUND )
  {

    if (delta_ref_count < 0)
    {
      goto _ruleparam_db_update_sanity_error;
    }

    id = ruleparam_db_add( ctx, name, param );

    if (id < 0)
      return id;

    nod = ruleparam_db_get_node( ctx, id );
    nod->reference_count += delta_ref_count;

    //ruleparam_db_save( ctx, ctx->db_filename );
    ruleparam_db_save( ctx );

    return id;
  }

  // Rule record was found, update reference count
  //
  nod = ruleparam_db_get_node( ctx, id );
  nod->reference_count += delta_ref_count;

  // check for sanity
  // 
  if (nod->reference_count < 0)
  {
    goto _ruleparam_db_update_sanity_error;
  }

  // simply return if there are more references to this record
  //
  if (nod->reference_count > 0)
  {
    return id;
  }

  // otherwise we need to remove this rule record
  //
  r = ruleparam_db_remove( ctx, id );

  if (r < 0)
    goto _ruleparam_db_update_sanity_error;


  //ruleparam_db_save( ctx, ctx->db_filename );
  ruleparam_db_save( ctx );

  return r;

_ruleparam_db_update_sanity_error:
    fprintf(stderr, "ERROR: sanity! ruleparam_db_update: id %i (%s,%s), delta_ref_count: %i, [ nod(%p).reference_count:%i ] \n",
        id, name, param, delta_ref_count,
        nod, nod ? nod->reference_count : 0 );
    fprintf(stdout, "ERROR: sanity! ruleparam_db_update: id %i (%s,%s), delta_ref_count: %i, [ nod(%p).reference_count:%i ] \n",
        id, name, param, delta_ref_count,
        nod, nod ? nod->reference_count : 0 );
    return RULEPARAM_DB_SANITY;

}

// Remove entries that have zero references
//
int ruleparam_db_clean (ruleparam_db_ctx *ctx)
{
  passdb_slim_ruleparam *nod;
  passdb_slim_ruleparam *prv = NULL;
  passdb_slim_ruleparam *nex ;

  nod = ctx->head;
  while (nod)
  {
    nex = nod->next;

    if (nod->reference_count == 0)
    {

      if (prv) prv->next = nex;
      else     ctx->head = nex;

      free(nod);
      ctx->n--;

    }
    else
    {
      prv = nod;
    }

    nod = nex;
  }

  if (ctx->n == 0)
    ctx->head = NULL;

  return 0;
}

int ruleparam_db_consistency_check( ruleparam_db_ctx *ctx )
{
  int n=0;
  passdb_slim_ruleparam *nod;

  if (!ctx)
  {
    printf("ruleparam_db_consistenc_check: error, ctx is null\n");
    return 0;
  }

  if ((ctx->n > 0) && (!(ctx->head)))
  {
    printf("ruleparam_db_consistenc_check: error, ctx->n > 0 (%i) but ctx->head is null \n", ctx->n);
  }

  nod = ctx->head;
  while(nod)
  {

    if (nod->name[0] == '\0')
    {
      printf("ruleparam_db_consistenc_check: error, node id %i (pos %i) name is empty\n", nod->id, n);
      return 0;
    }

    if (nod->reference_count <= 0)
    {
      printf("ruleparam_db_consistenc_check: error, node id %i (pos %i) (%s,%s) reference count (%i) <= 0 \n", nod->id, n, nod->name, nod->param, nod->reference_count);
      return 0;
    }

    n++;
    nod = nod->next;
  }

  if (n != ctx->n)
  {
    printf("ruleparam_db_consistenc_check: error, ctx->n (%i) != list length (%i) \n", ctx->n, n);
    return 0;
  }

  return 1;
}

int ruleparam_db_debug_dump( ruleparam_db_ctx *ctx) 
{
  int count=0;
  passdb_slim_ruleparam *nod;

  printf("ruleparam_db_ctx:\n");
  printf("  n: %i\n", ctx->n);
  printf("  db_filename: %s\n", ctx->db_filename);

  nod = ctx->head;
  while (nod)
  {
    printf("    {%i} id: %i, reference_count: %i, name: %s, param: %s\n", 
        count++, nod->id, nod->reference_count, nod->name, nod->param );
    nod = nod->next;
  }
  printf("\n");

  return 0;
}

int ruleparam_db_free( ruleparam_db_ctx *ctx )
{
  passdb_slim_ruleparam *nod, *prev = NULL;

  nod = ctx->head;
  while (nod)
  {
    prev = nod;
    nod = nod->next;
    free(prev);
  }

  if (ctx->db_filename)
    free(ctx->db_filename);

  free(ctx);

  return 0;
}

// Periodically refresh ruleparam_db, mostly for debugging purposes
// so we can see a somewhat recent snapshot of reference counts.
//
// ruleparam updates through the 'ruleparam_db_update' function
// will automatically write to the ruleparam_db file if a deletion
// or insertion happened.
//
int ruleparam_db_rate_limited_sync( ruleparam_db_ctx *ctx )
{
  int retval = 0;

  if (ctx->sync_every_n < 1)
  {
    ctx->sync_counter++;
    return retval;
  }

  if ((ctx->sync_counter % ctx->sync_every_n) == 0)
  {
    ruleparam_db_save( ctx );
    retval = 1;
  }

  // Else do a sequence number save regardless
  //
  else
  {
    ruleparam_db_seq_save( ctx );
    sync();
  }


  ctx->sync_counter++;

  return retval;
}

/*
int main(int argc, char **argv)
{
  int id=-1, k;
  char name[64], param[64];

  name[0] = '\0';
  param[0] = '\0';

  ruleparam_db_ctx *ctx;

  ruleparam_db_load(&ctx, RULEPARAM_DB_FILE);

  ruleparam_db_dump(ctx);

  ruleparam_db_add( ctx, "myrule", "myparam" );
  id = ruleparam_db_add( ctx, "myrule0", "myparam" );
  ruleparam_db_add( ctx, "myrule", "myparam0" );
  ruleparam_db_add( ctx, "myrule0", "myparam0" );

  ruleparam_db_dump(ctx);

  printf("removing %i\n", id);
  printf("\n\n");

  ruleparam_db_remove( ctx, id );
  ruleparam_db_dump(ctx);


  ruleparam_db_save(ctx, RULEPARAM_DB_FILE);

  id = ruleparam_db_find(ctx, "myrule0", "myparam");
  printf("got %i\n", id);


  k = ruleparam_db_get(name, param, ctx, id);
  printf("  (%i) %s %s\n", k, name, param);

}
*/
