// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package state

import (
	"net"

	"github.com/juju/errors"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
	"gopkg.in/mgo.v2/txn"

	"github.com/juju/juju/network"
)

// SubnetInfo describes a single subnet.
type SubnetInfo struct {
	// ProviderId is a provider-specific network id. This may be empty.
	ProviderId network.Id

	// CIDR of the network, in 123.45.67.89/24 format.
	CIDR string

	// VLANTag needs to be between 1 and 4094 for VLANs and 0 for normal
	// networks. It's defined by IEEE 802.1Q standard.
	VLANTag int

	// AvailabilityZone describes which availability zone this subnet is in. It can
	// be empty if the provider does not support availability zones.
	AvailabilityZone string

	// SpaceName is the name of the space the subnet is associated with. It
	// can be empty if the subnet is not associated with a space yet.
	SpaceName string
}

type Subnet struct {
	st  *State
	doc subnetDoc
}

type subnetDoc struct {
	DocID            string `bson:"_id"`
	ModelUUID        string `bson:"model-uuid"`
	Life             Life   `bson:"life"`
	ProviderId       string `bson:"providerid,omitempty"`
	CIDR             string `bson:"cidr"`
	VLANTag          int    `bson:"vlantag,omitempty"`
	AvailabilityZone string `bson:"availabilityzone,omitempty"`
	// TODO: add IsPublic to SubnetArgs, add an IsPublic method and add
	// IsPublic to migration import/export.
	IsPublic  bool   `bson:"is-public,omitempty"`
	SpaceName string `bson:"space-name,omitempty"`
}

// Life returns whether the subnet is Alive, Dying or Dead.
func (s *Subnet) Life() Life {
	return s.doc.Life
}

// ID returns the unique id for the subnet, for other entities to reference it.
func (s *Subnet) ID() string {
	return s.doc.DocID
}

// String implements fmt.Stringer.
func (s *Subnet) String() string {
	return s.CIDR()
}

// GoString implements fmt.GoStringer.
func (s *Subnet) GoString() string {
	return s.String()
}

// EnsureDead sets the Life of the subnet to Dead, if it's Alive. If the subnet
// is already Dead, no error is returned. When the subnet is no longer Alive or
// already removed, errNotAlive is returned.
func (s *Subnet) EnsureDead() (err error) {
	defer errors.DeferredAnnotatef(&err, "cannot set subnet %q to dead", s)

	if s.doc.Life == Dead {
		return nil
	}

	ops := []txn.Op{{
		C:      subnetsC,
		Id:     s.doc.DocID,
		Update: bson.D{{"$set", bson.D{{"life", Dead}}}},
		Assert: isAliveDoc,
	}}

	txnErr := s.st.runTransaction(ops)
	if txnErr == nil {
		s.doc.Life = Dead
		return nil
	}
	return onAbort(txnErr, errNotAlive)
}

// Remove removes a Dead subnet. If the subnet is not Dead or it is already
// removed, an error is returned. On success, all IP addresses added to the
// subnet are also removed.
func (s *Subnet) Remove() (err error) {
	defer errors.DeferredAnnotatef(&err, "cannot remove subnet %q", s)

	if s.doc.Life != Dead {
		return errors.New("subnet is not dead")
	}

	ops := []txn.Op{{
		C:      subnetsC,
		Id:     s.doc.DocID,
		Remove: true,
		Assert: isDeadDoc,
	}}
	if s.doc.ProviderId != "" {
		op := s.st.networkEntityGlobalKeyRemoveOp("subnet", s.ProviderId())
		ops = append(ops, op)
	}

	txnErr := s.st.runTransaction(ops)
	if txnErr == nil {
		return nil
	}
	return onAbort(txnErr, errors.New("not found or not dead"))
}

// ProviderId returns the provider-specific id of the subnet.
func (s *Subnet) ProviderId() network.Id {
	return network.Id(s.doc.ProviderId)
}

// CIDR returns the subnet CIDR (e.g. 192.168.50.0/24).
func (s *Subnet) CIDR() string {
	return s.doc.CIDR
}

// VLANTag returns the subnet VLAN tag. It's a number between 1 and
// 4094 for VLANs and 0 if the network is not a VLAN.
func (s *Subnet) VLANTag() int {
	return s.doc.VLANTag
}

// AvailabilityZone returns the availability zone of the subnet. If the subnet
// is not associated with an availability zone it will be the empty string.
func (s *Subnet) AvailabilityZone() string {
	return s.doc.AvailabilityZone
}

// SpaceName returns the space the subnet is associated with. If the subnet is
// not associated with a space it will be the empty string.
func (s *Subnet) SpaceName() string {
	return s.doc.SpaceName
}

