package oracle

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

// HTTPDoer is the subset of *http.Client the live probes need. Keeping it an
// interface means probes are testable against a fake transport and the live
// wiring can inject the campaign's session-authenticated client.
type HTTPDoer interface {
	Do(*http.Request) (*http.Response, error)
}

// maxProbeBody caps how much of a response body a probe reads — enough to
// compare/scan, bounded so a huge response can't blow up memory.
const maxProbeBody = 512 * 1024

// sendRaw parses a raw HTTP/1.1 request (the ProbeSpec base form, after marker
// substitution), re-targets it at target's scheme+host, sends it via doer, and
// returns the response status, body, and round-trip latency. The raw form keeps
// the probe faithful to the exact request a tool/exploit agent proved the
// finding with.
func sendRaw(ctx context.Context, doer HTTPDoer, target, raw string) (status int, body string, latency time.Duration, err error) {
	req, err := parseRaw(ctx, target, raw)
	if err != nil {
		return 0, "", 0, err
	}
	start := time.Now()
	resp, err := doer.Do(req)
	latency = time.Since(start)
	if err != nil {
		return 0, "", latency, err
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(io.LimitReader(resp.Body, maxProbeBody))
	return resp.StatusCode, string(b), latency, nil
}

// sendRawHeaders is sendRaw but returns the response status and headers (not the
// body) — for probes whose signal is a header, e.g. open-redirect's Location.
// The caller supplies a doer that does NOT follow redirects, so the 3xx and its
// Location are observable.
func sendRawHeaders(ctx context.Context, doer HTTPDoer, target, raw string) (status int, header http.Header, err error) {
	req, err := parseRaw(ctx, target, raw)
	if err != nil {
		return 0, nil, err
	}
	resp, err := doer.Do(req)
	if err != nil {
		return 0, nil, err
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, maxProbeBody))
	return resp.StatusCode, resp.Header, nil
}

// parseRaw turns a raw HTTP/1.1 request into a client *http.Request aimed at
// target's scheme and host. Request lines may use LF or CRLF.
func parseRaw(ctx context.Context, target, raw string) (*http.Request, error) {
	norm := normalizeCRLF(raw)
	// Split off the body ourselves at the first blank line: a hand-written spec
	// may omit Content-Length, and http.ReadRequest would then read no body.
	head, bodyStr, _ := strings.Cut(norm, "\r\n\r\n")
	bodyStr = strings.TrimSuffix(bodyStr, "\r\n\r\n")

	parsed, err := http.ReadRequest(bufio.NewReader(strings.NewReader(head + "\r\n\r\n")))
	if err != nil {
		return nil, fmt.Errorf("parse raw request: %w", err)
	}
	defer func() {
		if parsed.Body != nil {
			_ = parsed.Body.Close()
		}
	}()

	scheme, host := schemeHost(target, parsed.Host)
	if host == "" {
		return nil, fmt.Errorf("no host: target %q and request Host both empty", target)
	}
	url := scheme + "://" + host + parsed.URL.RequestURI()

	var body io.Reader
	if len(bodyStr) > 0 {
		if len(bodyStr) > maxProbeBody {
			bodyStr = bodyStr[:maxProbeBody]
		}
		body = strings.NewReader(bodyStr)
	}
	req, err := http.NewRequestWithContext(ctx, parsed.Method, url, body)
	if err != nil {
		return nil, err
	}
	// Carry the original headers except Host (set from the URL) and transfer
	// framing headers net/http recomputes.
	for k, vs := range parsed.Header {
		switch http.CanonicalHeaderKey(k) {
		case "Host", "Content-Length", "Transfer-Encoding", "Connection":
			continue
		}
		for _, v := range vs {
			req.Header.Add(k, v)
		}
	}
	return req, nil
}

// schemeHost derives the scheme and host for the outgoing request. The TARGET
// wins — we send to the real server under test, not to whatever placeholder
// Host the spec's raw request happens to carry. The request's Host header is
// only a fallback when the target has no host of its own.
func schemeHost(target, reqHost string) (scheme, host string) {
	scheme = "http"
	t := strings.TrimSpace(target)
	switch {
	case strings.HasPrefix(t, "https://"):
		scheme, t = "https", strings.TrimPrefix(t, "https://")
	case strings.HasPrefix(t, "http://"):
		scheme, t = "http", strings.TrimPrefix(t, "http://")
	}
	if t != "" {
		// host[:port] up to the first path/query separator.
		if i := strings.IndexAny(t, "/?"); i >= 0 {
			t = t[:i]
		}
		host = t
	}
	if host == "" {
		host = reqHost
	}
	return scheme, host
}

// normalizeCRLF ensures the request uses CRLF line endings and ends with a
// blank line, so http.ReadRequest accepts specs written with bare LF.
func normalizeCRLF(raw string) string {
	raw = strings.ReplaceAll(raw, "\r\n", "\n")
	raw = strings.ReplaceAll(raw, "\n", "\r\n")
	if !strings.HasSuffix(raw, "\r\n\r\n") {
		raw = strings.TrimRight(raw, "\r\n") + "\r\n\r\n"
	}
	return raw
}
