// officeHour
package repositories

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
	"officeHours/config"
	"officeHours/models"
)

type OfficeHourRepo struct {
	db         *sql.DB
	roomRepo   *RoomRepo
	tutorRepo  *TutorRepo
	courseRepo *CourseRepo
	config     config.Config
}

func NewOfficeHourRepo(db *sql.DB, roomRepo *RoomRepo, tutorRepo *TutorRepo, courseRepo *CourseRepo, conf config.Config) *OfficeHourRepo {
	return &OfficeHourRepo{
		db:         db,
		roomRepo:   roomRepo,
		tutorRepo:  tutorRepo,
		courseRepo: courseRepo,
		config:     conf,
	}
}

func (r *OfficeHourRepo) GetAll(activeOnly bool) ([]models.OfficeHour, error) {
	var rows *sql.Rows
	var err error
	if activeOnly {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE active")
	} else {
		rows, err = r.db.Query("SELECT * FROM officeHour")
	}
	if err != nil {
		err = fmt.Errorf("error getting all officeHours from database: %w", err)
		if !errors.Is(err, sql.ErrNoRows) {
			log.Println(err.Error())
		}
		return nil, err
	}
	defer rows.Close()
	return r.getFromRows(rows)
}

func (r *OfficeHourRepo) FindByCourse(course models.Course, activeOnly bool) ([]models.OfficeHour, error) {
	var rows *sql.Rows
	var err error
	if activeOnly {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE course=? and active", course.Id)
	} else {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE course=?", course.Id)
	}
	defer rows.Close()
	if err != nil {
		err = fmt.Errorf("error getting officeHours by course from database: %w", err)
		if !errors.Is(err, sql.ErrNoRows) {
			log.Println(err.Error())
		}
		return nil, err
	}
	return r.getFromRows(rows)
}

func (r *OfficeHourRepo) FindByRoom(room models.Room, activeOnly bool) ([]models.OfficeHour, error) {
	var rows *sql.Rows
	var err error
	if activeOnly {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE room=? AND active", room.Id)
	} else {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE room=?", room.Id)
	}
	if err != nil {
		err = fmt.Errorf("error getting officeHours by room from database: %w", err)
		if !errors.Is(err, sql.ErrNoRows) {
			log.Println(err.Error())
		}
		return nil, err
	}
	defer rows.Close()
	return r.getFromRows(rows)
}

func (r *OfficeHourRepo) FindById(id int) (models.OfficeHour, error) {
	return r.getFromRow(r.db.QueryRow("SELECT * FROM officeHour WHERE id=?", id))
}

// Add an office hour if it doesn't exist yet.
// Also add the incluyey tutor if it doesn't exist yet.
//
// Returns the id of the new office hour.
func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) (int, error) {
	// Find correct tutor or add if not existent
	_, err := r.tutorRepo.Add(officeHour.Tutor)
	if err != nil {
		return 0, err
	}
	officeHour.Tutor, err = r.tutorRepo.FindByNameAndEmail(officeHour.Tutor.Name, officeHour.Tutor.Email)
	if err != nil {
		err = fmt.Errorf("newly added tutor not found: %w", err)
		log.Println(err.Error())
		return 0, err
	}

	//Don't add identical officeHours
	officeHours, err := r.FindByCourse(officeHour.Course, false)
	if err != nil {
		return 0, err
	}
	for _, oldOfficeHour := range officeHours {
		if officeHour.Tutor == oldOfficeHour.Tutor &&
			officeHour.Date == oldOfficeHour.Date &&
			officeHour.Room == oldOfficeHour.Room &&
			// officeHour.Course == oldOfficeHour.Course && already filtered above
			officeHour.Info == oldOfficeHour.Info &&
			officeHour.Active == oldOfficeHour.Active &&
			officeHour.Duration == oldOfficeHour.Duration {
			return oldOfficeHour.Id, nil
		}
	}

	sqlResult, err := r.db.Exec("INSERT INTO `officeHour` (tutor, day, hour, minute, room, roomname, course, week, info, active, duration) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
		officeHour.Tutor.Id,
		officeHour.Date.Day,
		officeHour.Date.Hour,
		officeHour.Date.Minute,
		officeHour.Room.Id,
		officeHour.RoomName,
		officeHour.Course.Id,
		officeHour.Date.Week,
		officeHour.Info,
		officeHour.Active,
		officeHour.Duration)
	if err != nil {
		return 0, fmt.Errorf("SQL-error inserting new office hour: %w", err)
	}
	id, lastInsertIdErr := sqlResult.LastInsertId()
	if lastInsertIdErr != nil {
		log.Printf("error getting Id for new tutor: %s", lastInsertIdErr.Error())
	}
	return int(id), err
}

func (r *OfficeHourRepo) Delete(officeHour models.OfficeHour) error {
	_, err := r.db.Exec("DELETE FROM officeHour WHERE id=?", officeHour.Id)
	if err != nil {
		err = fmt.Errorf("error deleting officeHour from database: %w", err)
		log.Println(err.Error())
		return err
	}
	return nil
}

