//package cwtch

package main

// //Needed to invoke C.free
// #include <stdlib.h>
import "C"

import (
	"cwtch.im/cwtch/event"
	"cwtch.im/cwtch/model/attr"
	"cwtch.im/cwtch/model/constants"
	"cwtch.im/cwtch/settings"
	"encoding/json"
	"fmt"
	"git.openprivacy.ca/cwtch.im/autobindings/utils"
	"git.openprivacy.ca/openprivacy/connectivity/tor"
	"git.openprivacy.ca/openprivacy/log"
	"os"
	"os/user"
	path "path/filepath"
	"runtime"
	"runtime/pprof"
	"strings"
	"strconv"
	"time"
	mrand "math/rand"
	"crypto/rand"
	"encoding/base64"
	"unsafe"
	_ "git.openprivacy.ca/openprivacy/go-sqlcipher"
	"cwtch.im/cwtch/app"
	"git.openprivacy.ca/openprivacy/connectivity"
	"sync"

	{{IMPORTS}}
)

// supplied by make
var (
	buildVer  string
	buildDate string
)

var application app.Application
var globalAppDir string
var globalTorPath string
var eventHandler *utils.EventHandler
var globalACN connectivity.ProxyACN

// Dangerous function. Should only be used as documented in `MEMORY.md`
//
//export c_FreePointer
func c_FreePointer(ptr *C.char) {
	C.free(unsafe.Pointer(ptr))
}

//export c_Started
func c_Started() C.int {
	return C.int(Started())
}

// Started returns 1 if application is initialized and 0 if it is null
func Started() int {
	if application == nil {
		return 0
	}
	return 1
}

//export c_StartCwtch
func c_StartCwtch(dir_c *C.char, len C.int, tor_c *C.char, torLen C.int) C.int {
	applicationDirectory := C.GoStringN(dir_c, len)
	torDirectory := C.GoStringN(tor_c, torLen)
	return C.int(StartCwtch(applicationDirectory, torDirectory))
}

// StartCwtch starts cwtch in the library and initlaizes all data structures
//
//		 GetAppbusEvents is always safe to use
//	  the rest of functions are unsafe until the CwtchStarted event has been received indicating StartCwtch has completed
//	  returns:
//	    message: CwtchStarted when start up is complete and app is safe to use
//	             CwtchStartError message when start up fails (includes event.Error data field)
func StartCwtch(appDir string, torPath string) int {
	if logfile := os.Getenv("LOG_FILE"); logfile != "" {
		filelog, err := log.NewFile(log.LevelInfo, logfile)
		if err == nil {
			filelog.SetUseColor(false)
			log.SetStd(filelog)
		} else {
			// not so likely to be seen since we're usually creating file log in situations we can't access console logs...
			log.Errorf("could not create file log: %v\n", err)
		}
	}
	if runtime.GOOS == "android" {
		log.SetUseColor(false)
	}
	log.SetLevel(log.LevelInfo)
	if logLevel := os.Getenv("LOG_LEVEL"); strings.ToLower(logLevel) == "debug" {
		log.SetLevel(log.LevelDebug)
	}
	log.Infof("StartCwtch(...)")
	log.Debugf("builddate: %v buildver: %v", buildDate, buildVer)

	// Quick hack check that we're being called with the correct params
	// On android a stale worker could be calling us with "last apps" directory. Best to abort fast so the app can make a new worker
	if runtime.GOOS == "android" {
		fh, err := os.Open(torPath)
		if err != nil {
			log.Errorf("%v", err)
			log.Errorf("failed to stat tor, skipping StartCwtch(). potentially normal if the app was reinstalled or the device was restarted; this workorder should get canceled soon")
			return 1
		}
		_ = fh.Close()
	}
	go _startCwtch(appDir, torPath)
	return 0
}

