//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
//Copyright (C) 2024 HTWKalender support@htwkalender.de

//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Affero General Public License for more details.

//You should have received a copy of the GNU Affero General Public License
//along with this program.  If not, see <https://www.gnu.org/licenses/>.

package db

import (
	"fmt"
	"github.com/google/uuid"
	"github.com/pocketbase/pocketbase/daos"
	"github.com/pocketbase/pocketbase/tools/types"
	"htwkalender/data-manager/model"
	"log/slog"
	"time"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase"
)

func SaveSeminarGroupEvents(seminarGroup model.SeminarGroup, app *pocketbase.PocketBase) ([]model.Event, error) {
	var toBeSavedEvents model.Events
	var savedRecords model.Events

	// check if event is already in database and add to toBeSavedEvents if not
	for _, event := range seminarGroup.Events {
		event = event.SetCourse(seminarGroup.Course)
		existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, seminarGroup.Course, app.Dao())
		alreadyAddedToSave := toBeSavedEvents.Contains(event)

		if err != nil {
			return nil, err
		}

		if !existsInDatabase && !alreadyAddedToSave {
			toBeSavedEvents = append(toBeSavedEvents, event)
		}
	}

	// create record for each event that's not already in the database
	for _, event := range toBeSavedEvents {
		event.MarkAsNew()
		// auto mapping for event fields to record fields
		err := app.Dao().Save(&event)
		if err != nil {
			return nil, err
		} else {
			savedRecords = append(savedRecords, event)
		}
	}

	return savedRecords, nil
}

func SaveEvents(events []model.Event, txDao *daos.Dao) ([]model.Event, error) {
	var toBeSavedEvents model.Events
	var savedRecords model.Events

	// check if event is already in database and add to toBeSavedEvents if not
	for _, event := range events {
		existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, event.Course, txDao)
		alreadyAddedToSave := toBeSavedEvents.Contains(event)

		if err != nil {
			return nil, err
		}

		if !existsInDatabase && !alreadyAddedToSave {
			toBeSavedEvents = append(toBeSavedEvents, event)
		}
	}
	// create record for each event that's not already in the database
	for _, event := range toBeSavedEvents {
		event.MarkAsNew()
		// auto mapping for event fields to record fields
		err := txDao.Save(&event)
		if err != nil {
			return nil, err
		} else {
			savedRecords = append(savedRecords, event)
		}
	}
	return savedRecords, nil
}

// check if event is already in database and return true if it is and false if it's not
func findEventByDayWeekStartEndNameCourse(event model.Event, course string, dao *daos.Dao) (bool, error) {

	var dbEvent model.Event

	err := dao.DB().Select("*").From("events").
		Where(dbx.NewExp(
			"Day = {:day} AND "+
				"Week = {:week} AND "+
				"Start = {:start} AND "+
				"End = {:end} AND "+
				"Name = {:name} AND "+
				"course = {:course} AND "+
				"Prof = {:prof} AND "+
				"Rooms = {:rooms} AND "+
				"EventType = {:eventType}",
			dbx.Params{
				"day":       event.Day,
				"week":      event.Week,
				"start":     event.Start,
				"end":       event.End,
				"name":      event.Name,
				"course":    course,
				"prof":      event.Prof,
				"rooms":     event.Rooms,
				"eventType": event.EventType}),
		).One(&dbEvent)

	if err != nil {
		if err.Error() == "sql: no rows in result set" {
			return false, nil
		}
		return false, err
	} else {
		return true, nil
	}
}

func buildIcalQueryForModules(modulesUuid []string) dbx.Expression {

	// check uuids against sql injection
	// uuids are generated by the system and are not user input
	// following the pattern of only containing alphanumeric characters and dashes

	for _, moduleUuid := range modulesUuid {
		err := uuid.Validate(moduleUuid)
		if err != nil {
			slog.Warn("Module UUID is not safe: ", "moduleUuid", moduleUuid)
			return dbx.HashExp{}
		}
	}

	// build where conditions for each module

	//first check if modules is empty
	if len(modulesUuid) == 0 {
		return dbx.HashExp{}
	}

	//second check if modules has only one element
	if len(modulesUuid) == 1 {
		return dbx.HashExp{"uuid": modulesUuid[0]}
	}

	//third check if modules has more than one element
	var wheres []dbx.Expression

	for _, moduleUuid := range modulesUuid {
		where := dbx.HashExp{"uuid": moduleUuid}
		wheres = append(wheres, where)
	}

	// Use dbx.And or dbx.Or to combine the where conditions as needed
	where := dbx.Or(wheres...)

	return where

}

// GetPlanForModules returns all events for the given modules with the given course
// used for the ical feed
func GetPlanForModules(app *pocketbase.PocketBase, modules []string) (model.Events, error) {

	var events model.Events

	// iterate over modules in 100 batch sizes
	for i := 0; i < len(modules); i += 100 {
		var moduleBatch []string

		if i+100 > len(modules) {
			moduleBatch = modules[i:]
		} else {
			moduleBatch = modules[i : i+100]
		}

		var selectedModulesQuery = buildIcalQueryForModules(moduleBatch)
		// get all events from event records in the events collection
		err := app.Dao().DB().Select("*").From("events").Where(selectedModulesQuery).OrderBy("start").All(&events)
		if err != nil {
			return nil, err
		}
	}

	return events, nil
}

func GetAllEventsForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) {
	var events model.Events

	// get all events from event records in the events collection
	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": course})).All(&events)
	if err != nil {
		slog.Error("Error while getting events from database: ", "error", err)
		return nil, fmt.Errorf("error while getting events from database for course %s", course)
	}

	return events, nil
}

func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
	var events model.Events

	// get all events from event records in the events collection
	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).GroupBy("Name").Distinct(true).All(&events)
	if err != nil {
		slog.Error("Error while getting events from database: ", "error", err)
		return nil, fmt.Errorf("error while getting events from database for course %s and semester %s", course, semester)
	}

	return events, nil
}

func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) {
	var modules []model.ModuleDTO

	err := app.Dao().DB().Select("Name", "EventType", "Prof", "course", "semester", "uuid").From("events").GroupBy("Name", "Course").Distinct(true).All(&modules)
	if err != nil {
		slog.Error("Error while getting events from database: ", "error", err)
		return nil, fmt.Errorf("error while getting events distinct by name and course from data")
	}

	return modules, nil
}

func DeleteAllEventsByCourse(dao *daos.Dao, course string, semester string) error {
	_, err := dao.DB().Delete("events", dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute()

	if err != nil {
		return err
	}
	return nil
}

func DeleteAllEventsRatherThenCourse(dao *daos.Dao, course string, semester string) error {
	_, err := dao.DB().Delete("events", dbx.NewExp("course != {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute()

	if err != nil {
		return err
	}
	return nil
}

func DeleteAllEvents(app *pocketbase.PocketBase) error {

	_, err := app.Dao().DB().Delete("events", dbx.NewExp("1=1")).Execute()

	if err != nil {
		return err
	}

	return nil
}

func FindModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
	var module model.Module

	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("uuid = {:uuid}", dbx.Params{"uuid": uuid})).One(&module)
	if err != nil {
		return model.Module{}, err
	}

	return module, nil
}

func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (model.Events, error) {
	var events model.Events

	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:moduleName} AND course = {:course}", dbx.Params{"moduleName": module.Name, "course": module.Course})).All(&events)
	if err != nil {
		return nil, err
	}

	return events, nil
}

func GetAllModulesByNameAndDateRange(app *pocketbase.PocketBase, name string, startDate time.Time, endDate time.Time) (model.Events, error) {
	var events model.Events

	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:name} AND Start >= {:startDate} AND End <= {:endDate}", dbx.Params{"name": name, "startDate": startDate, "endDate": endDate})).All(&events)
	if err != nil {
		return nil, err
	}

	return events, nil
}

// GetEventsThatCollideWithTimeRange returns all events that collide with the given time range
// we have events with start and end in the database, we want to get all events that collide with the given time range
// we have 4 cases:
// 1. event starts before the given time range and ends after the given time range
// 2. event starts after the given time range and ends before the given time range
// 3. event starts before the given time range and ends before the given time range
// 4. event starts after the given time range and ends after the given time range
func GetEventsThatCollideWithTimeRange(app *pocketbase.PocketBase, from time.Time, to time.Time) (model.Events, error) {
	var fromTypeTime, _ = types.ParseDateTime(from)
	var toTypeTime, _ = types.ParseDateTime(to)

	events1, err := GetEventsThatStartBeforeAndEndAfter(app, fromTypeTime, toTypeTime)
	if err != nil {
		return nil, err
	}
	events2, err := GetEventsThatStartAfterAndEndBefore(app, fromTypeTime, toTypeTime)
	if err != nil {
		return nil, err
	}

	events3, err := GetEventsThatStartBeforeAndEndBefore(app, fromTypeTime, toTypeTime)
	if err != nil {
		return nil, err
	}

	events4, err := GetEventsThatStartAfterAndEndAfter(app, fromTypeTime, toTypeTime)
	if err != nil {
		return nil, err
	}

	var events model.Events
	events = append(events, events1...)
	events = append(events, events2...)
	events = append(events, events3...)
	events = append(events, events4...)

	return events, nil
}

func GetEventsThatStartBeforeAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
	var events model.Events

	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).Distinct(true).All(&events)

	if err != nil {
		return nil, err
	}

	return events, nil
}

func GetEventsThatStartAfterAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
	var events model.Events

	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events)

	if err != nil {
		return nil, err
	}

	return events, nil
}

func GetEventsThatStartBeforeAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
	var events model.Events

	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events)

	if err != nil {
		return nil, err
	}

	return events, nil
}

func GetAllEventTypes(app *pocketbase.PocketBase) ([]model.EventType, error) {
	var eventTypes []model.EventType

	err := app.Dao().DB().Select("EventType").From("events").GroupBy("EventType").Distinct(true).All(&eventTypes)
	if err != nil {
		return nil, err
	}

	return eventTypes, nil
}

func GetEventsThatStartAfterAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
	var events model.Events
	err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events)

	if err != nil {
		return nil, err
	}

	return events, nil
}

func DeleteEvents(list model.Events, app *pocketbase.PocketBase) error {
	for _, event := range list {
		err := app.Dao().Delete(&event)
		if err != nil {
			return err
		}
	}
	return nil
}