// Validate validates the subnet, checking the CIDR, and VLANTag, if present.
func (s *Subnet) Validate() error {
	if s.doc.CIDR != "" {
		_, _, err := net.ParseCIDR(s.doc.CIDR)
		if err != nil {
			return errors.Trace(err)
		}
	} else {
		return errors.Errorf("missing CIDR")
	}

	if s.doc.VLANTag < 0 || s.doc.VLANTag > 4094 {
		return errors.Errorf("invalid VLAN tag %d: must be between 0 and 4094", s.doc.VLANTag)
	}

	return nil
}

// Refresh refreshes the contents of the Subnet from the underlying
// state. It an error that satisfies errors.IsNotFound if the Subnet has
// been removed.
func (s *Subnet) Refresh() error {
	subnets, closer := s.st.getCollection(subnetsC)
	defer closer()

	err := subnets.FindId(s.doc.DocID).One(&s.doc)
	if err == mgo.ErrNotFound {
		return errors.NotFoundf("subnet %q", s)
	}
	if err != nil {
		return errors.Errorf("cannot refresh subnet %q: %v", s, err)
	}
	return nil
}

// AddSubnet creates and returns a new subnet
func (st *State) AddSubnet(args SubnetInfo) (subnet *Subnet, err error) {
	defer errors.DeferredAnnotatef(&err, "adding subnet %q", args.CIDR)

	subnet, err = st.newSubnetFromArgs(args)
	if err != nil {
		return nil, errors.Trace(err)
	}
	ops := st.addSubnetOps(args)
	ops = append(ops, assertModelActiveOp(st.ModelUUID()))
	buildTxn := func(attempt int) ([]txn.Op, error) {

		if attempt != 0 {
			if err := checkModelActive(st); err != nil {
				return nil, errors.Trace(err)
			}
			if _, err = st.Subnet(args.CIDR); err == nil {
				return nil, errors.AlreadyExistsf("subnet %q", args.CIDR)
			}
			if err := subnet.Refresh(); err != nil {
				if errors.IsNotFound(err) {
					return nil, errors.Errorf("ProviderId %q not unique", args.ProviderId)
				}
				return nil, errors.Trace(err)
			}
		}
		return ops, nil
	}
	err = st.run(buildTxn)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return subnet, nil
}

func (st *State) newSubnetFromArgs(args SubnetInfo) (*Subnet, error) {
	subnetID := st.docID(args.CIDR)
	subDoc := subnetDoc{
		DocID:            subnetID,
		ModelUUID:        st.ModelUUID(),
		Life:             Alive,
		CIDR:             args.CIDR,
		VLANTag:          args.VLANTag,
		ProviderId:       string(args.ProviderId),
		AvailabilityZone: args.AvailabilityZone,
		SpaceName:        args.SpaceName,
	}
	subnet := &Subnet{doc: subDoc, st: st}
	err := subnet.Validate()
	if err != nil {
		return nil, errors.Trace(err)
	}
	return subnet, nil
}

func (st *State) addSubnetOps(args SubnetInfo) []txn.Op {
	subnetID := st.docID(args.CIDR)
	subDoc := subnetDoc{
		DocID:            subnetID,
		ModelUUID:        st.ModelUUID(),
		Life:             Alive,
		CIDR:             args.CIDR,
		VLANTag:          args.VLANTag,
		ProviderId:       string(args.ProviderId),
		AvailabilityZone: args.AvailabilityZone,
		SpaceName:        args.SpaceName,
	}
	ops := []txn.Op{
		{
			C:      subnetsC,
			Id:     subnetID,
			Assert: txn.DocMissing,
			Insert: subDoc,
		},
	}
	if args.ProviderId != "" {
		ops = append(ops, st.networkEntityGlobalKeyOp("subnet", args.ProviderId))
	}
	return ops
}

// Subnet returns the subnet specified by the cidr.
func (st *State) Subnet(cidr string) (*Subnet, error) {
	subnets, closer := st.getCollection(subnetsC)
	defer closer()

	doc := &subnetDoc{}
	err := subnets.FindId(cidr).One(doc)
	if err == mgo.ErrNotFound {
		return nil, errors.NotFoundf("subnet %q", cidr)
	}
	if err != nil {
		return nil, errors.Annotatef(err, "cannot get subnet %q", cidr)
	}
	return &Subnet{st, *doc}, nil
}

// AllSubnets returns all known subnets in the model.
func (st *State) AllSubnets() (subnets []*Subnet, err error) {
	subnetsCollection, closer := st.getCollection(subnetsC)
	defer closer()

	docs := []subnetDoc{}
	err = subnetsCollection.Find(nil).All(&docs)
	if err != nil {
		return nil, errors.Annotatef(err, "cannot get all subnets")
	}
	for _, doc := range docs {
		subnets = append(subnets, &Subnet{st, doc})
	}
	return subnets, nil
}
