package backup

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"time"

	"github.com/portainer/portainer/api/archive"
	"github.com/portainer/portainer/api/crypto"
	"github.com/portainer/portainer/api/dataservices"
	"github.com/portainer/portainer/api/filesystem"
	"github.com/portainer/portainer/api/http/offlinegate"
	"github.com/portainer/portainer/api/logs"

	"github.com/pkg/errors"
	"github.com/rs/zerolog/log"
)

const rwxr__r__ os.FileMode = 0o744

var filesToBackup = []string{
	"certs",
	"chisel",
	"compose",
	"config.json",
	"custom_templates",
	"edge_jobs",
	"edge_stacks",
	"extensions",
	"portainer.key",
	"portainer.pub",
	"tls",
}

// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
	backupDirPath, err := backupDatabaseAndFilesystem(gate, datastore, filestorePath)
	if err != nil {
		return "", err
	}

	archivePath, err := archive.TarGzDir(backupDirPath)
	if err != nil {
		return "", errors.Wrap(err, "Failed to make an archive")
	}

	if password != "" {
		archivePath, err = encrypt(archivePath, password)
		if err != nil {
			return "", errors.Wrap(err, "Failed to encrypt backup with the password")
		}
	}

	return archivePath, nil
}

func backupDatabaseAndFilesystem(gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
	unlock := gate.Lock()
	defer unlock()

	backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
	if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
		return "", errors.Wrap(err, "Failed to create backup dir")
	}

	// new export
	exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))

	if err := datastore.Export(exportFilename); err != nil {
		log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
	} else {
		log.Debug().Str("filename", exportFilename).Msg("file exported")
	}

	if err := backupDb(backupDirPath, datastore); err != nil {
		return "", errors.Wrap(err, "Failed to backup database")
	}

	for _, filename := range filesToBackup {
		if err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath); err != nil {
			return "", errors.Wrap(err, "Failed to create backup file")
		}
	}

	return backupDirPath, nil
}

func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
	dbFileName := datastore.Connection().GetDatabaseFileName()
	_, err := datastore.Backup(filepath.Join(backupDirPath, dbFileName))
	return err
}

func encrypt(path string, passphrase string) (string, error) {
	in, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer logs.CloseAndLogErr(in)

	outFileName := path + ".encrypted"
	out, err := os.Create(outFileName)
	if err != nil {
		return "", err
	}

	return outFileName, crypto.AesEncrypt(in, out, []byte(passphrase))
}
