/*
 *
  ***** BEGIN LICENSE BLOCK *****
 
  Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
  Copyright (C) 2017-2019 Olof Hagsand
  Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate)

  This file is part of CLIXON.

  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.

  Alternatively, the contents of this file may be used under the terms of
  the GNU General Public License Version 3 or later (the "GPL"),
  in which case the provisions of the GPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of the GPL, and not to allow others to
  use your version of this file under the terms of Apache License version 2, 
  indicate your decision by deleting the provisions above and replace them with
  the  notice and other provisions required by the GPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the Apache License version 2 or the GPL.

  ***** END LICENSE BLOCK *****

 */

#ifdef HAVE_CONFIG_H
#include "clixon_config.h" /* generated by config & autoconf */
#endif

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <pwd.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>

/* cligen */
#include <cligen/cligen.h>

/* clicon */
#include <clixon/clixon.h>

#include "clixon_backend_transaction.h"
#include "clixon_backend_plugin.h"
#include "backend_handle.h"
#include "clixon_backend_commit.h"
#include "backend_startup.h"

/*! Merge db1 into db2 without commit 
 * @retval   -1       Error
 * @retval    0       Validation failed (with cbret set)
 * @retval    1       Validation OK       
 */
static int
db_merge(clicon_handle h,
         const char   *db1,
         const char   *db2,
         cbuf         *cbret)
{
    int    retval = -1;
    cxobj *xt = NULL;
    
    /* Get data as xml from db1 */
    if (xmldb_get0(h, (char*)db1, YB_MODULE, NULL, NULL, 1, WITHDEFAULTS_EXPLICIT, &xt, NULL, NULL) < 0)
        goto done;
    xml_name_set(xt, NETCONF_INPUT_CONFIG);
    /* Merge xml into db2. Without commit */
    retval = xmldb_put(h, (char*)db2, OP_MERGE, xt, clicon_username_get(h), cbret);
 done:
    xmldb_get0_free(h, &xt);
    return retval;
}

/*! Clixon startup startup mode: Commit startup configuration into running state
 * @param[in]  h       Clixon handle
 * @param[in]  db      tmp or startup
 * @param[out] cbret   If status is invalid contains error message
 * @retval    -1       Error
 * @retval     0       Validation failed
 * @retval     1       OK

OK:
                              reset     
running                         |--------+------------> RUNNING
                parse validate OK       / commit 
startup -------+--+-------+------------+          


INVALID (requires manual edit of candidate)
failsafe      ----------------------+
                            reset    \ commit
running                       |-------+---------------> RUNNING FAILSAFE
              parse validate fail 
startup      ---+-------------------------------------> INVALID XML

ERR: (requires repair of startup) NYI
failsafe      ----------------------+
                            reset    \ commit
running                       |-------+---------------> RUNNING FAILSAFE
              parse fail
startup       --+-------------------------------------> BROKEN XML

 * @note: if commit fails, copy factory to running
 */
int
startup_mode_startup(clicon_handle        h,
                     char                *db,
                     cbuf                *cbret)
{
    int        retval = -1;
    int        ret = 0;
    int        rollback_exists;
    yang_stmt *yspec = clicon_dbspec_yang(h);
    
    if (strcmp(db, "running")==0){
        clicon_err(OE_FATAL, 0, "Invalid startup db: %s", db);
        goto done;
    }
    /* If startup does not exist, create it empty */
    if (xmldb_exists(h, db) != 1){ /* diff */
        if (xmldb_create(h, db) < 0) /* diff */
            return -1;
    }

    /* When a confirming-commit is issued, the confirmed-commit timeout
     * callback is removed and then the rollback database is deleted.
     *
     * The presence of a rollback database means that before the rollback
     * database was deleted, either clixon_backend crashed or the machine
     * rebooted.
     */
    if (if_feature(yspec, "ietf-netconf", "confirmed-commit")) {
        if ((rollback_exists = xmldb_exists(h, "rollback")) < 0) {
            clicon_err(OE_DAEMON, 0, "Error checking for the existence of the rollback database");
            goto done;
        }
        if (rollback_exists == 1) {
            ret = startup_commit(h, "rollback", cbret);
            switch(ret) {
                case -1:
                case 0:
                    /* validation failed, cbret set */
                    if ((ret = startup_commit(h, "failsafe", cbret)) < 0)
                        goto fail;

                    /* Rename the errored rollback database so that it is not tried on a subsequent startup */
                    xmldb_rename(h, db, NULL, ".error");
                    goto ok;
                case 1:
                    /* validation ok */
                    xmldb_delete(h, "rollback");
                    goto ok;
                default:
                    /* Unexpected response */
                    goto fail;
            }
        }
        else {
            if ((ret = startup_commit(h, db, cbret)) < 0)
                goto done;
            if (ret == 0)
                goto fail;
        }
    }
    else {
        if ((ret = startup_commit(h, db, cbret)) < 0)
            goto done;
        if (ret == 0)
            goto fail;
    }
 ok:
    retval = 1;
 done:
    return retval;
 fail:
    retval = 0;
    goto done;
}

/*! Merge xml in filename into database
 * @retval   -1       Error
 * @retval    0       Validation failed (with cbret set)
 * @retval    1       Validation OK       
 */
