/* caldav_db.c -- implementation of per-user CalDAV database
 *
 * Copyright (c) 1994-2012 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include <config.h>

#include <sysexits.h>
#include <syslog.h>
#include <string.h>

#include <libical/ical.h>

#include "caldav_alarm.h"
#include "caldav_db.h"
#include "cyrusdb.h"
#include "httpd.h"
#include "http_dav.h"
#include "ical_support.h"
#include "libconfig.h"
#include "mboxname.h"
#include "util.h"
#include "xstrlcat.h"
#include "xmalloc.h"

/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"

struct caldav_db {
    sqldb_t *db;                        /* DB handle */
    char *sched_inbox;                  /* DB owner's scheduling Inbox */
    struct buf mailbox;                 /* buffers for copies of column text */
    struct buf resource;
    struct buf lock_token;
    struct buf lock_owner;
    struct buf lock_ownerid;
    struct buf ical_uid;
    struct buf organizer;
    struct buf dtstart;
    struct buf dtend;
    struct buf sched_tag;
    struct buf jmapdata;
};


static struct namespace caldav_namespace;
EXPORTED time_t caldav_epoch = -1;
EXPORTED time_t caldav_eternity = -1;

static int caldav_initialized = 0;

static void done_cb(void *rock __attribute__((unused))) {
    caldav_done();
}

static void init_internal() {
    if (!caldav_initialized) {
        caldav_init();
        cyrus_modules_add(done_cb, NULL);
    }
}

EXPORTED int caldav_init(void)
{
    int r;
    struct icaltimetype date;

    /* Set namespace -- force standard (internal) */
    if ((r = mboxname_init_namespace(&caldav_namespace, 1))) {
        syslog(LOG_ERR, "%s", error_message(r));
        fatal(error_message(r), EX_CONFIG);
    }

    /* Get min date-time */
    date = icaltime_from_string(config_getstring(IMAPOPT_CALDAV_MINDATETIME));
    if (!icaltime_is_null_time(date)) {
        caldav_epoch = icaltime_as_timet_with_zone(date, NULL);
    }
    if (caldav_epoch == -1) caldav_epoch = INT_MIN;

    /* Get max date-time */
    date = icaltime_from_string(config_getstring(IMAPOPT_CALDAV_MAXDATETIME));
    if (!icaltime_is_null_time(date)) {
        caldav_eternity = icaltime_as_timet_with_zone(date, NULL);
    }
    if (caldav_eternity == -1) caldav_eternity = INT_MAX;

    r = sqldb_init();
    caldav_alarm_init();

    if (!r) caldav_initialized = 1;
    return r;
}


EXPORTED int caldav_done(void)
{
    int r;
    caldav_alarm_done();
    r = sqldb_done();
    if (!r) caldav_initialized = 0;
    return r;
}

EXPORTED struct caldav_db *caldav_open_userid(const char *userid)
{
    struct caldav_db *caldavdb = NULL;

    init_internal();

    sqldb_t *db = dav_open_userid(userid);
    if (!db) return NULL;

    caldavdb = xzmalloc(sizeof(struct caldav_db));
    caldavdb->db = db;

    /* Construct mbox name corresponding to userid's scheduling Inbox */
    caldavdb->sched_inbox = caldav_mboxname(userid, SCHED_INBOX);

    return caldavdb;
}

/* Open DAV DB corresponding to userid */
EXPORTED struct caldav_db *caldav_open_mailbox(struct mailbox *mailbox)
{
    struct caldav_db *caldavdb = NULL;
    char *userid = mboxname_to_userid(mailbox->name);

    init_internal();

    if (userid) {
        caldavdb = caldav_open_userid(userid);
        free(userid);
        return caldavdb;
    }

    sqldb_t *db = dav_open_mailbox(mailbox);
    if (!db) return NULL;

    caldavdb = xzmalloc(sizeof(struct caldav_db));
    caldavdb->db = db;

    return caldavdb;
}

/* Close DAV DB */
EXPORTED int caldav_close(struct caldav_db *caldavdb)
{
    int r = 0;

    if (!caldavdb) return 0;

    free(caldavdb->sched_inbox);
    buf_free(&caldavdb->mailbox);
    buf_free(&caldavdb->resource);
    buf_free(&caldavdb->lock_token);
    buf_free(&caldavdb->lock_owner);
    buf_free(&caldavdb->lock_ownerid);
    buf_free(&caldavdb->ical_uid);
    buf_free(&caldavdb->organizer);
    buf_free(&caldavdb->dtstart);
    buf_free(&caldavdb->dtend);
    buf_free(&caldavdb->sched_tag);
    buf_free(&caldavdb->jmapdata);

    r = sqldb_close(&caldavdb->db);

    free(caldavdb);

    return r;
}

EXPORTED int caldav_begin(struct caldav_db *caldavdb)
{
    return sqldb_begin(caldavdb->db, "caldav");
}

