package orm

import (
	"errors"
	"fmt"
	"strings"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"gogs.carducci-dante.gov.it/karmen/datasource"
)

const (
	ADMINISTRATIVE_ROLE_PERSONAL = iota
	ADMINISTRATIVE_ROLE_TEACHING
	ADMINISTRATIVE_ROLE_DSGA
	ADMINISTRATIVE_ROLE_SCHOOL_CARETAKER
	ADMINISTRATIVE_ROLE_TECHNICAL_ASSISTANT
	ADMINISTRATIVE_ROLE_LIBRARIAN
	ADMINISTRATIVE_ROLE_UNDEFINED

	IssueTeacherHours = iota
)

var currDB *gorm.DB

type Timetable [][]Activity

type Person interface {
	GetCredential() *Credential
}

type TheBoss struct {
	gorm.Model
	Credential
	Teachers []Teacher
}

type Address struct {
	City    string
	Street  string
	ZipCode string
	Country string
}

type Location struct {
	Id      string
	Name    string
	Address *Address
}

type Room struct {
	Id       string
	Name     string
	Capacity int
	Location *Location
}

type Desk struct {
	Id       string
	Name     string
	Students []*Student
	Teacher  *Teacher
}

type Student struct {
	gorm.Model
	Credential
	Class *Class
}

type Director struct {
	gorm.Model
	Credential
}

type Administrative struct {
	gorm.Model
	Credential
	Role int
}

type Office struct {
	gorm.Model
	Credential
	Persons []Person
}

type Department struct {
	gorm.Model
	Name     string
	Subjects []Subject
	Teachers []Teacher `gorm:"many2many:department_teachers;"`
}

func (b *TheBoss) Create(db *gorm.DB, record map[string]interface{}) error {
	if record["role"] == "boss" {
		result := new(TheBoss)
		result.Name = record["name"].(string)
		result.Surname = record["surname"].(string)
		result.Username = result.GenerateUsername()
		result.Password = result.GenerateSaltedPassword(result.Username)
		db.Create(result)
	} else {
		boss := TheBoss{}
		teacher := Teacher{}
		db.Model(TheBoss{}).First(&boss)
		db.Where("username=?", record["username"]).First(&teacher)
		db.Model(&boss).Association("Teachers").Append(&teacher)
	}

	return nil
}

func (a *Administrative) Create(db *gorm.DB, record map[string]interface{}) error {
	result := Administrative{}
	result.Name = record["name"].(string)
	result.Surname = record["surname"].(string)
	result.Username = result.GenerateUsername()
	result.Password = result.GenerateSaltedPassword(result.Username)

	switch record["role"] {
	case "personal_administrator":
		result.Role = ADMINISTRATIVE_ROLE_PERSONAL
	case "teaching_administrator":
		result.Role = ADMINISTRATIVE_ROLE_TEACHING
	case "school_caretaker":
		result.Role = ADMINISTRATIVE_ROLE_SCHOOL_CARETAKER
	case "technical_assistant":
		result.Role = ADMINISTRATIVE_ROLE_TECHNICAL_ASSISTANT
	case "librarian":
		result.Role = ADMINISTRATIVE_ROLE_LIBRARIAN
	case "dsga":
		result.Role = ADMINISTRATIVE_ROLE_DSGA
	default:
		result.Role = ADMINISTRATIVE_ROLE_UNDEFINED
	}

	db.Create(&result)
	return nil
}

func (d *Director) Create(db *gorm.DB, record map[string]interface{}) error {
	result := new(Director)

	if name := record["name"]; name == nil {
		return errors.New("Error in creating director: field name is empty")
	} else {
		result.Name = name.(string)
	}
	if surname := record["surname"]; surname == nil {
		return errors.New("Error in creating director: field surname is empty")
	} else {
		result.Surname = surname.(string)
	}

	// Generate username and initial password

	result.Username = result.GenerateUsername()
	result.Password = result.GenerateSaltedPassword(result.Username)

	db.Set("gorm:save_associations", false).Create(result)

	return nil
}

func (t *Student) Create(db *gorm.DB, record map[string]interface{}) error {
	result := new(Student)

	if name := record["name"]; name == nil {
		return errors.New("Error in creating student: field name is empty")
	} else {
		result.Name = name.(string)
	}
	if surname := record["surname"]; surname == nil {
		return errors.New("Error in creating student: field surname is empty")
	} else {
		result.Surname = surname.(string)
	}

	// Generate username and initial password

	result.Username = result.GenerateUsername()
	result.Password = result.GenerateSaltedPassword(result.Username)

	db.Set("gorm:save_associations", false).Create(result)

	return nil
}

func (d *Department) Create(db *gorm.DB, record map[string]interface{}) error {
	result := new(Department)

	if name := record["name"]; name == nil {
		return errors.New("Error in updating subject: field name is empty")
	} else {
		result.Name = name.(string)
	}

	db.Set("gorm:save_associations", false).Create(result)

	// Handle department-subjects relationship

	for _, s := range record["subjects"].([]interface{}) {
		subject := Subject{}
		db.Where("name=?", strings.TrimSpace(s.(string))).Find(&subject)
		db.Model(result).Association("Subjects").Append(subject)
	}

	// Handle department-teachers relationship

	allTeachers := make([]Teacher, 0)
	db.Find(&allTeachers)

	for _, t := range allTeachers {
		db.Model(&t).Related(&t.Subjects, "Subjects")
		for _, s := range t.Subjects {
			for _, depS := range result.Subjects {
				if s.Name == depS.Name {
					db.Model(result).Association("Teachers").Append(t)
				}
			}
		}
	}

	return nil
}

func Import(importable datasource.Importable, importer datasource.Importer) error {
	for _, record := range importer.Records() {
		err := importable.Create(currDB, record)
		if err != nil {
			return err
		}
	}
	return nil
}

func New(connection string) (*gorm.DB, error) {
	db, err := gorm.Open("mysql", connection)
	if err != nil {
		return nil, err
	}
	return db, nil
}

func Use(db *gorm.DB) {
	currDB = db
}

func DB() *gorm.DB {
	return currDB
}

func Reset() {
	currDB.DropTableIfExists(
		&School{},
		&Subject{},
		&Teacher{},
		&Class{},
		&Activity{},
		&Student{},
		&Department{},
		&TheBoss{},
		&Administrative{},
		&Director{},
		&Issue{},
	)

	currDB.Exec("TRUNCATE teacher_subjects;")
	// 	currDB.Exec("TRUNCATE teacher_classes;")
	currDB.Exec("TRUNCATE department_teachers;")
}

func AutoMigrate() {
	if err := currDB.AutoMigrate(
		&School{},
		&Subject{},
		&Teacher{},
		&Class{},
		&Activity{},
		&Department{},
		&TheBoss{},
		&Administrative{},
		&Student{},
		&Director{},
		&Issue{},
	).Error; err != nil {
		panic(err)
	}

}

func GetTeacher(username string) (*Teacher, error) {
	teacher := Teacher{}
	currDB.Where(fmt.Sprintf("username='%s'", username)).Find(&teacher)
	return &teacher, nil
}
