package gowebdav

import (
	"io"
	"log"
	"net/http"
	"path"
	"strings"
)

func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) {
	var redo bool
	var r *http.Request
	var uri = PathEscape(Join(c.root, path))
	auth, body := c.auth.NewAuthenticator(body)
	defer auth.Close()

	for { // TODO auth.continue() strategy(true|n times|until)?
		if r, err = http.NewRequest(method, uri, body); err != nil {
			return
		}

		for k, vals := range c.headers {
			for _, v := range vals {
				r.Header.Add(k, v)
			}
		}

		if err = auth.Authorize(c.c, r, path); err != nil {
			return
		}

		if intercept != nil {
			intercept(r)
		}

		if c.interceptor != nil {
			c.interceptor(method, r)
		}

		if rs, err = c.c.Do(r); err != nil {
			return
		}

		if redo, err = auth.Verify(c.c, rs, path); err != nil {
			rs.Body.Close()
			return nil, err
		}
		if redo {
			rs.Body.Close()
			if body, err = r.GetBody(); err != nil {
				return nil, err
			}
			continue
		}
		break
	}

	return rs, err
}

func (c *Client) mkcol(path string) (status int, err error) {
	rs, err := c.req("MKCOL", path, nil, nil)
	if err != nil {
		return
	}
	defer rs.Body.Close()

	status = rs.StatusCode
	if status == 405 {
		status = 201
	}

	return
}

func (c *Client) options(path string) (*http.Response, error) {
	return c.req("OPTIONS", path, nil, func(rq *http.Request) {
		rq.Header.Add("Depth", "0")
	})
}

func (c *Client) propfind(path string, self bool, body string, resp interface{}, parse func(resp interface{}) error) error {
	rs, err := c.req("PROPFIND", path, strings.NewReader(body), func(rq *http.Request) {
		if self {
			rq.Header.Add("Depth", "0")
		} else {
			rq.Header.Add("Depth", "1")
		}
		rq.Header.Add("Content-Type", "application/xml;charset=UTF-8")
		rq.Header.Add("Accept", "application/xml,text/xml")
		rq.Header.Add("Accept-Charset", "utf-8")
		// TODO add support for 'gzip,deflate;q=0.8,q=0.7'
		rq.Header.Add("Accept-Encoding", "")
	})
	if err != nil {
		return err
	}
	defer rs.Body.Close()

	if rs.StatusCode != 207 {
		return NewPathError("PROPFIND", path, rs.StatusCode)
	}

	return parseXML(rs.Body, resp, parse)
}

func (c *Client) lock(path string, token string, refresh bool) error {
	var body io.Reader
	if !refresh {
		b := strings.Builder{}

		// build the lockinfo xml
		b.WriteString("<D:lockinfo>")
		// always use an exclusive lock
		b.WriteString("<D:lockscope><D:exclusive/></D:lockscope>")
		// always use a write lock
		b.WriteString("<D:locktype><D:write/></D:locktype>")
		// add the lock token
		b.WriteString("<d:locktoken><d:href>")
		b.WriteString(token)
		b.WriteString("</d:href></d:locktoken>")
		// close the lockinfo xml
		b.WriteString("</D:lockinfo>")

		body = strings.NewReader(b.String())
	}

	rs, err := c.req("LOCK", path, body, func(rq *http.Request) {
		rq.Header.Add("Content-Type", "application/xml;charset=UTF-8")
		rq.Header.Add("Accept", "application/xml,text/xml")
		rq.Header.Add("Accept-Charset", "utf-8")
		rq.Header.Add("Accept-Encoding", "")
		if refresh {
			rq.Header.Add("If", "("+token+")")
		}
	})
	if err != nil {
		return err
	}
	defer rs.Body.Close()

	if rs.StatusCode != 200 {
		return NewPathError("LOCK", path, rs.StatusCode)
	}

	return nil
}

func (c *Client) unlock(path string, token string) error {
	rs, err := c.req("UNLOCK", path, nil, func(rq *http.Request) {
		rq.Header.Add("Lock-Token", "<"+token+">")
	})
	if err != nil {
		return err
	}
	defer rs.Body.Close()

	if rs.StatusCode != http.StatusNoContent {
		return NewPathError("UNLOCK", path, rs.StatusCode)
	}
	return nil
}

func (c *Client) doCopyMove(
	method string,
	oldpath string,
	newpath string,
	overwrite bool,
) (
	status int,
	r io.ReadCloser,
	err error,
) {
	rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
		rq.Header.Add("Destination", PathEscape(Join(c.root, newpath)))
		if overwrite {
			rq.Header.Add("Overwrite", "T")
		} else {
			rq.Header.Add("Overwrite", "F")
		}
	})
	if err != nil {
		return
	}
	status = rs.StatusCode
	r = rs.Body
	return
}

func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) {
	s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite)
	if err != nil {
		return
	}
	if data != nil {
		defer data.Close()
	}

	switch s {
	case 201, 204:
		return nil

	case 207:
		// TODO handle multistat errors, worst case ...
		log.Printf("TODO handle %s - %s multistatus result %s\n", method, oldpath, String(data))

	case 409:
		err := c.createParentCollection(newpath)
		if err != nil {
			return err
		}

		return c.copymove(method, oldpath, newpath, overwrite)
	}

	return NewPathError(method, oldpath, s)
}

func (c *Client) put(path string, stream io.Reader, locktoken string) (status int, err error) {
	rs, err := c.req("PUT", path, stream, func(rq *http.Request) {
		rq.Header.Add("Lock-Token", "<"+locktoken+">")
	})
	if err != nil {
		return
	}
	defer rs.Body.Close()

	status = rs.StatusCode
	return
}

func (c *Client) createParentCollection(itemPath string) (err error) {
	parentPath := path.Dir(itemPath)
	if parentPath == "." || parentPath == "/" {
		return nil
	}

	return c.MkdirAll(parentPath, 0755)
}