static int
load_extraxml(clicon_handle h,
              char         *filename,
              const char   *db,
              cbuf         *cbret)
{
    int        retval =  -1;
    cxobj     *xt = NULL;
    cxobj     *xerr = NULL;
    FILE      *fp = NULL;
    yang_stmt *yspec = NULL;
    int        ret;

    if (filename == NULL)
        return 1;
    if ((fp = fopen(filename, "r")) == NULL){
        clicon_err(OE_UNIX, errno, "open(%s)", filename);
        goto done;
    }
    yspec = clicon_dbspec_yang(h);
    /* No yang check yet because it has <config> as top symbol, do it later after that is removed */
    if (clixon_xml_parse_file(fp, YB_NONE, yspec, &xt, &xerr) < 0)
        goto done;
    /* Replace parent w first child */
    if (xml_rootchild(xt, 0, &xt) < 0)
        goto done;
    /* Ensure edit-config "config" statement */
    if (xt)
        xml_name_set(xt, NETCONF_INPUT_CONFIG);
    /* Now we can yang bind */
    if ((ret = xml_bind_yang(h, xt, YB_MODULE, yspec, &xerr)) < 0)
        goto done;
    if (ret == 0){
        if (netconf_err2cb(xerr, cbret) < 0)
            goto done;
        retval = 0;
        goto done;
    }

    /* Merge user reset state */
    retval = xmldb_put(h, (char*)db, OP_MERGE, xt, clicon_username_get(h), cbret);
 done:
    if (fp)
        fclose(fp);
    if (xt)
        xml_free(xt);
    return retval;
}

/*! Load extra XML via file and/or reset callback, and merge with current
 * An application can add extra XML either via the -c <file> option or
 * via the .ca_reset callback. This XML is "merged" into running, that is,
 * it does not trigger validation calbacks.
 * The function uses an extra "tmp" database, loads the file to it, and calls
 * the reset function on it.
 * @param[in]  h    Clicon handle
 * @param[in]  file (Optional) extra xml file
 * @param[out] status  Startup status
 * @param[out] cbret   If status is invalid contains error message
 * @retval    -1       Error
 * @retval     0       Validation failed
 * @retval     1       OK
                
running -----------------+----+------>
           reset  loadfile   / merge
tmp     |-------+-----+-----+
             reset   extrafile
 */
int
startup_extraxml(clicon_handle        h,
                 char                *file,
                 cbuf                *cbret)
{
    int         retval = -1;
    char       *tmp_db = "tmp";
    int         ret;
    cxobj       *xt0 = NULL; 
    cxobj       *xt = NULL; 
    
    /* Clear tmp db */
    if (xmldb_db_reset(h, tmp_db) < 0)
        goto done;
    /* Application may define extra xml in its reset function */
    if (clixon_plugin_reset_all(h, tmp_db) < 0)   
        goto done;
    /* Extra XML can also be added via file */
    if (file){
        /* Parse and load file into tmp db */
        if ((ret = load_extraxml(h, file, tmp_db, cbret)) < 0)
            goto done;
        if (ret == 0)
            goto fail;
    }
    /* 
     * Check if tmp db is empty. 
     * It should be empty if extra-xml is null and reset plugins did nothing
     * then skip validation.
     */
    if (xmldb_get(h, tmp_db, NULL, NULL, &xt0) < 0)
        goto done;
    if ((ret = xmldb_empty_get(h, tmp_db)) < 0)
        goto done;
    if (ret == 1)
        goto ok;
    xt = NULL;
    /* Clear db cache so that it can be read by startup */
    xmldb_clear(h, tmp_db);
    /* Validate the tmp db and return possibly upgraded xml in xt */
    if ((ret = startup_validate(h, tmp_db, &xt, cbret)) < 0)
        goto done;
    if (ret == 0)
        goto fail;
    if (xt==NULL || xml_child_nr(xt)==0) 
        goto ok;
    /* Merge tmp into running (no commit) */
    if ((ret = db_merge(h, tmp_db, "running", cbret)) < 0)
        goto fail;
    if (ret == 0)
        goto fail;
 ok:
    retval = 1;
 done:
    if (xt0)
        xml_free(xt0);
    xmldb_get0_free(h, &xt);
    if (xmldb_delete(h, tmp_db) != 0 && errno != ENOENT) 
        return -1;
    return retval;
 fail:
    retval = 0;
    goto done;
}

/*! Init modules state of the backend (server). To compare with startup XML
 * Set the modules state as setopt to the datastore module.
 * Only if CLICON_XMLDB_MODSTATE is enabled
 * @retval -1 Error
 * @retval  0 OK
 */
int
startup_module_state(clicon_handle h,
                     yang_stmt    *yspec)
{
    int    retval = -1;
    cxobj *x = NULL;
    int    ret;
        
    if (!clicon_option_bool(h, "CLICON_XMLDB_MODSTATE"))
        goto ok;
    /* Set up cache 
     * Now, access brief module cache with clicon_modst_cache_get(h, 1) */
    if ((ret = yang_modules_state_get(h, yspec, NULL, NULL, 1, &x)) < 0)
        goto done;
    if (ret == 0)
        goto fail;
 ok:
    retval = 1;
 done:
    if (x)
        xml_free(x);
    return retval;
 fail:
    retval = 0;
    goto done;
}