func _startCwtch(appDir string, torPath string) {
	log.Infof("application: %v eventHandler: %v", application, eventHandler)

	if application != nil {
		log.Infof("_startCwtch detected existing application; resuming instead of relaunching")
		ReconnectCwtchForeground()
		return
	}

	log.Infof("Creating new EventHandler()")
	eventHandler = utils.NewEventHandler()


	// Exclude Tapir wire Messages
	//(We need a TRACE level)
	log.ExcludeFromPattern("service.go")

	// Environment variables don't get '~' expansion so if CWTCH_DIR was set, it likely needs manual handling
	usr, _ := user.Current()
	homeDir := usr.HomeDir
	if appDir == "~" {
		appDir = homeDir
	} else if strings.HasPrefix(appDir, "~/") {
		appDir = path.Join(homeDir, appDir[2:])
	}

	// Ensure that the application directory exists...and then initialize settings..
	err := os.MkdirAll(appDir, 0700)
	if err != nil {
		log.Errorf("Error creating appDir %v: %v\n", appDir, err)
		eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error creating appDir %v: %v", appDir, err)))
		return
	}

	log.Infof("Loading Cwtch Directory %v and tor path: %v", appDir, torPath)
	settings.InitGlobalSettingsFile(appDir, app.DefactoPasswordForUnencryptedProfiles)

	log.Infof("making directory %v", appDir)
	err = os.MkdirAll(path.Join(appDir, "tor"), 0700)

	if err != nil {
		log.Errorf("error creating tor data directory: %v. Aborting app start up", err)
		eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err)))
		return
	}

	// Allow the user of a custom torrc
	globalAppDir = appDir
	globalTorPath = torPath
	settingsFile := app.LoadAppSettings(appDir)
	// start with an Error ACN
	erracn := connectivity.NewErrorACN(fmt.Errorf("initializing tor"))
	globalACN = connectivity.NewProxyACN(&erracn)
	application = app.NewApp(&globalACN, appDir, settingsFile)

	// Subscribe to all App Events...
	eventHandler.HandleApp(application)

	// FIXME: This code exists to allow the Splash Screen test in the new UI integration tests to pass
	// it doesn't actually fix the problem in theory, and we should get around to ensuring that application
	// is safe to access even if shutdown is called concurrently...
	if application == nil {
		log.Errorf("startCwtch: primary application object has gone away. assuming application is closing.")
		return
	}
	// Send global settings to the UI...
	globalSettings := application.ReadSettings()
	settingsJson, _ := json.Marshal(globalSettings)
	application.GetPrimaryBus().Publish(event.NewEvent(settings.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)}))
	log.Infof("libcwtch-go application launched")
	application.GetPrimaryBus().Publish(event.NewEvent(settings.CwtchStarted, map[event.Field]string{}))
	application.QueryACNVersion()

	{{EXPERIMENT_REGISTER}}

	
	// Finally attempt to set up a proper Tor
	// Note: ResetTor launches an internal goroutine so this is non-blocking...
	ResetTor()
}

// the pointer returned from this function **must** be freed using c_Free
//
//export c_GetAppBusEvent
func c_GetAppBusEvent() *C.char {
	return C.CString(GetAppBusEvent())
}

// GetAppBusEvent blocks until an event
func GetAppBusEvent() string {
	for eventHandler == nil {
		log.Debugf("waiting for eventHandler != nil")
		time.Sleep(time.Second)
	}

	var json = ""
	for json == "" {
		json = eventHandler.GetNextEvent()
	}
	return json
}

//export c_ReconnectCwtchForeground
func c_ReconnectCwtchForeground() {
	ReconnectCwtchForeground()
}

// Like StartCwtch, but StartCwtch has already been called so we don't need to restart Tor etc (probably)
// Do need to re-send initial state tho, eg profiles that are already loaded
func ReconnectCwtchForeground() {
	log.Infof("Reconnecting cwtch foreground")
	if application == nil {
		log.Errorf("ReconnectCwtchForeground: Application is nil, presuming stale thread, EXITING Reconnect\n")
		return
	}

	// Repopulate the UI on Android by iterating through all loaded profiles and treating them as New Peer loads. The ReloadEvent field
	// suppresses any listen/connection actions and simply sends updated attributes to the UI.
	// The UI is designed in such a way that "NewPeer" events are treated as updates.
	// TODO: if/when we break apart NewPeer into smaller chunks for the UI to fetch, the need to do this here will go away.
	peerList := application.ListProfiles()
	for _, onion := range peerList {
		eventHandler.Push(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: onion, utils.ReloadEvent: event.True}))
	}



	settingsJson, _ := json.Marshal(application.ReadSettings())
	application.GetPrimaryBus().Publish(event.NewEvent(settings.UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)}))
	application.GetPrimaryBus().Publish(event.NewEvent(settings.CwtchStarted, map[event.Field]string{utils.ReloadEvent: event.True}))
	application.QueryACNStatus()
	application.QueryACNVersion()
}

//export c_ShutdownCwtch
func c_ShutdownCwtch() {
	ShutdownCwtch()
}

