package filesharing

import (
	"crypto/rand"
	"cwtch.im/cwtch/event"
	"cwtch.im/cwtch/settings"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math"
	"os"
	path "path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	"cwtch.im/cwtch/model"
	"cwtch.im/cwtch/model/attr"
	"cwtch.im/cwtch/model/constants"
	"cwtch.im/cwtch/peer"
	"cwtch.im/cwtch/protocol/files"
	"git.openprivacy.ca/openprivacy/log"
)

// Functionality groups some common UI triggered functions for contacts...
type Functionality struct {
}

func (f *Functionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}

func (f *Functionality) EventsToRegister() []event.Type {
	return []event.Type{event.ProtocolEngineCreated, event.ManifestReceived, event.FileDownloaded}
}

func (f *Functionality) ExperimentsToRegister() []string {
	return []string{constants.FileSharingExperiment}
}

// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
	if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
		switch ev.EventType {
		case event.ProtocolEngineCreated:
			f.ReShareFiles(profile)
		case event.ManifestReceived:
			log.Debugf("Manifest Received Event!: %v", ev)
			handle := ev.Data[event.Handle]
			fileKey := ev.Data[event.FileKey]
			serializedManifest := ev.Data[event.SerializedManifest]

			manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
			if exists {
				downloadFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey))
				if exists {
					log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
					var manifest files.Manifest
					err := json.Unmarshal([]byte(serializedManifest), &manifest)

					if err == nil {
						// We only need to check the file size here, as manifest is sent to engine and the file created
						// will be bound to the size advertised in manifest.
						fileSizeLimitValue, fileSizeLimitExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey))
						if fileSizeLimitExists {
							fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, 64)
							if err == nil {
								if manifest.FileSizeInBytes >= fileSizeLimit {
									log.Debugf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue)
								} else {
									manifest.Title = manifest.FileName
									manifest.FileName = downloadFilePath
									log.Debugf("saving manifest")
									err = manifest.Save(manifestFilePath)
									if err != nil {
										log.Errorf("could not save manifest: %v", err)
									} else {
										tempFile := ""
										if runtime.GOOS == "android" {
											tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
											log.Debugf("derived android temp path: %v", tempFile)
										}
										profile.PublishEvent(event.NewEvent(event.ManifestSaved, map[event.Field]string{
											event.FileKey:            fileKey,
											event.Handle:             handle,
											event.SerializedManifest: string(manifest.Serialize()),
											event.TempFile:           tempFile,
											event.NameSuggestion:     manifest.Title,
										}))
									}
								}
							} else {
								log.Errorf("error saving manifest: file size limit is incorrect: %v", err)
							}
						} else {
							log.Errorf("error saving manifest: could not find file size limit info")
						}
					} else {
						log.Errorf("error saving manifest: %v", err)
					}
				} else {
					log.Errorf("found manifest path but not download path for %v", fileKey)
				}
			} else {
				log.Errorf("no download path found for manifest: %v", fileKey)
			}
		case event.FileDownloaded:
			fileKey := ev.Data[event.FileKey]
			profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true")
		}
	} else {
		log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
	}
}

func (f *Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
	// nop
}

func (f *Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
	// Profile should not call us if FileSharing is disabled
	if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
		scope, zone, zpath := path.GetScopeZonePath()
		log.Debugf("file sharing contact receive value")
		if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") {
			fileKey := strings.Replace(zpath, ".manifest.size", "", 1)
			size, err := strconv.Atoi(value)
			// if size is valid and below the maximum size for a manifest
			// this is to prevent malicious sharers from using large amounts of memory when distributing
			// a manifest as we reconstruct this in-memory
			if err == nil && size < files.MaxManifestSize {
				profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle}))
			} else {
				profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle}))
			}
		}
	} else {
		log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
	}
}

// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *Functionality {
	return new(Functionality)
}

// PreviewFunctionalityGate returns filesharing if image previews are enabled
func PreviewFunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
	if experimentMap[constants.FileSharingExperiment] && experimentMap[constants.ImagePreviewsExperiment] {
		return new(Functionality), nil
	}
	return nil, errors.New("image previews are not enabled")
}

// OverlayMessage presents the canonical format of the File Sharing functionality Overlay Message
// This is the format that the UI will parse to display the message
type OverlayMessage struct {
	Name  string `json:"f"`
	Hash  string `json:"h"`
	Nonce string `json:"n"`
	Size  uint64 `json:"s"`
}

// FileKey is the unique reference to a file offer
func (om *OverlayMessage) FileKey() string {
	return fmt.Sprintf("%s.%s", om.Hash, om.Nonce)
}