EXPORTED int caldav_commit(struct caldav_db *caldavdb)
{
    return sqldb_commit(caldavdb->db, "caldav");
}

EXPORTED int caldav_abort(struct caldav_db *caldavdb)
{
    return sqldb_rollback(caldavdb->db, "caldav");
}

#define RROCK_FLAG_TOMBSTONES (1<<0)
struct read_rock {
    struct caldav_db *db;
    struct caldav_data *cdata;
    int flags;
    caldav_cb_t *cb;
    void *rock;
};

static const char *column_text_to_buf(const char *text, struct buf *buf)
{
    if (text) {
        buf_setcstr(buf, text);
        text = buf_cstring(buf);
    }

    return text;
}

static void _num_to_comp_flags(struct comp_flags *flags, unsigned num)
{
    flags->recurring =  num & 1;
    flags->transp    = (num >> 1) & 1;
    flags->status    = (num >> 2) & 3;
    flags->tzbyref   = (num >> 4) & 1;
    flags->mattach   = (num >> 5) & 1;
    flags->shared    = (num >> 6) & 1;
}

static unsigned _comp_flags_to_num(struct comp_flags *flags)
{
   return (flags->recurring & 1)
       + ((flags->transp    & 1) << 1)
       + ((flags->status    & 3) << 2)
       + ((flags->tzbyref   & 1) << 4)
       + ((flags->mattach   & 1) << 5)
       + ((flags->shared    & 1) << 6);
}

#define CMD_READFIELDS                                                  \
    "SELECT rowid, creationdate, mailbox, resource, imap_uid,"          \
    "  lock_token, lock_owner, lock_ownerid, lock_expire,"              \
    "  comp_type, ical_uid, organizer, dtstart, dtend,"                 \
    "  comp_flags, sched_tag, alive, modseq, createdmodseq,"            \
    "  NULL, NULL"                                                      \
    " FROM ical_objs"                                                   \

#define CMD_READFIELDS_JMAP                                             \
    "SELECT ical_objs.rowid, creationdate, mailbox, resource, imap_uid,"\
    "  lock_token, lock_owner, lock_ownerid, lock_expire,"              \
    "  comp_type, ical_uid, organizer, dtstart, dtend,"                 \
    "  comp_flags, sched_tag, alive, modseq, createdmodseq,"            \
    "  jmapversion, jmapdata"                                           \
    " FROM ical_objs LEFT JOIN ical_jmapcache"                          \
    " ON (ical_objs.rowid = ical_jmapcache.rowid AND ical_jmapcache.userid = :asuserid)"

static int read_cb(sqlite3_stmt *stmt, void *rock)
{
    struct read_rock *rrock = (struct read_rock *) rock;
    struct caldav_db *db = rrock->db;
    struct caldav_data *cdata = rrock->cdata;
    int r = 0;

    memset(cdata, 0, sizeof(struct caldav_data));

    cdata->dav.alive = sqlite3_column_int(stmt, 16);
    cdata->dav.modseq = sqlite3_column_int64(stmt, 17);
    cdata->dav.createdmodseq = sqlite3_column_int64(stmt, 18);
    if (!(rrock->flags & RROCK_FLAG_TOMBSTONES) && !cdata->dav.alive)
        return 0;

    cdata->dav.rowid = sqlite3_column_int(stmt, 0);
    cdata->dav.creationdate = sqlite3_column_int(stmt, 1);
    cdata->dav.imap_uid = sqlite3_column_int(stmt, 4);
    cdata->dav.lock_expire = sqlite3_column_int(stmt, 8);
    cdata->comp_type = sqlite3_column_int(stmt, 9);
    _num_to_comp_flags(&cdata->comp_flags, sqlite3_column_int(stmt, 14));
    cdata->jmapversion = sqlite3_column_int(stmt, 19);

    if (rrock->cb) {
        /* We can use the column data directly for the callback */
        cdata->dav.mailbox = (const char *) sqlite3_column_text(stmt, 2);
        cdata->dav.resource = (const char *) sqlite3_column_text(stmt, 3);
        cdata->dav.lock_token = (const char *) sqlite3_column_text(stmt, 5);
        cdata->dav.lock_owner = (const char *) sqlite3_column_text(stmt, 6);
        cdata->dav.lock_ownerid = (const char *) sqlite3_column_text(stmt, 7);
        cdata->ical_uid = (const char *) sqlite3_column_text(stmt, 10);
        cdata->organizer = (const char *) sqlite3_column_text(stmt, 11);
        cdata->dtstart = (const char *) sqlite3_column_text(stmt, 12);
        cdata->dtend = (const char *) sqlite3_column_text(stmt, 13);
        cdata->sched_tag = (const char *) sqlite3_column_text(stmt, 15);
        cdata->jmapdata = (const char *) sqlite3_column_text(stmt, 20);
        r = rrock->cb(rrock->rock, cdata);
    }
    else {
        /* For single row SELECTs like caldav_read(),
         * we need to make a copy of the column data before
         * it gets flushed by sqlite3_step() or sqlite3_reset() */
        cdata->dav.mailbox =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 2),
                               &db->mailbox);
        cdata->dav.resource =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 3),
                               &db->resource);
        cdata->dav.lock_token =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 5),
                               &db->lock_token);
        cdata->dav.lock_owner =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 6),
                               &db->lock_owner);
        cdata->dav.lock_ownerid =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 7),
                               &db->lock_ownerid);
        cdata->ical_uid =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 10),
                               &db->ical_uid);
        cdata->organizer =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 11),
                               &db->organizer);
        cdata->dtstart =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 12),
                               &db->dtstart);
        cdata->dtend =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 13),
                               &db->dtend);
        cdata->sched_tag =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 15),
                               &db->sched_tag);
        cdata->jmapdata =
            column_text_to_buf((const char *) sqlite3_column_text(stmt, 20),
                               &db->jmapdata);
    }

    return r;
}


