package copyas

import (
	"encoding/json"
	"fmt"
	"net/url"
	"strings"

	"github.com/anotherhadi/spilltea/internal/util"
)

type header struct{ key, value string }

type parsedRequest struct {
	method  string
	path    string
	host    string
	scheme  string
	headers []header
	body    string
}

func parseRaw(raw, scheme string) parsedRequest {
	r := util.ParseRawRequest(raw)
	pr := parsedRequest{
		method: r.Method,
		path:   r.Path,
		host:   r.Host,
		scheme: scheme,
	}
	for _, h := range r.Headers {
		pr.headers = append(pr.headers, header{h.Key, h.Value})
	}
	pr.body = r.Body
	return pr
}

func (pr parsedRequest) fullURL() string {
	scheme := pr.scheme
	if scheme == "" {
		scheme = "https"
	}
	return scheme + "://" + pr.host + pr.path
}

func formatAs(id, raw, scheme string) string {
	pr := parseRaw(raw, scheme)
	switch id {
	case "raw":
		return raw
	case "curl":
		return toCurl(pr)
	case "python":
		return toPython(pr)
	case "go":
		return toGo(pr)
	case "ffuf":
		return toFFUF(pr)
	case "markdown":
		return toMarkdown(pr)
	case "har":
		return toHAR(pr)
	case "httpie":
		return toHTTPie(pr)
	}
	return raw
}

func toHTTPie(pr parsedRequest) string {
	var sb strings.Builder
	method := strings.ToUpper(pr.method)
	fmt.Fprintf(&sb, "http %s '%s'", method, pr.fullURL())
	for _, h := range pr.headers {
		if strings.EqualFold(h.key, "content-length") {
			continue
		}
		fmt.Fprintf(&sb, " \\\n  '%s:%s'", h.key, h.value)
	}
	if pr.body != "" {
		// Pass body via stdin hint
		fmt.Fprintf(&sb, " \\\n  <<< %q", pr.body)
	}
	return sb.String()
}

func toMarkdown(pr parsedRequest) string {
	var sb strings.Builder
	fmt.Fprintf(&sb, "### %s %s\n\n", pr.method, pr.fullURL())
	sb.WriteString("```\n")
	sb.WriteString(pr.method + " " + pr.path + " HTTP/1.1\n")
	for _, h := range pr.headers {
		sb.WriteString(fmt.Sprintf("%s: %s\n", h.key, h.value))
	}
	if pr.body != "" {
		sb.WriteString("\n" + pr.body)
	}
	sb.WriteString("\n```")
	return sb.String()
}

func toCurl(pr parsedRequest) string {
	var sb strings.Builder
	fmt.Fprintf(&sb, "curl -X %s '%s'", pr.method, pr.fullURL())
	for _, h := range pr.headers {
		if strings.EqualFold(h.key, "content-length") {
			continue
		}
		fmt.Fprintf(&sb, " \\\n  -H '%s: %s'", h.key, h.value)
	}
	if pr.body != "" {
		body := strings.ReplaceAll(pr.body, "'", "'\\''")
		fmt.Fprintf(&sb, " \\\n  --data '%s'", body)
	}
	return sb.String()
}

func toPython(pr parsedRequest) string {
	var sb strings.Builder
	sb.WriteString("import requests\n\n")
	fmt.Fprintf(&sb, "url = %q\n", pr.fullURL())

	sb.WriteString("headers = {\n")
	for _, h := range pr.headers {
		if strings.EqualFold(h.key, "content-length") {
			continue
		}
		fmt.Fprintf(&sb, "    %q: %q,\n", h.key, h.value)
	}
	sb.WriteString("}\n")

	method := strings.ToLower(pr.method)
	if pr.body != "" {
		fmt.Fprintf(&sb, "data = %q\n\n", pr.body)
		fmt.Fprintf(&sb, "response = requests.%s(url, headers=headers, data=data)\n", method)
	} else {
		fmt.Fprintf(&sb, "\nresponse = requests.%s(url, headers=headers)\n", method)
	}
	sb.WriteString("print(response.status_code)\n")
	sb.WriteString("print(response.text)\n")
	return sb.String()
}

