// Copyright © 2016 Abcum Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fncs

import (
	"bytes"
	"context"
	"errors"
	"io"
	"io/ioutil"
	"net/http"
	"time"

	"encoding/json"

	"golang.org/x/net/context/ctxhttp"

	"github.com/abcum/surreal/util/build"
	"github.com/abcum/surreal/util/hook"
)

type opts map[string]interface{}

var version = build.GetInfo().Ver

var httpResponseError = errors.New("HTTP response error")

func httpHead(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runSync(ctx, "HEAD", args...)
}

func httpGet(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runSync(ctx, "GET", args...)
}

func httpPut(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runSync(ctx, "PUT", args...)
}

func httpPost(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runSync(ctx, "POST", args...)
}

func httpPatch(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runSync(ctx, "PATCH", args...)
}

func httpDelete(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runSync(ctx, "DELETE", args...)
}

func httpAsyncHead(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runAsync(ctx, "HEAD", args...)
}

func httpAsyncGet(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runAsync(ctx, "GET", args...)
}

func httpAsyncPut(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runAsync(ctx, "PUT", args...)
}

func httpAsyncPost(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runAsync(ctx, "POST", args...)
}

func httpAsyncPatch(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runAsync(ctx, "PATCH", args...)
}

func httpAsyncDelete(ctx context.Context, args ...interface{}) (interface{}, error) {
	return runAsync(ctx, "DELETE", args...)
}

func runSync(ctx context.Context, met string, args ...interface{}) (interface{}, error) {
	url, bdy, opt := httpCnf(met, args...)
	out, _ := httpRes(ctx, met, url, bdy, opt)
	return out, nil
}

func runAsync(ctx context.Context, met string, args ...interface{}) (interface{}, error) {
	url, bdy, opt := httpCnf(met, args...)
	ctx = context.Background()
	go hook.NewBackoff(5, 5, 10*time.Second).Run(ctx, func() error {
		return httpErr(httpReq(ctx, met, url, bdy, opt))
	})
	return nil, nil
}

func httpCnf(met string, args ...interface{}) (url string, bdy io.Reader, opt opts) {
	var bit []byte
	switch met {
	case "HEAD", "GET", "DELETE":
		switch len(args) {
		case 1:
			url, _ = ensureString(args[0])
		case 2:
			url, _ = ensureString(args[0])
			opt, _ = ensureObject(args[1])
		}
	case "PUT", "POST", "PATCH":
		switch len(args) {
		case 1:
			url, _ = ensureString(args[0])
		case 2:
			url, _ = ensureString(args[0])
			switch v := args[1].(type) {
			case []interface{}, map[string]interface{}:
				bit, _ = json.Marshal(v)
			default:
				bit, _ = ensureBytes(v)
			}
		case 3:
			url, _ = ensureString(args[0])
			switch v := args[1].(type) {
			case []interface{}, map[string]interface{}:
				bit, _ = json.Marshal(v)
			default:
				bit, _ = ensureBytes(v)
			}
			opt, _ = ensureObject(args[2])
		}
	}
	return url, bytes.NewReader(bit), opt
}

func httpReq(ctx context.Context, met, url string, body io.Reader, conf opts) (*http.Response, error) {

	cli := new(http.Client)

	req, err := http.NewRequest(met, url, body)
	if err != nil {
		return nil, err
	}

	req.Header.Set("User-Agent", "SurrealDB HTTP/"+version)

	if val, ok := conf["auth"]; ok {
		if opt, ok := ensureObject(val); ok {
			user, _ := ensureString(opt["user"])
			pass, _ := ensureString(opt["pass"])
			req.SetBasicAuth(user, pass)
		}
	}

	if val, ok := conf["head"]; ok {
		if opt, ok := ensureObject(val); ok {
			for key, v := range opt {
				head, _ := ensureString(v)
				req.Header.Set(key, head)
			}
		}
	}

	res, err := ctxhttp.Do(ctx, cli, req)
	if err != nil {
		return nil, err
	}

	return res, nil

}

func httpRes(ctx context.Context, met, url string, body io.Reader, conf opts) (interface{}, error) {

	var out interface{}

	res, err := httpReq(ctx, met, url, body, conf)
	if err != nil {
		return nil, err
	}

	bdy, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(bdy, &out)
	if err != nil {
		if len(bdy) != 0 {
			return bdy, nil
		}
	}

	return out, nil

}

func httpErr(res *http.Response, err error) error {
	if err != nil {
		return err
	}
	if res.StatusCode >= 500 {
		return httpResponseError
	}
	return nil
}