// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package appservice

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)

type IntentAPI struct {
	*mautrix.Client
	bot       *mautrix.Client
	as        *AppService
	Localpart string
	UserID    id.UserID

	IsCustomPuppet bool
}

func (as *AppService) NewIntentAPI(localpart string) *IntentAPI {
	userID := id.NewUserID(localpart, as.HomeserverDomain)
	bot := as.BotClient()
	if userID == bot.UserID {
		bot = nil
	}
	return &IntentAPI{
		Client:    as.Client(userID),
		bot:       bot,
		as:        as,
		Localpart: localpart,
		UserID:    userID,

		IsCustomPuppet: false,
	}
}

func (intent *IntentAPI) Register() error {
	_, _, err := intent.Client.Register(&mautrix.ReqRegister{
		Username:     intent.Localpart,
		Type:         mautrix.AuthTypeAppservice,
		InhibitLogin: true,
	})
	return err
}

func (intent *IntentAPI) EnsureRegistered() error {
	if intent.IsCustomPuppet || intent.as.StateStore.IsRegistered(intent.UserID) {
		return nil
	}

	err := intent.Register()
	if err != nil && !errors.Is(err, mautrix.MUserInUse) {
		return fmt.Errorf("failed to ensure registered: %w", err)
	}
	intent.as.StateStore.MarkRegistered(intent.UserID)
	return nil
}

type EnsureJoinedParams struct {
	IgnoreCache bool
	BotOverride *mautrix.Client
}

func (intent *IntentAPI) EnsureJoined(roomID id.RoomID, extra ...EnsureJoinedParams) error {
	var params EnsureJoinedParams
	if len(extra) > 1 {
		panic("invalid number of extra parameters")
	} else if len(extra) == 1 {
		params = extra[0]
	}
	if intent.as.StateStore.IsInRoom(roomID, intent.UserID) && !params.IgnoreCache {
		return nil
	}

	if err := intent.EnsureRegistered(); err != nil {
		return fmt.Errorf("failed to ensure joined: %w", err)
	}

	resp, err := intent.JoinRoomByID(roomID)
	if err != nil {
		bot := intent.bot
		if params.BotOverride != nil {
			bot = params.BotOverride
		}
		if !errors.Is(err, mautrix.MForbidden) || bot == nil {
			return fmt.Errorf("failed to ensure joined: %w", err)
		}
		_, inviteErr := bot.InviteUser(roomID, &mautrix.ReqInviteUser{
			UserID: intent.UserID,
		})
		if inviteErr != nil {
			return fmt.Errorf("failed to invite in ensure joined: %w", inviteErr)
		}
		resp, err = intent.JoinRoomByID(roomID)
		if err != nil {
			return fmt.Errorf("failed to ensure joined after invite: %w", err)
		}
	}
	intent.as.StateStore.SetMembership(resp.RoomID, intent.UserID, event.MembershipJoin)
	return nil
}

func (intent *IntentAPI) SendMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.SendMessageEvent(roomID, eventType, contentJSON)
}

func (intent *IntentAPI) SendMassagedMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.SendMessageEvent(roomID, eventType, contentJSON, mautrix.ReqSendEvent{Timestamp: ts})
}

func (intent *IntentAPI) updateStoreWithOutgoingEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, eventID id.EventID) {
	fakeEvt := &event.Event{
		StateKey: &stateKey,
		Sender:   intent.UserID,
		Type:     eventType,
		ID:       eventID,
		RoomID:   roomID,
		Content:  event.Content{},
	}
	var err error
	fakeEvt.Content.VeryRaw, err = json.Marshal(contentJSON)
	if err != nil {
		intent.Logger.Debugfln("Failed to marshal state event content to update state store: %v", err)
		return
	}
	err = json.Unmarshal(fakeEvt.Content.VeryRaw, &fakeEvt.Content.Raw)
	if err != nil {
		intent.Logger.Debugfln("Failed to unmarshal state event content to update state store: %v", err)
		return
	}
	err = fakeEvt.Content.ParseRaw(fakeEvt.Type)
	if err != nil {
		intent.Logger.Debugfln("Failed to parse state event content to update state store: %v", err)
		return
	}
	intent.as.UpdateState(fakeEvt)
}

func (intent *IntentAPI) SendStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	resp, err := intent.Client.SendStateEvent(roomID, eventType, stateKey, contentJSON)
	if err == nil && resp != nil {
		intent.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, contentJSON, resp.EventID)
	}
	return resp, err
}

