/*
 * Copyright 2014 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
 */

package govcd

import (
	"fmt"
	"net/http"
	"net/url"
	"os"
	"time"

	types "github.com/hmrc/vmware-govcd/types/v56"
)

// Client provides a client to vCloud Air, values can be populated automatically using the Authenticate method.
type VAClient struct {
	VAToken    string  // vCloud Air authorization token
	VAEndpoint url.URL // vCloud Air API endpoint
	Region     string  // Region where the compute resource lives.
	Client     Client  // Client for the underlying vCD instance
}

// VCHS API

type services struct {
	Service []struct {
		Region      string `xml:"region,attr"`
		ServiceID   string `xml:"serviceId,attr"`
		ServiceType string `xml:"serviceType,attr"`
		Type        string `xml:"type,attr"`
		HREF        string `xml:"href,attr"`
	} `xml:"Service"`
}

type session struct {
	Link []*types.Link `xml:"Link"`
}

type computeResources struct {
	VdcRef []struct {
		Status string        `xml:"status,attr"`
		Name   string        `xml:"name,attr"`
		Type   string        `xml:"type,attr"`
		HREF   string        `xml:"href,attr"`
		Link   []*types.Link `xml:"Link"`
	} `xml:"VdcRef"`
}

type vCloudSession struct {
	VdcLink []struct {
		AuthorizationToken  string `xml:"authorizationToken,attr"`
		AuthorizationHeader string `xml:"authorizationHeader,attr"`
		Name                string `xml:"name,attr"`
		HREF                string `xml:"href,attr"`
	} `xml:"VdcLink"`
}

//

func (c *VAClient) vaauthorize(user, pass string) (u url.URL, err error) {

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

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

	s := c.VAEndpoint
	s.Path += "/vchs/sessions"

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

	// Set Basic Authentication Header
	req.SetBasicAuth(user, pass)

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

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

	// Store the authentication header
	c.VAToken = resp.Header.Get("X-Vchs-Authorization")

	session := new(session)

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

	// Loop in the session struct to find right service and compute resource.
	for _, s := range session.Link {
		if s.Type == "application/xml;class=vnd.vmware.vchs.servicelist" && s.Rel == "down" {
			u, err := url.ParseRequestURI(s.HREF)
			return *u, err
		}
	}
	return url.URL{}, fmt.Errorf("couldn't find a Service List in current session")
}

func (c *VAClient) vaacquireservice(s url.URL, cid string) (u url.URL, err error) {

	if cid == "" {
		cid = os.Getenv("VCLOUDAIR_COMPUTEID")
	}

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

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

	// Set Authorization Header for vCA
	req.Header.Add("x-vchs-authorization", c.VAToken)

	resp, err := checkResp(c.Client.Http.Do(req))
	if err != nil {
		return url.URL{}, fmt.Errorf("error processing compute action: %s", err)
	}

	services := new(services)

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

	// Loop in the Services struct to find right service and compute resource.
	for _, s := range services.Service {
		if s.ServiceID == cid {
			c.Region = s.Region
			u, err := url.ParseRequestURI(s.HREF)
			return *u, err
		}
	}
	return url.URL{}, fmt.Errorf("couldn't find a Compute Resource in current service list")
}

func (c *VAClient) vaacquirecompute(s url.URL, vid string) (u url.URL, err error) {

	if vid == "" {
		vid = os.Getenv("VCLOUDAIR_VDCID")
	}

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

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

	// Set Authorization Header
	req.Header.Add("x-vchs-authorization", c.VAToken)

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

	computeresources := new(computeResources)

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

	// Iterate through the ComputeResources struct searching for the right
	// backend server
	for _, s := range computeresources.VdcRef {
		if s.Name == vid {
			for _, t := range s.Link {
				if t.Name == vid {
					u, err := url.ParseRequestURI(t.HREF)
					return *u, err
				}
			}
		}
	}
	return url.URL{}, fmt.Errorf("couldn't find a VDC Resource in current Compute list")
}

func (c *VAClient) vagetbackendauth(s url.URL, cid string) error {

	if cid == "" {
		cid = os.Getenv("VCLOUDAIR_COMPUTEID")
	}

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

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

	// Set Authorization Header
	req.Header.Add("x-vchs-authorization", c.VAToken)

	// TODO: wrap into checkresp to parse error
	resp, err := checkResp(c.Client.Http.Do(req))
	if err != nil {
		return fmt.Errorf("error processing backend url action: %s", err)
	}
	defer resp.Body.Close()

	vcloudsession := new(vCloudSession)

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

	// Get the backend session information
	for _, s := range vcloudsession.VdcLink {
		if s.Name == cid {
			// Fetch the authorization token
			c.Client.VCDToken = s.AuthorizationToken

			// Fetch the authorization header
			c.Client.VCDAuthHeader = s.AuthorizationHeader

			u, err := url.ParseRequestURI(s.HREF)
			if err != nil {
				return fmt.Errorf("error decoding href: %s", err)
			}
			c.Client.VCDVDCHREF = *u
			return nil
		}
	}
	return fmt.Errorf("error finding the right backend resource")
}

// NewVAClient returns a new empty client to authenticate against the vCloud Air
// service, the vCloud Air endpoint can be overridden by setting the
// VCLOUDAIR_ENDPOINT environment variable.
func NewVAClient() (*VAClient, error) {

	var u *url.URL
	var err error

	if os.Getenv("VCLOUDAIR_ENDPOINT") != "" {
		u, err = url.ParseRequestURI(os.Getenv("VCLOUDAIR_ENDPOINT"))
		if err != nil {
			return &VAClient{}, fmt.Errorf("cannot parse endpoint coming from VCLOUDAIR_ENDPOINT")
		}
	} else {
		// Implicitly trust this URL parse.
		u, _ = url.ParseRequestURI("https://vchs.vmware.com/api")
	}

	VAClient := VAClient{
		VAEndpoint: *u,
		Client: Client{
			APIVersion: "5.6",
			// Patching things up as we're hitting several TLS timeouts.
			Http: http.Client{
				Transport: &http.Transport{
					Proxy:               http.ProxyFromEnvironment,
					TLSHandshakeTimeout: 120 * time.Second,
				},
			},
		},
	}
	return &VAClient, nil
}

// Authenticate is an helper function that performs a complete login in vCloud
// Air and in the backend vCloud Director instance.
func (c *VAClient) Authenticate(username, password, computeid, vdcid string) (Vdc, error) {
	// Authorize
	vaservicehref, err := c.vaauthorize(username, password)
	if err != nil {
		return Vdc{}, fmt.Errorf("error Authorizing: %s", err)
	}

	// Get Service
	vacomputehref, err := c.vaacquireservice(vaservicehref, computeid)
	if err != nil {
		return Vdc{}, fmt.Errorf("error Acquiring Service: %s", err)
	}

	// Get Compute
	vavdchref, err := c.vaacquirecompute(vacomputehref, vdcid)
	if err != nil {
		return Vdc{}, fmt.Errorf("error Acquiring Compute: %s", err)
	}

	// Get Backend Authorization
	if err = c.vagetbackendauth(vavdchref, computeid); err != nil {
		return Vdc{}, fmt.Errorf("error Acquiring Backend Authorization: %s", err)
	}

	v, err := c.Client.retrieveVDC()
	if err != nil {
		return Vdc{}, fmt.Errorf("error Acquiring VDC: %s", err)
	}

	return v, nil

}

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

	s := c.VAEndpoint
	s.Path += "/vchs/session"

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

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

	// Set Authorization Header
	req.Header.Add("x-vchs-authorization", c.VAToken)

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

	return nil
}
