//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/core"
	"github.com/pocketbase/pocketbase/tools/types"
	"htwkalender/data-manager/model"
	"htwkalender/data-manager/service/functions"
	"log/slog"
	"time"

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

var _ core.RecordProxy = (*Event)(nil)

type Event struct {
	core.BaseRecordProxy
}

type Events []*Event

func (event *Event) GetDay() string {
	return event.GetString("Day")
}

func (event *Event) SetDay(day string) {
	event.Set("Day", day)
}

func (event *Event) GetWeek() string {
	return event.GetString("Week")
}

func (event *Event) SetWeek(week string) {
	event.Set("Week", week)
}

func (event *Event) GetName() string {
	return event.GetString("Name")
}

func (event *Event) SetName(name string) {
	event.Set("Name", name)
}

func (event *Event) GetEventType() string {
	return event.GetString("EventType")
}

func (event *Event) SetEventType(eventType string) {
	event.Set("EventType", eventType)
}

func (event *Event) GetProf() string {
	return event.GetString("Prof")
}

func (event *Event) SetProf(prof string) {
	event.Set("Prof", prof)
}

func (event *Event) GetRooms() string {
	return event.GetString("Rooms")
}

func (event *Event) SetRooms(rooms string) {
	event.Set("Rooms", rooms)
}

func (event *Event) GetNotes() string {
	return event.GetString("Notes")
}

func (event *Event) SetNotes(notes string) {
	event.Set("Notes", notes)
}

func (event *Event) GetBookedAt() string {
	return event.GetString("BookedAt")
}

func (event *Event) SetBookedAt(bookedAt string) {
	event.Set("BookedAt", bookedAt)
}

func (event *Event) GetCourse() string {
	return event.GetString("course")
}

func (event *Event) SetCourse(course string) {
	event.Set("course", course)
}

func (event *Event) GetSemester() string {
	return event.GetString("semester")
}

func (event *Event) SetSemester(semester string) {
	event.Set("semester", semester)
}

func (event *Event) GetUUID() string {
	return event.GetString("uuid")
}

func (event *Event) SetUUID(uuid string) {
	event.Set("uuid", uuid)
}

func (event *Event) GetStart() types.DateTime {
	return event.GetDateTime("start")
}

func (event *Event) SetStart(start types.DateTime) {
	event.Set("start", start)
}

func (event *Event) GetEnd() types.DateTime {
	return event.GetDateTime("end")
}

func (event *Event) SetEnd(end types.DateTime) {
	event.Set("end", end)
}

func (event *Event) GetCompulsory() string {
	return event.GetString("Compulsory")
}

func (event *Event) SetCompulsory(compulsory string) {
	event.Set("Compulsory", compulsory)
}

func (event *Event) GetCreated() types.DateTime {
	return event.GetDateTime("created")
}

func (event *Event) SetCreated(created types.DateTime) {
	event.Set("created", created)
}

func (event *Event) GetUpdated() types.DateTime {
	return event.GetDateTime("updated")
}

func (event *Event) SetUpdated(updated types.DateTime) {
	event.Set("updated", updated)
}

func newEvent(record *core.Record) *Event {
	return &Event{
		BaseRecordProxy: core.BaseRecordProxy{Record: record},
	}
}

func NewEvent(collection *core.Collection, event model.Event) (*Event, error) {
	// Create a new Event instance
	if collection.Name != "events" {
		return nil, core.ErrInvalidFieldValue
	}

	record := core.NewRecord(collection)
	record.Set("id:autogenerate", "")

	ev := newEvent(record)
	// Set the fields from the model
	ev.SetDay(event.Day)
	ev.SetWeek(event.Week)
	ev.SetStart(event.Start)
	ev.SetEnd(event.End)
	ev.SetName(event.Name)
	ev.SetEventType(event.EventType)
	ev.SetProf(event.Prof)
	ev.SetRooms(event.Rooms)
	ev.SetNotes(event.Notes)
	ev.SetBookedAt(event.BookedAt)
	ev.SetCourse(event.Course)
	ev.SetSemester(event.Semester)
	ev.SetCompulsory(event.Compulsory)

	if event.UUID == "" {
		event.UUID = functions.GenerateUUID(event)
	}
	ev.SetUUID(event.UUID)
	return ev, nil
}

func (event *Event) ToModel() model.Event {
	return model.Event{
		Day:       event.GetDay(),
		Week:      event.GetWeek(),
		Start:     event.GetStart(),
		End:       event.GetEnd(),
		Name:      event.GetName(),
		EventType: event.GetEventType(),
		Prof:      event.GetProf(),
		Rooms:     event.GetRooms(),
		Notes:     event.GetNotes(),
		BookedAt:  event.GetBookedAt(),
		Course:    event.GetCourse(),
		Semester:  event.GetSemester(),
		UUID:      event.GetUUID(),
	}
}

func (events *Events) ToEvents() model.Events {
	var result model.Events
	for _, event := range *events {
		result = append(result, event.ToModel())
	}
	return result
}

func SaveSeminarGroupEvents(seminarGroup model.SeminarGroup, base *pocketbase.PocketBase) (model.Events, error) {
	return SaveEvents(seminarGroup.Events, base.App)
}

func SaveEvents(events []model.Event, app core.App) (model.Events, 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, app)
		alreadyAddedToSave := toBeSavedEvents.Contains(event)

		if err != nil {
			return nil, err
		}

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

	collection, err := app.FindCollectionByNameOrId("events")
	if err != nil {
		return nil, err
	}

	// create record for each event that's not already in the database
	for _, event := range toBeSavedEvents {
		// auto mapping for event fields to record fields
		savedEvent, saveErr := saveEvent(collection, event, app)
		if saveErr != nil {
			return nil, saveErr
		} else {
			savedRecords = append(savedRecords, savedEvent.ToModel())
		}
	}
	return savedRecords, nil
}

func saveEvent(collection *core.Collection, event model.Event, app core.App) (*Event, error) {
	dbEvent, recordErr := NewEvent(collection, event)
	if recordErr != nil {
		return nil, recordErr
	}
	// auto mapping for event fields to record fields
	dbErr := app.Save(dbEvent)
	if dbErr != nil {
		return nil, dbErr
	}

	return dbEvent, 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, app core.App) (bool, error) {

	var dbEvent model.Event

	err := app.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.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.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.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.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(base *pocketbase.PocketBase, course string, semester string) error {
	_, err := base.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(app core.App, course string, semester string) error {
	_, err := app.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 core.App) error {

	_, err := app.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.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.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.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.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.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.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.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.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.Delete(&event)
		if err != nil {
			return err
		}
	}
	return nil
}
