package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"regexp"
	"strings"

	"code.hackerspace.pl/hscloud/go/mirko"
	"github.com/cenkalti/backoff"
	"github.com/golang/glog"
)

var (
	reSessionCookie = regexp.MustCompile("'SESSION_COOKIE' : '([^']*)'")
	reIpmiPriv      = regexp.MustCompile("'IPMI_PRIV' : ([^,]*)")
	reExtPriv       = regexp.MustCompile("'EXT_PRIV' : ([^,]*)")
	reSystemModel   = regexp.MustCompile("'SYSTEM_MODEL' : '([^']*)'")
	reArgument      = regexp.MustCompile("<argument>([^<]*)</argument>")
)

var (
	ErrorNoFreeSlot = fmt.Errorf("iDRAC reports no free slot")
)

type cmcRequestType int

const (
	cmcRequestKVMDetails cmcRequestType = iota
)

type cmcResponse struct {
	data interface{}
	err  error
}

type cmcRequest struct {
	t        cmcRequestType
	req      interface{}
	res      chan cmcResponse
	canceled bool
}

type KVMDetails struct {
	arguments []string
}

type cmcClient struct {
	session string
	req     chan *cmcRequest
}

func (c *cmcClient) RequestKVMDetails(ctx context.Context, slot int) (*KVMDetails, error) {
	r := &cmcRequest{
		t:   cmcRequestKVMDetails,
		req: slot,
		res: make(chan cmcResponse, 1),
	}
	mirko.Trace(ctx, "cmcRequestKVMDetails: requesting...")
	c.req <- r
	mirko.Trace(ctx, "cmcRequestKVMDetails: requested.")

	select {
	case <-ctx.Done():
		r.canceled = true
		return nil, context.Canceled
	case res := <-r.res:
		mirko.Trace(ctx, "cmcRequestKVMDetails: got response")
		if res.err != nil {
			return nil, res.err
		}
		return res.data.(*KVMDetails), nil
	}
}

func NewCMCClient() *cmcClient {
	return &cmcClient{
		req: make(chan *cmcRequest, 4),
	}
}

func (c *cmcClient) Run(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			c.logout()
			return
		case msg := <-c.req:
			c.handle(msg)
		}
	}
}

func (c *cmcClient) handle(r *cmcRequest) {
	switch {
	case r.t == cmcRequestKVMDetails:
		var details *KVMDetails
		slot := r.req.(int)
		err := backoff.Retry(func() error {
			if err := c.login(); err != nil {
				return err
			}
			url, err := c.getiDRACURL(slot)
			if err != nil {
				return err
			}
			details, err = c.getiDRACJNLP(url)
			return err
		}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 2))

		if err != nil {
			r.res <- cmcResponse{err: err}
		}

		r.res <- cmcResponse{data: details}
	default:
		panic("invalid cmcRequestType")
	}
}

func makeUrl(path string) string {
	if strings.HasSuffix(flagCMCAddress, "/") {
		return flagCMCAddress + path
	}
	return flagCMCAddress + "/" + path
}

func (c *cmcClient) transport() *http.Transport {
	return &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
}

func (c *cmcClient) addCookies(req *http.Request) {
	req.AddCookie(&http.Cookie{Name: "custom_domain", Value: ""})
	req.AddCookie(&http.Cookie{Name: "domain_selected", Value: "This Chassis"})
	if c.session != "" {
		glog.Infof("Session cookie: %v", c.session)
		req.AddCookie(&http.Cookie{Name: "sid", Value: c.session})
	}
}

func (c *cmcClient) getiDRACURL(slot int) (string, error) {
	if c.session == "" {
		return "", fmt.Errorf("not logged in")
	}

	url := makeUrl(pathiDRACURL) + fmt.Sprintf("?vKVM=1&serverSlot=%d", slot)

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", fmt.Errorf("GET prepare to %s failed: %v", pathLogin, err)
	}
	c.addCookies(req)

	cl := &http.Client{
		Transport: c.transport(),
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	resp, err := cl.Do(req)
	if err != nil {
		return "", fmt.Errorf("GET to %s failed: %v", pathLogin, err)
	}

	if resp.StatusCode != 302 {
		return "", fmt.Errorf("expected 302 on iDRAC URL redirect, got %v instead", resp.Status)
	}

	loc, _ := resp.Location()

	if !strings.Contains(loc.String(), "cmc_sess_id") {
		c.session = ""
		return "", fmt.Errorf("redirect URL contains no session ID - session timed out?")
	}

	return loc.String(), nil
}

