package httpfs

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"time"

	"gitlab.com/golang-utils/fs"
	"gitlab.com/golang-utils/fs/mapfs"
	"gitlab.com/golang-utils/fs/path"
)

/*
The idea is to make a remote filesystem based on the http methods get, put, post and delete

this library would mainly consist of a client (the FS), but for testing purposes we'll also implement a tiny
server based on a mapfs

for testing purposes we would also
*/

var _ fs.Remote = &FS{}

type TestHandler struct {
	Error error
	store *mapfs.FS
}

func NewTestHandler(rem *path.Remote) (*TestHandler, error) {
	st, err := mapfs.New(rem)
	if err != nil {
		return nil, err
	}
	return &TestHandler{
		store: st,
	}, nil
}

var _ http.Handler = &TestHandler{}

func (serv *TestHandler) serveGet(wr http.ResponseWriter, req *http.Request) {
	p := path.Relative(req.URL.Path[1:])
	defer req.Body.Close()
	rd, err := serv.store.Reader(p)
	if err != nil {
		switch {
		case errors.Is(err, fs.ErrNotFound):
			wr.WriteHeader(http.StatusNotFound)
		default:
			wr.WriteHeader(http.StatusBadRequest)
		}
		return
	}

	bt, err := ioutil.ReadAll(rd)
	defer rd.Close()
	if err != nil {
		wr.WriteHeader(http.StatusBadRequest)
		return
	}

	headers, err := serv.store.GetMeta(p)

	if err != nil {
		wr.WriteHeader(http.StatusBadRequest)
		return
	}

	if headers == nil {
		headers = map[string][]byte{}
	}

	_, has := headers["Content-Type"]
	if !has {
		headers["Content-Type"] = []byte("application/octet-stream")
	}

	for k, v := range headers {
		wr.Header().Set(k, string(v))
	}
	wr.Write(bt)
}

func (serv *TestHandler) servePut(wr http.ResponseWriter, req *http.Request) {
	p := path.Relative(req.URL.Path[1:])
	defer req.Body.Close()

	var meta = map[string][]byte{}
	for k := range req.Header {
		meta[k] = []byte(req.Header.Get(k))
	}

	err := serv.store.WriteWithMeta(p, req.Body, meta, true)
	if err != nil {
		switch {
		case errors.Is(err, fs.ErrNotFound):
			wr.WriteHeader(http.StatusNotFound)
		default:
			wr.WriteHeader(http.StatusBadRequest)
		}
		return
	}
	wr.WriteHeader(http.StatusOK)
}

func (serv *TestHandler) serveDelete(wr http.ResponseWriter, req *http.Request) {
	p := path.Relative(req.URL.Path[1:])
	defer req.Body.Close()
	serv.store.Delete(p, true)
	wr.WriteHeader(http.StatusOK)
}

func (serv *TestHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
	if len(req.URL.Path) < 2 {
		wr.WriteHeader(http.StatusNotFound)
		return
	}
	switch req.Method {
	case "GET":
		serv.serveGet(wr, req)
	case "PUT":
		serv.servePut(wr, req)
	case "DELETE":
		serv.serveDelete(wr, req)
	default:
		wr.WriteHeader(http.StatusNotImplemented)
	}
}

type FS struct {
	root        url.URL
	TestHandler http.Handler
	Timeout     time.Duration
	//*dirtreefs.FS
}

func New(rem *path.Remote) (*FS, error) {
	if !path.IsDir(rem) {
		return nil, fs.ErrExpectedDir.Params(rem)
	}

	u := rem.URL

	if u == nil {
		return nil, fmt.Errorf("url expected %s", rem)
	}

	m := &FS{
		root:    *u,
		Timeout: 2 * time.Second,
	}

	return m, nil
}

func (f *FS) head(rel path.Relative) (headers map[string][]byte, err error) {
	resp, err := f.request("HEAD", rel, nil, nil)

	//c := http.Client{Timeout: f.timeout}
	//resp, err := c.Head(f.Abs(rel).String())
	if err != nil {
		switch err {
		case http.ErrMissingFile:
			return nil, fs.ErrNotFound.Params(rel)
		default:
			return nil, err
		}
	}
	defer resp.Body.Close()

	headers = map[string][]byte{}
	for k := range resp.Header {
		headers[k] = []byte(resp.Header.Get(k))
	}
	return headers, nil
	//bt, err := io.ReadAll(resp.Body)

	//fmt.Println(resp.Header)
}

