import {C8oCore} from "./c8oCore";
import {C8oProgress} from "./c8oProgress";
import {C8oResponseListener, C8oResponseProgressListener} from "./c8oResponse";
import {FullSyncReplication} from "./fullSyncReplication";

import PouchDB from "pouchdb-browser";
import * as PouchDBLoad from "pouchdb-load";

/**
 * Created by charlesg on 10/01/2017.
 */
export class C8oFullSyncDatabase {

    /**
     * Used to log.
     */
    private c8o: C8oCore;

    /** TAG Attributes **/

    /**
     * The fullSync database name.
     */
    private databaseName: string;
    private c8oFullSyncDatabaseUrl: string;
    /**
     * The fullSync Database instance.
     */
    private database = null;
    /**
     * Used to make pull replication (uploads changes from the local database to the remote one).
     */
    private pullFullSyncReplication: FullSyncReplication = new FullSyncReplication(true);
    /**
     * Used to make push replication (downloads changes from the remote database to the local one).
     */
    private pushFullSyncReplication: FullSyncReplication = new FullSyncReplication(false);
    /**
     * Used to make pull replication (uploads changes from the local database to the remote one).
     */
    private syncFullSyncReplication: FullSyncReplication = new FullSyncReplication();

    private remotePouchHeader;

    /**
     * Creates a fullSync database with the specified name and its location.
     *
     * @param c8o
     * @param databaseName
     * @param fullSyncDatabases
     * @param localSuffix
     * @throws C8oException Failed to get the fullSync database.
     */
    constructor(c8o: C8oCore, databaseName: string, fullSyncDatabases: string, localSuffix: string) {
        this.c8o = c8o;
        const header = {
            "x-convertigo-sdk": this.c8o.sdkVersion,
        };
        Object.assign(header, this.c8o.headers);
        this.remotePouchHeader = {
            ajax: {
                headers: header,
            },
        };
        this.c8oFullSyncDatabaseUrl = fullSyncDatabases + databaseName;
        this.databaseName = databaseName + localSuffix;
        try {
            if (c8o.couchUrl != null) {
                this.database = new PouchDB(c8o.couchUrl + "/" + databaseName);
                this.c8o.log.debug("PouchDb launched on couchbaselite");
            } else {
                PouchDB.plugin(PouchDBLoad)
                this.database = new PouchDB(databaseName);
                this.c8o.log.debug("PouchDb launched normally");
            }
        } catch (error) {
            throw error;
        }
    }

    /**
     * Start pull and push replications.
     * @returns Promise<any>
     */
    public startAllReplications(parameters: Object, c8oResponseListener: C8oResponseListener): Promise<any> {
        return this.startSync(this.syncFullSyncReplication, parameters, c8oResponseListener);
    }

    /**
     * Start pull replication.
     * @returns Promise<any>
     */
    public startPullReplication(parameters: Object, c8oResponseListener: C8oResponseListener): Promise<any> {
        return this.startReplication(this.pullFullSyncReplication, parameters, c8oResponseListener);
    }

    /**
     * Start push replication.
     * @returns Promise<any>
     */
    public startPushReplication(parameters: Object, c8oResponseListener: C8oResponseListener): Promise<any> {
        return this.startReplication(this.pushFullSyncReplication, parameters, c8oResponseListener);
    }