#define CMD_SELRSRC CMD_READFIELDS \
    " WHERE mailbox = :mailbox AND resource = :resource;"

EXPORTED int caldav_lookup_resource(struct caldav_db *caldavdb,
                           const char *mailbox, const char *resource,
                           struct caldav_data **result,
                           int tombstones)
{
    struct sqldb_bindval bval[] = {
        { ":mailbox",  SQLITE_TEXT, { .s = mailbox       } },
        { ":resource", SQLITE_TEXT, { .s = resource      } },
        { NULL,        SQLITE_NULL, { .s = NULL          } } };
    static struct caldav_data cdata;
    struct read_rock rrock = { caldavdb, &cdata, tombstones, NULL, NULL };
    int r;

    *result = memset(&cdata, 0, sizeof(struct caldav_data));

    r = sqldb_exec(caldavdb->db, CMD_SELRSRC, bval, &read_cb, &rrock);
    if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND;

    /* always add the mailbox and resource, so error responses don't
     * crash out */
    cdata.dav.mailbox = mailbox;
    cdata.dav.resource = resource;

    return r;
}

#define CMD_SELIMAPUID CMD_READFIELDS \
    " WHERE mailbox = :mailbox AND imap_uid = :imap_uid;"

EXPORTED int caldav_lookup_imapuid(struct caldav_db *caldavdb,
                           const char *mailbox, int imap_uid,
                           struct caldav_data **result,
                           int tombstones)
{
    struct sqldb_bindval bval[] = {
        { ":mailbox",  SQLITE_TEXT,    { .s = mailbox       } },
        { ":imap_uid", SQLITE_INTEGER, { .i = imap_uid      } },
        { NULL,        SQLITE_NULL,    { .s = NULL          } } };
    static struct caldav_data cdata;
    struct read_rock rrock = { caldavdb, &cdata, tombstones, NULL, NULL };
    int r;

    *result = memset(&cdata, 0, sizeof(struct caldav_data));

    r = sqldb_exec(caldavdb->db, CMD_SELIMAPUID, bval, &read_cb, &rrock);
    if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND;

    cdata.dav.mailbox = mailbox;
    cdata.dav.imap_uid = imap_uid;

    return r;
}


#define CMD_SELUID CMD_READFIELDS \
    " WHERE ical_uid = :ical_uid AND mailbox != :inbox AND alive = 1;"

EXPORTED int caldav_lookup_uid(struct caldav_db *caldavdb, const char *ical_uid,
                               struct caldav_data **result)
{
    struct sqldb_bindval bval[] = {
        { ":ical_uid", SQLITE_TEXT, { .s = ical_uid              } },
        { ":inbox",    SQLITE_TEXT, { .s = caldavdb->sched_inbox } },
        { NULL,        SQLITE_NULL, { .s = NULL                  } } };
    static struct caldav_data cdata;
    struct read_rock rrock = { caldavdb, &cdata, 0, NULL, NULL };
    int r;

    /* XXX - ability to pass through the tombstones flag */

    *result = memset(&cdata, 0, sizeof(struct caldav_data));

    r = sqldb_exec(caldavdb->db, CMD_SELUID, bval, &read_cb, &rrock);
    if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND;

    return r;
}


#define CMD_SELMBOX CMD_READFIELDS \
    " WHERE mailbox = :mailbox AND alive = 1;"

#define CMD_SELALIVE CMD_READFIELDS \
    " WHERE alive = 1;"

EXPORTED int caldav_foreach(struct caldav_db *caldavdb, const char *mailbox,
                            caldav_cb_t *cb, void *rock)
{
    struct sqldb_bindval bval[] = {
        { ":mailbox", SQLITE_TEXT, { .s = mailbox } },
        { NULL,       SQLITE_NULL, { .s = NULL    } } };
    struct caldav_data cdata;
    struct read_rock rrock = { caldavdb, &cdata, 0, cb, rock };

    /* XXX - tombstones */

    if (mailbox) {
        return sqldb_exec(caldavdb->db, CMD_SELMBOX, bval, &read_cb, &rrock);
    } else {
        return sqldb_exec(caldavdb->db, CMD_SELALIVE, bval, &read_cb, &rrock);
    }
}