func (intent *IntentAPI) SendMassagedStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	resp, err := intent.Client.SendMassagedStateEvent(roomID, eventType, stateKey, contentJSON, ts)
	if err == nil && resp != nil {
		intent.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, contentJSON, resp.EventID)
	}
	return resp, err
}

func (intent *IntentAPI) StateEvent(roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) error {
	if err := intent.EnsureJoined(roomID); err != nil {
		return err
	}
	err := intent.Client.StateEvent(roomID, eventType, stateKey, outContent)
	if err == nil {
		intent.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, outContent, "")
	}
	return err
}

func (intent *IntentAPI) State(roomID id.RoomID) (mautrix.RoomStateMap, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	state, err := intent.Client.State(roomID)
	if err == nil {
		for _, events := range state {
			for _, evt := range events {
				intent.as.UpdateState(evt)
			}
		}
	}
	return state, err
}

func (intent *IntentAPI) InviteUser(roomID id.RoomID, req *mautrix.ReqInviteUser) (resp *mautrix.RespInviteUser, err error) {
	resp, err = intent.Client.InviteUser(roomID, req)
	if err == nil {
		intent.as.StateStore.SetMembership(roomID, req.UserID, event.MembershipInvite)
	}
	return
}

func (intent *IntentAPI) KickUser(roomID id.RoomID, req *mautrix.ReqKickUser) (resp *mautrix.RespKickUser, err error) {
	resp, err = intent.Client.KickUser(roomID, req)
	if err == nil {
		intent.as.StateStore.SetMembership(roomID, req.UserID, event.MembershipLeave)
	}
	return
}

func (intent *IntentAPI) BanUser(roomID id.RoomID, req *mautrix.ReqBanUser) (resp *mautrix.RespBanUser, err error) {
	resp, err = intent.Client.BanUser(roomID, req)
	if err == nil {
		intent.as.StateStore.SetMembership(roomID, req.UserID, event.MembershipBan)
	}
	return
}

func (intent *IntentAPI) UnbanUser(roomID id.RoomID, req *mautrix.ReqUnbanUser) (resp *mautrix.RespUnbanUser, err error) {
	resp, err = intent.Client.UnbanUser(roomID, req)
	if err == nil {
		intent.as.StateStore.SetMembership(roomID, req.UserID, event.MembershipLeave)
	}
	return
}

func (intent *IntentAPI) Member(roomID id.RoomID, userID id.UserID) *event.MemberEventContent {
	member, ok := intent.as.StateStore.TryGetMember(roomID, userID)
	if !ok {
		_ = intent.StateEvent(roomID, event.StateMember, string(userID), &member)
		intent.as.StateStore.SetMember(roomID, userID, member)
	}
	return member
}

func (intent *IntentAPI) PowerLevels(roomID id.RoomID) (pl *event.PowerLevelsEventContent, err error) {
	pl = intent.as.StateStore.GetPowerLevels(roomID)
	if pl == nil {
		pl = &event.PowerLevelsEventContent{}
		err = intent.StateEvent(roomID, event.StatePowerLevels, "", pl)
		if err == nil {
			intent.as.StateStore.SetPowerLevels(roomID, pl)
		}
	}
	return
}

func (intent *IntentAPI) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) (resp *mautrix.RespSendEvent, err error) {
	resp, err = intent.SendStateEvent(roomID, event.StatePowerLevels, "", &levels)
	if err == nil {
		intent.as.StateStore.SetPowerLevels(roomID, levels)
	}
	return
}

func (intent *IntentAPI) SetPowerLevel(roomID id.RoomID, userID id.UserID, level int) (*mautrix.RespSendEvent, error) {
	pl, err := intent.PowerLevels(roomID)
	if err != nil {
		return nil, err
	}

	if pl.GetUserLevel(userID) != level {
		pl.SetUserLevel(userID, level)
		return intent.SendStateEvent(roomID, event.StatePowerLevels, "", &pl)
	}
	return nil, nil
}

func (intent *IntentAPI) UserTyping(roomID id.RoomID, typing bool, timeout int64) (resp *mautrix.RespTyping, err error) {
	if intent.as.StateStore.IsTyping(roomID, intent.UserID) == typing {
		return
	}
	resp, err = intent.Client.UserTyping(roomID, typing, timeout)
	if err != nil {
		return
	}
	if !typing {
		timeout = -1
	}
	intent.as.StateStore.SetTyping(roomID, intent.UserID, timeout)
	return
}

