package group_manager

import (
	libapp "cwtch.im/cwtch/app"
	"cwtch.im/cwtch/model/attr"
	cwconst "cwtch.im/cwtch/model/constants"
	"cwtch.im/cwtch/peer"
	"cwtch.im/cwtch/event"
	"git.openprivacy.ca/cwtch.im/autobindings/constants"
	"cwtch.im/cwtch/functionality/hybrid"
	"cwtch.im/cwtch/functionality/inter"
	"git.openprivacy.ca/openprivacy/connectivity"
	"git.openprivacy.ca/openprivacy/log"
	"fmt"
	"strconv"
	"sync"
	//"time"
)

const groupManagerExperiment = cwconst.GroupManagerExperiment

const (
	ZeroHybridGroupsLoaded  = event.Type("ZeroHybridGroupsLoaded")
	NewHybridGroup          = event.Type("NewHybridGroup")
	HybridGroupIntentUpdate = event.Type("HybridGroupIntentUpdate")
	HybridGroupDeleted      = event.Type("HybridGroupDeleted")
	HybridGroupStatsUpdate  = event.Type("HybridGroupStatsUpdate")
)

const (
	Intent        = event.Field("Intent")
	TotalMessages = event.Field("TotalMessages")
	Connections   = event.Field("Connections")
)

const (
	IntentRunning = "running"
	IntentStopped = "stopped"
)

type HybridGroupInfo struct {
	Onion        string
	GroupBundle  string
	Autostart    bool
	Running      bool
	Description  string
	StorageType  string
}

var lock sync.Mutex
var app libapp.Application
var killStatsUpdate chan bool = make(chan bool, 1)
var enabled bool = false

func Init(acn connectivity.ACN, appdir string) *GroupManagerFunctionality {
	lock.Lock()
	defer lock.Unlock()
	groupmgrFunctionality := new(GroupManagerFunctionality)
	return groupmgrFunctionality
}

// track acnStatus across events
var acnStatus = -1

func (sf *GroupManagerFunctionality) OnACNStatusEvent(appl libapp.Application, e *event.Event) {
	newAcnStatus, err := strconv.Atoi(e.Data[event.Progress])
	if !appl.IsFeatureEnabled(groupManagerExperiment) || err != nil {
		return
	}
	if newAcnStatus == 100 {
		log.Debugf("OnACNStatus 2")
		if acnStatus != 100 {
			// just came online
			for _, onion := range sf.ListHybridGroups() {
				autostart := false
				if s := sf.GetHybridGroup(onion); s != nil {
					val, _ := s.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
					autostart = val == "true"
				}
				if autostart {
					sf.LaunchHybridGroup(appl, onion)
				}
			}
		}
	} else {
		if acnStatus == 100 {
			for _, onion := range sf.ListHybridGroups() {
				sf.StopHybridGroup(appl, onion)
			}
		}
	}
	acnStatus = newAcnStatus
}

func (sh *GroupManagerFunctionality) Disable() {
	lock.Lock()
	defer lock.Unlock()
	if app != nil {
		//app.Shutdown()
	}
	if enabled {
		enabled = false
		killStatsUpdate <- true
	}
}

func (sh *GroupManagerFunctionality) Enabled() bool {
	lock.Lock()
	defer lock.Unlock()
	return enabled
}

// GroupManagerFunctionality provides experiment gated server functionality
type GroupManagerFunctionality struct {
}

func (sh *GroupManagerFunctionality) UpdateSettings(appl libapp.Application, acn connectivity.ACN) {
	if appl.IsFeatureEnabled(groupManagerExperiment) {
		app = appl
		sh.LoadHybridGroups(appl, acn, libapp.DefactoPasswordForUnencryptedProfiles)
		if !sh.Enabled() {
			sh.Enable(appl, acn)

			// republish servers
			serversList := sh.ListHybridGroups()
			for _, server := range serversList {
				serverInfo := sh.GetHybridGroupInfo(server)
				ev := event.NewEvent(NewHybridGroup, make(map[event.Field]string))
				serverInfo.EnrichEvent(&ev)
				appl.GetPrimaryBus().Publish(ev)
			}
			sh.LaunchHybridGroups(appl, acn)
		}
	} else {
		sh.Disable()
	}
}

func (sh *GroupManagerFunctionality) SetHybridGroupAttribute(appl libapp.Application, handle string, key string, val string) {
	if appl.IsFeatureEnabled(groupManagerExperiment) {
		server := sh.GetHybridGroup(handle)
		if server != nil {
			server.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, key, val)
		}
	}
}