    private startSync(fullSyncReplication: FullSyncReplication, parameters: Object, c8oResponseListener: C8oResponseListener): Promise<any> {
        let continuous: boolean = false;
        let cancel: boolean = false;
        const parametersObj: Object = {};

        //stop replication if exists
        if (fullSyncReplication.replication != null) {
            fullSyncReplication.replication.cancel();
        }

        //check continuous flag
        if (parameters["continuous"] != null) {
            if (parameters["continuous"] as boolean === true) {
                continuous = true;
            } else {
                continuous = false;
            }
        }
        //check cancel flag
        if (parameters["cancel"] != null) {
            //noinspection RedundantIfStatementJS
            if (parameters["cancel"] as boolean === true) {
                cancel = true;
            } else {
                cancel = false;
            }
        }
        // Set retry true by default...
        parametersObj["retry"] = true;
        
        //check parameters to throw to pouchDB
        if (parameters["retry"] != null) {
            parametersObj["retry"] = parameters["retry"];
        }
        if (parameters["filter"] != null) {
            parametersObj["filter"] = parameters["filter"];
        }
        if (parameters["doc_ids"] != null) {
            parametersObj["doc_ids"] = parameters["doc_ids"];
        }
        if (parameters["query_params"] != null) {
            parametersObj["query_params"] = parameters["query_params"];
        }
        if (parameters["view"] != null) {
            parametersObj["view"] = parameters["view"];
        }
        if (parameters["since"] != null) {
            parametersObj["since"] = parameters["since"];
        }
        if (parameters["heartbeat"] != null) {
            parametersObj["heartbeat"] = parameters["heartbeat"];
        }
        if (parameters["timeout"] != null) {
            parametersObj["timeout"] = parameters["timeout"];
        }
        if (parameters["batch_size"] != null) {
            parametersObj["batch_size"] = parameters["batch_size"];
        }
        if (parameters["batches_limit"] != null) {
            parametersObj["batches_limit"] = parameters["batches_limit"];
        }
        if (parameters["back_off_function"] != null) {
            parametersObj["back_off_function"]  = parameters["back_off_function"];
        }
        if (parameters["checkpoint"] != null) {
            parametersObj["checkpoint"] = parameters["checkpoint"];
        }
        if (parameters["seq_interval"] != null) {
            parametersObj["seq_interval"] = parameters["seq_interval"];
        }

        const remoteDB = new PouchDB(this.c8oFullSyncDatabaseUrl, this.remotePouchHeader);
        let rep = fullSyncReplication.replication = this.database.sync(remoteDB, parametersObj);
        const param = parameters;
        const progress: C8oProgress = new C8oProgress();
        progress.raw = rep;
        progress.continuous = false;

        return new Promise((resolve, reject) => {
            rep.on("change", (info) => {
                progress.finished = false;
                if (info.direction === "pull") {
                    progress.pull = true;
                    progress.status = rep.pull.state;
                    progress.finished = rep.pull.state !== "active";
                } else if (info.direction === "push") {
                    progress.pull = false;
                    progress.status = rep.push.state;
                    progress.finished = rep.push.state !== "active";
                }
                progress.total = info.change.docs_read;
                progress.current = info.change.docs_written;
                param[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);

            }).on("complete", (info) => {
                progress.finished = true;
                progress.pull = false;
                progress.total = info.push.docs_read;
                progress.current = info.push.docs_written;
                progress.status = info.status;
                progress.finished = true;
                param[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                progress.pull = true;
                progress.total = info.pull.docs_read;
                progress.current = info.pull.docs_written;
                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                rep.cancel();

                if (continuous) {
                    parametersObj["live"] = true;
                    rep = fullSyncReplication.replication = this.database.sync(remoteDB, parametersObj);
                    progress.continuous = true;
                    progress.raw = rep;
                    progress.taskInfo = "n/a";
                    progress.pull = true;
                    progress.status = "live";
                    progress.finished = false;
                    progress.pull = true;
                    progress.total = 0;
                    progress.current = 0;
                    (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                    progress.pull = false;
                    (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                    rep.on("change", (info) => {
                        progress.finished = false;
                        if (info.direction === "pull") {
                            progress.pull = true;
                            progress.status = rep.pull.state;
                        } else if (info.direction === "push") {
                            progress.pull = false;
                            progress.status = rep.push.state;
                        }
                        progress.total = info.change.docs_read;
                        progress.current = info.change.docs_written;
                        param[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                        (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                    })
                        .on("paused", function() {
                            progress.finished = true;
                            (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                            if (progress.total === 0 && progress.current === 0) {
                                progress.pull = !progress.pull;
                                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, param);
                            }
                        })
                        .on("error", (err) => {
                            if (err.message === "Unexpected end of JSON input") {
                                progress.finished = true;
                                progress.status = "live";
                                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);
                            } else {
                                rep.cancel();
                                if (err.code === "ETIMEDOUT" && err.status === 0) {
                                    if (parameters["force_retry"] == true) {
                                        this.c8o.log.warn("C80=>FullSyncDatabase: Timeout handle during fullsync replication (fs://.sync) \n Forcing Restarting replication");
                                        this.database.sync(remoteDB, {timeout: 600000, retry: true});
                                    } else {
                                        this.c8o.log.warn("C80=>FullSyncDatabase: Timeout handle during fullsync replication (fs://.sync) \n Restarting automatically replication");
                                    }
                                } else if (err.name === "unknown" && err.status === 0 && err.message === "getCheckpoint rejected with ") {
                                    reject("NO_NETWORK");
                                } else {
                                    reject(err);
                                }
                            }
                        });

                }
            }).on("error", (err) => {
                rep.cancel();
                if (err.message === "Unexpected end of JSON input") {
                    progress.finished = true;
                    progress.status = "Complete";
                    (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);
                    rep.cancel();

                } else if (err.code === "ETIMEDOUT" && err.status === 0) {
                    if (parameters["force_retry"] == true) {
                        this.c8o.log.warn("C80=>FullSyncDatabase: Timeout handle during fullsync replication (fs://.sync) \n Forcing Restarting replication");
                        this.database.sync(remoteDB, {timeout: 600000, retry: true});
                    } else {
                        this.c8o.log.warn("C80=>FullSyncDatabase: Timeout handle during fullsync replication (fs://.sync) \n Restarting automatically replication");
                    }
                } else if (err.name === "unknown" && err.status === 0 && err.message === "getCheckpoint rejected with ") {
                    reject("NO_NETWORK");
                } else {
                    reject(err);
                }
            });

            if (cancel) {
                if (rep != null) {
                    rep.cancel();
                    progress.finished = true;
                    if (c8oResponseListener != null && c8oResponseListener instanceof C8oResponseProgressListener) {
                        c8oResponseListener.onProgressResponse(progress, parameters);
                    }
                }
            }
        }).catch((error) => {
            throw error.toString();
        });
    }

    /**
     * Starts a replication taking into account parameters.<br/>
     * This action does not directly return something but setup a callback raised when the replication raises change events.
     *
     * @param fullSyncReplication
     * @param c8oResponseListener
     * @param parameters
     */
    private startReplication(fullSyncReplication: FullSyncReplication, parameters: Object, c8oResponseListener: C8oResponseListener): Promise<any> {
        let continuous: boolean = false;
        let cancel: boolean = false;
        const parametersObj: Object = {};
        //stop replication if exists
        if (fullSyncReplication.replication != null) {
            fullSyncReplication.replication.cancel();
        }
        //check continuous flag
        if (parameters["continuous"] != null) {
            if (parameters["continuous"] as boolean == true) {
                continuous = true;
            } else {
                continuous = false;
            }
        }
        //check cancel flag
        if (parameters["cancel"] != null) {
            //noinspection RedundantIfStatementJS
            if (parameters["cancel"] as boolean == true) {
                cancel = true;
            } else {
                cancel = false;
            }
        }
        //check parameters to throw to pouchDB
        // Set retry true by default...
        parametersObj["retry"] = true;
        if (parameters["retry"] != null) {
            parametersObj["retry"] = parameters["retry"];
        }
        if (parameters["filter"] != null) {
            parametersObj["filter"] = parameters["filter"];
        }
        if (parameters["doc_ids"] != null) {
            parametersObj["doc_ids"] = parameters["doc_ids"];
        }
        if (parameters["query_params"] != null) {
            parametersObj["query_params"] = parameters["query_params"];
        }
        if (parameters["view"] != null) {
            parametersObj["view"] = parameters["view"];
        }
        if (parameters["since"] != null) {
            parametersObj["since"] = parameters["since"];
        }
        if (parameters["heartbeat"] != null) {
            parametersObj["heartbeat"] = parameters["heartbeat"];
        }
        if (parameters["timeout"] != null) {
            parametersObj["timeout"] = parameters["timeout"];
        }
        if (parameters["batch_size"] != null) {
            parametersObj["batch_size"] = parameters["batch_size"];
        }
        if (parameters["batches_limit"] != null) {
            parametersObj["batches_limit"] = parameters["batches_limit"];
        }
        if (parameters["back_off_function"] != null) {
            parametersObj["back_off_function"]  = parameters["back_off_function"];
        }
        if (parameters["checkpoint"] != null) {
            parametersObj["checkpoint"] = parameters["checkpoint"];
        }
        if (parameters["seq_interval"] != null) {
            parametersObj["seq_interval"] = parameters["seq_interval"];
        }

        const remoteDB = new PouchDB(this.c8oFullSyncDatabaseUrl, this.remotePouchHeader);
        let rep = fullSyncReplication.replication = fullSyncReplication.pull ? this.database.replicate.from(remoteDB, parametersObj) : this.database.replicate.to(remoteDB, parametersObj);

        const progress: C8oProgress = new C8oProgress();
        progress.raw = rep;
        progress.pull = fullSyncReplication.pull;
        progress.continuous = false;
        return new Promise((resolve, reject) => {

            rep.on("change", (info) => {
                progress.total = info.docs_read;
                progress.current = info.docs_written;
                progress.status = "change";
                parameters[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);
            }).on("complete", (info) => {
                progress.finished = true;
                progress.total = info.docs_read;
                progress.current = info.docs_written;
                progress.status = "complete";
                parameters[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);
                rep.cancel();
                if (continuous) {
                    parametersObj["live"] = true;
                    rep = fullSyncReplication.replication = fullSyncReplication.pull ? this.database.replicate.from(remoteDB, parametersObj) : this.database.replicate.to(remoteDB, parametersObj);
                    progress.continuous = true;
                    progress.raw = rep;
                    progress.taskInfo = "n/a";
                    rep.on("change", (info) => {
                        progress.finished = false;
                        progress.total = info.docs_read;
                        progress.current = info.docs_written;
                        progress.status = "change";
                        parameters[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                        (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);
                    })
                        .on("error", (err) => {
                            if (err.message === "Unexpected end of JSON input") {
                                progress.finished = true;
                                progress.status = "live";
                                (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);

                            } else {
                                rep.cancel();
                                if (err.code === "ETIMEDOUT" && err.status === 0) {
                                    reject("TIMEOUT");
                                } else if (err.name === "unknown" && err.status === 0 && err.message === "getCheckpoint rejected with ") {
                                    reject("NO_NETWORK");
                                } else {
                                    reject(err);
                                }
                            }
                        });
                }
            }).on("error", (err) => {
                if (err.message === "Unexpected end of JSON input") {
                    progress.finished = true;
                    progress.status = "complete";
                    parameters[C8oCore.ENGINE_PARAMETER_PROGRESS] = progress;
                    (c8oResponseListener as C8oResponseProgressListener).onProgressResponse(progress, parameters);
                    rep.cancel();
                } else if (err.code === "ETIMEDOUT" && err.status === 0) {
                    reject("TIMEOUT");
                } else if (err.name === "unknown" && err.status === 0 && err.message === "getCheckpoint rejected with ") {
                    reject("NO_NETWORK");
                } else {
                    reject(err);
                }
            });

            if (cancel) {
                if (rep != null) {
                    rep.cancel();
                    progress.finished = true;
                    if (c8oResponseListener != null && c8oResponseListener instanceof C8oResponseProgressListener) {
                        c8oResponseListener.onProgressResponse(progress, parameters);
                    }
                }
            }

        }).catch((error) => {
            throw error.toString();
        });

    }

    //noinspection JSUnusedGlobalSymbols
    public get getdatabseName(): string {
        return this.databaseName;
    }

    public get getdatabase(): any {
        return this.database;
    }

    public deleteDB(): Promise<any> {
        return new Promise((resolve, reject) => {
            if (this.database != null) {
                this.database.destroy().then((response) => {
                    this.database = null;
                    resolve(response);
                }).catch((error) => {
                    this.c8o.log.debug("Failed to close DB, will retry: ", error.message);
                    this.database.destroy().then((response) => {
                        this.database = null;
                        resolve(response);
                    }).catch((error) => {
                        this.c8o.log.debug("Failed to close DB, second attempt has failed ", error.message);
                        reject(error);
                    });
                });
            }
        });
    }
}