// ShouldAutoDL checks file size and file name. *DOES NOT* check user settings or contact state
func (om *OverlayMessage) ShouldAutoDL() bool {
	if om.Size > constants.ImagePreviewMaxSizeInBytes {
		return false
	}
	lname := strings.ToLower(om.Name)
	for _, s := range constants.AutoDLFileExts {
		if strings.HasSuffix(lname, s) {
			return true
		}
	}
	return false
}

func (f *Functionality) VerifyOrResumeDownloadDefaultLimit(profile peer.CwtchPeer, conversation int, fileKey string) error {
	return f.VerifyOrResumeDownload(profile, conversation, fileKey, files.MaxManifestSize*files.DefaultChunkSize)
}

func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversation int, fileKey string, size uint64) error {
	if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", fileKey)); exists {
		if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists {
			manifest, err := files.LoadManifest(manifestFilePath)
			if err == nil {
				// Assert the filename...this is technically not necessary, but is here for completeness
				manifest.FileName = downloadfilepath
				if manifest.VerifyFile() == nil {
					// Send a FileDownloaded Event. Usually when VerifyOrResumeDownload is triggered it's because some UI is awaiting the results of a
					// Download.
					ci, err := profile.GetConversationInfo(conversation)
					if ci == nil || err != nil {
						return err
					}
					profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{event.RemotePeer: ci.Handle, event.FileKey: fileKey, event.FilePath: downloadfilepath, event.TempFile: downloadfilepath}))
					// File is verified and there is nothing else to do...
					return nil
				}
			}
			// The manifest is corrupted, we need to fetch it again...
			return f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, size)
		}
	}
	return errors.New("file download metadata does not exist, or is corrupted")
}

func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) error {
	path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
	if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
		profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
			event.ProfileOnion: profile.GetOnion(),
			event.FileKey:      fileKey,
			event.FilePath:     path,
			event.TempFile:     "",
		}))
	} else {
		log.Debugf("CheckDownloadStatus found .path but not .complete")
		profile.PublishEvent(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{
			event.ProfileOnion:     profile.GetOnion(),
			event.FileKey:          fileKey,
			event.Progress:         "-1",
			event.FileSizeInChunks: "-1",
			event.FilePath:         path,
		}))
	}
	return nil // cannot fail
}

func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
	fileKey, overlay, err := f.ShareFile(sharefilepath, profile)
	if err != nil {
		log.Errorf("error sharing file: %v", err)
	} else if conversationID == -1 {
		// FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume
		// there is only one, and it is the custom profile image...
		profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey, fileKey)
	} else {
		// Set a new attribute so we can associate this download with this conversation...
		profile.SetConversationAttribute(conversationID, attr.ConversationScope.ConstructScopedZonedPath(attr.FilesharingZone.ConstructZonedPath(fileKey)), "")
		id, err := profile.SendMessage(conversationID, overlay)
		if err == nil {
			return profile.EnhancedGetMessageById(conversationID, id)
		}
	}
	return ""
}

// DownloadFileDefaultLimit given a profile, a conversation handle and a file sharing key, start off a download process
// to downloadFilePath with a default filesize limit
func (f *Functionality) DownloadFileDefaultLimit(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) error {
	return f.DownloadFile(profile, conversationID, downloadFilePath, manifestFilePath, key, files.MaxManifestSize*files.DefaultChunkSize)
}

// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
// to downloadFilePath
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string, limit uint64) error {

	// assert that we are allowed to download the file
	if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
		return errors.New("filesharing functionality is not enabled")
	}

	// Don't download files if the download or manifest path is not set
	if downloadFilePath == "" || manifestFilePath == "" {
		return errors.New("download path or manifest path is empty")
	}

	// Don't download files if the download file directory does not exist
	// Unless we are on Android where the kernel wishes to keep us ignorant of the
	// actual path and/or existence of the file. We handle this case further down
	// the line when the manifest is received and protocol engine and the Android layer
	// negotiate a temporary local file -> final file copy. We don't want to worry
	// about that here...
	if runtime.GOOS != "android" {
		if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
			return errors.New("download directory does not exist")
		}

		// Don't download files if the manifest file directory does not exist
		if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
			return errors.New("manifest directory does not exist")
		}
	}
	// Store local.filesharing.filekey.manifest as the location of the manifest
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)

	// Store local.filesharing.filekey.path as the location of the download
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), downloadFilePath)

	// Store local.filesharing.filekey.limit as the max file size of the download
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.limit", key), strconv.FormatUint(limit, 10))

	// Get the value of conversation.filesharing.filekey.manifest.size from `handle`
	profile.SendScopedZonedGetValToContact(conversationID, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))

	return nil
}

// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
// if force is set to true, this function will ignore timestamp checks...
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string, force bool) error {
	tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
	if exists && !force {
		ts, err := strconv.ParseInt(tsStr, 10, 64)
		if err != nil || ts < time.Now().Unix()-2592000 {
			log.Errorf("ignoring request to download a file offered more than 30 days ago")
			return err
		}
	}

	// set the filekey status to active
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
	// reset the timestamp...
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey), strconv.FormatInt(time.Now().Unix(), 10))
	// share the manifest
	profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
	return nil
}

// RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest
// by default this function always forces a file share, even if the file has timed out.
func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) error {
	return f.restartFileShareAdvanced(profile, filekey, true)
}

// RestartFileShareAdvanced takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest in addition
// to a set of parameters
func (f *Functionality) restartFileShareAdvanced(profile peer.CwtchPeer, filekey string, force bool) error {

	// assert that we are allowed to restart filesharing
	if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
		return errors.New("filesharing functionality is not enabled")
	}

	// check that a manifest exists
	manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey))
	if manifestExists {
		// everything is in order, so reshare this file with the engine
		log.Debugf("restarting file share: %v", filekey)
		return f.startFileShare(profile, filekey, manifest, force)
	}
	return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
}

// ReShareFiles given a profile we iterate through all existing fileshares and re-share them
// if the time limit has not expired
func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {

	// assert that we are allowed to restart filesharing
	if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
		return errors.New("filesharing functionality is not enabled")
	}

	keys, err := profile.GetScopedZonedAttributeKeys(attr.LocalScope, attr.FilesharingZone)
	if err != nil {
		return err
	}

	for _, key := range keys {
		// only look at timestamp keys
		// this is an arbitrary choice

		if strings.HasSuffix(key, ".ts") {
			_, zonedpath := attr.ParseScope(key)
			_, keypath := attr.ParseZone(zonedpath)
			keyparts := strings.Split(keypath, ".")

			// assert that the key is well-formed
			if len(keyparts) == 3 && keyparts[2] == "ts" {
				// fetch the timestamp key
				filekey := strings.Join(keyparts[:2], ".")
				sharedFile, err := f.GetFileShareInfo(profile, filekey)

				// If we haven't explicitly stopped sharing the file then attempt a reshare
				if err == nil && sharedFile.Active {
					// this reshare can fail because we don't force sharing of files older than 30 days...
					err := f.restartFileShareAdvanced(profile, filekey, false)
					if err != nil {
						log.Debugf("could not reshare file: %v", err)
					}
				} else {
					log.Debugf("could not get fileshare info %v", err)
				}
			}
		}
	}
	return nil
}

// GetFileShareInfo returns information related to a known fileshare.
// An error is returned if the data is incomplete
func (f *Functionality) GetFileShareInfo(profile peer.CwtchPeer, filekey string) (*SharedFile, error) {
	timestampString, tsExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
	pathString, pathExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", filekey))
	activeString, activeExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey))
	if tsExists && pathExists && activeExists {
		timestamp, err := strconv.Atoi(timestampString)
		if err == nil {

			dateShared := time.Unix(int64(timestamp), 0)
			expired := time.Since(dateShared) >= time.Hour*24*30

			return &SharedFile{
				FileKey:    filekey,
				Path:       pathString,
				DateShared: dateShared,
				Active:     !expired && activeString == constants.True,
				Expired:    expired,
			}, nil
		}
	}
	return nil, fmt.Errorf("nonexistant or malformed fileshare %v", filekey)
}

// ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file
// at filepath
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (string, string, error) {

	// assert that we are allowed to share files
	if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
		return "", "", errors.New("filesharing functionality is not enabled")
	}

	manifest, err := files.CreateManifest(filepath)
	if err != nil {
		return "", "", err
	}

	var nonce [24]byte
	if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
		log.Errorf("Cannot read from random: %v\n", err)
		return "", "", err
	}

	message := OverlayMessage{
		Name:  path.Base(manifest.FileName),
		Hash:  hex.EncodeToString(manifest.RootHash),
		Nonce: hex.EncodeToString(nonce[:]),
		Size:  manifest.FileSizeInBytes,
	}

	data, _ := json.Marshal(message)

	wrapper := model.MessageWrapper{
		Overlay: model.OverlayFileSharing,
		Data:    string(data),
	}

	wrapperJSON, _ := json.Marshal(wrapper)
	key := fmt.Sprintf("%x.%x", manifest.RootHash, nonce)
	serializedManifest, _ := json.Marshal(manifest)

	// Store the size of the manifest (in chunks) as part of the public scope so contacts who we share the file with
	// can fetch the manifest as if it were a file.
	// manifest.FileName gets redacted in filesharing_subsystem (to remove the system-specific file hierarchy),
	// but we need to *store* the full path because the sender also uses it to locate the file
	lenDiff := len(filepath) - len(path.Base(filepath))

	// the sender needs to know the location of the file so they can display it in a preview...
	// This eventually becomes a message attribute, but we don't have access to the message identifier until
	// the message gets sent.
	// In the worst case, this can be obtained using CheckDownloadStatus (though in practice this lookup will be
	// rare because the UI will almost always initiate the construction of a preview a file directly after sending it).
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), filepath)

	// Store the timestamp, manifest and manifest size for later.
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10))
	profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), string(serializedManifest))
	profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize)))))

	err = f.startFileShare(profile, key, string(serializedManifest), false)

	return key, string(wrapperJSON), err
}