#define CMD_SELRANGE_MBOX CMD_READFIELDS \
    " WHERE dtend > :after AND dtstart < :before " \
    " AND mailbox = :mailbox AND alive = 1 "

#define CMD_SELRANGE CMD_READFIELDS \
    " WHERE dtend > :after AND dtstart < :before " \
    " AND alive = 1 "

EXPORTED int caldav_foreach_timerange(struct caldav_db *caldavdb,
                                      const char *mailbox,
                                      time_t after, time_t before,
                                      enum caldav_sort* sort, size_t nsort,
                                      caldav_cb_t *cb, void *rock)
{
    struct sqldb_bindval bval[] = {
        { ":after",     SQLITE_TEXT, { .s = NULL    } },
        { ":before",   SQLITE_TEXT, { .s = NULL    } },
        { ":mailbox", SQLITE_TEXT, { .s = mailbox } },
        { NULL,       SQLITE_NULL, { .s = NULL    } } };
    struct caldav_data cdata;
    struct read_rock rrock = { caldavdb, &cdata, 0, cb, rock };
    icaltimetype dtafter, dtbefore;
    icaltimezone *utc = icaltimezone_get_utc_timezone();

    dtafter = icaltime_from_timet_with_zone(after, 0, utc);
    dtbefore= icaltime_from_timet_with_zone(before, 0, utc);

    bval[0].val.s = icaltime_as_ical_string(dtafter);
    bval[1].val.s = icaltime_as_ical_string(dtbefore);

    /* XXX - if 'before' defines the zero second of a day, a full-day
     * event starting on that day matches. That's not entirely correct,
     * since 'before' is defined to be exclusive. */

    /* XXX - tombstones */

    struct buf stmt = BUF_INITIALIZER;
    buf_setcstr(&stmt, mailbox ? CMD_SELRANGE_MBOX : CMD_SELRANGE);
    if (nsort) {
        buf_appendcstr(&stmt, " ORDER BY ");
        size_t i;
        for (i = 0; i < nsort; i++) {
            if (i) buf_appendcstr(&stmt, ", ");
            switch (sort[i] & ~CAL_SORT_DESC) {
                case CAL_SORT_UID:
                    buf_appendcstr(&stmt, "ical_uid");
                    break;
                case CAL_SORT_START:
                    buf_appendcstr(&stmt, "dtstart");
                    break;
                case CAL_SORT_MAILBOX:
                    buf_appendcstr(&stmt, "mailbox");
                    break;
                default:
                    continue;
            }
            buf_appendcstr(&stmt, sort[i] & CAL_SORT_DESC ? " DESC" : " ASC");
        }
    }
    buf_putc(&stmt, ';');

    int r = sqldb_exec(caldavdb->db, buf_cstring(&stmt), bval, &read_cb, &rrock);
    buf_free(&stmt);
    return r;
}


#define CMD_INSERT                                                      \
    "INSERT INTO ical_objs ("                                           \
    "  alive, mailbox, resource, creationdate, imap_uid, modseq,"       \
    "  createdmodseq,"                                                  \
    "  lock_token, lock_owner, lock_ownerid, lock_expire,"              \
    "  comp_type, ical_uid, organizer, dtstart, dtend,"                 \
    "  comp_flags, sched_tag )"                                         \
    " VALUES ("                                                         \
    "  :alive, :mailbox, :resource, :creationdate, :imap_uid, :modseq," \
    "  :createdmodseq,"                                                 \
    "  :lock_token, :lock_owner, :lock_ownerid, :lock_expire,"          \
    "  :comp_type, :ical_uid, :organizer, :dtstart, :dtend,"            \
    "  :comp_flags, :sched_tag );"

#define CMD_UPDATE                      \
    "UPDATE ical_objs SET"              \
    "  alive        = :alive,"          \
    "  creationdate = :creationdate,"   \
    "  imap_uid     = :imap_uid,"       \
    "  modseq       = :modseq,"         \
    "  createdmodseq = :createdmodseq,"  \
    "  lock_token   = :lock_token,"     \
    "  lock_owner   = :lock_owner,"     \
    "  lock_ownerid = :lock_ownerid,"   \
    "  lock_expire  = :lock_expire,"    \
    "  comp_type    = :comp_type,"      \
    "  ical_uid     = :ical_uid,"       \
    "  organizer    = :organizer,"      \
    "  dtstart      = :dtstart,"        \
    "  dtend        = :dtend,"          \
    "  comp_flags   = :comp_flags,"     \
    "  sched_tag    = :sched_tag"       \
    " WHERE rowid = :rowid;"

#define CMD_DELETE_JMAPCACHE "DELETE FROM ical_jmapcache WHERE rowid = :rowid"

