package libpod

import (
	"encoding/json"
	"path/filepath"

	"github.com/boltdb/bolt"
	"github.com/containers/storage"
	"github.com/pkg/errors"
)

const (
	idRegistryName    = "id-registry"
	nameRegistryName  = "name-registry"
	ctrConfigName     = "container-config"
	ctrStateName      = "container-state"
	netNSName         = "net-ns"
	runtimeConfigName = "runtime-config"
	ctrDependsName    = "container-depends"
	podName           = "pod"
	podContainersName = "pod-containers"
)

var (
	idRegistryBkt    = []byte(idRegistryName)
	nameRegistryBkt  = []byte(nameRegistryName)
	ctrConfigBkt     = []byte(ctrConfigName)
	ctrStateBkt      = []byte(ctrStateName)
	netNSBkt         = []byte(netNSName)
	runtimeConfigBkt = []byte(runtimeConfigName)
	ctrDependsBkt    = []byte(ctrDependsName)
	podBkt           = []byte(podName)
	podContainersBkt = []byte(podContainersName)
)

// Check if the configuration of the database is compatible with the
// configuration of the runtime opening it
// If there is no runtime configuration loaded, load our own
func checkRuntimeConfig(db *bolt.DB, runtime *Runtime) error {
	var (
		staticDir       = []byte("static-dir")
		tmpDir          = []byte("tmp-dir")
		runRoot         = []byte("run-root")
		graphRoot       = []byte("graph-root")
		graphDriverName = []byte("graph-driver-name")
	)

	err := db.Update(func(tx *bolt.Tx) error {
		configBkt, err := getRuntimeConfigBucket(tx)
		if err != nil {
			return err
		}

		if err := validateDBAgainstConfig(configBkt, "static dir",
			runtime.config.StaticDir, staticDir); err != nil {
			return err
		}

		if err := validateDBAgainstConfig(configBkt, "tmp dir",
			runtime.config.TmpDir, tmpDir); err != nil {
			return err
		}

		if err := validateDBAgainstConfig(configBkt, "run root",
			runtime.config.StorageConfig.RunRoot, runRoot); err != nil {
			return err
		}

		if err := validateDBAgainstConfig(configBkt, "graph root",
			runtime.config.StorageConfig.GraphRoot, graphRoot); err != nil {
			return err
		}

		return validateDBAgainstConfig(configBkt, "graph driver name",
			runtime.config.StorageConfig.GraphDriverName,
			graphDriverName)
	})

	return err
}

// Validate a configuration entry in the DB against current runtime config
// If the given configuration key does not exist it will be created
func validateDBAgainstConfig(bucket *bolt.Bucket, fieldName, runtimeValue string, keyName []byte) error {
	keyBytes := bucket.Get(keyName)
	if keyBytes == nil {
		if err := bucket.Put(keyName, []byte(runtimeValue)); err != nil {
			return errors.Wrapf(err, "error updating %s in DB runtime config", fieldName)
		}
	} else {
		if runtimeValue != string(keyBytes) {
			return errors.Wrapf(ErrDBBadConfig, "database %s %s does not match our %s %s",
				fieldName, string(keyBytes), fieldName, runtimeValue)
		}
	}

	return nil
}

func (s *BoltState) getDBCon() (*bolt.DB, error) {
	db, err := bolt.Open(s.dbPath, 0600, nil)
	if err != nil {
		return nil, errors.Wrapf(err, "error opening database %s", s.dbPath)
	}

	return db, nil
}

func getIDBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(idRegistryBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "id registry bucket not found in DB")
	}
	return bkt, nil
}

func getNamesBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(nameRegistryBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "name registry bucket not found in DB")
	}
	return bkt, nil
}

func getCtrConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(ctrConfigBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "container config bucket not found in DB")
	}
	return bkt, nil
}

func getCtrStateBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(ctrStateBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "container state bucket not found in DB")
	}
	return bkt, nil
}

func getNetNSBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(netNSBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "network namespace bucket not found in DB")
	}
	return bkt, nil
}

func getRuntimeConfigBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(runtimeConfigBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "runtime configuration bucket not found in DB")
	}
	return bkt, nil
}

func getCtrDependsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(ctrDependsBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "container dependencies bucket not found in DB")
	}
	return bkt, nil
}

func getPodBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(podBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "pods bucket not found in DB")
	}
	return bkt, nil
}

func getPodContainersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
	bkt := tx.Bucket(podContainersBkt)
	if bkt == nil {
		return nil, errors.Wrapf(ErrDBBadConfig, "pod containers bucket not found in DB")
	}
	return bkt, nil
}

func (s *BoltState) getContainerFromDB(id []byte, ctr *Container, config, state, netNS *bolt.Bucket) error {
	configBytes := config.Get(id)
	if configBytes == nil {
		return errors.Wrapf(ErrNoSuchCtr, "error unmarshalling container %s config", string(id))
	}

	if err := json.Unmarshal(configBytes, ctr.config); err != nil {
		return errors.Wrapf(err, "error unmarshalling container %s config", string(id))
	}

	stateBytes := state.Get(id)
	if stateBytes == nil {
		return errors.Wrapf(ErrInternal, "container %s has config but no state", string(id))
	}

	if err := json.Unmarshal(stateBytes, ctr.state); err != nil {
		return errors.Wrapf(err, "error unmarshalling container %s state", string(id))
	}

	// The container may not have a network namespace, so it's OK if this is
	// nil
	netNSBytes := netNS.Get(id)
	if netNSBytes != nil {
		nsPath := string(netNSBytes)
		netNS, err := joinNetNS(nsPath)
		if err != nil {
			return errors.Wrapf(err, "error joining network namespace for container %s", string(id))
		}
		ctr.state.NetNS = netNS
	}

	// Get the lock
	lockPath := filepath.Join(s.lockDir, string(id))
	lock, err := storage.GetLockfile(lockPath)
	if err != nil {
		return errors.Wrapf(err, "error retrieving lockfile for container %s", string(id))
	}
	ctr.lock = lock

	ctr.runtime = s.runtime
	ctr.valid = true

	return nil
}