func (intent *IntentAPI) SendText(roomID id.RoomID, text string) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.SendText(roomID, text)
}

// Deprecated: This does not allow setting image metadata, you should prefer SendMessageEvent with a properly filled &event.MessageEventContent
func (intent *IntentAPI) SendImage(roomID id.RoomID, body string, url id.ContentURI) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.SendImage(roomID, body, url)
}

// Deprecated: This does not allow setting video metadata, you should prefer SendMessageEvent with a properly filled &event.MessageEventContent
func (intent *IntentAPI) SendVideo(roomID id.RoomID, body string, url id.ContentURI) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.SendVideo(roomID, body, url)
}

func (intent *IntentAPI) SendNotice(roomID id.RoomID, text string) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.SendNotice(roomID, text)
}

func (intent *IntentAPI) RedactEvent(roomID id.RoomID, eventID id.EventID, req ...mautrix.ReqRedact) (*mautrix.RespSendEvent, error) {
	if err := intent.EnsureJoined(roomID); err != nil {
		return nil, err
	}
	return intent.Client.RedactEvent(roomID, eventID, req...)
}

func (intent *IntentAPI) SetRoomName(roomID id.RoomID, roomName string) (*mautrix.RespSendEvent, error) {
	return intent.SendStateEvent(roomID, event.StateRoomName, "", map[string]interface{}{
		"name": roomName,
	})
}

func (intent *IntentAPI) SetRoomAvatar(roomID id.RoomID, avatarURL id.ContentURI) (*mautrix.RespSendEvent, error) {
	return intent.SendStateEvent(roomID, event.StateRoomAvatar, "", map[string]interface{}{
		"url": avatarURL.String(),
	})
}

func (intent *IntentAPI) SetRoomTopic(roomID id.RoomID, topic string) (*mautrix.RespSendEvent, error) {
	return intent.SendStateEvent(roomID, event.StateTopic, "", map[string]interface{}{
		"topic": topic,
	})
}

func (intent *IntentAPI) SetDisplayName(displayName string) error {
	if err := intent.EnsureRegistered(); err != nil {
		return err
	}
	resp, err := intent.Client.GetOwnDisplayName()
	if err != nil {
		return fmt.Errorf("failed to check current displayname: %w", err)
	} else if resp.DisplayName == displayName {
		// No need to update
		return nil
	}
	return intent.Client.SetDisplayName(displayName)
}

func (intent *IntentAPI) SetAvatarURL(avatarURL id.ContentURI) error {
	if err := intent.EnsureRegistered(); err != nil {
		return err
	}
	resp, err := intent.Client.GetOwnAvatarURL()
	if err != nil {
		return fmt.Errorf("failed to check current avatar URL: %w", err)
	} else if resp.FileID == avatarURL.FileID && resp.Homeserver == avatarURL.Homeserver {
		// No need to update
		return nil
	}
	return intent.Client.SetAvatarURL(avatarURL)
}

func (intent *IntentAPI) Whoami() (*mautrix.RespWhoami, error) {
	if err := intent.EnsureRegistered(); err != nil {
		return nil, err
	}
	return intent.Client.Whoami()
}

func (intent *IntentAPI) JoinedMembers(roomID id.RoomID) (resp *mautrix.RespJoinedMembers, err error) {
	resp, err = intent.Client.JoinedMembers(roomID)
	if err != nil {
		return
	}
	for userID, member := range resp.Joined {
		var displayname string
		var avatarURL id.ContentURIString
		if member.DisplayName != nil {
			displayname = *member.DisplayName
		}
		if member.AvatarURL != nil {
			avatarURL = id.ContentURIString(*member.AvatarURL)
		}
		intent.as.StateStore.SetMember(roomID, userID, &event.MemberEventContent{
			Membership:  event.MembershipJoin,
			AvatarURL:   avatarURL,
			Displayname: displayname,
		})
	}
	return
}

func (intent *IntentAPI) Members(roomID id.RoomID, req ...mautrix.ReqMembers) (resp *mautrix.RespMembers, err error) {
	resp, err = intent.Client.Members(roomID, req...)
	if err != nil {
		return
	}
	for _, evt := range resp.Chunk {
		intent.as.UpdateState(evt)
	}
	return
}

func (intent *IntentAPI) EnsureInvited(roomID id.RoomID, userID id.UserID) error {
	if !intent.as.StateStore.IsInvited(roomID, userID) {
		_, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{
			UserID: userID,
		})
		if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
			return nil
		}
		return err
	}
	return nil
}
