package gowebdav

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

// PassportAuth structure holds our credentials
type PassportAuth struct {
	user            string
	pw              string
	cookies         []http.Cookie
	inhibitRedirect bool
}

// constructor for PassportAuth creates a new PassportAuth object and
// automatically authenticates against the given partnerURL
func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) {
	p := &PassportAuth{
		user:            user,
		pw:              pw,
		inhibitRedirect: true,
	}
	err := p.genCookies(c, partnerURL, header)
	return p, err
}

// Authorize the current request
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
	// prevent redirects to detect subsequent authentication requests
	if p.inhibitRedirect {
		rq.Header.Set(XInhibitRedirect, "1")
	} else {
		p.inhibitRedirect = true
	}
	for _, cookie := range p.cookies {
		rq.AddCookie(&cookie)
	}
	return nil
}

// Verify verifies if the authentication is good
func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
	switch rs.StatusCode {
	case 301, 302, 307, 308:
		redo = true
		if rs.Header.Get("Www-Authenticate") != "" {
			// re-authentication required as we are redirected to the login page
			err = p.genCookies(c, rs.Request.URL.String(), &rs.Header)
		} else {
			// just a redirect, follow it
			p.inhibitRedirect = false
		}
	case 401:
		err = NewPathError("Authorize", path, rs.StatusCode)
	}
	return
}

// Close cleans up all resources
func (p *PassportAuth) Close() error {
	return nil
}

// Clone creates a Copy of itself
func (p *PassportAuth) Clone() Authenticator {
	// create a copy to allow independent cookie updates
	clonedCookies := make([]http.Cookie, len(p.cookies))
	copy(clonedCookies, p.cookies)

	return &PassportAuth{
		user:            p.user,
		pw:              p.pw,
		cookies:         clonedCookies,
		inhibitRedirect: true,
	}
}

// String toString
func (p *PassportAuth) String() string {
	return fmt.Sprintf("PassportAuth login: %s", p.user)
}

func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, header *http.Header) error {
	// For more details refer to:
	// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pass/2c80637d-438c-4d4b-adc5-903170a779f3
	// Skipping step 1 and 2 as we already have the partner server challenge

	baseAuthenticationServer := header.Get("Location")
	baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer)
	if err != nil {
		return err
	}

	// Skipping step 3 and 4 as we already know that we need and have the user's credentials
	// Step 5 (Sign-in request)
	authenticationServerUrl := url.URL{
		Scheme: baseAuthenticationServerURL.Scheme,
		Host:   baseAuthenticationServerURL.Host,
		Path:   "/login2.srf",
	}

	partnerServerChallenge := strings.Split(header.Get("Www-Authenticate"), " ")[1]

	req := http.Request{
		Method: "GET",
		URL:    &authenticationServerUrl,
		Header: http.Header{
			"Authorization": []string{"Passport1.4 sign-in=" + url.QueryEscape(p.user) + ",pwd=" + url.QueryEscape(p.pw) + ",OrgVerb=GET,OrgUrl=" + partnerUrl + "," + partnerServerChallenge},
		},
	}

	rs, err := c.Do(&req)
	if err != nil {
		return err
	}
	io.Copy(io.Discard, rs.Body)
	rs.Body.Close()
	if rs.StatusCode != 200 {
		return NewPathError("Authorize", "/", rs.StatusCode)
	}

	// Step 6 (Token Response from Authentication Server)
	tokenResponseHeader := rs.Header.Get("Authentication-Info")
	if tokenResponseHeader == "" {
		return NewPathError("Authorize", "/", 401)
	}
	tokenResponseHeaderList := strings.Split(tokenResponseHeader, ",")
	token := ""
	for _, tokenResponseHeader := range tokenResponseHeaderList {
		if strings.HasPrefix(tokenResponseHeader, "from-PP='") {
			token = tokenResponseHeader
			break
		}
	}
	if token == "" {
		return NewPathError("Authorize", "/", 401)
	}

	// Step 7 (First Authentication Request to Partner Server)
	origUrl, err := url.Parse(partnerUrl)
	if err != nil {
		return err
	}
	req = http.Request{
		Method: "GET",
		URL:    origUrl,
		Header: http.Header{
			"Authorization": []string{"Passport1.4 " + token},
		},
	}

	rs, err = c.Do(&req)
	if err != nil {
		return err
	}
	io.Copy(io.Discard, rs.Body)
	rs.Body.Close()
	if rs.StatusCode != 200 && rs.StatusCode != 302 {
		return NewPathError("Authorize", "/", rs.StatusCode)
	}

	// Step 8 (Set Token Message from Partner Server)
	cookies := rs.Header.Values("Set-Cookie")
	p.cookies = make([]http.Cookie, len(cookies))
	for i, cookie := range cookies {
		cookieParts := strings.Split(cookie, ";")
		cookieName := strings.Split(cookieParts[0], "=")[0]
		cookieValue := strings.Split(cookieParts[0], "=")[1]

		p.cookies[i] = http.Cookie{
			Name:  cookieName,
			Value: cookieValue,
		}
	}

	return nil
}
