package database

import (
	"embed"
	"errors"
	"fmt"
	"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
	DeleteProject(name string, username string) error
	Migrate() error
	MigrateSampleData() 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
	ChangeUserName(username string, newname 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)
	GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
	GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error)
	GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error)
	SignWeeklyReport(reportId int, projectManagerId int) error
	IsSiteAdmin(username string) (bool, error)
	IsProjectManager(username string, projectname string) (bool, error)
	GetProjectTimes(projectName string) (map[string]int, error)
	UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
	RemoveProject(projectname 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

//go:embed sample_data
var sampleData 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) VALUES (?, ?, (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 OR IGNORE INTO user_roles (user_id, project_id, p_role)
			  				VALUES ((SELECT id FROM users WHERE username = ?),
				  			(SELECT id FROM projects WHERE name = ?), ?)`
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)"
const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p	
							JOIN user_roles ur ON p.id = ur.project_id
							JOIN users u ON ur.user_id = u.id
							WHERE u.username = ?`
const deleteProject = `DELETE FROM projects
						WHERE id = ? AND owner_username = ?`

const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles
								JOIN users ON user_roles.user_id = users.id
								JOIN projects ON user_roles.project_id = projects.id
								WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'`

// 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.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId)
	if err != nil {
		println("Error getting project: ", err)
	}
	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 {
	_, err := d.Exec(addUserToProject, username, projectname, role)
	return err
}

// ChangeUserRole changes the role of a user within a project.
func (d *Db) ChangeUserRole(username string, projectname string, role string) error {
	// Execute the SQL query to change the user's role
	_, err := d.Exec(changeUserRole, role, username, projectname)
	return err
}

// ChangeUserName changes the username of a user.
func (d *Db) ChangeUserName(username string, newname string) error {
	// Execute the SQL query to update the username
	_, err := d.Exec("UPDATE users SET username = ? WHERE username = ?", newname, username)
	return err
}

// 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 {
	tx := d.MustBegin()
	// Insert the project into the database
	_, err := tx.Exec(projectInsert, name, description, username)
	if err != nil {
		if err := tx.Rollback(); err != nil {
			return err
		}
		return err
	}

	// Add creator to project as project manager
	_, err = tx.Exec(addUserToProject, username, name, "project_manager")
	if err != nil {
		if err := tx.Rollback(); err != nil {
			return err
		}
		return err
	}
	if err := tx.Commit(); err != nil {
		return err
	}

	return err
}

func (d *Db) DeleteProject(projectID string, username string) error {
	tx := d.MustBegin()

	_, err := tx.Exec(deleteProject, projectID, username)

	if err != nil {
		if rollbackErr := tx.Rollback(); rollbackErr != nil {
			return fmt.Errorf("error rolling back transaction: %v, delete error: %v", rollbackErr, err)
		}
		panic(err)
	}

	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
}

func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) {
	var report types.WeeklyReport
	query := `
        SELECT
            report_id,
            user_id,
            project_id,
            week,
            development_time,
            meeting_time,
            admin_time,
            own_work_time,
            study_time,
            testing_time,
			signed_by
        FROM
            weekly_reports
        WHERE
            user_id = (SELECT id FROM users WHERE username = ?)
            AND project_id = (SELECT id FROM projects WHERE name = ?)
            AND week = ?
    `
	err := d.Get(&report, query, username, projectName, week)
	return report, err
}

// SignWeeklyReport signs a weekly report by updating the signed_by field
// with the provided project manager's ID, but only if the project manager
// is in the same project as the report
func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
	// Retrieve the project ID associated with the report
	var reportProjectID int
	err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId)
	if err != nil {
		return err
	}

	// Retrieve the project ID associated with the project manager
	var managerProjectID int
	err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId)
	if err != nil {
		return err
	}

	// Check if the project manager is in the same project as the report
	if reportProjectID != managerProjectID {
		return errors.New("project manager doesn't have permission to sign the report")
	}

	// Update the signed_by field of the specified report
	_, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId)
	return err
}

