package database

import (
	"embed"
	"os"
	"path/filepath"
	"ttime/internal/types"

	"github.com/jmoiron/sqlx"
	_ "modernc.org/sqlite"
)

// Interface for the database
type Database interface {
	// Insert a new user into the database, password should be hashed before calling
	AddUser(username string, password string) error
	CheckUser(username string, password string) bool
	RemoveUser(username string) error
	PromoteToAdmin(username string) error
	GetUserId(username string) (int, error)
	AddProject(name string, description string, username string) error
	Migrate(dirname string) error
	GetProjectId(projectname string) (int, error)
	AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
	AddUserToProject(username string, projectname string, role string) error
	ChangeUserRole(username string, projectname string, role string) error
	GetAllUsersProject(projectname string) ([]UserProjectMember, error)
	GetAllUsersApplication() ([]string, error)
	GetProjectsForUser(username string) ([]types.Project, error)
	GetAllProjects() ([]types.Project, error)
	GetProject(projectId int) (types.Project, error)
	GetUserRole(username string, projectname string) (string, error)
}

// This struct is a wrapper type that holds the database connection
// Internally DB holds a connection pool, so it's safe for concurrent use
type Db struct {
	*sqlx.DB
}

type UserProjectMember struct {
	Username string `db:"username"`
	UserRole string `db:"p_role"`
}

//go:embed migrations
var scripts embed.FS

// TODO: Possibly break these out into separate files bundled with the embed package?
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?"
const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?"
const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), 
						ProjectLookup AS (SELECT id FROM projects WHERE name = ?)
						INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time)
						VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);`
const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?"

const getProjectsForUser = `SELECT projects.id, projects.name, projects.description, projects.owner_user_id 
							FROM projects JOIN user_roles ON projects.id = user_roles.project_id
							JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;`

// DbConnect connects to the database
func DbConnect(dbpath string) Database {
	// Open the database
	db, err := sqlx.Connect("sqlite", dbpath)
	if err != nil {
		panic(err)
	}

	// Ping forces the connection to be established
	err = db.Ping()
	if err != nil {
		panic(err)
	}

	return &Db{db}
}

func (d *Db) CheckUser(username string, password string) bool {
	var dbPassword string
	err := d.Get(&dbPassword, "SELECT password FROM users WHERE username = ?", username)
	if err != nil {
		return false
	}
	return dbPassword == password
}

// GetProjectsForUser retrieves all projects associated with a specific user.
func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) {
	var projects []types.Project
	err := d.Select(&projects, getProjectsForUser, username)
	return projects, err
}

// GetAllProjects retrieves all projects from the database.
func (d *Db) GetAllProjects() ([]types.Project, error) {
	var projects []types.Project
	err := d.Select(&projects, "SELECT * FROM projects")
	return projects, err
}

// GetProject retrieves a specific project by its ID.
func (d *Db) GetProject(projectId int) (types.Project, error) {
	var project types.Project
	err := d.Select(&project, "SELECT * FROM projects WHERE id = ?")
	return project, err
}

func (d *Db) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error {
	_, err := d.Exec(addWeeklyReport, userName, projectName, week, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime)
	return err
}

// AddUserToProject adds a user to a project with a specified role.
func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP
	var userid int
	userid, err := d.GetUserId(username)
	if err != nil {
		panic(err)
	}

	var projectid int
	projectid, err2 := d.GetProjectId(projectname)
	if err2 != nil {
		panic(err2)
	}

	_, err3 := d.Exec(addUserToProject, userid, projectid, role)
	return err3
}

// ChangeUserRole changes the role of a user within a project.
func (d *Db) ChangeUserRole(username string, projectname string, role string) error {
	// Get the user ID
	var userid int
	userid, err := d.GetUserId(username)
	if err != nil {
		panic(err)
	}

	// Get the project ID
	var projectid int
	projectid, err2 := d.GetProjectId(projectname)
	if err2 != nil {
		panic(err2)
	}

	// Execute the SQL query to change the user's role
	_, err3 := d.Exec(changeUserRole, role, userid, projectid)
	return err3
}

// GetUserRole retrieves the role of a user within a project.
func (d *Db) GetUserRole(username string, projectname string) (string, error) {
	var role string
	err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname)
	return role, err
}

// AddUser adds a user to the database
func (d *Db) AddUser(username string, password string) error {
	_, err := d.Exec(userInsert, username, password)
	return err
}

// Removes a user from the database
func (d *Db) RemoveUser(username string) error {
	_, err := d.Exec("DELETE FROM users WHERE username = ?", username)
	return err
}

func (d *Db) PromoteToAdmin(username string) error {
	_, err := d.Exec(promoteToAdmin, username)
	return err
}

func (d *Db) GetUserId(username string) (int, error) {
	var id int
	err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) // Borde det inte vara "user" i singular
	return id, err
}

func (d *Db) GetProjectId(projectname string) (int, error) {
	var id int
	err := d.Get(&id, "SELECT id FROM projects WHERE name = ?", projectname)
	return id, err
}

// Creates a new project in the database, associated with a user
func (d *Db) AddProject(name string, description string, username string) error {
	_, err := d.Exec(projectInsert, name, description, username)
	return err
}

func (d *Db) GetAllUsersProject(projectname string) ([]UserProjectMember, error) {
	// Define the SQL query to fetch users and their roles for a given project
	query := `
        SELECT u.username, ur.p_role
        FROM users u
        INNER JOIN user_roles ur ON u.id = ur.user_id
        INNER JOIN projects p ON ur.project_id = p.id
        WHERE p.name = ?
    `

	// Execute the query
	rows, err := d.Queryx(query, projectname)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	// Iterate over the rows and populate the result slice
	var users []UserProjectMember
	for rows.Next() {
		var user UserProjectMember
		if err := rows.StructScan(&user); err != nil {
			return nil, err
		}
		users = append(users, user)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return users, nil
}

// GetAllUsersApplication retrieves all usernames from the database
func (d *Db) GetAllUsersApplication() ([]string, error) {
	// Define the SQL query to fetch all usernames
	query := `
        SELECT username FROM users
    `

	// Execute the query
	rows, err := d.Queryx(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	// Iterate over the rows and populate the result slice
	var usernames []string
	for rows.Next() {
		var username string
		if err := rows.Scan(&username); err != nil {
			return nil, err
		}
		usernames = append(usernames, username)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return usernames, nil
}

// Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory
func (d *Db) Migrate(dirname string) error {
	// Read the embedded scripts directory
	files, err := scripts.ReadDir("migrations")
	if err != nil {
		return err
	}

	tr := d.MustBegin()

	// Iterate over each SQL file and execute it
	for _, file := range files {
		if file.IsDir() || filepath.Ext(file.Name()) != ".sql" {
			continue
		}

		// This is perhaps not the most elegant way to do this
		sqlFile := filepath.Join("migrations", file.Name())
		sqlBytes, err := os.ReadFile(sqlFile)
		if err != nil {
			return err
		}

		sqlQuery := string(sqlBytes)
		_, err = tr.Exec(sqlQuery)
		if err != nil {
			return err
		}
	}

	if tr.Commit() != nil {
		return err
	}

	return nil
}