package httputil

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

const DefaultTimeout = 10 * time.Second

type HTTPResponse struct {
	StatusCode int                    `json:"statusCode"`
	Status     string                 `json:"status"`
	Headers    map[string]string      `json:"headers"`
	Body       string                 `json:"body"`
	JSON       map[string]interface{} `json:"json,omitempty"`
	Error      string                 `json:"error,omitempty"`
}

type HTTPClient struct {
	client  *http.Client
	timeout time.Duration
}

func NewHTTPClient(timeout time.Duration) *HTTPClient {
	if timeout <= 0 {
		timeout = DefaultTimeout
	}

	proxyURL, _ := url.Parse("http://127.0.0.1:10809")

	return &HTTPClient{
		client: &http.Client{
			Timeout: timeout,
			Transport: &http.Transport{
				Proxy: http.ProxyURL(proxyURL),
			},
		},
		timeout: timeout,
	}
}

func (c *HTTPClient) Get(urlStr string, headers map[string]string) *HTTPResponse {
	return c.doRequest("GET", urlStr, headers, nil)
}

func (c *HTTPClient) Post(urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
	return c.doRequest("POST", urlStr, headers, body)
}

func (c *HTTPClient) doRequest(method, urlStr string, headers map[string]string, body interface{}) *HTTPResponse {
	resp := &HTTPResponse{
		Headers: make(map[string]string),
	}

	if err := validateURL(urlStr); err != nil {
		resp.Error = err.Error()
		return resp
	}

	var reqBody []byte
	var err error
	contentType := "application/json"

	if body != nil {
		switch v := body.(type) {
		case string:
			reqBody = []byte(v)
			contentType = "text/plain"
		case map[string]interface{}:
			reqBody, err = json.Marshal(v)
			if err != nil {
				resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
				return resp
			}
		default:
			reqBody, err = json.Marshal(v)
			if err != nil {
				resp.Error = fmt.Sprintf("failed to marshal body: %v", err)
				return resp
			}
		}
	}

	var req *http.Request
	if reqBody != nil {
		req, err = http.NewRequest(method, urlStr, bytes.NewReader(reqBody))
	} else {
		req, err = http.NewRequest(method, urlStr, nil)
	}
	if err != nil {
		resp.Error = fmt.Sprintf("failed to create request: %v", err)
		return resp
	}

	if headers != nil {
		for k, v := range headers {
			req.Header.Set(k, v)
		}
	}

	if body != nil && req.Header.Get("Content-Type") == "" {
		req.Header.Set("Content-Type", contentType)
	}

	httpResp, err := c.client.Do(req)
	if err != nil {
		resp.Error = fmt.Sprintf("request failed: %v", err)
		return resp
	}
	defer httpResp.Body.Close()

	resp.StatusCode = httpResp.StatusCode
	resp.Status = httpResp.Status

	for k, v := range httpResp.Header {
		if len(v) > 0 {
			resp.Headers[k] = v[0]
		}
	}

	buf := new(bytes.Buffer)
	if _, err := buf.ReadFrom(httpResp.Body); err != nil {
		resp.Error = fmt.Sprintf("failed to read response body: %v", err)
		return resp
	}
	resp.Body = buf.String()

	if strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") {
		var jsonData map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &jsonData); err == nil {
			resp.JSON = jsonData
		}
	}

	return resp
}

func validateURL(urlStr string) error {
	parsedURL, err := url.Parse(urlStr)
	if err != nil {
		return fmt.Errorf("invalid URL: %v", err)
	}

	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
		return fmt.Errorf("only http and https protocols are allowed")
	}

	host := parsedURL.Hostname()
	if host == "" {
		return fmt.Errorf("empty host")
	}

	if isPrivateIP(host) {
		return fmt.Errorf("access to private IP addresses is not allowed")
	}

	if isLocalhost(host) {
		return fmt.Errorf("access to localhost is not allowed")
	}

	return nil
}

func isLocalhost(host string) bool {
	lowerHost := strings.ToLower(host)
	return lowerHost == "localhost" || lowerHost == "127.0.0.1" || lowerHost == "::1"
}

func isPrivateIP(host string) bool {
	ip := net.ParseIP(host)
	if ip == nil {
		return false
	}

	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
		return true
	}

	privateBlocks := []string{
		"10.0.0.0/8",
		"172.16.0.0/12",
		"192.168.0.0/16",
		"169.254.0.0/16",
		"127.0.0.0/8",
		"::1/128",
		"fc00::/7",
		"fe80::/10",
	}

	for _, block := range privateBlocks {
		_, cidr, _ := net.ParseCIDR(block)
		if cidr != nil && cidr.Contains(ip) {
			return true
		}
	}

	return false
}
