package remote

import (
	"fmt"
	"net"

	log "github.com/Sirupsen/logrus"
	"github.com/docker/docker/pkg/plugins"
	"github.com/docker/libnetwork/driverapi"
	"github.com/docker/libnetwork/types"
)

type driver struct {
	endpoint    *plugins.Client
	networkType string
}

func newDriver(name string, client *plugins.Client) driverapi.Driver {
	return &driver{networkType: name, endpoint: client}
}

// Init makes sure a remote driver is registered when a network driver
// plugin is activated.
func Init(dc driverapi.DriverCallback) error {
	plugins.Handle(driverapi.NetworkPluginEndpointType, func(name string, client *plugins.Client) {
		if err := dc.RegisterDriver(name, newDriver(name, client)); err != nil {
			log.Errorf("error registering driver for %s due to %v", name, err)
		}
	})
	return nil
}

// Config is not implemented for remote drivers, since it is assumed
// to be supplied to the remote process out-of-band (e.g., as command
// line arguments).
func (d *driver) Config(option map[string]interface{}) error {
	return &driverapi.ErrNotImplemented{}
}

func (d *driver) call(methodName string, arg interface{}, retVal maybeError) error {
	method := driverapi.NetworkPluginEndpointType + "." + methodName
	err := d.endpoint.Call(method, arg, retVal)
	if err != nil {
		return err
	}
	if e := retVal.getError(); e != "" {
		return fmt.Errorf("remote: %s", e)
	}
	return nil
}

func (d *driver) CreateNetwork(id types.UUID, options map[string]interface{}) error {
	create := &createNetworkRequest{
		NetworkID: string(id),
		Options:   options,
	}
	return d.call("CreateNetwork", create, &createNetworkResponse{})
}

func (d *driver) DeleteNetwork(nid types.UUID) error {
	delete := &deleteNetworkRequest{NetworkID: string(nid)}
	return d.call("DeleteNetwork", delete, &deleteNetworkResponse{})
}

func (d *driver) CreateEndpoint(nid, eid types.UUID, epInfo driverapi.EndpointInfo, epOptions map[string]interface{}) error {
	if epInfo == nil {
		return fmt.Errorf("must not be called with nil EndpointInfo")
	}

	reqIfaces := make([]*endpointInterface, len(epInfo.Interfaces()))
	for i, iface := range epInfo.Interfaces() {
		addr4 := iface.Address()
		addr6 := iface.AddressIPv6()
		reqIfaces[i] = &endpointInterface{
			ID:          iface.ID(),
			Address:     addr4.String(),
			AddressIPv6: addr6.String(),
			MacAddress:  iface.MacAddress().String(),
		}
	}
	create := &createEndpointRequest{
		NetworkID:  string(nid),
		EndpointID: string(eid),
		Interfaces: reqIfaces,
		Options:    epOptions,
	}
	var res createEndpointResponse
	if err := d.call("CreateEndpoint", create, &res); err != nil {
		return err
	}

	ifaces, err := res.parseInterfaces()
	if err != nil {
		return err
	}
	if len(reqIfaces) > 0 && len(ifaces) > 0 {
		// We're not supposed to add interfaces if there already are
		// some. Attempt to roll back
		return errorWithRollback("driver attempted to add more interfaces", d.DeleteEndpoint(nid, eid))
	}
	for _, iface := range ifaces {
		var addr4, addr6 net.IPNet
		if iface.Address != nil {
			addr4 = *(iface.Address)
		}
		if iface.AddressIPv6 != nil {
			addr6 = *(iface.AddressIPv6)
		}
		if err := epInfo.AddInterface(iface.ID, iface.MacAddress, addr4, addr6); err != nil {
			return errorWithRollback(fmt.Sprintf("failed to AddInterface %v: %s", iface, err), d.DeleteEndpoint(nid, eid))
		}
	}
	return nil
}

func errorWithRollback(msg string, err error) error {
	rollback := "rolled back"
	if err != nil {
		rollback = "failed to roll back: " + err.Error()
	}
	return fmt.Errorf("%s; %s", msg, rollback)
}

func (d *driver) DeleteEndpoint(nid, eid types.UUID) error {
	delete := &deleteEndpointRequest{
		NetworkID:  string(nid),
		EndpointID: string(eid),
	}
	return d.call("DeleteEndpoint", delete, &deleteEndpointResponse{})
}

func (d *driver) EndpointOperInfo(nid, eid types.UUID) (map[string]interface{}, error) {
	info := &endpointInfoRequest{
		NetworkID:  string(nid),
		EndpointID: string(eid),
	}
	var res endpointInfoResponse
	if err := d.call("EndpointOperInfo", info, &res); err != nil {
		return nil, err
	}
	return res.Value, nil
}

// Join method is invoked when a Sandbox is attached to an endpoint.
func (d *driver) Join(nid, eid types.UUID, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error {
	join := &joinRequest{
		NetworkID:  string(nid),
		EndpointID: string(eid),
		SandboxKey: sboxKey,
		Options:    options,
	}
	var (
		res joinResponse
		err error
	)
	if err = d.call("Join", join, &res); err != nil {
		return err
	}

	// Expect each interface ID given by CreateEndpoint to have an
	// entry at that index in the names supplied here. In other words,
	// if you supply 0..n interfaces with IDs 0..n above, you should
	// supply the names in the same order.
	ifaceNames := res.InterfaceNames
	for _, iface := range jinfo.InterfaceNames() {
		i := iface.ID()
		if i >= len(ifaceNames) || i < 0 {
			return fmt.Errorf("no correlating interface %d in supplied interface names", i)
		}
		supplied := ifaceNames[i]
		if err := iface.SetNames(supplied.SrcName, supplied.DstName); err != nil {
			return errorWithRollback(fmt.Sprintf("failed to set interface name: %s", err), d.Leave(nid, eid))
		}
	}

	var addr net.IP
	if res.Gateway != "" {
		if addr = net.ParseIP(res.Gateway); addr == nil {
			return fmt.Errorf(`unable to parse Gateway "%s"`, res.Gateway)
		}
		if jinfo.SetGateway(addr) != nil {
			return errorWithRollback(fmt.Sprintf("failed to set gateway: %v", addr), d.Leave(nid, eid))
		}
	}
	if res.GatewayIPv6 != "" {
		if addr = net.ParseIP(res.GatewayIPv6); addr == nil {
			return fmt.Errorf(`unable to parse GatewayIPv6 "%s"`, res.GatewayIPv6)
		}
		if jinfo.SetGatewayIPv6(addr) != nil {
			return errorWithRollback(fmt.Sprintf("failed to set gateway IPv6: %v", addr), d.Leave(nid, eid))
		}
	}
	if jinfo.SetHostsPath(res.HostsPath) != nil {
		return errorWithRollback(fmt.Sprintf("failed to set hosts path: %s", res.HostsPath), d.Leave(nid, eid))
	}
	if jinfo.SetResolvConfPath(res.ResolvConfPath) != nil {
		return errorWithRollback(fmt.Sprintf("failed to set resolv.conf path: %s", res.ResolvConfPath), d.Leave(nid, eid))
	}
	return nil
}

// Leave method is invoked when a Sandbox detaches from an endpoint.
func (d *driver) Leave(nid, eid types.UUID) error {
	leave := &leaveRequest{
		NetworkID:  string(nid),
		EndpointID: string(eid),
	}
	return d.call("Leave", leave, &leaveResponse{})
}

func (d *driver) Type() string {
	return d.networkType
}