func (c *cmcClient) getiDRACJNLP(loginUrl string) (*KVMDetails, error) {
	lurl, err := url.Parse(loginUrl)
	if err != nil {
		return nil, err
	}

	sessid := lurl.Query().Get("cmc_sess_id")
	if sessid == "" {
		return nil, fmt.Errorf("no cmc_sess_id in iDRAC login URL")
	}

	createURL := *lurl
	createURL.Path = "/Applications/dellUI/RPC/WEBSES/create.asp"
	createURL.RawQuery = ""

	values := url.Values{}
	values.Set("WEBVAR_USERNAME", "cmc")
	values.Set("WEBVAR_PASSWORD", sessid)
	values.Set("WEBVAR_ISCMCLOGIN", "1")
	valuesString := values.Encode()
	req, err := http.NewRequest("POST", createURL.String(), strings.NewReader(valuesString))

	cl := &http.Client{
		Transport: c.transport(),
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	resp, err := cl.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	data, _ := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	first := func(v [][]byte) string {
		if len(v) < 1 {
			return ""
		}
		return string(v[1])
	}

	sessionCookie := first(reSessionCookie.FindSubmatch(data))
	ipmiPriv := first(reIpmiPriv.FindSubmatch(data))
	extPriv := first(reExtPriv.FindSubmatch(data))
	systemModel := first(reSystemModel.FindSubmatch(data))

	if sessionCookie == "Failure_No_Free_Slot" {
		return nil, ErrorNoFreeSlot
	}

	jnlpURL := *lurl
	jnlpURL.Path = "/Applications/dellUI/Java/jviewer.jnlp"
	jnlpURL.RawQuery = ""

	req, err = http.NewRequest("GET", jnlpURL.String(), nil)
	for _, cookie := range resp.Cookies() {
		req.AddCookie(cookie)
	}
	req.AddCookie(&http.Cookie{Name: "SessionCookie", Value: sessionCookie})
	req.AddCookie(&http.Cookie{Name: "SessionCookieUser", Value: "cmc"})
	req.AddCookie(&http.Cookie{Name: "IPMIPriv", Value: ipmiPriv})
	req.AddCookie(&http.Cookie{Name: "ExtPriv", Value: extPriv})
	req.AddCookie(&http.Cookie{Name: "SystemModel", Value: systemModel})

	resp, err = cl.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	data, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	// yes we do parse xml with regex why are you asking
	matches := reArgument.FindAllSubmatch(data, -1)

	res := &KVMDetails{
		arguments: []string{},
	}
	for _, match := range matches {
		res.arguments = append(res.arguments, string(match[1]))
	}

	return res, nil
}

func (c *cmcClient) login() error {
	if c.session != "" {
		return nil
	}

	values := url.Values{}
	values.Set("ST2", "NOTSET")
	values.Set("user", flagCMCUsername)
	values.Set("user_id", flagCMCUsername)
	values.Set("password", flagCMCPassword)
	values.Set("WEBSERVER_timeout", "1800")
	values.Set("WEBSERVER_timeout_select", "1800")
	valuesString := values.Encode()
	req, err := http.NewRequest("POST", makeUrl(pathLogin), strings.NewReader(valuesString))
	if err != nil {
		return fmt.Errorf("POST prepare to %s failed: %v", pathLogin, err)
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	c.addCookies(req)

	cl := &http.Client{
		Transport: c.transport(),
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	resp, err := cl.Do(req)
	if err != nil {
		return fmt.Errorf("POST to %s failed: %v", pathLogin, err)
	}
	glog.Infof("Login response: %s", resp.Status)
	defer resp.Body.Close()
	for _, cookie := range resp.Cookies() {
		if cookie.Name == "sid" {
			c.session = cookie.Value
			break
		}
	}
	if c.session == "" {
		return fmt.Errorf("login unsuccesful")
	}
	return nil
}

func (c *cmcClient) logout() {
	glog.Infof("Killing session..")
	if c.session == "" {
		return
	}

	req, err := http.NewRequest("GET", makeUrl(pathLogout), nil)
	if err != nil {
		glog.Errorf("GET prepare to %s failed: %v", pathLogin, err)
	}
	c.addCookies(req)

	cl := &http.Client{
		Transport: c.transport(),
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	resp, err := cl.Do(req)
	if err != nil {
		glog.Errorf("GET to %s failed: %v", pathLogin, err)
	}
	glog.Infof("Logout response: %s", resp.Status)
	return
}