EXPORTED int caldav_write(struct caldav_db *caldavdb, struct caldav_data *cdata)
{
    unsigned comp_flags = _comp_flags_to_num(&cdata->comp_flags);
    struct sqldb_bindval bval[] = {
        { ":rowid",        SQLITE_INTEGER, { .i = cdata->dav.rowid        } },
        { ":alive",        SQLITE_INTEGER, { .i = cdata->dav.alive        } },
        { ":mailbox",      SQLITE_TEXT,    { .s = cdata->dav.mailbox      } },
        { ":resource",     SQLITE_TEXT,    { .s = cdata->dav.resource     } },
        { ":creationdate", SQLITE_INTEGER, { .i = cdata->dav.creationdate } },
        { ":imap_uid",     SQLITE_INTEGER, { .i = cdata->dav.imap_uid     } },
        { ":modseq",       SQLITE_INTEGER, { .i = cdata->dav.modseq       } },
        { ":createdmodseq", SQLITE_INTEGER, { .i = cdata->dav.createdmodseq } },
        { ":lock_token",   SQLITE_TEXT,    { .s = cdata->dav.lock_token   } },
        { ":lock_owner",   SQLITE_TEXT,    { .s = cdata->dav.lock_owner   } },
        { ":lock_ownerid", SQLITE_TEXT,    { .s = cdata->dav.lock_ownerid } },
        { ":lock_expire",  SQLITE_INTEGER, { .i = cdata->dav.lock_expire  } },
        { ":comp_type",    SQLITE_INTEGER, { .i = cdata->comp_type        } },
        { ":ical_uid",     SQLITE_TEXT,    { .s = cdata->ical_uid         } },
        { ":organizer",    SQLITE_TEXT,    { .s = cdata->organizer        } },
        { ":dtstart",      SQLITE_TEXT,    { .s = cdata->dtstart          } },
        { ":dtend",        SQLITE_TEXT,    { .s = cdata->dtend            } },
        { ":sched_tag",    SQLITE_TEXT,    { .s = cdata->sched_tag        } },
        { ":comp_flags",   SQLITE_INTEGER, { .i = comp_flags              } },
        { NULL,            SQLITE_NULL,    { .s = NULL                    } } };

    if (cdata->dav.rowid) {
        int r = sqldb_exec(caldavdb->db, CMD_DELETE_JMAPCACHE, bval, NULL, NULL);
        if (r) return r;
        r = sqldb_exec(caldavdb->db, CMD_UPDATE, bval, NULL, NULL);
        if (r) return r;
    }
    else {
        int r = sqldb_exec(caldavdb->db, CMD_INSERT, bval, NULL, NULL);
        if (r) return r;
        cdata->dav.rowid = sqldb_lastid(caldavdb->db);
    }

    return 0;
}


#define CMD_DELETE "DELETE FROM ical_objs WHERE rowid = :rowid;"

EXPORTED int caldav_delete(struct caldav_db *caldavdb, unsigned rowid)
{
    struct sqldb_bindval bval[] = {
        { ":rowid", SQLITE_INTEGER, { .i = rowid } },
        { NULL,     SQLITE_NULL,    { .s = NULL  } } };
    int r;

    r = sqldb_exec(caldavdb->db, CMD_DELETE, bval, NULL, NULL);

    return r;
}


#define CMD_DELMBOX "DELETE FROM ical_objs WHERE mailbox = :mailbox;"

EXPORTED int caldav_delmbox(struct caldav_db *caldavdb, const char *mailbox)
{
    struct sqldb_bindval bval[] = {
        { ":mailbox", SQLITE_TEXT, { .s = mailbox } },
        { NULL,       SQLITE_NULL, { .s = NULL    } } };
    int r;

    r = sqldb_exec(caldavdb->db, CMD_DELMBOX, bval, NULL, NULL);

    return r;
}

EXPORTED int caldav_get_updates(struct caldav_db *caldavdb,
                                modseq_t oldmodseq, const char *mboxname,
                                int kind, int limit,
                                int (*cb)(void *rock, struct caldav_data *cdata),
                                void *rock)
{
    struct sqldb_bindval bval[] = {
        { ":mailbox",      SQLITE_TEXT,    { .s = mboxname  } },
        { ":modseq",       SQLITE_INTEGER, { .i = oldmodseq } },
        { ":comp_type",    SQLITE_INTEGER, { .i = kind      } },
        /* SQLite interprets a negative limit as unbounded. */
        { ":limit",        SQLITE_INTEGER, { .i = limit > 0 ? limit : -1 } },
        { NULL,            SQLITE_NULL,    { .s = NULL      } }
    };
    static struct caldav_data cdata;
    struct read_rock rrock =
        { caldavdb, &cdata, RROCK_FLAG_TOMBSTONES, cb, rock };
    struct buf sqlbuf = BUF_INITIALIZER;
    int r;

    buf_setcstr(&sqlbuf, CMD_READFIELDS " WHERE");
    if (mboxname) buf_appendcstr(&sqlbuf, " mailbox = :mailbox AND");
    if (kind >= 0) {
        /* Use a negative value to signal that we accept ALL components types */
        buf_appendcstr(&sqlbuf, " comp_type = :comp_type AND");
    }
    if (!oldmodseq) buf_appendcstr(&sqlbuf, " alive = 1 AND");
    buf_appendcstr(&sqlbuf, " modseq > :modseq ORDER BY modseq LIMIT :limit;");

    r = sqldb_exec(caldavdb->db, buf_cstring(&sqlbuf), bval, &read_cb, &rrock);
    buf_free(&sqlbuf);

    if (r) {
        syslog(LOG_ERR, "caldav error %s", error_message(r));
    }
    return r;
}


