package database

import (
	"encoding/hex"
	"fmt"

	"github.com/asdine/storm/q"
)

// CheckIfFileCurrentlyMonitored checks if the file is already being monitored. This is a read-only check
// to see whether the file was correctly initialised
// (BUG) The hash causes the same file to be in database multiple times!
func (db *DB) CheckIfFileCurrentlyMonitored(path string) bool {
	var file File
	//TODO: check this actually works still (don't need hash passed to this anymore)
	if err := db.One("Path", path, &file); err != nil {
		if err.Error() != "not found" {
			db.Err(err).Msg("Error finding file by path")
			fmt.Printf("Database Error occurred: %s error: %s\n", path, err)
			return false
		}
		db.Warn().Msgf("%s not found in tracked database", path)
		fmt.Printf("%s not found in tracked file database", path)
		return false
	}
	fmt.Printf("Found!: %s searched: %s\n", file.Path, path)
	return true
}

// RetrieveTrackedFiles all files that are in the database as "watched files"
// This can be used to trigger the same files to be watched again
func (db *DB) RetrieveTrackedFiles() ([]File, error) {
	var files []File
	fmt.Println("Starting file extraction")
	if err := db.All(&files); err != nil {
		db.Err(err).Msg("Error retrieving all watched files")
		return []File{}, err
	}
	fmt.Println("Ending file extraction")
	return files, nil
}

// RetrieveAllDiffs retrieves all files that are in the database as "watched files"
// This can be used to trigger the same files to be watched again
// func (db *DB) RetrieveAllDiffs() ([]DiffObject, error) {
// 	var diffs []DiffObject
// 	if err := db.All(&diffs); err != nil {
// 		db.Err(err).Msg("Error retrieving all diffs ")
// 		return []DiffObject{}, err
// 	} else {
// 		return diffs, nil
// 	}
// }

// InitialiseFileInDatabase should be called before any file is copied/renamed/diff'd/patched,
// and this should be checked before any operation occurs on a file. Any loss of data is completely as a result
// of losing references
func (db *DB) InitializeFileInDatabase(file File) (int, error) {
	if err := db.Save(&file); err != nil {
		db.Err(err).Msg("Error initialising file in database")
		return file.ID, err
	}
	return file.ID, nil
}

// FindFileByPath is a search function looking for file details
func (db *DB) FindFileByPath(filePath string) (File, error) {
	var file File
	if err := db.One("Path", filePath, &file); err != nil {
		db.Err(err).Msg("Error finding file by path")
		return File{}, err
	}
	return file, nil
}

// FindFileByID is a search function looking for file details
func (db *DB) FindFileByID(ID int) (File, error) {
	var file File
	if err := db.One("ID", ID, &file); err != nil {
		db.Err(err).Msg("Error finding file by path")
		return File{}, err
	}
	return file, nil
}

// UpdateFileData updates the current base file that diffs will compare to
func (db *DB) UpdateFileData(filePath, basePath string, hash []byte) error {
	if file, err := db.FindFileByPath(filePath); err != nil {
		db.Err(err).Msg("Error updating the file base")
		return err
	} else {
		err := db.Update(&File{ID: file.ID, BaseFilePath: basePath, Hash: hash})
		return err
	}
}

// RetrieveDiffsForFileByHash returns the diffs for a file. Diffs can be applied to a specific file (by its hash),
// so when looking for the diffs, the hash is a good place to start in terms of finding the diffs
func (db *DB) RetrieveDiffsForFileByHash(fileHash [16]byte, direction bool) ([]DiffObject, error) {
	var diffs []DiffObject
	var field string
	if direction {
		field = "ObjectHash"
	} else {
		field = "SubjectHash"
	}
	if err := db.Find(field, fileHash, &diffs); err != nil {
		return []DiffObject{}, err
	}
	return diffs, nil
}

