package manager

import (
	"encoding/base64"
	"fmt"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/deranjer/gvc/common/database"
	engine "github.com/deranjer/gvc/common/engine"
	"github.com/rs/zerolog"
)

// NewManager creates a new manager interface that contains all the needed information to make changes to the repo
// rootPath is passed by client or server to let the manager know where to look for the .gvc folder and all the components needed
func NewManager(rootDir string, version string, dbPath string, informer chan OperatingMessage, dirPaths *FilePaths, log *zerolog.Logger) (*Manager, error) {
	log.Info().Msg("Creating new Manager...")

	// Create new patcher
	patcher := engine.Patcher{
		Logger:         log,
		KeyFolder:      dirPaths.KeyFolder,
		DownloadFolder: dirPaths.DownloadFolder,
		SyncFolder:     dirPaths.SyncFolder,
		ThumbFolder:    dirPaths.ThumbFolder,
		DiffFolder:     dirPaths.DiffFolder,
	}
	gvcDB, err := database.OpenOrCreateDB(dbPath, log)
	if err != nil {
		log.Fatal().Msgf("unable to create or open db: %s", err)
	}

	m := Manager{
		version,

		//settings,
		log,
		patcher,
		gvcDB,
		informer,
		dirPaths,
	}
	return &m, nil
}

// CheckPaths just checks the .gvc folder structure
func CheckPaths(rootDir string) (filePaths *FilePaths, err error) {
	// checking for the .gvc folder (the client (but not the server) already checks for the .gvc folder, but this checks all subdirects to make sure they are there)
	rootFolder, err := filepath.Abs(rootDir)
	if err != nil {
		return &FilePaths{}, err
	}
	path := rootFolder + string(filepath.Separator) + ".gvc"
	//path = filepath.Join(rootFolder, filepath.Separator+".gvc")
	var fullFilePaths FilePaths
	//where private and public keys are kept
	fullFilePaths.KeyFolder = filepath.Join(path, "keys")
	//where downloaded files start
	fullFilePaths.DownloadFolder = filepath.Join(path, "downloads")
	//where file originals live
	fullFilePaths.SyncFolder = filepath.Join(path, "sync")
	//where patches and last versions live
	fullFilePaths.DiffFolder = filepath.Join(path, "diff")
	//where the thumbnails are stored
	fullFilePaths.ThumbFolder = filepath.Join(path, "thumb")
	//where the logs are stored
	fullFilePaths.LogFolder = filepath.Join(path, "logs")
	//where plugins are stored
	fullFilePaths.PluginFolder = filepath.Join(path, "plugins")

	engine.InitiateDirectory(fullFilePaths.KeyFolder)
	engine.InitiateDirectory(fullFilePaths.DownloadFolder)
	engine.InitiateDirectory(fullFilePaths.SyncFolder)
	engine.InitiateDirectory(fullFilePaths.DiffFolder)
	engine.InitiateDirectory(fullFilePaths.ThumbFolder)
	engine.InitiateDirectory(fullFilePaths.LogFolder)
	engine.InitiateDirectory(fullFilePaths.PluginFolder)
	return &fullFilePaths, nil
}

// This adds a file for the watcher to keep an eye on
// however the file will also need to be backedup
// and added to the database.
// This changes all paths to absolute paths rather than relative
// when adding a file to monitor, this should check if the database
// is already expecting to monitor this file. If it is this function should
// do checks to make sure that it is successfully monitoring it, and that there
// is a historical breadcrumb trail to recreate all the versions that the database
// claims to have a copy of
func (m *Manager) AddFileToRepo(relFilePath string) error {
	var err error
	// the filepath should be absolute, but this should be done dynamically
	// if file, err = filepath.Abs(file); err != nil {
	// 	return "", err
	// }
	//TODO: what needs to happen is a channel for errors/progress is created
	//then pass that channel to a routine, and put all of the following in it
	// whenever an error returns, fire the string to the channel,
	// or send progress on the progress channel
	//however need to work out best way of returning the final result to the caller
	//- the way to do that is send the result on a third channel, for which is just the result
	//see commsManagment.go
	// f := NewFileManager()
	//DELAYED: this feature affects only large files and user experience. It can wait.
	relFilePath = strings.TrimSpace(relFilePath) //purging any odd spaces TODO: Make sure not needed
	var tmpFile database.File
	filename := filepath.Base(relFilePath)
	var hash [16]byte
	//check that the file actually exists (currently done by client/server)
	// if filename, err = engine.VerifySrcFile(relFilePath); err != nil {
	// 	//there was no source file or it was not recognisable as a file
	// 	return "", err
	// }
	//generate a unique file name from the hash and the moment it was created
	//a sampled (and therefore) fast, hash of the file for 'uniqueness'
	if hash, err = engine.UniqueFileHash(relFilePath); err != nil {
		return err
	}
	alreadyTracked := m.dB.CheckIfFileCurrentlyMonitored(relFilePath)
	if alreadyTracked {
		return fmt.Errorf("file already found in tracked files, not adding: %s", relFilePath)
	}
	tmpFile = database.File{}

	tmpFile.CurrentHash = hash
	tmpFile.Name = filename
	tmpFile.Path = relFilePath
	tmpFile.CreatedAt = time.Now()
	tmpFile.Unique = base64.URLEncoding.EncodeToString([]byte(filename)) + "_" + base64.URLEncoding.EncodeToString((tmpFile.CurrentHash[:])) + "_" + strconv.FormatInt(tmpFile.CreatedAt.Unix(), 10) + "_" + filename
	//tmpFile.BkpLocation = filepath.Join(m.SyncFolder, tmpFile.Unique)
	//tmpFile.CurrentBase = tmpFile.BkpLocation
	//tmpFile.Ignore = false //we can have files in the database that are ignored. TODO: This was initially added so that 'All Files' would show up as a file (its a hack as it adds a dummy to the database)
	//we should now have a unique name for this file
	//if needs be, we can find out the real file name from the string
	//the hash will give us a reasonable indication of the similarity of the files
	//define filename of backup(s)
	_, err = m.prepareDatabaseForFile(tmpFile)
	if err != nil {
		return err
	}

	m.Info().Msgf("added file: %s at path: %s with hash: %s at time: %s", filename, relFilePath, tmpFile.CurrentHash, tmpFile.CreatedAt.String)
	return nil
}

// prepareDatabaseForFile is responsible for keeping all references to the version of the file,
// the diff and the metadata of the diffs. Before any file is copied and stored, it should be managed by the database
//
// TODO: This will need to initialise a diff object in the database, currently created by the diff package,
// however going forward a diff maybe defined by the manager.
func (m *Manager) prepareDatabaseForFile(tmpFile database.File) (int, error) {
	fileID, err := m.dB.InitializeFileInDatabase(tmpFile)
	if err != nil {
		m.Error().Msgf("Error checking if file [%s] is monitored. Error %s", tmpFile.Path, err)
		return 0, err
	}
	return fileID, nil

}

// FetchTrackedFiles just grabbes all the files currently tracked in the repo
func (m *Manager) FetchTrackedFiles() ([]database.File, error) {
	files, err := m.dB.RetrieveTrackedFiles()
	if err != nil {
		return nil, fmt.Errorf("unable to retrieve tracked files: %s", err)
	}
	return files, nil
}
