#include "Room/internal.h"

#include <Schema/PduV1.h>

#include <Parser.h>

static char *
RoomHashEventV1(PduV1 pdu)
{
    HashMap *json = PduV1ToJson(&pdu);
    char *b64;
    
    b64 = EventContentHash(json);

    JsonFree(json);
    return b64;
}
static bool
EventFits(HashMap *pdu)
{
    int size = CanonicalJsonEncode(pdu, NULL);
    JsonValue *key;
    
    /* Main PDU length is 65536 bytes */ 
    if (size > 65536)
    {
        return false;
    }
#define VerifyKey(k,s)  do \
                        { \
                            if ((key = JsonGet(pdu, 1, k)) && \
                                (strlen(JsonValueAsString(key)) > s)) \
                            { \
                                return false; \
                            } \
                        } \
                        while (0)
    VerifyKey("sender", 255);
    VerifyKey("room_id", 255);
    VerifyKey("state_key", 255);
    VerifyKey("type", 255);
    VerifyKey("event_id", 255);

    return true;
#undef VerifyKey
}
static bool
EventFitsV1(PduV1 pdu)
{
    HashMap *hm;
    bool ret;

    hm = PduV1ToJson(&pdu);
    ret = EventFits(hm);

    JsonFree(hm);
    return ret;
}
static PduV1Status
RoomGetEventStatusV1(Room *room, PduV1 *pdu, State *prev, bool client, char **errp)
{
    if (!room || !pdu || !prev)
    {
        if (errp)
        {
            *errp = "Illegal arguments given to RoomGetEventStatusV1";
        }
        return PDUV1_STATUS_DROPPED;
    }

    if (!EventFitsV1(*pdu))
    {
        /* Reject this event as it is too large. */
        if (errp)
        {
            *errp = "PDU is too large to fit";
        }
        return PDUV1_STATUS_DROPPED;
    }
    if (!RoomAuthoriseEventV1(room, *pdu, prev, errp))
    {
        /* Reject this event as the current state does not allow it.
         * TODO: Make the auth check function return a string showing the 
         * errorr status to the user. */
        return PDUV1_STATUS_DROPPED;
    }

    if (!client)
    {
        State *current = StateCurrent(room);

        if (!RoomAuthoriseEventV1(room, *pdu, current, NULL))
        {
            StateFree(current);
            return PDUV1_STATUS_SOFTFAIL;
        }
        StateFree(current);
    }

    return PDUV1_STATUS_ACCEPTED;
}
bool
RoomAddEventV1(Room *room, PduV1 pdu)
{
    DbRef *event_ref;
    Array *prev_events = NULL, *leaves = NULL;
    HashMap *leaves_json = NULL, *pdu_json = NULL;
    JsonValue *leaves_val;
    char *safe_id;
    size_t i;

    if (!room || room->version >= 3 ||
        pdu._unsigned.pdu_status == PDUV1_STATUS_DROPPED)
    {
        return false;
    }

    /* Insert our PDU into the event table, regardless of status */
    safe_id = CreateSafeID(pdu.event_id);
    event_ref = DbCreate(room->db, 4, "rooms", room->id, "events", safe_id);
    pdu_json = PduV1ToJson(&pdu);

    DbJsonSet(event_ref, pdu_json);

    DbUnlock(room->db, event_ref);
    JsonFree(pdu_json);
    Free(safe_id);

    /* Only accepted PDUs get to do the news */
    if (pdu._unsigned.pdu_status == PDUV1_STATUS_ACCEPTED)
    {
        /* Remove managed leaves here. */
        leaves_json = DbJson(room->leaves_ref);
        leaves_val = JsonValueDuplicate(JsonGet(leaves_json, 1, "leaves"));
        leaves = JsonValueAsArray(leaves_val);
        Free(leaves_val);   /* We do not care about the array's JSON shell. */

        prev_events = pdu.prev_events;
        for (i = 0; i < ArraySize(prev_events); i++)
        {
            JsonValue *event_val    = ArrayGet(prev_events, i);
            char *event_id          = JsonValueAsString(event_val);
            size_t j;
            ssize_t delete_index = -1;

            for (j = 0; j < ArraySize(leaves); j++)
            {
                JsonValue *leaf_val = ArrayGet(leaves, j);
                HashMap *leaf_object = JsonValueAsObject(leaf_val);
                char *leaf_id =
                    JsonValueAsString(JsonGet(leaf_object, 1, "event_id"));

                if (StrEquals(leaf_id, event_id))
                {
                    delete_index = j;
                    break;
                }
            }
            if (delete_index == -1)
            {
                continue;
            }
            JsonValueFree(ArrayDelete(leaves, delete_index));
        }

        /* Add our current PDU to the leaves. */
        ArrayAdd(leaves, JsonValueObject(PduV1ToJson(&pdu)));
        leaves_json = JsonDuplicate(leaves_json);
        JsonValueFree(HashMapDelete(leaves_json, "leaves"));
        JsonSet(leaves_json, JsonValueArray(leaves), 1, "leaves");
        DbJsonSet(room->leaves_ref, leaves_json);
        JsonFree(leaves_json);
    }

    for (i = 0; i < ArraySize(prev_events); i++)
    {
        JsonValue *event_val    = ArrayGet(prev_events, i);
        char *id                = JsonValueAsString(event_val);
        char *error             = NULL;
        PduV1 prev_pdu          = { 0 };
        HashMap *prev_object    = NULL;
        Array *next_events      = NULL;
        event_ref = DbLock(room->db, 4, "rooms", room->id, "events", id);
        PduV1FromJson(DbJson(event_ref), &prev_pdu, &error);

        /* Update the next events view. Note that this works even if
         * the event is soft-failed/rejected. */
        if (!prev_pdu._unsigned.next_events)
        {
            prev_pdu._unsigned.next_events = ArrayCreate();
        }
        next_events = prev_pdu._unsigned.next_events;

        ArrayAdd(next_events, StrDuplicate(pdu.event_id));

        prev_object = PduV1ToJson(&prev_pdu);
        DbJsonSet(event_ref, prev_object);

        JsonFree(prev_object);
        PduV1Free(&prev_pdu);
        DbUnlock(room->db, event_ref);
    }

    /* Accepted PDUs should be the only one that users should be
     * notified about. */
    if (pdu._unsigned.pdu_status == PDUV1_STATUS_ACCEPTED)
    {
        State *state;
        char *type, *state_key, *event_id;

        pdu_json = PduV1ToJson(&pdu);

        /* If we have a membership change, then add it to the
         * proper table. */
        if (StrEquals(pdu.type, "m.room.member"))
        {
            CommonID *id = UserIdParse(pdu.state_key, NULL);
            User *user   = UserLock(room->db, id->local);
            char *membership = JsonValueAsString(
                HashMapGet(pdu.content, "membership")
            );

            if (StrEquals(membership, "join") && user)
            {
                UserAddJoin(user, room->id);
                UserPushJoinSync(user, room->id);
            }
            else if (StrEquals(membership, "invite") && user)
            {
                UserAddInvite(user, room->id);
                UserPushInviteSync(user, room->id);
            }
            else if ((StrEquals(membership, "leave") && user) ||
                    StrEquals(membership, "ban"))
            {
                UserRemoveInvite(user, room->id);
                UserRemoveJoin(user, room->id);
            }

            UserIdFree(id);
            UserUnlock(user);
        }

        /* Notify the user by pushing out the user */
        state = StateCurrent(room);
        while (StateIterate(state, &type, &state_key, (void **) &event_id))
        {
            if (StrEquals(type, "m.room.member"))
            {
                CommonID *id = UserIdParse(state_key, NULL);
                User *user   = UserLock(room->db, id->local);

                UserPushEvent(user, pdu_json);

                UserIdFree(id);
                UserUnlock(user);
            }
            Free(type);
            Free(state_key);
        }
        StateFree(state);
        JsonFree(pdu_json);
    }

    return true;
}
HashMap *
RoomEventSendV1(Room * room, HashMap * event, char **errp)
{
    PduV1 pdu = { 0 };
    HashMap *pdu_object = NULL;
    bool client_event, valid = false;
    State *state = NULL;
    PduV1Status status;

    client_event = !PopulateEventV1(room, event, &pdu, RoomGetCreator(room));
    pdu_object = PduV1ToJson(&pdu);

    state = StateResolve(room, pdu_object);

    if (client_event)
    {
        char *ev_id;
#define AddState(type, key) do \
                            { \
                                ev_id = StateGet(state, type, key); \
                                if (ev_id) \
                                { \
                                    JsonValue *v = JsonValueString(ev_id); \
                                    ArrayAdd(pdu.auth_events, v); \
                                } \
                            } \
                            while (0)

         /* 
          * Implemented from 
          * https://spec.matrix.org/v1.7/server-server-api/
          * #auth-events-selection */
        AddState("m.room.create", "");
        AddState("m.room.power_levels", "");
        AddState("m.room.member", pdu.sender);
        if (StrEquals(pdu.type, "m.room.member"))
        {
            char *target = pdu.state_key;
            char *membership = 
                JsonValueAsString(JsonGet(pdu.content, 1, "membership"));
            char *auth_via = 
                JsonValueAsString(
                    JsonGet(
                        pdu.content, 1, "join_authorised_via_users_server")
                );
            HashMap *inv = 
                JsonValueAsObject(
                    JsonGet(pdu.content, 1, "third_party_invite"));

            if (target && !StrEquals(target, pdu.sender))
            {
                AddState("m.room.member", target);
            }
            if (StrEquals(membership, "join") ||
                StrEquals(membership, "invite"))
            {
                AddState("m.room.join_rules", "");
            }
            if (StrEquals(membership, "invite") && inv)
            {
                char *token = 
                    JsonValueAsString(JsonGet(inv, 2, "signed", "token"));
                AddState("m.room.third_party_invite", token);
            }
            if (auth_via && room->version >= 8)
            {
                AddState("m.room.member", auth_via);
            }
        }
        pdu.hashes.sha256 = RoomHashEventV1(pdu);
#undef AddState
    }
    /* It seems like we need to behave differently in terms of
     * verifying PDUs from the client/federation.
     * - In the client, we just do not care about any events that 
     *   are incorrect. We simply drop them, as if they never existed.
     * - In the server on the otherhand, the only place where we can 
     *   possibly drop events as such is if it fails signatures. In 
     *   other cases, we *have* to store it(ableit with flags, to 
     *   restrict what we can do).
     *      - Rejection: We avoid relaying/linking those to anything. 
     *      They must NOT be used for stateres.
     *      - Softfail: Essentially almost the same as rejects, except 
     *      that they *are* used for stateres.
     * I guess a way to do this may be to add a CheckAuthStatus 
     * function that also verifies if it is a client event, and returns 
     * an enum:
     *  - DROPPED: Do NOT process it _at all_
     *  - REJECT: Process that event as if it was rejected
     *  - SOFTFAIL: Process the event as if it was softfailed
     * The main issue is storing it in the PDU. A naive approach would be to 
     * add the status to the unsigned field of the PDU, and add functions to 
     * return the status. I guess that is possible, but then again, can we 
     * really abuse the unsigned field for this?
     */

    /* TODO: For PDU events, we should verify their hashes. */
    status = RoomGetEventStatusV1(room, &pdu, state, client_event, errp);
    if (status == PDUV1_STATUS_DROPPED)
    {
        goto finish;
    }
    pdu._unsigned.pdu_status = status;

    StateFree(state);

    RoomAddEventV1(room, pdu);
    state = NULL;
    valid = true;

    /* If it is a client event, we should make sure that we shout at 
     * every other homeserver about our new event. */

finish:
    if (state)
    {
        StateFree(state);
    }
    if (pdu_object)
    {
        JsonFree(pdu_object);
        pdu_object = NULL;
    }
    if (valid)
    {
        pdu_object = PduV1ToJson(&pdu);
    }
    PduV1Free(&pdu);
    return pdu_object;
}