// RetrieveDiffsForFileByPath returns the diffs for a file base on the file path. Diffs are very specific to a file,
// so this may not be as robust as searching by diff, however we may not have the diff available
func (db *DB) RetrieveDiffsForFileByPath(filePath string) ([]DiffObject, error) {
	var objDiffs []DiffObject
	var subDiffs []DiffObject
	if err := db.Find("Object", filePath, &objDiffs); err != nil && err.Error() != "not found" {
		db.Err(err).Msg("Error finding diff by object")
		return []DiffObject{}, err
	}
	if err := db.Find("Subject", filePath, &subDiffs); err != nil && err.Error() != "not found" {
		db.Err(err).Msg("Error finding diff by subject")
		return []DiffObject{}, err
	}
	return append(objDiffs, subDiffs...), nil
}

// StoreDiff just places the information about a diff in the database. Currently there is no protection
// to stop the entire diff entering the database (if fs is false), which may be very slow/bulky...
// TODO: decide what to do with diffs in memory
// func (db *DB) StoreDiff(diff DiffObject) error {
// 	if err := db.Save(&diff); err != nil {
// 		db.Err(err).Msg("Error storing the diff")
// 		return err
// 	}
// 	return nil
// }

// FindDiffByPath is a search function looking for a diff
func (db *DB) FindDiffByPath(patchPath string) (DiffObject, error) {
	var diff DiffObject
	if err := db.One("DiffPath", patchPath, &diff); err != nil {
		db.Err(err).Msg("Error finding diff by path")
		return DiffObject{}, err
	}
	return diff, nil
}

//RetrieveDiffsByID returns a diff based on the id it has in the database
func (db *DB) RetrieveDiffsByID(ID int) (DiffObject, error) {
	var diff DiffObject
	if err := db.One("ID", ID, &diff); err != nil {
		db.Err(err).Msg("Error finding diff by ID")
		return DiffObject{}, err
	}
	return diff, nil
}

// UpdateDescription is a simple function to set the label on a patch
func (db *DB) UpdateDescription(patchID int, description string) error {
	fmt.Println("attempting to path with id ", patchID, " description ", description)
	if err := db.Update(&DiffObject{ID: patchID}); err != nil {
		db.Err(err).Msg("Error changing diff label")
		return err
	}
	return nil
}

// FetchCommitByNumber fetches the commit number of the passed branch
func (db *DB) FetchCommitByNumber(branch string, commitNumber string) (commitResult Commit, err error) {
	var commit Commit
	db.Info().Msgf("fetching commit by number: %s in branch: %s", commitNumber, branch)
	query := db.Select(q.And(q.Eq("Branch", branch), q.Eq("Number", commitNumber)))
	err = query.Find(&commit)
	if err != nil {
		db.Err(err).Msgf("Failed to find commit by number: %s on branch: %s", commitNumber, branch)
		return commit, err
	}
	return commit, nil
}

// FetchCommitByHash fetches a single commit on any branch by hash //TODO: Hash collision?
func (db *DB) FetchCommitByHash(hash string) (commitResult Commit, err error) {
	var commit Commit
	db.Info().Msgf("Searching for commit by hash: %s", hash)
	if err := db.One("CommitHash", hash, &commit); err != nil {
		db.Err(err).Msgf("Failed to find commit by hash: %s", hash)
		return commit, err
	}
	return commit, nil
}

// FetchLastCommitOnBranch gets the latest commit to the provided branch
func (db *DB) FetchLastCommitOnBranch(branch string) (commitResult Commit, err error) {
	var commit Commit
	db.Info().Msgf("Fetching last commit on branch: %s", branch)
	query := db.Select(q.Eq("Branch", branch))             //Selecting everything that applies to that branch
	err = query.OrderBy("Number").Reverse().First(&commit) // Getting the last entry by number
	if err != nil {
		db.Err(err).Msgf("Failed to find last commit on branch: %s", branch)
		return commit, err
	}
	db.Info().Msgf("Last commit was of number: %d Last Commit Hash was: %s Last commit occurred on date: %s", commit.Number, hex.EncodeToString(commit.CommitHash), commit.Date)
	return commit, nil
}

// NewCommit writes a new commit to the specified branch into the database
func (db *DB) NewCommit(newCommit Commit, branch string) error {
	db.Info().Msgf("Writing new commit: %s on branch: %s", newCommit.Number, branch)
	err := db.Save(&newCommit)
	if err != nil {
		db.Err(err).Msg("failure to write commit to database")
		return err
	}
	return nil
}