static void check_mattach_cb(icalcomponent *comp, void *rock)
{
    int *mattach = (int *) rock;

    /* Check for managed attachment */
    if (!*mattach) {
        icalproperty *prop;

        for (prop = icalcomponent_get_first_property(comp, ICAL_ATTACH_PROPERTY);
             prop;
             prop = icalcomponent_get_next_property(comp, ICAL_ATTACH_PROPERTY)) {
            
            if (icalproperty_get_managedid_parameter(prop)) *mattach = 1;
        }
    }
}

EXPORTED int caldav_writeentry(struct caldav_db *caldavdb, struct caldav_data *cdata,
                               icalcomponent *ical)
{
    icalcomponent *comp = icalcomponent_get_first_real_component(ical);
    icalcomponent_kind kind;
    icalproperty *prop;
    unsigned mykind = 0, recurring = 0, transp = 0, status = 0, mattach = 0;
    struct icalperiodtype span;

    /* Get iCalendar UID */
    cdata->ical_uid = icalcomponent_get_uid(comp);

    /* Get component type and optional status */
    kind = icalcomponent_isa(comp);
    switch (kind) {
    case ICAL_VEVENT_COMPONENT:
        mykind = CAL_COMP_VEVENT;
        switch (icalcomponent_get_status(comp)) {
        case ICAL_STATUS_CANCELLED: status = CAL_STATUS_CANCELED; break;
        case ICAL_STATUS_TENTATIVE: status = CAL_STATUS_TENTATIVE; break;
        default: status = CAL_STATUS_BUSY; break;
        }
        break;
    case ICAL_VTODO_COMPONENT: mykind = CAL_COMP_VTODO; break;
    case ICAL_VJOURNAL_COMPONENT: mykind = CAL_COMP_VJOURNAL; break;
    case ICAL_VFREEBUSY_COMPONENT: mykind = CAL_COMP_VFREEBUSY; break;
    case ICAL_VAVAILABILITY_COMPONENT: mykind = CAL_COMP_VAVAILABILITY; break;
    case ICAL_VPOLL_COMPONENT: mykind = CAL_COMP_VPOLL; break;
    default: break;
    }
    cdata->comp_type = mykind;
    cdata->comp_flags.status = status;

    cdata->organizer = NULL;

    /* Get organizer */
    prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
    if (prop) {
        cdata->organizer = icalproperty_get_organizer(prop);
        if (cdata->organizer && !strncasecmp(cdata->organizer, "mailto:", 7))
            cdata->organizer += 7;
    }
    /* maybe it's only on a sub event */
    icalcomponent *nextcomp;
    while (!cdata->organizer &&
           (nextcomp = icalcomponent_get_next_component(ical, kind))) {
        prop = icalcomponent_get_first_property(nextcomp, ICAL_ORGANIZER_PROPERTY);
        if (prop) {
            cdata->organizer = icalproperty_get_organizer(prop);
            if (cdata->organizer && !strncasecmp(cdata->organizer, "mailto:", 7))
                cdata->organizer += 7;
        }
    }

    /* Get transparency */
    prop = icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
    if (prop) {
        icalvalue *transp_val = icalproperty_get_value(prop);

        switch (icalvalue_get_transp(transp_val)) {
        case ICAL_TRANSP_TRANSPARENT:
        case ICAL_TRANSP_TRANSPARENTNOCONFLICT:
            transp = 1;
            break;

        default:
            transp = 0;
            break;
        }
    }
    cdata->comp_flags.transp = transp;

    /* Get span of component set and check for managed attachments */
    span = icalrecurrenceset_get_utc_timespan(ical, kind, NULL, &recurring,
                                              &check_mattach_cb, &mattach);

    cdata->dtstart = icaltime_as_ical_string(span.start);
    cdata->dtend = icaltime_as_ical_string(span.end);
    cdata->comp_flags.recurring = recurring;
    cdata->comp_flags.mattach = mattach;
    
    return caldav_write(caldavdb, cdata);
}