func (sh *GroupManagerFunctionality) StopHybridGroups(appl libapp.Application) {
	// we are always permitted to stop servers
	for _, onion := range sh.ListHybridGroups() {
		sh.StopHybridGroup(appl, onion)
	}
}

func (sh *GroupManagerFunctionality) LaunchHybridGroups(appl libapp.Application, acn connectivity.ACN) {
	if appl.IsFeatureEnabled(groupManagerExperiment) {
		acnStatus, _ := acn.GetBootstrapStatus()
		if acnStatus == 100 {
			for _, onion := range sh.ListHybridGroups() {
				autostart := false
				if s := sh.GetHybridGroup(onion); s != nil {
					val, _ := s.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
					autostart = val == "true"
				}
				if autostart {
					sh.LaunchHybridGroup(appl, onion)
				}
			}
		}
	}
}

func (sf *GroupManagerFunctionality) Enable(application libapp.Application, acn connectivity.ACN) {
	lock.Lock()
	defer lock.Unlock()
	app = application
	if app != nil && !enabled {
		enabled = true

		// load unencrypted profiles
		sf.LoadHybridGroups(application, acn, libapp.DefactoPasswordForUnencryptedProfiles)

		go cacheForwardHybridGroupMetricUpdates(application)
	}
}

func (sf *GroupManagerFunctionality) LoadHybridGroups(appl libapp.Application, acn connectivity.ACN, password string) {
	oldList := app.ListProfilesOfTag(cwconst.ProfileTypeV1HybridGroup)
	app.LoadProfiles(password)
	groupList := app.ListProfilesOfTag(cwconst.ProfileTypeV1HybridGroup)
	numLoaded := len(groupList) - len (oldList)
	if numLoaded == 0 {
		log.Debugln("Loaded 0 groups")
		appl.GetPrimaryBus().Publish(event.NewEventList(ZeroHybridGroupsLoaded))
	} else {
		acnStatus, _ := acn.GetBootstrapStatus()
		for _, serverOnion := range groupList {
			serverInfo := sf.GetHybridGroupInfo(serverOnion)
			log.Debugf("Load Hybrid Groups NewGroup event: %s", serverInfo)
			ev := event.NewEvent(NewHybridGroup, make(map[event.Field]string))
			serverInfo.EnrichEvent(&ev)
			appl.GetPrimaryBus().Publish(ev)
			if serverInfo.Autostart && acnStatus == 100 {
				sf.LaunchHybridGroup(appl, serverOnion)
			}
		}
	}
}

func (sf *GroupManagerFunctionality) CreateHybridGroup(appl libapp.Application, password string, description string, autostart bool, groupAdmin string) {
	onion := appl.CreateProfileOfTag(description, password, autostart, cwconst.ProfileTypeV1HybridGroup)
	pr := appl.GetPeer(onion)
	pr.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.GroupManagerDescription, description)
	pr.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.GroupManagerAdmin, groupAdmin)

	if autostart {
		pr.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart, "true")
	} else {
		pr.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart, "false")
	}

	hgmf := hybrid.GroupManagerFunctionality{}
	_, err := hgmf.ManageNewGroup(pr)
	if err != nil {
		log.Errorf("could not create hybrid group: %v", err)
		return
	}
	err = hgmf.AddHybridContact(pr, groupAdmin)
	if err != nil {
		log.Errorf("could not add member to hybrid group: %v", err)
		return
	}

	hgmf.PromoteAdmin(pr, groupAdmin)

	// add group as contact for groupAdmin manually for now
	// TODO: eventually, modify heartbeat to allow outgoing connections to add hybrid group-type contacts
	adminPeer := appl.GetPeer(groupAdmin)
	interf := inter.FunctionalityGate()
	if adminPeer == nil {
		log.Errorf("CreateHybridGroup could not find group admin: %v", groupAdmin)
	} else {
		interf.ImportBundle(adminPeer, fmt.Sprintf("managed:%s", onion))
	}

	serverInfo := sf.GetHybridGroupInfo(onion)
	log.Debugf("NewHybridGroup event: %s", serverInfo)
	ev := event.NewEvent(NewHybridGroup, make(map[event.Field]string))
	serverInfo.EnrichEvent(&ev)
	appl.GetPrimaryBus().Publish(ev)
	if autostart {
		sf.LaunchHybridGroup(appl, onion)
	}
}

func (sf *GroupManagerFunctionality) GetHybridGroup(onion string) peer.CwtchPeer {
	return app.GetPeer(onion)
}