func (r *OfficeHourRepo) getFromRow(row *sql.Row) (models.OfficeHour, error) {
	var officeHour models.OfficeHour
	var week, day, hour, minute, tutorId, courseId, roomId int
	err := row.Scan(&officeHour.Id, &tutorId, &day, &hour, &minute, &roomId, &officeHour.RoomName, &courseId, &week, &officeHour.Info, &officeHour.Active, &officeHour.Duration)
	if err != nil {
		err = fmt.Errorf("error getting single officeHour row from database: %w", err)
		log.Println(err.Error())
		return models.OfficeHour{}, err
	}
	officeHour.Date = models.Date{Week: week, Day: day, Hour: hour, Minute: minute}
	officeHour.Room, err = r.roomRepo.FindById(roomId)
	if err != nil {
		err = fmt.Errorf("error finding room by id %d for office hour: %w", roomId, err)
		log.Println(err.Error())
		return officeHour, err
	}
	officeHour.Tutor, err = r.tutorRepo.FindById(tutorId)
	if err != nil {
		err = fmt.Errorf("error finding tutor by id %d for office hour: %w", tutorId, err)
		log.Println(err.Error())
		return officeHour, err
	}
	officeHour.Course, err = r.courseRepo.FindById(courseId)
	if err != nil {
		err = fmt.Errorf("error finding course by id %d for office hour: %w", courseId, err)
		log.Println(err.Error())
		return officeHour, err
	}
	return officeHour, nil
}

func (r *OfficeHourRepo) getFromRows(rows *sql.Rows) ([]models.OfficeHour, error) {
	var officeHours []models.OfficeHour
	for rows.Next() {
		var officeHour models.OfficeHour
		var week, day, hour, minute, tutorId, roomId, courseId int
		var err error
		if err := rows.Scan(&officeHour.Id, &tutorId, &day, &hour, &minute, &roomId, &officeHour.RoomName, &courseId, &week, &officeHour.Info, &officeHour.Active, &officeHour.Duration); err != nil {
			return officeHours, fmt.Errorf("error getting multiple officeHour rows from database: %w", err)
		}
		officeHour.Date = models.Date{Week: week, Day: day, Hour: hour, Minute: minute}
		officeHour.Room, err = r.roomRepo.FindById(roomId)
		if err != nil {
			err = fmt.Errorf("error finding room by id %d for office hour: %w", roomId, err)
			log.Println(err.Error())
			return officeHours, err
		}
		officeHour.Tutor, err = r.tutorRepo.FindById(tutorId)
		if err != nil {
			err = fmt.Errorf("error finding tutor by id %d for office hour: %w", tutorId, err)
			log.Println(err.Error())
			return officeHours, err
		}
		officeHour.Course, err = r.courseRepo.FindById(courseId)
		if err != nil {
			err = fmt.Errorf("error finding course by id %d for office hour: %w", courseId, err)
			log.Println(err.Error())
			return officeHours, err
		}
		officeHours = append(officeHours, officeHour)
	}
	return officeHours, nil
}

// Get the number of office hours that are maximally parallel during a time span.
func (r *OfficeHourRepo) NumberByTimeSpanAndRoom(date models.Date, duration int, room models.Room, activeOnly bool) (int, error) {
	var rows *sql.Rows
	var err error
	if activeOnly {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE room=? AND day =? AND active", room.Id, date.Day)
	} else {
		rows, err = r.db.Query("SELECT * FROM officeHour WHERE room=?", room.Id)
	}
	if err != nil {
		err = fmt.Errorf("error getting officeHours by timespan and room from database: %w", err)
		log.Println(err.Error())
		return 0, err
	}
	defer rows.Close()
	officeHours, err := r.getFromRows(rows)
	if err != nil {
		return 0, err
	}
	var count int
	// iterate over all points in the new officehour to get the maximum parallel officehours
	for minute := 0; minute < duration; minute += r.config.Date.MinuteGranularity {
		var minuteCount int = 0
		for _, officeHour := range officeHours {
			// increase count if officehour starts before this point in time and ends later
			if models.DateLess(officeHour.Date, models.GetEndDate(date, minute, false)) && models.DateLess(models.GetEndDate(date, minute, false), models.GetEndDate(officeHour.Date, officeHour.Duration, false)) {
				if date.Week == 0 || officeHour.Week == 0 || date.Week == officeHour.Week { // office hours in alternating weeks should not collide
					minuteCount += 1
				}
			}
		}
		if minuteCount > count {
			count = minuteCount
		}
	}
	return count, nil
}

// Check whether the room capacity allows for another office hour during a time span.
func (r *OfficeHourRepo) AllowedAt(date models.Date, duration int, room models.Room, activeOnly bool) (bool, error) {
	numberOfOfficeHours, err := r.NumberByTimeSpanAndRoom(date, duration, room, activeOnly)
	if err != nil {
		return false, err
	}
	return numberOfOfficeHours < room.MaxOccupy, nil
}
