// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package routing

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/matrix-org/dendrite/clientapi/jsonerror"
	"github.com/matrix-org/dendrite/roomserver/api"
	userapi "github.com/matrix-org/dendrite/userapi/api"
	"github.com/matrix-org/gomatrixserverlib"
	"github.com/matrix-org/util"
	log "github.com/sirupsen/logrus"
)

type stateEventInStateResp struct {
	gomatrixserverlib.ClientEvent
	PrevContent   json.RawMessage `json:"prev_content,omitempty"`
	ReplacesState string          `json:"replaces_state,omitempty"`
}

// OnIncomingStateRequest is called when a client makes a /rooms/{roomID}/state
// request. It will fetch all the state events from the specified room and will
// append the necessary keys to them if applicable before returning them.
// Returns an error if something went wrong in the process.
// TODO: Check if the user is in the room. If not, check if the room's history
// is publicly visible. Current behaviour is returning an empty array if the
// user cannot see the room's history.
func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, roomID string) util.JSONResponse {
	var worldReadable bool
	var wantLatestState bool

	// First of all, get the latest state of the room. We need to do this
	// so that we can look at the history visibility of the room. If the
	// room is world-readable then we will always return the latest state.
	stateRes := api.QueryLatestEventsAndStateResponse{}
	if err := rsAPI.QueryLatestEventsAndState(ctx, &api.QueryLatestEventsAndStateRequest{
		RoomID:       roomID,
		StateToFetch: []gomatrixserverlib.StateKeyTuple{},
	}, &stateRes); err != nil {
		util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed")
		return jsonerror.InternalServerError()
	}

	// Look at the room state and see if we have a history visibility event
	// that marks the room as world-readable. If we don't then we assume that
	// the room is not world-readable.
	for _, ev := range stateRes.StateEvents {
		if ev.Type() == gomatrixserverlib.MRoomHistoryVisibility {
			content := map[string]string{}
			if err := json.Unmarshal(ev.Content(), &content); err != nil {
				util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed")
				return jsonerror.InternalServerError()
			}
			if visibility, ok := content["history_visibility"]; ok {
				worldReadable = visibility == "world_readable"
				break
			}
		}
	}

	// If the room isn't world-readable then we will instead try to find out
	// the state of the room based on the user's membership. If the user is
	// in the room then we'll want the latest state. If the user has never
	// been in the room and the room isn't world-readable, then we won't
	// return any state. If the user was in the room previously but is no
	// longer then we will return the state at the time that the user left.
	// membershipRes will only be populated if the room is not world-readable.
	var membershipRes api.QueryMembershipForUserResponse
	if !worldReadable {
		// The room isn't world-readable so try to work out based on the
		// user's membership if we want the latest state or not.
		err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{
			RoomID: roomID,
			UserID: device.UserID,
		}, &membershipRes)
		if err != nil {
			util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
			return jsonerror.InternalServerError()
		}
		// If the user has never been in the room then stop at this point.
		// We won't tell the user about a room they have never joined.
		if !membershipRes.HasBeenInRoom {
			return util.JSONResponse{
				Code: http.StatusForbidden,
				JSON: jsonerror.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
			}
		}
		// Otherwise, if the user has been in the room, whether or not we
		// give them the latest state will depend on if they are *still* in
		// the room.
		wantLatestState = membershipRes.IsInRoom
	} else {
		// The room is world-readable so the user join state is irrelevant,
		// just get the latest room state instead.
		wantLatestState = true
	}

	util.GetLogger(ctx).WithFields(log.Fields{
		"roomID":         roomID,
		"state_at_event": !wantLatestState,
	}).Info("Fetching all state")

	stateEvents := []gomatrixserverlib.ClientEvent{}
	if wantLatestState {
		// If we are happy to use the latest state, either because the user is
		// still in the room, or because the room is world-readable, then just
		// use the result of the previous QueryLatestEventsAndState response
		// to find the state event, if provided.
		for _, ev := range stateRes.StateEvents {
			stateEvents = append(
				stateEvents,
				gomatrixserverlib.HeaderedToClientEvent(ev, gomatrixserverlib.FormatAll),
			)
		}
	} else {
		// Otherwise, take the event ID of their leave event and work out what
		// the state of the room was before that event.
		var stateAfterRes api.QueryStateAfterEventsResponse
		err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{
			RoomID:       roomID,
			PrevEventIDs: []string{membershipRes.EventID},
			StateToFetch: []gomatrixserverlib.StateKeyTuple{},
		}, &stateAfterRes)
		if err != nil {
			util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
			return jsonerror.InternalServerError()
		}
		for _, ev := range stateAfterRes.StateEvents {
			stateEvents = append(
				stateEvents,
				gomatrixserverlib.HeaderedToClientEvent(ev, gomatrixserverlib.FormatAll),
			)
		}
	}

	// Return the results to the requestor.
	return util.JSONResponse{
		Code: http.StatusOK,
		JSON: stateEvents,
	}
}