EXPORTED char *caldav_mboxname(const char *userid, const char *name)
{
    struct buf boxbuf = BUF_INITIALIZER;
    char *res = NULL;

    buf_setcstr(&boxbuf, config_getstring(IMAPOPT_CALENDARPREFIX));

    if (name) {
        size_t len = strcspn(name, "/");
        buf_putc(&boxbuf, '.');
        buf_appendmap(&boxbuf, name, len);
    }

    res = mboxname_user_mbox(userid, buf_cstring(&boxbuf));

    buf_free(&boxbuf);

    return res;
}

EXPORTED int caldav_get_events(struct caldav_db *caldavdb, const char *asuserid,
                               const char *mailbox, const char *ical_uid,
                               caldav_cb_t *cb, void *rock)
{
    struct sqldb_bindval bval[] = {
        { ":mailbox",  SQLITE_TEXT, { .s = mailbox } },
        { ":ical_uid", SQLITE_TEXT, { .s = ical_uid } },
        { ":asuserid", SQLITE_TEXT, { .s = asuserid } },
        { NULL,        SQLITE_NULL, { .s = NULL    } } };
    struct caldav_data cdata;
    struct read_rock rrock = { caldavdb, &cdata, 0, cb, rock };
    struct buf sqlbuf = BUF_INITIALIZER;

    buf_setcstr(&sqlbuf, CMD_READFIELDS_JMAP);
    buf_appendcstr(&sqlbuf, " WHERE alive = 1");
    if (mailbox)
        buf_appendcstr(&sqlbuf, " AND mailbox = :mailbox");
    if (ical_uid)
        buf_appendcstr(&sqlbuf, " AND ical_uid = :ical_uid");
    buf_appendcstr(&sqlbuf, " ORDER BY mailbox, imap_uid;");

    /* XXX - tombstones */

    int r = sqldb_exec(caldavdb->db, buf_cstring(&sqlbuf), bval, &read_cb, &rrock);
    buf_free(&sqlbuf);

    if (r) {
        syslog(LOG_ERR, "caldav error %s", error_message(r));
        /* XXX - free memory */
    }

    return r;
}

#define CMD_DELETE_JMAPCACHE_USER "DELETE FROM ical_jmapcache WHERE rowid = :rowid AND userid = :userid"
#define CMD_INSERT_JMAPCACHE_USER                                           \
    "INSERT INTO ical_jmapcache ( rowid, userid, jmapversion, jmapdata )"   \
    " VALUES ( :rowid, :userid, :jmapversion, :jmapdata );"

EXPORTED int caldav_write_jmapcache(struct caldav_db *caldavdb, int rowid, const char *userid, int version, const char *data)
{
    struct sqldb_bindval bval[] = {
        { ":rowid",        SQLITE_INTEGER, { .i = rowid  } },
        { ":userid",       SQLITE_TEXT,    { .s = userid } },
        { ":jmapversion",  SQLITE_INTEGER, { .i = version } },
        { ":jmapdata",     SQLITE_TEXT,    { .s = data   } },
        { NULL,            SQLITE_NULL,    { .s = NULL   } } };
    int r;

    /* clean up existing records if any */
    r = sqldb_exec(caldavdb->db, CMD_DELETE_JMAPCACHE_USER, bval, NULL, NULL);
    if (r) return r;

    /* insert the cache record */
    return sqldb_exec(caldavdb->db, CMD_INSERT_JMAPCACHE_USER, bval, NULL, NULL);
}

struct shareacls_rock {
    const char *userid;
    char *principalname;
    char *principalacl;
    char *newprincipalacl;
    char *outboxname;
    char *outboxacl;
    char *newoutboxacl;
    hash_table user_access;
};

#define CALSHARE_WANTSCHED 1
#define CALSHARE_HAVESCHED 2
#define CALSHARE_WANTPRIN 4
#define CALSHARE_HAVEPRIN 8

static int _add_shareacls(const mbentry_t *mbentry, void *rock)
{
    struct shareacls_rock *share = rock;

    char *acl = xstrdup(mbentry->acl);

    int isprincipal = !strcmp(mbentry->name, share->principalname);
    int isoutbox = !strcmp(mbentry->name, share->outboxname);

    if (isprincipal) {
        share->principalacl = xstrdup(acl);
        share->newprincipalacl = xstrdup(acl);
    }

    if (isoutbox) {
        share->outboxacl = xstrdup(acl);
        share->newoutboxacl = xstrdup(acl);
    }

    char *userid;
    char *nextid = NULL;
    for (userid = acl; userid; userid = nextid) {
        char *rightstr;
        int access;

        rightstr = strchr(userid, '\t');
        if (!rightstr) break;
        *rightstr++ = '\0';

        nextid = strchr(rightstr, '\t');
        if (!nextid) break;
        *nextid++ = '\0';

        /* skip system users and owner */
        if (is_system_user(userid)) continue;
        if (!strcmp(userid, share->userid)) continue;

        cyrus_acl_strtomask(rightstr, &access);

        uintptr_t have = (uintptr_t)hash_lookup(userid, &share->user_access);
        uintptr_t set = have;

        // if it's the principal, we have each user with principal read access
        if (isprincipal) {
            if ((access & DACL_READ) == DACL_READ)
                set |= CALSHARE_HAVEPRIN;
        }
        // if it's the Outbox, we have each user with reply ability
        else if (isoutbox) {
            if ((access & (DACL_INVITE|DACL_REPLY)) == (DACL_INVITE|DACL_REPLY))
                set |= CALSHARE_HAVESCHED;
        }
        // and if they can see anything else, then we NEED the above!
        else {
            if (access & ACL_READ)
                set |= CALSHARE_WANTPRIN;
            if (access & ACL_INSERT)
                set |= CALSHARE_WANTSCHED;
        }

        if (set != have) hash_insert(userid, (void *)set, &share->user_access);
    }

    free(acl);
    return 0;
}