// TODO: authentication
func (f *FS) mkrequestStr(relpath path.Relative) string {
	str := f.root.JoinPath(relpath.String()).String()
	if path.IsDir(relpath) {
		str += "/"
	}
	return path.MustRemote(str).String()

	//return f.Abs(relpath).String()
	//return f.requestURL.ResolveReference(relpath).String()
	//return f.requestURL.ResolveReference(relpath).String()
	//path.MustRemote().
	/*
		if relpath.String() == "" {
			return f.requestURL.String()
		}

		return f.requestURL.JoinPath() .Join(relpath.String())
	*/
}

func (f *FS) doRequest(req *http.Request) (resp *http.Response, err error) {
	if f.TestHandler != nil {
		rec := httptest.NewRecorder()
		f.TestHandler.ServeHTTP(rec, req)
		return rec.Result(), nil
	}
	c := http.Client{Timeout: f.Timeout}
	return c.Do(req)
}

func (f *FS) request(method string, relpath path.Relative, body io.ReadCloser, headers map[string][]byte) (resp *http.Response, err error) {
	switch method {
	case "GET", "POST", "PUT", "DELETE", "HEAD":
	default:
		return nil, fmt.Errorf("unknown HTTP method: %s", method)
	}

	//c := http.Client{Timeout: f.Timeout}
	req, err := http.NewRequest(method, f.mkrequestStr(relpath), body)
	if err != nil {
		fmt.Printf("error %s", err)
		return
	}

	if len(headers) > 0 {
		for k, v := range headers {
			req.Header.Add(k, string(v))
		}
	}
	//return c.Do(req)
	return f.doRequest(req)
}

/*
For a PUT request: HTTP 200, HTTP 204 should imply "resource updated successfully". HTTP 201 if the PUT request created a new resource.

For a DELETE request: HTTP 200 or HTTP 204 should imply "resource deleted successfully".
*/

func (f *FS) put(rel path.Relative, data io.ReadCloser, headers map[string][]byte) error {

	//c := http.Client{Timeout: f.timeout}
	if headers == nil {
		headers = map[string][]byte{}
	}

	_, has := headers["Content-Type"]

	if !has {
		headers["Content-Type"] = []byte("application/octet-stream")
	}

	resp, err := f.request("PUT", rel, data, headers)

	//c.
	//resp, err := c.Post(f.Abs(rel).String(), contenttype, data)
	defer resp.Body.Close()
	defer data.Close()
	if err != nil {
		switch err {
		case http.ErrMissingFile:
			return fs.ErrNotFound.Params(rel)
		default:
			return err
		}
	}

	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusCreated:
	case 204:
	default:
		return fs.ErrNotFound.Params(rel)
	}
	return nil
	/*
			myJson := bytes.NewBuffer([]byte(`{"name":"Maximilien"}`))
		resp, err := c.Post("https://www.google.com", "application/json", myJson)
		if err != nil {
		    fmt.Errorf("Error %s", err)
		    return
		}
	*/
}

/*
req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN"))
*/

func (f *FS) get(rel path.Relative) (io.ReadCloser, error) {
	resp, err := f.request("GET", rel, nil, nil)
	if err != nil {
		switch err {
		case http.ErrMissingFile:
			return nil, fs.ErrNotFound.Params(rel)
		default:
			return nil, err
		}
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fs.ErrNotFound.Params(rel)
	}

	if path.IsDir(rel) {

		defer resp.Body.Close()
		bt, err := io.ReadAll(resp.Body)

		if err != nil {
			return fs.ReadCloser(bytes.NewReader(bt)), nil
		}
	}
	//defer resp.Body.Close()
	//body, err := ioutil.ReadAll(resp.Body)
	//fmt.Printf("Body : %s", body)
	return resp.Body, nil
}

func (m *FS) Delete(p path.Relative, recursive bool) error {
	resp, err := m.request("DELETE", p, nil, nil)
	defer resp.Body.Close()
	switch err {
	case http.ErrMissingFile:
		return fs.ErrNotFound.Params(p)
	default:
		return err
	}
}

