var util = require("util");
var _ = require("lodash");
var mu = require("masm-util");

module.exports = function (cdb, logger) {
    // TODO: handle unautorized requests

    logger = logger || console;

    var ops = {
        allDBs: function (options) {
            checkArgumentObject(options, and("ok"));
            mu.retryWithThrottling(function (retry) {
                cdb.getJSON("/_all_dbs", {}, function (err, names) {
                    if (err) {
                        logger.warn("(Will retry) Failed to get all databases list. Error follows");
                        logger.warn(util.inspect(err));
                        retry();
                    } else {
                        callOneOf(options, ["ok"], [names]);
                    }
                });
            }, 10); // max 10 seconds before retrying
        },

        replicate: function (data, options) {
            checkArgumentObject(options, and("ok"));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/_replicate", {}, function (err, result) {
                    if (err) {
                        logger.warn("(Will retry) Failed to replicate. Data and error follow");
                        logger.warn(util.inspect(data));
                        logger.warn(util.inspect(err));
                        retry();
                    } else {
                        callOneOf(options, ["ok"], [result]);
                    }
                }, JSON.stringify(data));
            }, 10); // max 10 seconds before retrying
        },

        replicateDocs: function (source, target, ids, options) {
            ops.replicate({source: source, target: target, doc_ids: ids}, options);
        },

        getDB: function (dbName, options) {
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.getJSON("/" + encodeURIComponent(dbName), {}, function (err, db) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to get database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to get database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], [db]);
                    }
                });
            }, 10); // max 10 seconds before retrying
        },

        putDB: function (dbName, options) {
            checkArgumentObject(options, and("ok", "exists"));
            mu.retryWithThrottling(function (retry) {
                cdb.putJSON("/" + encodeURIComponent(dbName), {}, function (err, result) {
                    if (err) {
                        if (err.error === "file_exists") {
                            // there is a bug in CouchDB in that even if this error is returned, the database might not exist; lets check that
                            ops.getDB(dbName, {
                                not_found: function (err, ign) { retry(); },
                                ok: function (db) {
                                    callOneOf(options, ["exists"], [err, retry]);
                                }
                            });
                        } else {
                            logger.warn("(Will retry) Failed to create database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], []);
                    }
                }, "");
            }, 10); // max 10 seconds before retrying
        },

        deleteDB: function (dbName, options) {
            checkArgumentObject(options, and("ok", "not_found"));
            mu.retryWithThrottling(function (retry) {
                cdb.deleteJSON("/" + encodeURIComponent(dbName), {}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            callOneOf(options, ["not_found"], [err, retry]);
                        } else {
                            logger.warn("(Will retry) Failed to delete database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], []);
                    }
                }, "");
            }, 10); // max 10 seconds before retrying
        },

        getDoc: function (dbName, id, options) {
            checkArgumentObject(options, and("ok", or("not_found", "missing"), or("not_found", "deleted"), or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.getJSON("/" + encodeURIComponent(dbName) + "/" + encodeURIComponent(id), {}, function (err, doc) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "missing") {
                                callOneOf(options, ["missing", "not_found"], [err, retry]);
                            } else if (err.reason === "deleted") {
                                callOneOf(options, ["deleted", "not_found"], [err, retry]);
                            } else if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to get " + id + " from database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to get " + id + " from database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], [doc]);
                    }
                });
            }, 10); // max 10 seconds before retrying
        },

        putDoc: function (dbName, doc, options) {
            checkArgumentObject(options, and("ok", "conflict", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/" + encodeURIComponent(dbName), {}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            callOneOf(options, ["not_found", "no_db_file"], [err, retry]);
                        } else if (err.error === "conflict") {
                            callOneOf(options, ["conflict"], [err, retry]);
                        } else {
                            logger.warn("(Will retry) Failed to put " + (doc._id || "(unknown)")+ " in database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], [result]);
                    }
                }, JSON.stringify(doc));
            }, 10); // max 10 seconds before retrying
        },

        view: function (dbName, ddName, viewName, query, options) {
            checkArgumentObject(options, and("ok", or("not_found", "missing"), or("not_found", "deleted"), or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.getJSON("/" + encodeURIComponent(dbName) + "/_design/" + encodeURIComponent(ddName) + "/_view/" + encodeURIComponent(viewName), query, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "missing" || err.reason === "missing_named_view") {
                                callOneOf(options, ["missing", "not_found"], [err, retry]);
                            } else if (err.reason === "deleted") {
                                callOneOf(options, ["deleted", "not_found"], [err, retry]);
                            } else if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to get view " + ddName + "/" + viewName + " from database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to get " + ddName + "/" + viewName + " from database " + dbName + ". URL query and error follows");
                            logger.warn(util.inspect(query));
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], [result]);
                    }
                });
            }, 10); // max 10 seconds before retrying
        },

        getAllDocs: function (dbName, query, options) {
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.getJSON("/" + encodeURIComponent(dbName) + "/_all_docs" , query, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to get all docs from database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to get all docs from database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], [result]);
                    }
                });
            }, 10); // max 10 seconds before retrying
        },

        getDocList: function (dbName, ids, options) {
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/" + encodeURIComponent(dbName) + "/_all_docs" , {include_docs: true}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to get doc list " + JSON.stringify(ids) + " from database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to get doc list " + JSON.stringify(ids) + " from database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        var existing = [];
                        var deleted = [];
                        var missing = [];
                        result.rows.forEach(function (row) {
                            if (row.error) {
                                missing.push({_id: row.key});
                            } else if (row.value.deleted) {
                                deleted.push({_id: row.id});
                            } else {
                                existing.push(row.doc);
                            }
                        });
                        callOneOf(options, ["ok"], [existing, deleted, missing]);
                    }
                }, JSON.stringify({keys: ids}));
            }, 10); // max 10 seconds before retrying
        },

        putDocList: function (dbName, docs, options) {
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/" + encodeURIComponent(dbName) + "/_bulk_docs" , {include_docs: true}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to put doc list into database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to put doc list into database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        var saved = [];
                        var conflicted = [];
                        result.forEach(function (d) {
                            if (d.error === "conflict") {
                                conflicted.push(d);
                            } else {
                                saved.push(d);
                            }
                        });
                        callOneOf(options, ["ok"], [saved, conflicted, result]);
                    }
                }, JSON.stringify({docs: docs}));
            }, 10); // max 10 seconds before retrying
        },

        ddocInfo: function (dbName, ddocName, options) {
            var m = ddocName.match(/^_design\/(.*)$/);
            if (m) {
                ddocName = m[1];
            }
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.getJSON("/" + encodeURIComponent(dbName) + "/_design/" + ddocName + "/_info", {}, function (err, info) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to get info about " + ddocName + " in database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to get info about " + ddocName + " in database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], [info]);
                    }
                });
            }, 10); // max 10 seconds before retrying
        },

        viewCleanup: function (dbName, options) {
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/" + encodeURIComponent(dbName) + "/_view_cleanup", {}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to cleanup views in database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to cleanup views in database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], []);
                    }
                }, "");
            }, 10); // max 10 seconds before retrying
        },

        compactDB: function (dbName, options) {
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/" + encodeURIComponent(dbName) + "/_compact", {}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to compact database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to compact database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], []);
                    }
                }, "");
            }, 10); // max 10 seconds before retrying
        },

        compactViews: function (dbName, ddocName, options) {
            var m = ddocName.match(/^_design\/(.*)$/);
            if (m) {
                ddocName = m[1];
            }
            checkArgumentObject(options, and("ok", or("not_found", "no_db_file")));
            mu.retryWithThrottling(function (retry) {
                cdb.postJSON("/" + encodeURIComponent(dbName) + "/_compact/" + ddocName, {}, function (err, result) {
                    if (err) {
                        if (err.error === "not_found") {
                            if (err.reason === "no_db_file") {
                                callOneOf(options, ["no_db_file", "not_found"], [err, retry]);
                            } else {
                                logger.warn("(Will retry) Failed to compact views " + ddocName + " in database " + dbName + ". Error follows");
                                logger.warn(util.inspect(err));
                                retry();
                            }
                        } else {
                            logger.warn("(Will retry) Failed to compact view " + ddocName + " in database " + dbName + ". Error follows");
                            logger.warn(util.inspect(err));
                            retry();
                        }
                    } else {
                        callOneOf(options, ["ok"], []);
                    }
                }, "");
            }, 10); // max 10 seconds before retrying
        }
    };

    return ops;

    function callOneOf (dict, properties, args) {
        var name = _.find(properties, function (name) { return name in dict; });
        dict[name].apply(null, args);
    }

    function checkArgumentObject (options, proc) {
        if (!options) {
            throw new Error("Invalid options object");
        } else if (! "ok" in options) {
            throw new Error("Invalid options object");
        } else {
            var ret = proc(options);
            if (ret) {
                throw new Error(ret.join("; "));
            } else {
                // do nothing
            }
        }
    }

    function has (arg) {
        return (function (obj) {
            if ((arg in obj) && obj[arg]) {
                return false;
            } else {
                return [arg + " missing"];
            }
        });
    }

    function maybeMakeHas (arg, obj) {
        if (typeof arg === "string") {
            return has(arg)(obj);
        } else {
            return arg(obj);
        }
    }

    function and () {
        var args = Array.prototype.slice.call(arguments);
        return function (obj) {
            var v = args.map(function (arg) {
                return maybeMakeHas(arg, obj);
            }).filter(function (x) { return x; });
            if (v.length === 0) { // all succeeded
                return false;
            } else {
                return ["(" + v.join(" and ") + ")"];
            }
        };
    }

    function or () {
        var args = Array.prototype.slice.call(arguments);
        return function (obj) {
            var v = args.map(function (arg) {
                return maybeMakeHas(arg, obj);
            }).filter(function (x) { return x; });
            if (v.length === args.length) { // all failed
                return ["(" + v.join(" and ") + ")"];
            } else {
                return false;
            }
        };
    }

    function __stack () {
        var orig = Error.prepareStackTrace;
        Error.prepareStackTrace = function (ign, stack) { return stack; };
        var err = new Error;
        Error.captureStackTrace(err, arguments.callee);
        var stack = err.stack;
        Error.prepareStackTrace = orig;
        return stack;
    }

    function __line () {
        return __stack()[1].getLineNumber();
    }
};
