package files

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	path "path/filepath"
	"strconv"
	"strings"
	"sync"

	"cwtch.im/cwtch/event"
	"cwtch.im/cwtch/protocol/model"
	"git.openprivacy.ca/openprivacy/log"
)

// FileSharingSubSystem encapsulates the functionality necessary to share and download files via Cwtch
type FileSharingSubSystem struct {

	// for sharing files
	activeShares sync.Map // file key to manifest

	// for downloading files
	prospectiveManifests sync.Map // file key to serialized manifests
	activeDownloads      sync.Map // file key to manifests
}

// ShareFile given a file key and a serialized manifest, allow the serialized manifest to be downloaded
// by Cwtch profiles in possession of the fileKey
func (fsss *FileSharingSubSystem) ShareFile(fileKey string, serializedManifest string) {
	var manifest Manifest
	err := json.Unmarshal([]byte(serializedManifest), &manifest)
	if err != nil {
		log.Errorf("could not share file %v", err)
		return
	}
	log.Debugf("sharing file: %v %v", fileKey, serializedManifest)
	fsss.activeShares.Store(fileKey, &manifest)
}

// StopFileShare given a file key removes the serialized manifest from consideration by the file sharing
// subsystem. Future requests on this manifest will fail, as will any in-progress chunk requests.
func (fsss *FileSharingSubSystem) StopFileShare(fileKey string) {
	fsss.activeShares.Delete(fileKey)
}

// StopAllFileShares removes all active file shares from consideration
func (fsss *FileSharingSubSystem) StopAllFileShares() {
	fsss.activeShares.Range(func(key, value interface{}) bool {
		fsss.activeShares.Delete(key)
		return true
	})
}

// FetchManifest given a file key and knowledge of the manifest size in chunks (obtained via an attribute lookup)
// construct a request to download the manifest.
func (fsss *FileSharingSubSystem) FetchManifest(fileKey string, manifestSize uint64) model.PeerMessage {
	fsss.prospectiveManifests.Store(fileKey, strings.Repeat("\"", int(manifestSize*DefaultChunkSize)))
	return model.PeerMessage{
		Context: event.ContextRequestManifest,
		ID:      fileKey,
		Data:    []byte{},
	}
}

// CompileChunkRequests takes in a complete serializedManifest and returns a set of chunk request messages
// TODO in the future we will want this to return the handles of contacts to request chunks from
func (fsss *FileSharingSubSystem) CompileChunkRequests(fileKey, serializedManifest, tempFile, title string) []model.PeerMessage {
	var manifest Manifest
	err := json.Unmarshal([]byte(serializedManifest), &manifest)
	var messages []model.PeerMessage
	if err == nil {
		manifest.TempFileName = tempFile
		manifest.Title = title
		err := manifest.PrepareDownload()
		if err == nil {
			fsss.activeDownloads.Store(fileKey, &manifest)
			log.Debugf("downloading file chunks: %v", manifest.GetChunkRequest().Serialize())
			messages = append(messages, model.PeerMessage{
				ID:      fileKey,
				Context: event.ContextRequestFile,
				Data:    []byte(manifest.GetChunkRequest().Serialize()),
			})
		} else {
			log.Errorf("couldn't prepare download: %v", err)
		}
	}
	return messages
}

// RequestManifestParts given a fileKey construct a set of messages representing requests to download various
// parts of the Manifest
func (fsss *FileSharingSubSystem) RequestManifestParts(fileKey string) []model.PeerMessage {
	manifestI, exists := fsss.activeShares.Load(fileKey)
	var messages []model.PeerMessage
	if exists {
		oldManifest := manifestI.(*Manifest)
		serializedOldManifest := oldManifest.Serialize()
		log.Debugf("found serialized manifest")

		// copy so we dont get threading issues by modifying the original
		// and then redact the file path before sending
		// nb: manifest.size has already been corrected elsewhere
		var manifest Manifest
		json.Unmarshal([]byte(serializedOldManifest), &manifest)
		manifest.FileName = path.Base(manifest.FileName)
		serializedManifest := manifest.Serialize()

		chunkID := 0
		for i := 0; i < len(serializedManifest); i += DefaultChunkSize {
			offset := i
			end := i + DefaultChunkSize
			// truncate end
			if end > len(serializedManifest) {
				end = len(serializedManifest)
			}
			chunk := serializedManifest[offset:end]
			// request this manifest part
			messages = append(messages, model.PeerMessage{
				Context: event.ContextSendManifest,
				ID:      fmt.Sprintf("%s.%d", fileKey, chunkID),
				Data:    chunk,
			})
			chunkID++
		}
	}
	return messages
}