// ShutdownCwtch is a safe way to shutdown any active cwtch applications and associated ACNs
func ShutdownCwtch() {
	if application != nil {
		// Kill the isolate
		eventHandler.Push(event.NewEvent(event.Shutdown, map[event.Field]string{}))
		// Allow for the shutdown events to go through and then purge everything else...
		log.Infof("Shutting Down Application...")
		application.Shutdown()
		log.Infof("Shutting Down ACN...")
		globalACN.Close()
		log.Infof("Library Shutdown Complete!")
		// do not remove - important for state checks elsewhere
		application = nil
		eventHandler = nil
	}
}

// TODO: At some point these functions should also be autogenerated

// Attribute is a struct to return the dual values of an attempt at a Get*Attribute API call, meant to be json serialized
type Attribute struct {
	Exists bool
	Value  string
}

//export c_SetProfileAttribute
func c_SetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) {
	profileOnion := C.GoStringN(profile_ptr, profile_len)
	key := C.GoStringN(key_ptr, key_len)
	value := C.GoStringN(val_ptr, val_len)
	SetProfileAttribute(profileOnion, key, value)
}

// SetProfileAttribute provides a wrapper around profile.SetScopedZonedAttribute
// WARNING: Because this function is potentially dangerous all keys and zones must be added
// explicitly. If you are attempting to added behaviour to the UI that requires the existence of new keys
// you probably want to be building out functionality/subsystem in the UI itself.
// Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted.
func SetProfileAttribute(profileOnion string, key string, value string) {
	profile := application.GetPeer(profileOnion)
	if profile != nil {
		zone, key := attr.ParseZone(key)

		// TODO We only allow certain public.profile.key to be set for now.
		// All other scopes and zones need to be added explicitly or handled by Cwtch.
		if zone == attr.ProfileZone && key == constants.Name {
			profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, value)
		} else if zone == attr.ProfileZone && key == constants.ProfileAttribute1 {
			profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute1, value)
		} else if zone == attr.ProfileZone && key == constants.ProfileAttribute2 {
			profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute2, value)
		} else if zone == attr.ProfileZone && key == constants.ProfileAttribute3 {
			profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute3, value)
		} else if zone == attr.ProfileZone && key == constants.ProfileStatus {
			profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.ProfileStatus, value)
		} else if zone == attr.ProfileZone && key == constants.PeerAutostart {
			profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart, value)
		} else if zone == attr.ProfileZone && key == constants.PeerAppearOffline {
			profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAppearOffline, value)
		} else if zone == attr.ProfileZone && key == constants.PrivateName {
				profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PrivateName, value)
		} else {
			log.Errorf("attempted to set an attribute with an unknown zone: %v", key)
		}
	}
}

//export c_GetProfileAttribute
func c_GetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int) *C.char {
	profileOnion := C.GoStringN(profile_ptr, profile_len)
	key := C.GoStringN(key_ptr, key_len)
	return C.CString(GetProfileAttribute(profileOnion, key))
}

// GetProfileAttribute provides a wrapper around profile.GetScopedZonedAttribute
// Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted.
// Currently forcing the Public Scope
// Returns json of Attribute
func GetProfileAttribute(profileOnion string, key string) string {
	profile := application.GetPeer(profileOnion)
	if profile != nil {
		zone, key := attr.ParseZone(key)

		res, exists := profile.GetScopedZonedAttribute(attr.PublicScope, zone, key)
		attr := Attribute{exists, res}
		json, _ := json.Marshal(attr)
		return string(json)

	}
	empty := Attribute{false, ""}
	json, _ := json.Marshal(empty)
	return (string(json))
}

//export c_SetConversationAttribute
func c_SetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) {
	profileOnion := C.GoStringN(profile_ptr, profile_len)
	key := C.GoStringN(key_ptr, key_len)
	value := C.GoStringN(val_ptr, val_len)
	SetConversationAttribute(profileOnion, int(conversation_id), key, value)
}

// SetConversationAttribute provides a wrapper around profile.SetProfileAttribute
// key is of format Zone.Key, and the API forces the Local Scope
func SetConversationAttribute(profileOnion string, conversationID int, key string, value string) {
	profile := application.GetPeer(profileOnion)
	zone, key := attr.ParseZone(key)
	profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)), value)
}

