package govcd

import (
	"crypto/tls"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"sync"
	"time"
)

type VCDClient struct {
	OrgHREF     url.URL // vCloud Director OrgRef
	Org         Org     // Org
	OrgVdc      Vdc     // Org vDC
	Client      Client  // Client for the underlying VCD instance
	sessionHREF url.URL // HREF for the session API
	QueryHREF   url.URL // HREF for the query API
	Mutex       sync.Mutex
}

type supportedVersions struct {
	VersionInfo struct {
		Version  string `xml:"Version"`
		LoginUrl string `xml:"LoginUrl"`
	} `xml:"VersionInfo"`
}

func (c *VCDClient) vcdloginurl() error {

	s := c.Client.VCDVDCHREF
	s.Path += "/versions"

	// No point in checking for errors here
	req := c.Client.NewRequest(map[string]string{}, "GET", s, nil)

	resp, err := checkResp(c.Client.Http.Do(req))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	supportedVersions := new(supportedVersions)

	err = decodeBody(resp, supportedVersions)

	if err != nil {
		return fmt.Errorf("error decoding versions response: %s", err)
	}

	u, err := url.Parse(supportedVersions.VersionInfo.LoginUrl)
	if err != nil {
		return fmt.Errorf("couldn't find a LoginUrl in versions")
	}
	c.sessionHREF = *u
	return nil
}

func (c *VCDClient) vcdauthorize(user, pass, org string) error {

	if user == "" {
		user = os.Getenv("VCLOUD_USERNAME")
	}

	if pass == "" {
		pass = os.Getenv("VCLOUD_PASSWORD")
	}

	if org == "" {
		org = os.Getenv("VCLOUD_ORG")
	}

	// No point in checking for errors here
	req := c.Client.NewRequest(map[string]string{}, "POST", c.sessionHREF, nil)

	// Set Basic Authentication Header
	req.SetBasicAuth(user+"@"+org, pass)

	// Add the Accept header for vCA
	req.Header.Add("Accept", "application/*+xml;version=5.5")

	resp, err := checkResp(c.Client.Http.Do(req))
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Store the authentication header
	c.Client.VCDToken = resp.Header.Get("x-vcloud-authorization")
	c.Client.VCDAuthHeader = "x-vcloud-authorization"

	session := new(session)
	err = decodeBody(resp, session)

	if err != nil {
		fmt.Errorf("error decoding session response: %s", err)
	}

	org_found := false
	// Loop in the session struct to find the organization and query api.
	for _, s := range session.Link {
		if s.Type == "application/vnd.vmware.vcloud.org+xml" && s.Rel == "down" {
			u, err := url.Parse(s.HREF)
			if err != nil {
				return fmt.Errorf("couldn't find a Organization in current session, %v", err)
			}
			c.OrgHREF = *u
			org_found = true
		}
		if s.Type == "application/vnd.vmware.vcloud.query.queryList+xml" && s.Rel == "down" {
			u, err := url.Parse(s.HREF)
			if err != nil {
				return fmt.Errorf("couldn't find a Query API in current session, %v", err)
			}
			c.QueryHREF = *u
		}
	}
	if !org_found {
		return fmt.Errorf("couldn't find a Organization in current session")
	}

	// Loop in the session struct to find the session url.
	session_found := false
	for _, s := range session.Link {
		if s.Rel == "remove" {
			u, err := url.Parse(s.HREF)
			if err != nil {
				return fmt.Errorf("couldn't find a logout HREF in current session, %v", err)
			}
			c.sessionHREF = *u
			session_found = true
		}
	}
	if !session_found {
		return fmt.Errorf("couldn't find a logout HREF in current session")
	}
	return nil
}

func (c *VCDClient) RetrieveOrg(vcdname string) (Org, error) {

	req := c.Client.NewRequest(map[string]string{}, "GET", c.OrgHREF, nil)
	req.Header.Add("Accept", "vnd.vmware.vcloud.org+xml;version=5.5")

	// TODO: wrap into checkresp to parse error
	resp, err := checkResp(c.Client.Http.Do(req))
	if err != nil {
		return Org{}, fmt.Errorf("error retreiving org: %s", err)
	}

	org := NewOrg(&c.Client)

	if err = decodeBody(resp, org.Org); err != nil {
		return Org{}, fmt.Errorf("error decoding org response: %s", err)
	}

	// Get the VDC ref from the Org
	for _, s := range org.Org.Link {
		if s.Type == "application/vnd.vmware.vcloud.vdc+xml" && s.Rel == "down" {
			if vcdname != "" && s.Name != vcdname {
				continue
			}
			u, err := url.Parse(s.HREF)
			if err != nil {
				return Org{}, err
			}
			c.Client.VCDVDCHREF = *u
		}
	}

	if &c.Client.VCDVDCHREF == nil {
		return Org{}, fmt.Errorf("error finding the organization VDC HREF")
	}

	return *org, nil
}

func NewVCDClient(vcdEndpoint url.URL, insecure bool) *VCDClient {

	return &VCDClient{
		Client: Client{
			APIVersion: "5.5",
			VCDVDCHREF: vcdEndpoint,
			Http: http.Client{
				Transport: &http.Transport{
					TLSClientConfig: &tls.Config{
						InsecureSkipVerify: insecure,
					},
					Proxy:               http.ProxyFromEnvironment,
					TLSHandshakeTimeout: 120 * time.Second,
				},
			},
		},
	}
}

// Authenticate is an helper function that performs a login in vCloud Director.
func (c *VCDClient) Authenticate(username, password, org, vdcname string) (Org, Vdc, error) {

	// LoginUrl
	err := c.vcdloginurl()
	if err != nil {
		return Org{}, Vdc{}, fmt.Errorf("error finding LoginUrl: %s", err)
	}
	// Authorize
	err = c.vcdauthorize(username, password, org)
	if err != nil {
		return Org{}, Vdc{}, fmt.Errorf("error authorizing: %s", err)
	}

	// Get Org
	o, err := c.RetrieveOrg(vdcname)
	if err != nil {
		return Org{}, Vdc{}, fmt.Errorf("error acquiring Org: %s", err)
	}

	vdc, err := c.Client.retrieveVDC()

	if err != nil {
		return Org{}, Vdc{}, fmt.Errorf("error retrieving the organization VDC")
	}

	return o, vdc, nil
}

// Disconnect performs a disconnection from the vCloud Director API endpoint.
func (c *VCDClient) Disconnect() error {
	if c.Client.VCDToken == "" && c.Client.VCDAuthHeader == "" {
		return fmt.Errorf("cannot disconnect, client is not authenticated")
	}

	req := c.Client.NewRequest(map[string]string{}, "DELETE", c.sessionHREF, nil)

	// Add the Accept header for vCA
	req.Header.Add("Accept", "application/xml;version=5.5")

	// Set Authorization Header
	req.Header.Add(c.Client.VCDAuthHeader, c.Client.VCDToken)

	if _, err := checkResp(c.Client.Http.Do(req)); err != nil {
		return fmt.Errorf("error processing session delete for vCloud Director: %s", err)
	}
	return nil
}
