package proxyprocess

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"

	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/lib/file"
)

// snapshot is the structure of the snapshot file. This is unexported because
// we don't want this being a public API.
//
// The snapshot doesn't contain any configuration for the manager. We only
// want to restore the proxies that we're managing, and we use the config
// set at runtime to sync and reconcile what proxies we should start,
// restart, stop, or have already running.
type snapshot struct {
	// Version is the version of the snapshot format and can be used
	// to safely update the format in the future if necessary.
	Version int

	// Proxies are the set of proxies that the manager has.
	Proxies map[string]snapshotProxy
}

// snapshotProxy represents a single proxy.
type snapshotProxy struct {
	// Mode corresponds to the type of proxy running.
	Mode structs.ProxyExecMode

	// Config is an opaque mapping of primitive values that the proxy
	// implementation uses to restore state.
	Config map[string]interface{}
}

// snapshotVersion is the current version to encode within the snapshot.
const snapshotVersion = 1

// SnapshotPath returns the default snapshot path for this manager. This
// will return empty if DataDir is not set. This file may not exist yet.
func (m *Manager) SnapshotPath() string {
	if m.DataDir == "" {
		return ""
	}

	return filepath.Join(m.DataDir, "snapshot.json")
}

// Snapshot will persist a snapshot of the proxy manager state that
// can be restored with Restore.
//
// If DataDir is non-empty, then the Manager will automatically snapshot
// whenever the set of managed proxies changes. This method generally doesn't
// need to be called manually.
func (m *Manager) Snapshot(path string) error {
	m.lock.Lock()
	defer m.lock.Unlock()
	return m.snapshot(path, false)
}

// snapshot is the internal function analogous to Snapshot but expects
// a lock to already be held.
//
// checkDup when set will store the snapshot on lastSnapshot and use
// reflect.DeepEqual to verify that its not writing an identical snapshot.
func (m *Manager) snapshot(path string, checkDup bool) error {
	// Build the snapshot
	s := snapshot{
		Version: snapshotVersion,
		Proxies: make(map[string]snapshotProxy, len(m.proxies)),
	}
	for id, p := range m.proxies {
		// Get the snapshot configuration. If the configuration is nil or
		// empty then we don't persist this proxy.
		config := p.MarshalSnapshot()
		if len(config) == 0 {
			continue
		}

		s.Proxies[id] = snapshotProxy{
			Mode:   proxyExecMode(p),
			Config: config,
		}
	}

	// Dup detection, if the snapshot is identical to the last, do nothing
	if checkDup && reflect.DeepEqual(m.lastSnapshot, &s) {
		return nil
	}

	// Encode as JSON
	encoded, err := json.Marshal(&s)
	if err != nil {
		return err
	}

	// Write the file
	err = file.WriteAtomic(path, encoded)

	// If we are checking for dups and we had a successful write, store
	// it so we don't rewrite the same value.
	if checkDup && err == nil {
		m.lastSnapshot = &s
	}
	return err
}

// Restore restores the manager state from a snapshot at path. If path
// doesn't exist, this does nothing and no error is returned.
//
// This restores proxy state but does not restore any Manager configuration
// such as DataDir, Logger, etc. All of those should be set _before_ Restore
// is called.
//
// Restore must be called before Run. Restore will immediately start
// supervising the restored processes but will not sync with the local
// state store until Run is called.
//
// If an error is returned the manager state is left untouched.
func (m *Manager) Restore(path string) error {
	buf, err := ioutil.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}

		return err
	}

	var s snapshot
	if err := json.Unmarshal(buf, &s); err != nil {
		return err
	}

	// Verify the version matches so we can be more confident that we're
	// decoding a structure that we expect.
	if s.Version != snapshotVersion {
		return fmt.Errorf("unknown snapshot version, expecting %d", snapshotVersion)
	}

	// Build the proxies from the snapshot
	proxies := make(map[string]Proxy, len(s.Proxies))
	for id, sp := range s.Proxies {
		p, err := m.newProxyFromMode(sp.Mode, id)
		if err != nil {
			return err
		}

		// Unmarshal the proxy. If there is an error we just continue on and
		// ignore it. Errors restoring proxies should be exceptionally rare
		// and only under scenarios where the proxy isn't running anymore or
		// we won't have permission to access it. We log and continue.
		if err := p.UnmarshalSnapshot(sp.Config); err != nil {
			m.Logger.Printf("[WARN] agent/proxy: error restoring proxy %q: %s", id, err)
			continue
		}

		proxies[id] = p
	}

	// Overwrite the proxies. The documentation notes that this will happen.
	m.lock.Lock()
	defer m.lock.Unlock()
	m.proxies = proxies
	return nil
}