//export c_GetConversationAttribute
func c_GetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int) *C.char {
	profileOnion := C.GoStringN(profile_ptr, profile_len)
	key := C.GoStringN(key_ptr, key_len)
	return C.CString(GetConversationAttribute(profileOnion, int(conversation_id), key))
}

// GetGonversationAttribute provides a wrapper around profile.GetGonversationAttribute
// key is of format Scope.Zone.Key
// Returns json of an Attribute
func GetConversationAttribute(profileOnion string, conversationID int, key string) string {
	profile := application.GetPeer(profileOnion)
	if profile != nil {
		scope, zonekey := attr.ParseScope(key)
		zone, key := attr.ParseZone(zonekey)

		res, err := profile.GetConversationAttribute(conversationID, scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)))
		attr := Attribute{err == nil, res}
		json, _ := json.Marshal(attr)
		return string(json)
	}
	empty := Attribute{false, ""}
	json, _ := json.Marshal(empty)
	return (string(json))
}


//export c_ResetTor
func c_ResetTor() {
	ResetTor()
}


var torLock sync.Mutex

func ResetTor() {
	go func() {
		// prevent concurrent calls to this method...
		torLock.Lock()
		defer torLock.Unlock()
		log.Infof("Replacing ACN with new Tor...")
		settings := application.ReadSettings()
	
		globalACN.Close() // we need to close first if dateDir is the same, otherwise buildACN can't launch tor.
		newAcn, settings, err := buildACN(settings, globalTorPath, globalAppDir)
		// only update settings if successful.
		if err == nil {
			// Only update Tor specific settings...
			currentSettings := application.ReadSettings()
			currentSettings.TorCacheDir = settings.TorCacheDir
			currentSettings.CustomControlPort = settings.CustomControlPort
			currentSettings.CustomSocksPort = settings.CustomSocksPort
			currentSettings.CustomTorrc = settings.CustomTorrc
			application.UpdateSettings(currentSettings)
	
			// We need to update settings on reset as buildACN can alter settings, otherwise the next reset will be broken...
			settings = application.ReadSettings()
			settingsJson, _ := json.Marshal(settings)
			application.GetPrimaryBus().Publish(event.NewEvent(UpdateGlobalSettings, map[event.Field]string{event.Data: string(settingsJson)}))
		} 

		// replace ACN regardlesss
		globalACN.ReplaceACN(newAcn)
		application.QueryACNStatus()
		application.QueryACNVersion()
		log.Infof("Restarted")
	}()
}

const (
	CwtchStarted         = event.Type("CwtchStarted")
	CwtchStartError      = event.Type("CwtchStartError")
	UpdateGlobalSettings = event.Type("UpdateGlobalSettings")
)

