/*
 * 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 <sys/user.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#include <openssl/md5.h>

#include "../common/common_defs.h"

#include "billdb.h"

static void free_billing_node_list(billing_node *head)
{
    billing_node *p = head;
    billing_node *q;
    
    while(p)
    {
        q = p;
        p = p->next;
        
        free(q);
    }
}

int format_new_billdb()
{
    char blank[MEMORY_PAGE_SIZE] = {0};
    
    int i, n;
    int fd;
    
    fd = creat(BILLING_FILE, S_IRUSR | S_IWUSR);
    
    if( fd < 0 )
    {
        fprintf(stderr, "Cannot create billing file!\n");
        return FAIL_DATABASE;
    }
    
    n = BILLING_MAP_SIZE / MEMORY_PAGE_SIZE;
    
    for(i = 0; i < n; i++)
    {
        if( write(fd, &blank, sizeof(blank)) != sizeof(blank) )
        {
            fprintf(stderr, "Cannot write blank data to billing file!\n");
            close(fd);
            return FAIL_DATABASE;;
        }
    }
    
    close(fd);
    return 0;
}

int detach_from_billdb(billdb_context *ctx)
{
    if(!ctx)
      return FAIL_PARAM;
    
    if(ctx->last_tx != NULL)
    {
        free(ctx->last_tx);
    }
    
    free_billing_node_list(ctx->freelist);
    free_billing_node_list(ctx->activelist);
    
    if(ctx->bills != NULL)
    {
        munmap(ctx->bills, BILLING_MAP_SIZE);
    }
    
    memset(ctx, 0, sizeof(billdb_context));

    if(ctx->mmap_broken)
    {
        close(ctx->billing_fd);
    }

    return 0;
}

int attach_to_billdb(billdb_context *ctx)
{
    int n;
    int retval;
    struct stat st;
    int fd;
    billing_record *foo;
    
    int mmap_broken = 0;
    
    billing_node *freehead, *acthead, *q;
    int numfree, numact;
    
    time_t *last_tx;
    
    int i;
    
    //--------
    
    if(!ctx)		//fail if we get passed a null pointer
        return FAIL_PARAM;
    
    //We also want to fail if we get passed a pointer to an active/in-use context...
    if(ctx->bills || ctx->activelist || ctx->freelist || ctx->last_tx)
    {
        return FAIL_PARAM;
    }
    
    //Go and stat the billing database file
    retval = stat(BILLING_FILE, &st);
    
    if(retval)
    {
        fprintf(stderr, "Cannot find billing file!\n");
        return FAIL_DATABASE;
    }
    
    //Make sure it is the right size...
    n = (st.st_size / sizeof(billing_record));
    
    if(n != NUM_BILLING_ENTRIES)
    {
        fprintf(stderr, "Billing file contains %d records, expecting %d!\n", n, NUM_BILLING_ENTRIES);
        return FAIL_DATABASE;;
    }    

    //open the file 
    fd = open(BILLING_FILE, O_RDWR | O_SYNC);
    
    if(fd < 0)
    {
        fprintf(stderr, "Cannot open billing file!\n");
        return FAIL_DATABASE;;
    }

    //mmap() the file into a pointer in our address space
    foo = (billing_record *) mmap(NULL, BILLING_MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    if( (foo == NULL) || (foo == MAP_FAILED) )	//if the MAP_SHARED option fails...
    {
        //try again with MAP_PRIVATE and see if it works...
        foo = (billing_record *) mmap(NULL, BILLING_MAP_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);    
    
        if( (foo == NULL) || (foo == MAP_FAILED) )
        {
            close(fd);
            fprintf(stderr, "Cannot mmap billing file! Try checking sysctl settings kernel.shmall and kernel.shmmax (return == %p errno == %d)\n",foo,errno);
            return FAIL_DATABASE;;
        }
        else
        {
            //set our mmap broken flag
            printf("mmap seems to be broken...  operating in braindead file IO mode...\n");
            mmap_broken = 1;
        }
    }
    else
    {
        //close the file (we no longer need it open once it is mmap()'d) (provided mmap() isn't broken)
        close(fd);
    }
    
    last_tx = (time_t *)malloc(sizeof(time_t) * NUM_BILLING_ENTRIES);
    
    if(last_tx == NULL)
    {
        fprintf(stderr, "Cannot allocate transmit timestamps for billing log\n");
        munmap(foo, n * sizeof(billing_record));
        return FAIL_MEM;
    }
    else
    {
        memset(last_tx, 0, sizeof(time_t) * NUM_BILLING_ENTRIES);
    }
    
    //------
    
    freehead = acthead = q = NULL;
    numfree = numact = 0;
    
    //For all records in our flat file
    for(i=0; i < n; i++)
    {
        //if the record is not in use        
        if(foo[i].checksum[0] == '\0')
        {
            //add it to the freelist
            q = (billing_node *) malloc( sizeof(billing_node) );
            
            if(!q)
            {
                free_billing_node_list(freehead);
                free_billing_node_list(acthead);
                fprintf(stderr, "Malloc returned NULL loading billing data!\n");
                munmap(foo, n * sizeof(billing_record));
                return FAIL_MEM;
            }
            else
            {
                numfree++;
                
                q->next = freehead;
                q->idx = i;
                
                freehead = q;
            }
        }
        else
        {
            //add it to the active list
            q = (billing_node *) malloc( sizeof(billing_node) );
            
            if(!q)
            {
                free_billing_node_list(freehead);
                free_billing_node_list(acthead);
                fprintf(stderr, "Malloc returned NULL loading billing data!\n");
                munmap(foo, n * sizeof(billing_record));
                return FAIL_MEM;
            }
            else
            {
                numact++;
                
                q->next = acthead;
                q->idx = i;
                
                acthead = q;
            }
        }  
    }    


    ctx->bills = foo;
    ctx->freelist = freehead;
    ctx->activelist = acthead;

    if(mmap_broken)
    {
        ctx->mmap_broken = 1;
        ctx->billing_fd = fd;
    }
    else
    {
        ctx->mmap_broken = 0;
        ctx->billing_fd = 0;
    }

    ctx->last_tx = last_tx;
    ctx->num_free_records = numfree;

    printf("Loaded %d records (%d used, %d free);\n", n, numact, numfree);
    
    return n;
} 

static void sync_bill_change(billdb_context *ctx, int idx)
{
    int offset;
    
    if(!ctx)		//fail if we get passed a null pointer
        return;
    
    if(idx < 0)
        return;
        
    if(idx >= NUM_BILLING_ENTRIES)
        return;
    
    if(ctx->mmap_broken)
    {
        offset = (idx * sizeof(billing_record)) / MEMORY_PAGE_SIZE;	//calculate the beginning page number
        offset *= MEMORY_PAGE_SIZE;					//multiply by page size
        
        lseek(ctx->billing_fd, offset, SEEK_SET);
        write(ctx->billing_fd, ((void *)ctx->bills) + offset, MEMORY_PAGE_SIZE);
    }
    else
    {
        msync(ctx->bills, BILLING_MAP_SIZE, MS_SYNC | MS_INVALIDATE);
    }
}

static int alloc_bill(billdb_context *ctx)
{
    billing_node *p;
    
    if(!ctx)
        return FAIL_PARAM;

    p = ctx->freelist;
    
    if(p)					//this should take the freelist and look for the first free node
    {
        ctx->freelist = ctx->freelist->next;	//advance the freelist to remove the first node
        
        p->next = ctx->activelist;		//and roll it onto the head of the active list
        ctx->activelist = p;
        
        ctx->num_free_records--;		//decrement the counter of free records
            
        return p->idx;				//return the index of the newly allocated record in the mmap()'d billing file
    }
    else					//if there are no free nodes, return failure
    {
        return FAIL_MEM;
    }
}

static int free_bill(billdb_context *ctx, int idx)
{
    billing_node *p, *q;
    
    if(!ctx)
        return FAIL_PARAM;

    q = NULL;
    p = ctx->activelist;

    while(p)					//this takes the list of active nodes
    {
        if( p->idx == idx )			//if the current node is the one requested for freeing
            break;				//stop now
    
        q = p;					//otherwise, advance the traveling pointers.
        p = p->next;
    }
    
    if(p)					//if we found the node to free
    {
        if(q)					//and it is NOT the list head
        {
            q->next = p->next;  		//snip it out of the active list
        }
        else					//if it is the list head
        {
            ctx->activelist = p->next;		//advance the list head to the next node
        }
        
        p->next = ctx->freelist;		//insert this node at the head of the freelist
        ctx->freelist = p;
        
        memset(&ctx->bills[idx], 0, sizeof(billing_record));	//clear the corresponding record in the mmap()'d biling file
        ctx->last_tx[idx] = 0;

        ctx->num_free_records++;		//increment the counter of free records
        
        return 0;
    }
    else
    {
        return WARN_NOTFOUND;
    }
}

int add_billing_entry(billdb_context *ctx, char *line)
{
    int n, i, idx;
    billing_record foo = {{0},{0}};

    unsigned char cksum[MD5_DIGEST_LENGTH];

    if(!ctx)
        return FAIL_PARAM;
        
    if(!ctx->freelist)
    {
        return FAIL_FULL;
    }
    
    //Translate any mid-string linebreaks to spaces, and trim any trailing linebreaks
    strip_crlf(line);
    
    //See how many bytes we have left over once the message has been trimmed.
    n = strlen(line);
    
    //If the billing entry is too long to process, return error
    if( n >= BILLING_LINE_SIZE )
    {
        return FAIL_PARAM;
    }
    
    //If the billing entry is blank, return error
    if( n == 0 )
    {
        return FAIL_PARAM;
    }
    
    //Calculate the MD5 checksum of our message
    MD5( (unsigned char *) line, n, cksum);
    
    //Convert it into lower-case hex notation to match what the server does
    //and stick that into our temporary billing entry
    for(i = 0; i < MD5_DIGEST_LENGTH; i++)
    {
        sprintf(foo.checksum + (i * 2), "%02x", cksum[i]);
    }

    //Copy the data into our billing entry
    strcpy(foo.data, line);

    //allocate ourselves a billing record entry
    idx = alloc_bill(ctx);

    //If our billing entry allocation fails
    if(idx < 0)
    {
        return idx;	//pass that error back to the caller
    }
    
    //and copy our entry into that slog
    memcpy(&ctx->bills[idx], &foo, sizeof(billing_record));

    //Sync the SHM
    sync_bill_change(ctx, idx);    

    return 0;
}

int clear_billing_entry(billdb_context *ctx, char *checksum)
{
    int i;

    unsigned long *a = (unsigned long *) checksum;
    unsigned long *b;

    if(!ctx)
        return FAIL_PARAM;
        
    
    for(i=0; i < NUM_BILLING_ENTRIES; i++)
    {
        b = (unsigned long *)ctx->bills[i].checksum;
    
        if(a[0] != b[0]) continue;
        if(a[1] != b[1]) continue;
        if(a[2] != b[2]) continue;
        if(a[3] != b[3]) continue;
        if(a[4] != b[4]) continue;
        if(a[5] != b[5]) continue;
        if(a[6] != b[6]) continue;
        if(a[7] != b[7]) continue;
        
        free_bill(ctx, i);
        
        sync_bill_change(ctx, i);
        break;    
    }
    
    if(i == NUM_BILLING_ENTRIES)
    {
        return WARN_NOTFOUND;
    }
    
    return 0;
}

int next_pending_entry(billdb_context *ctx)
{
    if(!ctx)
        return -1;

    if( !ctx->bills || !ctx->last_tx )
        return -1;

    time_t oldest = time(NULL) - DEFAULT_BILL_SYNC_RETRY;
    int oldest_idx = -1;
    
    billing_node *p;

    p = ctx->activelist;
    
    while(p)
    {
        if(ctx->last_tx[p->idx] <= oldest)
        {
            oldest = ctx->last_tx[p->idx];
            oldest_idx = p->idx;
        }
    
        p = p->next;
    }    
    
    return oldest_idx;
}

/*
int main()
{
    billdb_context ctx={0};
    char linebuffer[1024];
    
    if( attach_to_billdb(&ctx) < 0 )
    {
        format_new_billdb();
        
        if( attach_to_billdb(&ctx) < 0 )
            return -1;
    }
    
//    add_billing_entry(&ctx, "Kaboom");
//    add_billing_entry(&ctx, "Splat");
//    clear_billing_entry(&ctx, "01677e4c0ae5468b9b8b823487f14524");
    
    reset_billing_cursor(&ctx);
    
    while(next_billing_entry(&ctx, linebuffer))
    {
        printf("\"%s\"\n", linebuffer);
    }
    
    return 0;  
}
*/