// SharedFile encapsulates information about a shared file
// including the file key, file path, the original share date and the
// current sharing status
type SharedFile struct {

	// The roothash.nonce identifier derived for this file share
	FileKey string

	// Path is the OS specific location of the file
	Path string

	// DateShared is the original datetime the file was shared
	DateShared time.Time

	// Active is true if the file is currently being shared, false otherwise
	Active bool

	// Expired is true if the file is not eligible to be shared (because e.g. it has been too long since the file was originally shared,
	// or the file no longer exists).
	Expired bool
}

func (f *Functionality) EnhancedGetSharedFiles(profile peer.CwtchPeer, conversationID int) string {
	data, err := json.Marshal(f.GetSharedFiles(profile, conversationID))
	if err == nil {
		return string(data)
	}
	return ""
}

// GetSharedFiles returns all file shares associated with a given conversation
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
	var sharedFiles []SharedFile
	ci, err := profile.GetConversationInfo(conversationID)
	if err == nil {
		for k := range ci.Attributes {
			// when we share a file with a conversation we set a single attribute conversation.filesharing.<filekey>
			if strings.HasPrefix(k, "conversation.filesharing") {
				parts := strings.SplitN(k, ".", 3)
				if len(parts) == 3 {
					key := parts[2]
					sharedFile, err := f.GetFileShareInfo(profile, key)
					if err == nil {
						sharedFiles = append(sharedFiles, *sharedFile)
					}
				}
			}
		}
	}
	return sharedFiles
}

// GenerateDownloadPath creates a file path that doesn't currently exist on the filesystem
func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath, manifestPath string) {
	// avoid all kina funky shit
	re := regexp.MustCompile(`[^A-Za-z0-9._-]`)
	filePath = re.ReplaceAllString(filePath, "")
	// avoid hidden files on linux
	for strings.HasPrefix(filePath, ".") {
		filePath = strings.TrimPrefix(filePath, ".")
	}
	// avoid empties
	if strings.TrimSpace(filePath) == "" {
		filePath = "untitled"
	}
	// if you like it, put a / on it
	if !strings.HasSuffix(basePath, string(os.PathSeparator)) {
		basePath = fmt.Sprintf("%s%s", basePath, string(os.PathSeparator))
	}
	filePath = fmt.Sprintf("%s%s", basePath, fileName)
	manifestPath = fmt.Sprintf("%s.manifest", filePath)

	// if file is named "file", iterate "file", "file (2)", "file (3)", ... until DNE
	// if file is named "file.ext", iterate "file.ext", "file (2).ext", "file (3).ext", ... until DNE
	parts := strings.Split(fileName, ".")
	fileNameBase := parts[0]
	fileNameExt := ""
	if len(parts) > 1 {
		fileNameBase = strings.Join(parts[0:len(parts)-1], ".")
		fileNameExt = fmt.Sprintf(".%s", parts[len(parts)-1])
	}

	if !overwrite {
		for i := 2; ; i++ {
			if _, err := os.Open(filePath); os.IsNotExist(err) {
				if _, err := os.Open(manifestPath); os.IsNotExist(err) {
					return
				}
			}
			filePath = fmt.Sprintf("%s%s (%d)%s", basePath, fileNameBase, i, fileNameExt)
			manifestPath = fmt.Sprintf("%s.manifest", filePath)
		}
	}
	return
}

// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) error {
	// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
	// set the filekey status to inactive
	profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
	profile.PublishEvent(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
	return nil // cannot fail
}

// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
func (f *Functionality) StopAllFileShares(profile peer.CwtchPeer) {
	// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
	profile.PublishEvent(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
}