// OnIncomingStateTypeRequest is called when a client makes a
// /rooms/{roomID}/state/{type}/{statekey} request. It will look in current
// state to see if there is an event with that type and state key, if there
// is then (by default) we return the content, otherwise a 404.
// If eventFormat=true, sends the whole event else just the content.
// nolint:gocyclo
func OnIncomingStateTypeRequest(
	ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI,
	roomID, evType, stateKey string, eventFormat bool,
) util.JSONResponse {
	var worldReadable bool
	var wantLatestState bool

	// Always fetch visibility so that we can work out whether to show
	// the latest events or the last event from when the user was joined.
	// Then include the requested event type and state key, assuming it
	// isn't for the same.
	stateToFetch := []gomatrixserverlib.StateKeyTuple{
		{
			EventType: evType,
			StateKey:  stateKey,
		},
	}
	if evType != gomatrixserverlib.MRoomHistoryVisibility && stateKey != "" {
		stateToFetch = append(stateToFetch, gomatrixserverlib.StateKeyTuple{
			EventType: gomatrixserverlib.MRoomHistoryVisibility,
			StateKey:  "",
		})
	}

	// First of all, get the latest state of the room. We need to do this
	// so that we can look at the history visibility of the room. If the
	// room is world-readable then we will always return the latest state.
	stateRes := api.QueryLatestEventsAndStateResponse{}
	if err := rsAPI.QueryLatestEventsAndState(ctx, &api.QueryLatestEventsAndStateRequest{
		RoomID:       roomID,
		StateToFetch: stateToFetch,
	}, &stateRes); err != nil {
		util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed")
		return jsonerror.InternalServerError()
	}

	// Look at the room state and see if we have a history visibility event
	// that marks the room as world-readable. If we don't then we assume that
	// the room is not world-readable.
	for _, ev := range stateRes.StateEvents {
		if ev.Type() == gomatrixserverlib.MRoomHistoryVisibility {
			content := map[string]string{}
			if err := json.Unmarshal(ev.Content(), &content); err != nil {
				util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for history visibility failed")
				return jsonerror.InternalServerError()
			}
			if visibility, ok := content["history_visibility"]; ok {
				worldReadable = visibility == "world_readable"
				break
			}
		}
	}

	// If the room isn't world-readable then we will instead try to find out
	// the state of the room based on the user's membership. If the user is
	// in the room then we'll want the latest state. If the user has never
	// been in the room and the room isn't world-readable, then we won't
	// return any state. If the user was in the room previously but is no
	// longer then we will return the state at the time that the user left.
	// membershipRes will only be populated if the room is not world-readable.
	var membershipRes api.QueryMembershipForUserResponse
	if !worldReadable {
		// The room isn't world-readable so try to work out based on the
		// user's membership if we want the latest state or not.
		err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{
			RoomID: roomID,
			UserID: device.UserID,
		}, &membershipRes)
		if err != nil {
			util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
			return jsonerror.InternalServerError()
		}
		// If the user has never been in the room then stop at this point.
		// We won't tell the user about a room they have never joined.
		if !membershipRes.HasBeenInRoom {
			return util.JSONResponse{
				Code: http.StatusForbidden,
				JSON: jsonerror.Forbidden(fmt.Sprintf("Unknown room %q or user %q has never joined this room", roomID, device.UserID)),
			}
		}
		// Otherwise, if the user has been in the room, whether or not we
		// give them the latest state will depend on if they are *still* in
		// the room.
		wantLatestState = membershipRes.IsInRoom
	} else {
		// The room is world-readable so the user join state is irrelevant,
		// just get the latest room state instead.
		wantLatestState = true
	}

	util.GetLogger(ctx).WithFields(log.Fields{
		"roomID":         roomID,
		"evType":         evType,
		"stateKey":       stateKey,
		"state_at_event": !wantLatestState,
	}).Info("Fetching state")

	var event *gomatrixserverlib.HeaderedEvent
	if wantLatestState {
		// If we are happy to use the latest state, either because the user is
		// still in the room, or because the room is world-readable, then just
		// use the result of the previous QueryLatestEventsAndState response
		// to find the state event, if provided.
		for _, ev := range stateRes.StateEvents {
			if ev.Type() == evType && ev.StateKeyEquals(stateKey) {
				event = ev
				break
			}
		}
	} else {
		// Otherwise, take the event ID of their leave event and work out what
		// the state of the room was before that event.
		var stateAfterRes api.QueryStateAfterEventsResponse
		err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{
			RoomID:       roomID,
			PrevEventIDs: []string{membershipRes.EventID},
			StateToFetch: []gomatrixserverlib.StateKeyTuple{
				{
					EventType: evType,
					StateKey:  stateKey,
				},
			},
		}, &stateAfterRes)
		if err != nil {
			util.GetLogger(ctx).WithError(err).Error("Failed to QueryMembershipForUser")
			return jsonerror.InternalServerError()
		}
		if len(stateAfterRes.StateEvents) > 0 {
			event = stateAfterRes.StateEvents[0]
		}
	}

	// If there was no event found that matches all of the above criteria then
	// return an error.
	if event == nil {
		return util.JSONResponse{
			Code: http.StatusNotFound,
			JSON: jsonerror.NotFound(fmt.Sprintf("Cannot find state event for %q", evType)),
		}
	}

	stateEvent := stateEventInStateResp{
		ClientEvent: gomatrixserverlib.HeaderedToClientEvent(event, gomatrixserverlib.FormatAll),
	}

	var res interface{}
	if eventFormat {
		res = stateEvent
	} else {
		res = stateEvent.Content
	}

	return util.JSONResponse{
		Code: http.StatusOK,
		JSON: res,
	}
}