func toGo(pr parsedRequest) string {
	var sb strings.Builder
	sb.WriteString("package main\n\nimport (\n")
	if pr.body != "" {
		sb.WriteString("\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n")
	} else {
		sb.WriteString("\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n")
	}
	sb.WriteString("func main() {\n")

	if pr.body != "" {
		fmt.Fprintf(&sb, "\tbody := strings.NewReader(%q)\n", pr.body)
		fmt.Fprintf(&sb, "\treq, err := http.NewRequest(%q, %q, body)\n", pr.method, pr.fullURL())
	} else {
		fmt.Fprintf(&sb, "\treq, err := http.NewRequest(%q, %q, nil)\n", pr.method, pr.fullURL())
	}
	sb.WriteString("\tif err != nil { panic(err) }\n")

	for _, h := range pr.headers {
		if strings.EqualFold(h.key, "host") || strings.EqualFold(h.key, "content-length") {
			continue
		}
		fmt.Fprintf(&sb, "\treq.Header.Set(%q, %q)\n", h.key, h.value)
	}

	sb.WriteString("\n\tclient := &http.Client{}\n")
	sb.WriteString("\tresp, err := client.Do(req)\n")
	sb.WriteString("\tif err != nil { panic(err) }\n")
	sb.WriteString("\tdefer resp.Body.Close()\n")
	sb.WriteString("\tbody2, _ := io.ReadAll(resp.Body)\n")
	sb.WriteString("\tfmt.Printf(\"Status: %d\\n\", resp.StatusCode)\n")
	sb.WriteString("\tfmt.Println(string(body2))\n")
	sb.WriteString("}\n")
	return sb.String()
}

func toFFUF(pr parsedRequest) string {
	// Place FUZZ in the path: replace query string or append ?FUZZ
	fuzzURL := pr.scheme + "://" + pr.host
	if idx := strings.Index(pr.path, "?"); idx != -1 {
		fuzzURL += pr.path[:idx] + "?FUZZ"
	} else {
		fuzzURL += pr.path + "?FUZZ"
	}

	var sb strings.Builder
	fmt.Fprintf(&sb, "ffuf -u '%s'", fuzzURL)
	sb.WriteString(" \\\n  -w wordlist.txt")
	fmt.Fprintf(&sb, " \\\n  -X %s", pr.method)
	for _, h := range pr.headers {
		if strings.EqualFold(h.key, "content-length") {
			continue
		}
		fmt.Fprintf(&sb, " \\\n  -H '%s: %s'", h.key, h.value)
	}
	if pr.body != "" {
		body := strings.ReplaceAll(pr.body, "'", "'\\''")
		fmt.Fprintf(&sb, " \\\n  -d '%s'", body)
	}
	return sb.String()
}

func toHAR(pr parsedRequest) string {
	type harNameValue struct {
		Name  string `json:"name"`
		Value string `json:"value"`
	}
	type harPostData struct {
		MimeType string `json:"mimeType"`
		Text     string `json:"text"`
	}
	type harRequest struct {
		Method      string         `json:"method"`
		URL         string         `json:"url"`
		HTTPVersion string         `json:"httpVersion"`
		Headers     []harNameValue `json:"headers"`
		QueryString []harNameValue `json:"queryString"`
		Cookies     []harNameValue `json:"cookies"`
		HeadersSize int            `json:"headersSize"`
		BodySize    int            `json:"bodySize"`
		PostData    *harPostData   `json:"postData,omitempty"`
	}
	type harEntry struct {
		StartedDateTime string     `json:"startedDateTime"`
		Time            int        `json:"time"`
		Request         harRequest `json:"request"`
		Cache           struct{}   `json:"cache"`
		Timings         struct {
			Send    int `json:"send"`
			Wait    int `json:"wait"`
			Receive int `json:"receive"`
		} `json:"timings"`
	}
	type harLog struct {
		Version string `json:"version"`
		Creator struct {
			Name    string `json:"name"`
			Version string `json:"version"`
		} `json:"creator"`
		Entries []harEntry `json:"entries"`
	}
	type harRoot struct {
		Log harLog `json:"log"`
	}

	headers := make([]harNameValue, 0, len(pr.headers))
	for _, h := range pr.headers {
		headers = append(headers, harNameValue{h.key, h.value})
	}

	var qs []harNameValue
	if idx := strings.Index(pr.path, "?"); idx != -1 {
		vals, err := url.ParseQuery(pr.path[idx+1:])
		if err == nil {
			for k, vs := range vals {
				for _, v := range vs {
					qs = append(qs, harNameValue{k, v})
				}
			}
		}
	}
	if qs == nil {
		qs = []harNameValue{}
	}

	req := harRequest{
		Method:      pr.method,
		URL:         pr.fullURL(),
		HTTPVersion: "HTTP/1.1",
		Headers:     headers,
		QueryString: qs,
		Cookies:     []harNameValue{},
		HeadersSize: -1,
		BodySize:    len(pr.body),
	}
	if pr.body != "" {
		mimeType := "application/octet-stream"
		for _, h := range pr.headers {
			if strings.EqualFold(h.key, "content-type") {
				mimeType = h.value
				break
			}
		}
		req.PostData = &harPostData{MimeType: mimeType, Text: pr.body}
	}

	root := harRoot{Log: harLog{
		Version: "1.2",
		Entries: []harEntry{{
			StartedDateTime: "1970-01-01T00:00:00.000Z",
			Time:            -1,
			Request:         req,
		}},
	}}
	root.Log.Creator.Name = "spilltea"

	b, err := json.MarshalIndent(root, "", "  ")
	if err != nil {
		return ""
	}
	return string(b)
}