func (sf *GroupManagerFunctionality) GetHybridGroupStatistics(onion string) Statistics {
	return Statistics{}
}

func (sf *GroupManagerFunctionality) ListHybridGroups() []string {
	return app.ListProfilesOfTag(cwconst.ProfileTypeV1HybridGroup)
}

func (sf *GroupManagerFunctionality) DeleteHybridGroup(appl libapp.Application, onion string, currentPassword string) error {
	sf.StopHybridGroup(appl, onion)
	appl.GetPrimaryBus().Publish(event.NewEventList(HybridGroupIntentUpdate, event.Identity, onion, Intent, IntentStopped))
	oldList := app.ListProfilesOfTag(cwconst.ProfileTypeV1HybridGroup)
	app.DeleteProfile(onion, currentPassword)
	newList := app.ListProfilesOfTag(cwconst.ProfileTypeV1HybridGroup)
	if len(newList) != len(oldList) {
		appl.GetPrimaryBus().Publish(event.NewEventList(HybridGroupDeleted, event.Status, constants.StatusSuccess, event.Identity, onion))
	} else {
		appl.GetPrimaryBus().Publish(event.NewEventList(HybridGroupDeleted, event.Status, constants.StatusError, event.Error, "cantdelete", event.Identity, onion))
	}
	return nil
}

func (sf *GroupManagerFunctionality) LaunchHybridGroup(appl libapp.Application, onion string) {
	if appl.IsFeatureEnabled(groupManagerExperiment) {
		log.Debugf("LaunchHybridGroup called for %s", onion)
		// we don't want hybrid groups initiating outgoing connections
		// (otherwise they'll get created as "normal" peer contacts)
		appl.ConfigureConnections(onion, true, false, false)
		//app.ActivatePeerEngine(onion)
	}
}

func (sf *GroupManagerFunctionality) StopHybridGroup(appl libapp.Application, onion string) {
	// we are always permitted to stop servers
	app.DeactivatePeerEngine(onion)
	appl.GetPrimaryBus().Publish(event.NewEventList(HybridGroupIntentUpdate, event.Identity, onion, Intent, IntentStopped))
}

func (sf *GroupManagerFunctionality) DestroyHybridGroups() {
	// TODO: not this
	app.Shutdown()
}

func (sf *GroupManagerFunctionality) GetHybridGroupInfo(onion string) *HybridGroupInfo {
	s := app.GetPeer(onion)
	var serverInfo HybridGroupInfo
	serverInfo.Onion = s.GetOnion()
	serverInfo.GroupBundle = fmt.Sprintf("managed:%s", s.GetOnion())
	val, _ := s.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
	serverInfo.Autostart = val == "true"
	serverInfo.Running = val == "true" // TODO: add api to query this status
	serverInfo.Description, _ = s.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.GroupManagerDescription)
	serverInfo.StorageType = "n/a"
	return &serverInfo
}

func (si *HybridGroupInfo) EnrichEvent(e *event.Event) {
	e.Data["Onion"] = si.Onion
	e.Data["GroupBundle"] = si.GroupBundle
	e.Data["Description"] = si.Description
	e.Data["StorageType"] = si.StorageType
	if si.Autostart {
		e.Data["Autostart"] = "true"
	} else {
		e.Data["Autostart"] = "false"
	}
	if si.Running {
		e.Data["Running"] = "true"
	} else {
		e.Data["Running"] = "false"
	}
}

// cacheForwardServerMetricUpdates every minute gets metrics for all servers, and if they have changed, sends events to the UI
func cacheForwardHybridGroupMetricUpdates(appl libapp.Application) {
	/*
	var cache map[string]Statistics = make(map[string]Statistics)
	duration := time.Second // allow first load
	for {
		select {
		case <-time.After(duration):
			duration = time.Minute
			serverList := app.ListProfiles()
			for _, serverOnion := range serverList {
				server := app.GetPeer(serverOnion)
				if running, err := server.CheckStatus(); running && err == nil {
					newStats := server.GetStatistics()
					if stats, ok := cache[serverOnion]; !ok || stats.TotalConnections != newStats.TotalConnections || stats.TotalMessages != newStats.TotalMessages {
						cache[serverOnion] = newStats
						appl.GetPrimaryBus().Publish(event.NewEventList(ServerStatsUpdate, event.Identity, serverOnion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections)))
					}
				}
			}
		case <-killStatsUpdate:
			return
		}
	}
	*/
}

func (sh *GroupManagerFunctionality) Shutdown() {
	sh.Disable()
}

type Statistics struct {
	TotalMessages    int
	TotalConnections int
}