// ReceiveManifestPart given a manifestKey reconstruct part the manifest from the provided part
func (fsss *FileSharingSubSystem) ReceiveManifestPart(manifestKey string, part []byte) (fileKey string, serializedManifest string) {
	fileKeyParts := strings.Split(manifestKey, ".")
	if len(fileKeyParts) == 3 { // rootHash.nonce.manifestPart
		fileKey = fmt.Sprintf("%s.%s", fileKeyParts[0], fileKeyParts[1])
		log.Debugf("manifest filekey: %s", fileKey)
		manifestPart, err := strconv.Atoi(fileKeyParts[2])
		if err == nil {
			serializedManifest, exists := fsss.prospectiveManifests.Load(fileKey)
			if exists {
				serializedManifest := serializedManifest.(string)
				log.Debugf("loaded manifest")
				offset := manifestPart * DefaultChunkSize
				end := (manifestPart + 1) * DefaultChunkSize

				log.Debugf("storing manifest part %v %v", offset, end)
				serializedManifestBytes := []byte(serializedManifest)
				if len(serializedManifestBytes) > offset && len(serializedManifestBytes) >= end {
					copy(serializedManifestBytes[offset:end], part[:])

					if len(part) < DefaultChunkSize {
						serializedManifestBytes = serializedManifestBytes[0 : len(serializedManifestBytes)-(DefaultChunkSize-len(part))]
					}

					serializedManifest = string(serializedManifestBytes)
					fsss.prospectiveManifests.Store(fileKey, serializedManifest)
					log.Debugf("current manifest: [%s]", serializedManifest)
					var manifest Manifest
					err := json.Unmarshal([]byte(serializedManifest), &manifest)
					if err == nil && hex.EncodeToString(manifest.RootHash) == fileKeyParts[0] {
						log.Debugf("valid manifest received! %x", manifest.RootHash)
						return fileKey, serializedManifest
					}
				}
			}
		}
	}
	return "", ""
}

// ProcessChunkRequest given a fileKey, and a chunk request, compile a set of responses for each requested Chunk
func (fsss *FileSharingSubSystem) ProcessChunkRequest(fileKey string, serializedChunkRequest []byte) []model.PeerMessage {
	log.Debugf("chunk request: %v", fileKey)
	// fileKey is rootHash.nonce
	manifestI, exists := fsss.activeShares.Load(fileKey)
	var messages []model.PeerMessage
	if exists {
		manifest := manifestI.(*Manifest)
		log.Debugf("manifest found: %x", manifest.RootHash)
		chunkSpec, err := Deserialize(string(serializedChunkRequest))
		log.Debugf("deserialized chunk spec found: %v [%s]", chunkSpec, serializedChunkRequest)
		if err == nil {
			for _, chunk := range *chunkSpec {
				contents, err := manifest.GetChunkBytes(chunk)
				if err == nil {
					log.Debugf("sending chunk: %v %x", chunk, contents)
					messages = append(messages, model.PeerMessage{
						ID:      fmt.Sprintf("%v.%d", fileKey, chunk),
						Context: event.ContextSendFile,
						Data:    contents,
					})
				}
			}
		}
	}
	return messages
}

// ProcessChunk given a chunk key and a chunk attempt to store and verify the chunk  as part of an active download
// If this results in the file download being completed return downloaded = true
// Always return the progress of a matched download if it exists along with the total number of chunks and the
// given chunk ID
// If not such active download exists then return an empty file key and ignore all further processing.
func (fsss *FileSharingSubSystem) ProcessChunk(chunkKey string, chunk []byte) (fileKey string, progress uint64, totalChunks uint64, chunkID uint64, title string) {
	fileKeyParts := strings.Split(chunkKey, ".")
	log.Debugf("got chunk for %s", fileKeyParts)
	if len(fileKeyParts) == 3 { // fileKey is rootHash.nonce.chunk
		// recalculate file key
		fileKey = fmt.Sprintf("%s.%s", fileKeyParts[0], fileKeyParts[1])
		derivedChunkID, err := strconv.Atoi(fileKeyParts[2])
		if err == nil {
			chunkID = uint64(derivedChunkID)
			log.Debugf("got chunk id %d", chunkID)
			manifestI, exists := fsss.activeDownloads.Load(fileKey)
			if exists {
				manifest := manifestI.(*Manifest)
				totalChunks = uint64(len(manifest.Chunks))
				title = manifest.Title
				log.Debugf("found active manifest %v", manifest)
				progress, err = manifest.StoreChunk(chunkID, chunk)
				log.Debugf("attempts to store chunk %v %v", progress, err)
				if err != nil {
					log.Debugf("error storing chunk: %v", err)
					// malicious contacts who share conversations can share random chunks
					// these will not match the chunk hash and as such will fail.
					// at this point we can't differentiate between a malicious chunk and failure to store a
					// legitimate chunk, so if there is an error we silently drop it and expect the higher level callers (e.g. the ui)
					//to detect and respond to missing chunks if it detects them..
				}
			}
		}
	}
	return
}

// VerifyFile returns true if the file has been downloaded, false otherwise
// as well as the temporary filename, if one was used
func (fsss *FileSharingSubSystem) VerifyFile(fileKey string) (tempFile string, filePath string, downloaded bool) {
	manifestI, exists := fsss.activeDownloads.Load(fileKey)
	if exists {
		manifest := manifestI.(*Manifest)
		if manifest.VerifyFile() == nil {
			manifest.Close()
			fsss.activeDownloads.Delete(fileKey)
			log.Debugf("file verified and downloaded!")
			return manifest.TempFileName, manifest.FileName, true
		}
	}
	return "", "", false
}
