package exploit

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

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/verify"
)

// ssrfParamNames are query/body parameter names that conventionally carry a URL
// or host the server will dereference — the classic SSRF entry points. Matched
// case-insensitively.
var ssrfParamNames = map[string]struct{}{
	"url": {}, "uri": {}, "u": {}, "link": {}, "href": {}, "src": {}, "source": {},
	"target": {}, "dest": {}, "destination": {}, "redirect": {}, "redirect_uri": {},
	"redirecturl": {}, "redirect_url": {}, "return": {}, "returnurl": {}, "return_url": {},
	"next": {}, "continue": {}, "goto": {}, "go": {}, "out": {}, "to": {},
	"callback": {}, "webhook": {}, "webhook_url": {}, "callback_url": {},
	"feed": {}, "rss": {}, "image": {}, "img": {}, "imageurl": {}, "image_url": {},
	"avatar": {}, "avatar_url": {}, "icon": {}, "logo": {}, "photo": {},
	"fetch": {}, "load": {}, "open": {}, "proxy": {}, "proxy_url": {},
	"host": {}, "domain": {}, "site": {}, "server": {}, "upstream": {}, "endpoint": {},
	"document": {}, "doc": {}, "file": {}, "path": {}, "page": {}, "data": {},
}

// URLShapedValue reports whether v looks like a URL or bare host a server might
// fetch: an http(s):// or scheme-relative (//) prefix, or a dotted hostname with
// at least one letter (so "3.14" or a plain number is not mistaken for a host).
func URLShapedValue(v string) bool {
	v = strings.TrimSpace(v)
	if v == "" {
		return false
	}
	low := strings.ToLower(v)
	if strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://") || strings.HasPrefix(v, "//") {
		return true
	}
	// Bare host[:port][/path]: take the authority up to the first / ? #.
	authority := v
	if i := strings.IndexAny(v, "/?#"); i >= 0 {
		authority = v[:i]
	}
	if host, _, ok := strings.Cut(authority, ":"); ok {
		authority = host
	}
	if !strings.Contains(authority, ".") || strings.ContainsAny(authority, " \t") {
		return false
	}
	for _, r := range authority {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
			return true // has a letter → plausible hostname, not a bare number
		}
	}
	return false
}

// DetectSSRFParams returns the query parameters of rawURL that are likely SSRF
// sinks: those whose NAME is a conventional URL/host parameter, or whose VALUE
// is URL-shaped. Parameters are returned in the order they appear, de-duplicated.
// A URL with no query (or an unparseable one) yields nothing.
func DetectSSRFParams(rawURL string) []string {
	u, err := url.Parse(rawURL)
	if err != nil || u.RawQuery == "" {
		return nil
	}
	var out []string
	seen := map[string]struct{}{}
	// Walk the raw query in order (url.Values is an unordered map).
	for _, pair := range strings.Split(u.RawQuery, "&") {
		if pair == "" {
			continue
		}
		name := pair
		value := ""
		if i := strings.IndexByte(pair, '='); i >= 0 {
			name, value = pair[:i], pair[i+1:]
		}
		decodedName, err := url.QueryUnescape(name)
		if err == nil {
			name = decodedName
		}
		if _, dup := seen[name]; dup {
			continue
		}
		decodedVal, err := url.QueryUnescape(value)
		if err == nil {
			value = decodedVal
		}
		if isSSRFParam(name, value) {
			seen[name] = struct{}{}
			out = append(out, name)
		}
	}
	return out
}

// isSSRFParam reports whether a parameter name/value pair is a likely SSRF sink.
func isSSRFParam(name, value string) bool {
	if _, ok := ssrfParamNames[strings.ToLower(name)]; ok {
		return true
	}
	return URLShapedValue(value)
}

// SSRFOOBSpec builds an out-of-band re-proof recipe for a suspected SSRF: it
// points the controllable URL/host parameter at a callback URL the OOB oracle
// reserves (the §OOB§ slot), so a server that dereferences the parameter makes
// an outbound request to infrastructure we control — ground truth no model or
// WAF can fake. Silence never disproves SSRF (egress filtering, a slow fetch, or
// a WAF all look like silence), which is exactly the OOB oracle's honest
// asymmetry.
//
// targetURL is the endpoint under test; param is the SSRF-sink parameter (from
// DetectSSRFParams); authHeader is an optional session credential. The whole
// parameter value becomes the callback URL — interactsh hands back a full
// http:// URL — so the default payload is the bare marker. Returns an error if
// the result is invalid.
func SSRFOOBSpec(targetURL, param, authHeader string) (*verify.ProbeSpec, error) {
	raw, err := ssrfRawRequest(targetURL, param, authHeader)
	if err != nil {
		return nil, err
	}
	return verify.NewOOBSpec(raw, "", verify.OOBMarker)
}

// ssrfRawRequest builds a raw GET request with the SSRF-sink parameter's value
// replaced by the injection marker. Host and path are split by hand so the
// marker is placed verbatim.
func ssrfRawRequest(targetURL, param, authHeader string) (string, error) {
	if strings.TrimSpace(param) == "" {
		return "", fmt.Errorf("ssrf: empty parameter")
	}
	rest := targetURL
	for _, p := range []string{"https://", "http://"} {
		rest = strings.TrimPrefix(rest, p)
	}
	host, pathQuery := rest, "/"
	if i := strings.IndexAny(rest, "/?#"); i >= 0 {
		host, pathQuery = rest[:i], rest[i:]
	}
	path, query := pathQuery, ""
	if i := strings.IndexByte(pathQuery, '?'); i >= 0 {
		path, query = pathQuery[:i], pathQuery[i+1:]
	}
	if path == "" {
		path = "/"
	}

	var b strings.Builder
	b.WriteString("GET " + path + "?" + markParam(query, param) + " HTTP/1.1\r\n")
	writeCommonHeaders(&b, host, authHeader)
	b.WriteString("\r\n")
	return b.String(), nil
}