static void _update_acls(const char *userid, void *data, void *rock)
{
    struct shareacls_rock *share = rock;
    uintptr_t aclstatus = (uintptr_t)data;

    if ((aclstatus & CALSHARE_WANTSCHED) && !(aclstatus & CALSHARE_HAVESCHED)) {
        cyrus_acl_set(&share->newoutboxacl, userid, ACL_MODE_ADD, (DACL_INVITE|DACL_REPLY), NULL, NULL);
    }

    if (!(aclstatus & CALSHARE_WANTSCHED) && (aclstatus & CALSHARE_HAVESCHED)) {
        cyrus_acl_set(&share->newoutboxacl, userid, ACL_MODE_REMOVE, (DACL_INVITE|DACL_REPLY), NULL, NULL);
    }

    if ((aclstatus & CALSHARE_WANTPRIN) && !(aclstatus & CALSHARE_HAVEPRIN)) {
        cyrus_acl_set(&share->newprincipalacl, userid, ACL_MODE_ADD, DACL_READ, NULL, NULL);
    }

    if (!(aclstatus & CALSHARE_WANTPRIN) && (aclstatus & CALSHARE_HAVEPRIN)) {
        cyrus_acl_set(&share->newprincipalacl, userid, ACL_MODE_REMOVE, DACL_READ, NULL, NULL);
    }
}

/* update the share acls.  We do this by:
 * 1) iterating all the calendars for this user, looking at all the ACLs and
 *    tracking for each user mentioned, whether they have or need principal
 *    access or scheduling access.
 * 2) when we see the inbox and outbox, clone the ACLs.
 * 3) iterate all seen users, and decide whether we need to change the ACLs
 *    for either of those mailboxes.
 */
EXPORTED int caldav_update_shareacls(const char *userid)
{
    struct shareacls_rock rock = {
        userid,
        NULL, NULL, NULL,
        NULL, NULL, NULL,
        HASH_TABLE_INITIALIZER
    };
    construct_hash_table(&rock.user_access, 10, 0);
    rock.principalname = caldav_mboxname(userid, NULL);
    rock.outboxname = caldav_mboxname(userid, SCHED_OUTBOX);

    // find out what the values should be
    int r = mboxlist_mboxtree(rock.principalname, _add_shareacls, &rock, 0);

    // did we find the ACLs?  If not, bail now!
    if (!rock.principalacl || !rock.outboxacl) {
        r = IMAP_MAILBOX_NONEXISTENT;
        goto done;
    }

    // change the ACLs as required
    hash_enumerate(&rock.user_access, _update_acls, &rock);

    if (strcmp(rock.principalacl, rock.newprincipalacl)) {
        r = mboxlist_updateacl_raw(rock.principalname, rock.newprincipalacl);
        if (r) goto done;
    }

    if (strcmp(rock.outboxacl, rock.newoutboxacl)) {
        r = mboxlist_updateacl_raw(rock.outboxname, rock.newoutboxacl);
        if (r) goto done;
    }

done:
    free(rock.principalname);
    free(rock.principalacl);
    free(rock.newprincipalacl);
    free(rock.outboxname);
    free(rock.outboxacl);
    free(rock.newoutboxacl);
    free_hash_table(&rock.user_access, NULL);

    return r;
}


EXPORTED const char *caldav_comp_type_as_string(unsigned comp_type)
{
    switch (comp_type) {
        /* "Real" components */
        case CAL_COMP_VEVENT:
            return "VEVENT";
        case CAL_COMP_VTODO:
            return "VTODO";
        case CAL_COMP_VJOURNAL:
            return "VJOURNAL";
        case CAL_COMP_VFREEBUSY:
            return "VFREEBUSY";
        case CAL_COMP_VAVAILABILITY:
            return "VAVAILABILITY";
        case CAL_COMP_VPOLL:
            return "VPOLL";
        /* Other components */
        case CAL_COMP_VALARM:
            return "VALARM";
        case CAL_COMP_VTIMEZONE:
            return "VTIMEZONE";
        case CAL_COMP_VCALENDAR:
            return "VCALENDAR";
        default:
            return NULL;
    }
}

