package weave

import (
	"bytes"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"os"
	realexec "os/exec"
	"regexp"
	"strconv"

	"github.com/ugorji/go/codec"

	"github.com/weaveworks/common/exec"
)

const dockerAPIVersion = "1.22" // Support Docker Engine >= 1.10

// Client for Weave Net API
type Client interface {
	Status() (Status, error)
	AddDNSEntry(fqdn, containerid string, ip net.IP) error
	Expose() error // on the interface for mocking
}

// Status describes whats happen in the Weave Net router.
type Status struct {
	Version string
	Router  Router
	DNS     *DNS
	IPAM    *IPAM
	Proxy   *Proxy
	Plugin  *Plugin
}

// Router describes the status of the Weave Router
type Router struct {
	Name               string
	Encryption         bool
	ProtocolMinVersion int
	ProtocolMaxVersion int
	PeerDiscovery      bool
	Peers              []Peer
	Connections        []struct {
		Address  string
		Outbound bool
		State    string
		Info     string
	}
	Targets        []string
	TrustedSubnets []string
}

// Peer describes a peer in the weave network
type Peer struct {
	Name        string
	NickName    string
	Connections []struct {
		Name        string
		NickName    string
		Address     string
		Outbound    bool
		Established bool
	}
}

// DNS describes the status of Weave DNS
type DNS struct {
	Domain   string
	Upstream []string
	TTL      uint32
	Entries  []struct {
		Hostname    string
		ContainerID string
		Tombstone   int64
	}
}

// IPAM describes the status of Weave IPAM
type IPAM struct {
	Paxos *struct {
		Elector    bool
		KnownNodes int
		Quorum     uint
	}
	Range         string
	DefaultSubnet string
	Entries       []struct {
		Size        uint32
		IsKnownPeer bool
	}
	PendingAllocates []string
}

// Proxy describes the status of Weave Proxy
type Proxy struct {
	Addresses []string
}

// Plugin describes the status of the Weave Plugin
type Plugin struct {
	DriverName string
}

var ipMatch = regexp.MustCompile(`([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})(/[0-9]+)`)

type client struct {
	url string
}

// NewClient makes a new Client
func NewClient(url string) Client {
	return &client{
		url: url,
	}
}

func (c *client) Status() (Status, error) {
	req, err := http.NewRequest("GET", c.url+"/report", nil)
	if err != nil {
		return Status{}, err
	}
	req.Header.Add("Accept", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return Status{}, errorf("%v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return Status{}, fmt.Errorf("Got %d", resp.StatusCode)
	}

	var status Status
	decoder := codec.NewDecoder(resp.Body, &codec.JsonHandle{})
	if err := decoder.Decode(&status); err != nil {
		return Status{}, err
	}
	return status, nil
}

func (c *client) AddDNSEntry(fqdn, containerID string, ip net.IP) error {
	data := url.Values{
		"fqdn": []string{fqdn},
	}
	url := fmt.Sprintf("%s/name/%s/%s", c.url, containerID, ip.String())
	req, err := http.NewRequest("PUT", url, bytes.NewBufferString(data.Encode()))
	if err != nil {
		return err
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return errorf("%v", err)
	}
	if err := resp.Body.Close(); err != nil {
		return err
	}
	if resp.StatusCode != http.StatusNoContent {
		return fmt.Errorf("Got %d", resp.StatusCode)
	}
	return nil
}

func (c *client) Expose() error {
	cmd := weaveCommand("--local", "ps", "weave:expose")
	output, err := cmd.Output()
	if err != nil {
		stdErr := []byte{}
		if exitErr, ok := err.(*realexec.ExitError); ok {
			stdErr = exitErr.Stderr
		}
		return errorf("Error running weave ps: %s: %q", err, stdErr)
	}
	ips := ipMatch.FindAllSubmatch(output, -1)
	if ips != nil {
		// Alread exposed!
		return nil
	}
	cmd = weaveCommand("--local", "expose")
	if _, err := cmd.Output(); err != nil {
		stdErr := []byte{}
		if exitErr, ok := err.(*realexec.ExitError); ok {
			stdErr = exitErr.Stderr
		}
		return errorf("Error running weave expose: %s: %q", err, stdErr)
	}
	return nil
}

func weaveCommand(arg ...string) exec.Cmd {
	cmd := exec.Command("weave", arg...)
	cmd.SetEnv(append(os.Environ(), "DOCKER_API_VERSION="+dockerAPIVersion))
	return cmd
}

func errorf(format string, a ...interface{}) error {
	return fmt.Errorf(format+". If you are not running Weave Net, you may wish to suppress this warning by launching scope with the `--weave=false` option.", a...)
}