func (m *FS) Abs(p path.Relative) path.Absolute {
	str := m.root.JoinPath(p.String()).String()
	if path.IsDir(p) {
		str += "/"
	}
	return path.MustRemote(str)
}

func (m *FS) Exists(p path.Relative) bool {
	_, err := m.head(p)
	return err == nil
}

func (m *FS) ModTime(p path.Relative) (t time.Time, err error) {
	headers, err := m.head(p)
	if err != nil {
		return t, err
	}

	return http.ParseTime(string(headers["Date"]))
}

// we don't need to write all directories, since they are just implicit
// func (m *MapFS) Write(p path.Relative, rd io.ReadCloser, recursive bool) (err error) {
func (m *FS) write(p path.Relative, rd io.ReadCloser, meta map[string][]byte, recursive bool) (err error) {
	return m.put(p, rd, meta)

	/*
		if path.IsDir(p) && recursive {
			return m.Mkdirall(p)
		}

		if recursive {
			m.Mkdirall(path.Dir(p).Relative())
		}
	*/
	/*
		var bt []byte
		_ = bt

		if rd != nil {
			bt, err = io.ReadAll(rd)
			rd.Close()
			if err != nil {
				return err
			}
		}

		//m.add(p, bt, perm, meta)
		return nil
	*/
}

func (m *FS) Reader(p path.Relative) (io.ReadCloser, error) {
	return m.get(p)
	/*
		var bf bytes.Buffer

		_ = bf
		if path.IsDir(p) {
			// find all subdirs and files
			pstr := p.String()
			_ = pstr
	*/
	/*
		for k := range m.Files {
			kstr := k.String()

			if kstr == pstr {
				continue
			}

			if strings.HasPrefix(kstr, pstr) {
				rel, err := filepath.Rel(pstr, kstr)
				if err == nil {
					if strings.Contains(rel, "/") {
						continue
					}
					bf.WriteString(rel + "\n")
				}
				continue
			}
		}

		return fs.ReadCloser(strings.NewReader(bf.String())), nil
	*/
	//}

	/*
		f, has := m.Files[p]
		if !has {
			return nil, fs.ErrNotFound
		}
	*/

	//return fs.ReadCloser(bytes.NewReader(nil)), nil
}

func (m *FS) Write(p path.Relative, rd io.ReadCloser, recursive bool) (err error) {
	return m.WriteWithMeta(p, rd, nil, recursive)
}

func (m *FS) WriteWithMeta(p path.Relative, rd io.ReadCloser, meta map[string][]byte, recursive bool) error {
	return m.write(p, rd, meta, recursive)
}

func (d *FS) Size(name path.Relative) int64 {
	if path.IsDir(name) {
		return 0
	}

	headers, err := d.head(name)
	if err != nil {
		return -1
	}
	l, has := headers["Content-Length"]

	if !has {
		return -1
	}

	i, err := strconv.Atoi(string(l))
	if err != nil {
		return -1
	}

	return int64(i)

	/*
		f, has := d.Files[name]
		if !has {
			return -1
		}
	*/

	//return int64(len(f.Data))
	//return 0
}

func (f *FS) GetMeta(rel path.Relative) (map[string][]byte, error) {
	return f.head(rel)
	//return nil, fs.ErrNotFound
}

func (f *FS) SetMeta(p path.Relative, meta map[string][]byte) error {
	return fs.ErrNotSupported.Params("SetMeta", f)
	//return fs.ErrNotFound
}

func (f *FS) HostName() string {
	return f.root.Host
}

func (f *FS) Password() string {
	u := f.root.User
	if u == nil {
		return ""
	}

	pw, ok := u.Password()
	if !ok {
		return ""
	}
	return pw
}

func (f *FS) Port() int {
	port := f.root.Port()

	if f.root.Port() == "" {
		return 0
	}

	po, err := strconv.Atoi(port)

	if err != nil {
		return 0
	}

	return po
}

func (f *FS) UserName() string {
	if f.root.User == nil {
		return ""
	}

	return f.root.User.Username()
}

func (f *FS) Scheme() string {
	return f.root.Scheme
}