package diagnostics

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strings"
)

const Redacted = "[REDACTED]"

var (
	bearerPattern      = regexp.MustCompile(`(?i)bearer\s+[^\s,;"'{}\[\]<>]+`)
	authHeaderPattern  = regexp.MustCompile(`(?im)(authorization\s*[:=]\s*)([^\r\n,;]+)`)
	cookieLinePattern  = regexp.MustCompile(`(?im)((?:set-)?cookie\s*[:=]\s*)([^\r\n]+)`)
	secretPairPattern  = regexp.MustCompile(`(?i)([?&;\s,]*(?:access[_-]?token|auth[_-]?token|gitcode[_-]?token|token|secret|api[_-]?key|access[_-]?key|private[_-]?key|password|credential)\s*[=:]\s*)([^\s&;,"'{}\[\]<>]+)`)
	jsonSecretPattern  = regexp.MustCompile(`(?i)("[^"]*(?:token|secret|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|password)[^"]*"\s*:\s*")([^"]*)(")`)
)

type Filter struct {
	SensitiveTerms []string
	ApprovedHosts  []string
}

func NewFilter(sensitiveTerms ...string) Filter {
	return Filter{SensitiveTerms: compact(sensitiveTerms)}
}

func (f Filter) WithApprovedHosts(hosts ...string) Filter {
	f.ApprovedHosts = compact(hosts)
	return f
}

func (f Filter) RedactText(text string) string {
	redacted := bearerPattern.ReplaceAllString(text, "Bearer "+Redacted)
	redacted = authHeaderPattern.ReplaceAllString(redacted, "${1}"+Redacted)
	redacted = cookieLinePattern.ReplaceAllString(redacted, "${1}"+Redacted)
	redacted = jsonSecretPattern.ReplaceAllString(redacted, "${1}"+Redacted+"${3}")
	redacted = secretPairPattern.ReplaceAllString(redacted, "${1}"+Redacted)
	for _, term := range f.SensitiveTerms {
		if term == "" {
			continue
		}
		redacted = strings.ReplaceAll(redacted, term, Redacted)
	}
	return redacted
}

func (f Filter) RedactBytes(payload []byte) []byte {
	if len(payload) == 0 {
		return nil
	}
	return []byte(f.RedactText(string(payload)))
}

func (f Filter) RedactHeaders(headers http.Header) map[string][]string {
	out := map[string][]string{}
	for key, values := range headers {
		lower := strings.ToLower(key)
		redactValue := lower == "authorization" || lower == "cookie" || lower == "set-cookie" || strings.Contains(lower, "token") || strings.Contains(lower, "secret") || strings.Contains(lower, "key")
		for _, value := range values {
			if redactValue {
				out[key] = append(out[key], Redacted)
				continue
			}
			out[key] = append(out[key], f.RedactText(value))
		}
	}
	return out
}

func (f Filter) RedactURL(raw string) string {
	if raw == "" {
		return ""
	}
	parsed, err := url.Parse(raw)
	if err != nil {
		return f.RedactText(raw)
	}
	if parsed.Host != "" && !hostAllowed(parsed.Hostname(), f.ApprovedHosts) {
		parsed.Host = "redacted.example.com"
	}
	query := parsed.Query()
	for key := range query {
		if isSecretKey(key) {
			query.Set(key, Redacted)
		}
	}
	parsed.RawQuery = query.Encode()
	return f.RedactText(parsed.String())
}

func (f Filter) RedactJSONBody(body []byte) []byte {
	if len(body) == 0 {
		return nil
	}
	var value any
	if json.Unmarshal(body, &value) != nil {
		return f.RedactBytes(body)
	}
	encoded, err := json.Marshal(f.redactJSONValue(value))
	if err != nil {
		return f.RedactBytes(body)
	}
	return encoded
}

func (f Filter) RawAPIResponseSummary(statusCode int, body []byte) string {
	if len(body) == 0 {
		return "api_response: empty"
	}
	return "api_response: status=" + http.StatusText(statusCode) + " body=" + Redacted
}

func (f Filter) RedactedWriter(w io.Writer) io.Writer {
	return Writer{Writer: w, Filter: f}
}

type Writer struct {
	Writer io.Writer
	Filter Filter
}

func (w Writer) Write(p []byte) (int, error) {
	if w.Writer == nil {
		return len(p), nil
	}
	_, err := w.Writer.Write(w.Filter.RedactBytes(p))
	return len(p), err
}

func (f Filter) CaptureIsSanitized(parts ...string) bool {
	text := strings.Join(parts, "")
	for _, term := range f.SensitiveTerms {
		if term != "" && strings.Contains(text, term) {
			return false
		}
	}
	return !bearerPattern.MatchString(text) && !authHeaderPattern.MatchString(text)
}

func (f Filter) redactJSONValue(value any) any {
	switch v := value.(type) {
	case map[string]any:
		out := map[string]any{}
		for key, child := range v {
			if isJSONSecretKey(key) {
				out[key] = Redacted
				continue
			}
			out[key] = f.redactJSONValue(child)
		}
		return out
	case []any:
		out := make([]any, len(v))
		for i, child := range v {
			out[i] = f.redactJSONValue(child)
		}
		return out
	case string:
		return f.RedactText(v)
	default:
		return v
	}
}

func RedactText(text string, sensitiveTerms ...string) string {
	return NewFilter(sensitiveTerms...).RedactText(text)
}

func RedactBytes(payload []byte, sensitiveTerms ...string) []byte {
	return NewFilter(sensitiveTerms...).RedactBytes(payload)
}

func RedactHeaders(headers http.Header, sensitiveTerms ...string) map[string][]string {
	return NewFilter(sensitiveTerms...).RedactHeaders(headers)
}

func RedactURL(raw string, approvedHosts []string, sensitiveTerms ...string) string {
	return NewFilter(sensitiveTerms...).WithApprovedHosts(approvedHosts...).RedactURL(raw)
}

func RedactJSONBody(body []byte, sensitiveTerms ...string) []byte {
	return NewFilter(sensitiveTerms...).RedactJSONBody(body)
}

func NewRedactedWriter(w io.Writer, sensitiveTerms ...string) io.Writer {
	return NewFilter(sensitiveTerms...).RedactedWriter(w)
}

func BufferIsSanitized(buf bytes.Buffer, forbidden ...string) bool {
	return NewFilter(forbidden...).CaptureIsSanitized(buf.String())
}

func isSecretKey(key string) bool {
	lower := strings.ToLower(key)
	return strings.Contains(lower, "token") || strings.Contains(lower, "key") || strings.Contains(lower, "secret") || strings.Contains(lower, "password") || strings.Contains(lower, "credential")
}

func isJSONSecretKey(key string) bool {
	lower := strings.ToLower(key)
	return lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "owner" || lower == "repo" || lower == "repository" || isSecretKey(lower)
}

func hostAllowed(host string, approved []string) bool {
	host = strings.ToLower(strings.TrimSpace(host))
	if host == "" {
		return true
	}
	for _, allowed := range approved {
		if strings.EqualFold(host, strings.TrimSpace(allowed)) {
			return true
		}
	}
	return false
}

func compact(values []string) []string {
	out := make([]string, 0, len(values))
	seen := map[string]bool{}
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}