func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) {
	// Define the SQL query to fetch unsigned reports for a given user
	query := `
		SELECT
			report_id,
			user_id,
			project_id,
			week,
			development_time,
			meeting_time,
			admin_time,
			own_work_time,
			study_time,
			testing_time,
			signed_by
		FROM
			weekly_reports
		WHERE
			signed_by IS NULL
			AND project_id = (SELECT id FROM projects WHERE 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 reports []types.WeeklyReport
	for rows.Next() {
		var report types.WeeklyReport
		if err := rows.StructScan(&report); err != nil {
			return nil, err
		}
		reports = append(reports, report)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return reports, nil
}

// IsSiteAdmin checks if a given username is a site admin
func (d *Db) IsSiteAdmin(username string) (bool, error) {
	// Define the SQL query to check if the user is a site admin
	query := `
        SELECT COUNT(*) FROM site_admin 
        JOIN users ON site_admin.admin_id = users.id 
        WHERE users.username = ?
    `

	// Execute the query
	var count int
	err := d.Get(&count, query, username)
	if err != nil {
		return false, err
	}

	// If count is greater than 0, the user is a site admin
	return count > 0, 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() error {
	// Read the embedded scripts directory
	files, err := scripts.ReadDir("migrations")
	if err != nil {
		return err
	}

	if len(files) == 0 {
		println("No migration files found")
		return nil
	}

	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
		sqlBytes, err := scripts.ReadFile("migrations/" + file.Name())
		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
}

// GetWeeklyReportsUser retrieves weekly reports for a specific user and project.
func (d *Db) GetWeeklyReportsUser(username string, projectName string) ([]types.WeeklyReportList, error) {
	query := `
		SELECT
			wr.week,
			wr.development_time,
			wr.meeting_time,
			wr.admin_time,
			wr.own_work_time,
			wr.study_time,
			wr.testing_time,
			wr.signed_by
		FROM
			weekly_reports wr
		INNER JOIN
			users u ON wr.user_id = u.id
		INNER JOIN
			projects p ON wr.project_id = p.id
		WHERE
			u.username = ? AND p.name = ?
	`

	var reports []types.WeeklyReportList
	if err := d.Select(&reports, query, username, projectName); err != nil {
		return nil, err
	}

	return reports, nil
}

// IsProjectManager checks if a given username is a project manager for the specified project
func (d *Db) IsProjectManager(username string, projectname string) (bool, error) {
	var manager bool
	err := d.Get(&manager, isProjectManagerQuery, username, projectname)
	return manager, err
}

func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error {
	query := `
		UPDATE weekly_reports
		SET
			development_time = ?,
			meeting_time = ?,
			admin_time = ?,
			own_work_time = ?,
			study_time = ?,
			testing_time = ?
		WHERE
			user_id = (SELECT id FROM users WHERE username = ?)
			AND project_id = (SELECT id FROM projects WHERE name = ?)
			AND week = ?
	`

	_, err := d.Exec(query, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime, userName, projectName, week)
	return err
}

// MigrateSampleData applies sample data to the database.
func (d *Db) MigrateSampleData() error {
	// Insert sample data
	files, err := sampleData.ReadDir("sample_data")
	if err != nil {
		return err
	}

	if len(files) == 0 {
		println("No sample data files found")
	}
	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
		sqlBytes, err := sampleData.ReadFile("sample_data/" + file.Name())
		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
}

// GetProjectTimes retrieves a map with times per "Activity" for a given project
func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) {
	query := `
        SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time
        FROM weekly_reports
        JOIN projects ON weekly_reports.project_id = projects.id
        WHERE projects.name = ?
    `

	rows, err := d.DB.Query(query, projectName)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	totalTime := make(map[string]int)

	for rows.Next() {
		var developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime int
		if err := rows.Scan(&developmentTime, &meetingTime, &adminTime, &ownWorkTime, &studyTime, &testingTime); err != nil {
			return nil, err
		}

		totalTime["development"] += developmentTime
		totalTime["meeting"] += meetingTime
		totalTime["admin"] += adminTime
		totalTime["own_work"] += ownWorkTime
		totalTime["study"] += studyTime
		totalTime["testing"] += testingTime
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return totalTime, nil
}

func (d *Db) RemoveProject(projectname string) error {
	_, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname)
	return err
}