func buildACN(globalSettings settings.GlobalSettings, torPath string, appDir string) (connectivity.ACN, settings.GlobalSettings, error) {

	socksPort := mrand.Intn(1000) + 9600
	controlPort := socksPort + 1

	// generate a random password (actually random, stored in memory, for the control port)
	key := make([]byte, 64)
	_, err := rand.Read(key)
	if err != nil {
		panic(err)
	}

	log.Infof("making directory %v", appDir)
	err = os.MkdirAll(path.Join(appDir, "tor"), 0700)

	if err != nil {
		log.Errorf("error creating tor data directory: %v. Aborting app start up", err)
		eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err)))
		erracn := connectivity.NewErrorACN(err)
		return &erracn, globalSettings, err
	}

	if globalSettings.AllowAdvancedTorConfig {
		controlPort = globalSettings.CustomControlPort
		socksPort = globalSettings.CustomSocksPort
	}

	
	// Override Ports if on Tails...
	if cwtchTails := os.Getenv("CWTCH_TAILS"); strings.ToLower(cwtchTails) == "true" {
		log.Infof("CWTCH_TAILS environment variable set... overriding tor config...")

		controlPort = 9051

		// In tails 5.13 the control port was changed to 951
		// so read the Tails Version File and if it exists...
		b, err := os.ReadFile("/etc/amnesia/version")
		if err == nil {
				// the file should start with the version followed
				// by a space...
				versionEnd := strings.Index(string(b), " ")
				if versionEnd == -1 {
						log.Infof("Assuming Tails Version > 6")
					controlPort = 951 
				} else {
				versionStr := string(b)[:versionEnd]
				version, err := strconv.ParseFloat(versionStr, 64)
				if err == nil {
					log.Infof("Confirming Tails Version: %v", version)
					// assert the control port if we are at the dedicated version...
					// we know this change happened sometime after 5.11
					if version >= 5.13 {
							controlPort = 951
					}
				} else {
					log.Errorf("Unable to confirm Tails version. CWTCH_TAILS options may not function correctly.")
					controlPort = 951
				}
			}
		} else {
			// version file is missing post 6.x 
			log.Infof("Assuming Tails Version > 6")
			controlPort = 951 
		}
	
		socksPort = 9050
		globalSettings.CustomControlPort = controlPort
		globalSettings.CustomSocksPort = socksPort
		globalSettings.AllowAdvancedTorConfig = true
	}

	torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key))
	// torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice)
	if globalSettings.UseCustomTorrc {
		customTorrc := globalSettings.CustomTorrc
		torrc.WithCustom(strings.Split(customTorrc, "\n"))
	} else {
		// Fallback to showing the freshly generated torrc for this session.
		globalSettings.CustomTorrc = torrc.Preview()
		globalSettings.CustomControlPort = controlPort
		globalSettings.CustomSocksPort = socksPort
	}

	err = torrc.Build(path.Join(appDir, "tor", "torrc"))

	if err != nil {
		log.Errorf("error constructing torrc: %v", err)
		eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err)))
		erracn := connectivity.NewErrorACN(err)
		return &erracn, globalSettings, err
	}

	dataDir := globalSettings.TorCacheDir
	if !globalSettings.UseTorCache {

		// purge data dir directories if we are not using them for a cache
		torDir := path.Join(appDir, "tor")
		files, err := path.Glob(path.Join(torDir, "data-dir-*"))
		if err != nil {
			log.Errorf("could not construct filesystem glob: %v", err)
		}
		for _, f := range files {
			if err := os.RemoveAll(f); err != nil {
				log.Errorf("could not remove data-dir: %v", err)
			}
		}

		if dataDir, err = os.MkdirTemp(torDir, "data-dir-"); err != nil {
			eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err)))
			erracn := connectivity.NewErrorACN(err)
			return &erracn, globalSettings, err
		}
	}

	// Persist Current Data Dir as Tor Cache...
	globalSettings.TorCacheDir = dataDir

	acn, err := tor.NewTorACNWithAuth(appDir, torPath, dataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
	if err != nil {
		log.Errorf("Error connecting to Tor replacing with ErrorACN: %v\n", err)
		eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err)))
		erracn := connectivity.NewErrorACN(err)
		acn = &erracn
	}
	return acn, globalSettings, err
}


//export c_UpdateSettings
func c_UpdateSettings(json_ptr *C.char, json_len C.int) {
	settingsJson := C.GoStringN(json_ptr, json_len)
	UpdateSettings(settingsJson)
}

func UpdateSettings(settingsJson string) {
	var newSettings settings.GlobalSettings
	json.Unmarshal([]byte(settingsJson), &newSettings)
	application.UpdateSettings(newSettings)
	{{EXPERIMENT_UPDATESETTINGS}}
}

//export c_GetDebugInfo
func c_GetDebugInfo() *C.char {
	return C.CString(GetDebugInfo())
}

type DebugInfo struct {
	BuildVersion  string
	BuildDate     string
	HeapAllocated float64
	HeapInUse     float64
	HeapReleased  float64
	HeapObjects   uint64
	NumThreads    uint64
	SystemMemory  float64
}

func GetDebugInfo() string {

	var memstats runtime.MemStats
	runtime.ReadMemStats(&memstats)

	const MegaByte = 1024.0 * 1024.0

	debugInfo := new(DebugInfo)
	debugInfo.BuildVersion = buildVer
	debugInfo.BuildDate = buildDate
	debugInfo.HeapAllocated = float64(memstats.HeapAlloc) / MegaByte
	debugInfo.HeapObjects = memstats.HeapObjects
	debugInfo.NumThreads = uint64(runtime.NumGoroutine())
	debugInfo.HeapReleased = float64(memstats.HeapReleased) / MegaByte
	debugInfo.HeapInUse = float64(memstats.HeapInuse) / MegaByte
	debugInfo.SystemMemory = float64(memstats.Sys) / MegaByte

	if os.Getenv("CWTCH_PROFILE") == "1" {
		pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
		f, _ := os.Create("mem.prof")
		pprof.WriteHeapProfile(f)
	}

	data, _ := json.Marshal(debugInfo)
	return string(data)
}

{{BINDINGS}}

// Leave as is, needed by ffi
func main() {}
