package tools

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"sync"
)

// HTTP Method Enum defines the different methods that can be used to interact with the HTTP server
type METHOD int

const (
	GET METHOD = iota
	PUT
	POST
	POSTCHECK
	DELETE

	STRICT_INTERNAL_GET
	STRICT_INTERNAL_PUT
	STRICT_INTERNAL_POST
	STRICT_INTERNAL_DELETE
)

// String returns the string of the enum
func (m METHOD) String() string {
	return [...]string{"GET", "PUT", "POST", "POST", "DELETE", "INTERNALGET", "INTERNALPUT", "INTERNALPOST", "INTERNALDELETE"}[m]
}

// EnumIndex returns the index of the enum
func (m METHOD) EnumIndex() int {
	return int(m)
}

// ToMethod returns the method from a string
func ToMethod(str string) METHOD {
	for _, s := range []METHOD{GET, PUT, POST, POSTCHECK, DELETE,
		STRICT_INTERNAL_GET, STRICT_INTERNAL_PUT, STRICT_INTERNAL_POST, STRICT_INTERNAL_DELETE} {
		if s.String() == str {
			return s
		}
	}
	return GET
}

type HTTPCallerITF interface {
	GetUrls() map[DataType]map[METHOD]string
	CallGet(url string, subpath string, types ...string) ([]byte, error)
	CallPost(url string, subpath string, body interface{}, types ...string) ([]byte, error)
	CallDelete(url string, subpath string) ([]byte, error)
}

type HTTPCaller struct {
	URLS        map[DataType]map[METHOD]string // Map of the different methods and their urls
	Disabled    bool                           // Disabled flag
	Mu          sync.RWMutex
	LastResults map[string]interface{} // Used to store information regarding the last execution of a given method on a given data type
}

// NewHTTPCaller creates a new instance of the HTTP Caller
func NewHTTPCaller(urls map[DataType]map[METHOD]string) *HTTPCaller {
	return &HTTPCaller{
		URLS:     urls, // Set the urls defined in the config & based on the data name type & method
		Disabled: false,
	}
}

func (c *HTTPCaller) GetUrls() map[DataType]map[METHOD]string {
	return c.URLS
}

// Creates a copy of the current caller, in order to have parallelized executions without race condition
func (c *HTTPCaller) DeepCopy(dst HTTPCaller) error {
	bytes, err := json.Marshal(c)
	if err != nil {
		return err
	}

	return json.Unmarshal(bytes, &dst)
}

// CallGet calls the GET method on the HTTP server
func (caller *HTTPCaller) CallGet(url string, subpath string, types ...string) ([]byte, error) {
	req, err := http.NewRequest(http.MethodGet, url+subpath, bytes.NewBuffer([]byte("")))
	if err != nil {
		return nil, err
	}
	for _, t := range types {
		req.Header.Set("Content-Type", t)
	}
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	err = caller.StoreResp(resp)
	if err != nil {
		return nil, err
	}

	return caller.LastResults["body"].([]byte), nil
}

// CallPut calls the DELETE method on the HTTP server
func (caller *HTTPCaller) CallDelete(url string, subpath string) ([]byte, error) {
	req, err := http.NewRequest("DELETE", url+subpath, nil)
	if err != nil {
		return nil, err
	}
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	err = caller.StoreResp(resp)
	if err != nil {
		return nil, err
	}

	return caller.LastResults["body"].([]byte), nil
}

// CallPost calls the POST method on the HTTP server
func (caller *HTTPCaller) CallPost(url string, subpath string, body interface{}, types ...string) ([]byte, error) {
	postBody, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	responseBody := bytes.NewBuffer(postBody)
	contentType := "application/json"
	if len(types) > 0 {
		contentType = types[0]
	}
	resp, err := http.Post(url+subpath, contentType, responseBody)
	if err != nil || resp == nil || resp.Body == nil {
		return nil, err
	}
	defer resp.Body.Close()
	err = caller.StoreResp(resp)
	if err != nil {
		return nil, err
	}

	return caller.LastResults["body"].([]byte), nil
}

// CallPost calls the POST method on the HTTP server
func (caller *HTTPCaller) CallPut(url string, subpath string, body map[string]interface{}) ([]byte, error) {
	postBody, _ := json.Marshal(body)
	responseBody := bytes.NewBuffer(postBody)
	req, err := http.NewRequest(http.MethodPut, url+subpath, responseBody)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	err = caller.StoreResp(resp)
	if err != nil {
		return nil, err
	}

	return caller.LastResults["body"].([]byte), nil
}

// CallRaw calls the Raw method on the HTTP server
func (caller *HTTPCaller) CallRaw(method string, url string, subpath string,
	body map[string]interface{}, content_type string, fakeTLSTermination bool, cookies ...*http.Cookie) (*http.Response, error) {
	postBody, _ := json.Marshal(body)
	responseBody := bytes.NewBuffer(postBody)
	req, err := http.NewRequest(method, url+subpath, responseBody)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", content_type)
	if fakeTLSTermination {
		req.Header.Add("X-Forwarded-Proto", "https")
	}
	for _, c := range cookies {
		req.AddCookie(c)
	}
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

// CallRaw calls the Raw method on the HTTP server
func (caller *HTTPCaller) CallForm(method string, url string, subpath string,
	body url.Values, content_type string, fakeTLSTermination bool, cookies ...*http.Cookie) (*http.Response, error) {
	req, err := http.NewRequest(method, url+subpath, strings.NewReader(body.Encode()))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", content_type)
	if fakeTLSTermination {
		req.Header.Add("X-Forwarded-Proto", "https")
	}
	for _, c := range cookies {
		req.AddCookie(c)
	}
	client := &http.Client{}
	return client.Do(req)
}

func (caller *HTTPCaller) StoreResp(resp *http.Response) error {
	caller.Mu.Lock()
	defer caller.Mu.Unlock()
	caller.LastResults = make(map[string]interface{})
	caller.LastResults["header"] = resp.Header
	caller.LastResults["code"] = resp.StatusCode
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Error reading the body of the last request")
		return err
	}

	caller.LastResults["body"] = data
	return nil
}
