/*
 * Copyright © 2022 Kevin Kandlbinder.
 *
 * 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 config

import (
	"context"
	"errors"
	"github.com/Unkn0wnCat/matrix-veles/internal/db"
	"github.com/Unkn0wnCat/matrix-veles/internal/tracer"
	"github.com/spf13/viper"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"log"
	"maunium.net/go/mautrix/id"
	"sync"
)

var (
	roomConfigWg sync.WaitGroup
)

// SetRoomConfigActive updates the active state for a given room
func SetRoomConfigActive(id string, active bool) {
	// Lock room config system to prevent race conditions
	roomConfigWg.Wait()
	roomConfigWg.Add(1)

	roomConfig := GetRoomConfig(id)
	roomConfig.Active = active

	err := SaveRoomConfig(&roomConfig)
	if err != nil {
		log.Panicf("Error writing room config to database: %v", err)
	}

	// Unlock room config system
	roomConfigWg.Done()
}

// GetRoomConfig returns the RoomConfig linked to the specified ID
func GetRoomConfig(id string) RoomConfig {
	config, err := GetRoomConfigByRoomID(id)
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return AddRoomConfig(id)
		}
	}

	return *config
}

// RoomConfigInitialUpdate updates all RoomConfig entries to set activity and create blank configs
func RoomConfigInitialUpdate(ids []id.RoomID, parentCtx context.Context) {
	ctx, span := tracer.Tracer.Start(parentCtx, "initial_room_config_update")
	defer span.End()

	_, dbGetAllSpan := tracer.Tracer.Start(ctx, "db_fetch_room_configs")
	database := db.DbClient.Database(viper.GetString("bot.mongo.database"))

	cursor, err := database.Collection("rooms").Find(context.TODO(), bson.D{}, nil)
	if err != nil {
		dbGetAllSpan.RecordError(err)
		log.Panicf("Error querying room configs: %v", err)
	}

	var roomConfigs []RoomConfig

	err = cursor.All(context.TODO(), &roomConfigs)
	if err != nil {
		dbGetAllSpan.RecordError(err)
		log.Panicf("Error querying room configs: %v", err)
	}

	dbGetAllSpan.End()

	ctx2, updateConfigs := tracer.Tracer.Start(ctx, "update_room_configs")

	activeRooms := make(map[string]bool)

	// Set all active states to "false" for a blank start
	for _, roomConfig := range roomConfigs {
		activeRooms[roomConfig.RoomID] = false
	}

	_, updateRooms := tracer.Tracer.Start(ctx2, "update_joined_rooms")
	// Go over all joined rooms
	for _, roomID := range ids {
		activeRooms[roomID.String()] = true

		GetRoomConfig(roomID.String())
	}
	updateRooms.End()

	_, saveConfigs := tracer.Tracer.Start(ctx2, "save_room_configs")
	for roomID, isActive := range activeRooms {
		SetRoomConfigActive(roomID, isActive)
	}
	saveConfigs.End()

	updateConfigs.End()
}

func AddRoomConfig(id string) RoomConfig {
	// Lock room config system to prevent race conditions
	roomConfigWg.Wait()
	roomConfigWg.Add(1)

	config := GetDefaultRoomConfig()
	config.RoomID = id

	err := SaveRoomConfig(&config)
	if err != nil {
		log.Panicf("Error writing room config to database: %v", err)
	}

	// Unlock room config system
	roomConfigWg.Done()

	return config
}

func SaveRoomConfig(roomConfig *RoomConfig) error {
	database := db.DbClient.Database(viper.GetString("bot.mongo.database"))

	opts := options.Replace().SetUpsert(true)

	filter := bson.D{{"room_id", roomConfig.RoomID}}

	_, err := database.Collection(viper.GetString("bot.mongo.collection.rooms")).ReplaceOne(context.TODO(), filter, roomConfig, opts)

	return err
}

func GetRoomConfigByRoomID(id string) (*RoomConfig, error) {
	database := db.DbClient.Database(viper.GetString("bot.mongo.database"))

	res := database.Collection(viper.GetString("bot.mongo.collection.rooms")).FindOne(context.TODO(), bson.D{{"room_id", id}})
	if res.Err() != nil {
		return nil, res.Err()
	}

	object := GetDefaultRoomConfig()

	err := res.Decode(&object)
	if err != nil {
		return nil, err
	}

	return &object, nil
}

func GetRoomConfigByObjectID(id primitive.ObjectID) (*RoomConfig, error) {
	database := db.DbClient.Database(viper.GetString("bot.mongo.database"))

	res := database.Collection(viper.GetString("bot.mongo.collection.rooms")).FindOne(context.TODO(), bson.D{{"_id", id}})
	if res.Err() != nil {
		return nil, res.Err()
	}

	object := GetDefaultRoomConfig()

	err := res.Decode(&object)
	if err != nil {
		return nil, err
	}

	return &object, nil
}

func GetDefaultRoomConfig() RoomConfig {
	return RoomConfig{
		ID:              primitive.NewObjectID(),
		Active:          true,
		RoomID:          "",
		Debug:           false,
		AdminPowerLevel: 100,
		HashChecker: HashCheckerConfig{
			NoticeToChat:           true,
			NotificationPowerLevel: 50,
			HashCheckMode:          1,
		},
	}
}