// Package proxy provides a logging reverse proxy used for speaker traffic debugging.
package proxy

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httputil"
	"os"
	"strings"
)

var sensitiveHeaders = []string{
	"Authorization",
	"Cookie",
	"X-Bose-Token",
}

// LoggingProxy wraps a ReverseProxy to provide instrumentation.
type LoggingProxy struct {
	Proxy         *httputil.ReverseProxy
	Redact        bool
	LogBody       bool
	RecordEnabled bool
	MaxBodySize   int64
	Recorder      *Recorder
}

// NewLoggingProxy creates a lightweight logger for HTTP requests/responses.
func NewLoggingProxy(_ string, redact bool) *LoggingProxy {
	// targetURL logic should be handled by the caller or we can parse it here
	return &LoggingProxy{
		Redact:      redact,
		LogBody:     os.Getenv("LOG_PROXY_BODY") == "true",
		MaxBodySize: 1024 * 10, // 10KB default limit for logging
	}
}

// SetRecorder sets the recorder for the proxy.
func (lp *LoggingProxy) SetRecorder(r *Recorder) {
	lp.Recorder = r
}

// LogRequest prints an abbreviated request with optional header/body redaction.
func (lp *LoggingProxy) LogRequest(r *http.Request) {
	headers := formatHeaders(r.Header, lp.Redact)

	bodyStr := "[HIDDEN]"

	if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
		if r.Body != nil {
			bodyBytes, _ := io.ReadAll(r.Body)

			r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
			if int64(len(bodyBytes)) > lp.MaxBodySize {
				bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
			} else {
				bodyStr = string(bodyBytes)
			}
		} else {
			bodyStr = "[EMPTY]"
		}
	}

	log.Printf("[PROXY_REQ] %s %s\n  Headers:\n%s\n  Body: %s", r.Method, r.URL.String(), headers, bodyStr)
}

// LogResponse prints an abbreviated response with optional header/body redaction.
func (lp *LoggingProxy) LogResponse(r *http.Response) {
	headers := formatHeaders(r.Header, lp.Redact)

	bodyStr := "[HIDDEN]"

	if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
		if r.Body != nil {
			bodyBytes, _ := io.ReadAll(r.Body)

			r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
			if int64(len(bodyBytes)) > lp.MaxBodySize {
				bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
			} else {
				bodyStr = string(bodyBytes)
			}
		} else {
			bodyStr = "[EMPTY]"
		}
	}

	log.Printf("[PROXY_RES] %d %s\n  Headers:\n%s\n  Body: %s", r.StatusCode, r.Request.URL.String(), headers, bodyStr)

	if lp.Recorder != nil && lp.RecordEnabled {
		_ = lp.Recorder.Record("upstream", r.Request, r)
	}
}

func formatHeaders(h http.Header, redact bool) string {
	var sb strings.Builder
	// In Go, http.Header is a map[string][]string.
	// Iterating over the map directly allows us to see the actual keys
	// stored in the map, which might not be canonical if set directly.
	for k, vv := range h {
		val := strings.Join(vv, ", ")
		if redact && isSensitive(k) {
			val = "[REDACTED]"
		}

		sb.WriteString(fmt.Sprintf("    %s: %s\n", k, val))
	}

	return strings.TrimSuffix(sb.String(), "\n")
}

func isSensitive(header string) bool {
	for _, h := range sensitiveHeaders {
		if strings.EqualFold(h, header) {
			return true
		}
	}

	return false
}

func shouldLogBody(contentType string) bool {
	contentType = strings.ToLower(contentType)

	return strings.Contains(contentType, "xml") ||
		strings.Contains(contentType, "json") ||
		strings.Contains(contentType, "text") ||
		contentType == ""
}
