package proxy

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/http"
	"net/http/httputil"
	"sync"
	"time"

	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/tidbcloud/service-proxy/pkg/audit"
	"github.com/tidbcloud/service-proxy/pkg/config"
	"github.com/tidbcloud/service-proxy/pkg/observability"
	"github.com/tidbcloud/service-proxy/pkg/route"
	"github.com/tidbcloud/service-proxy/pkg/secret"
)

const (
	defaultIdleConnTimeout = 60 * time.Second
)

// cachedProxy holds a cached reverse proxy with metadata.
type cachedProxy struct {
	proxy     *httputil.ReverseProxy
	transport *http.Transport // stored for cleanup
}

// close releases transport resources.
func (c *cachedProxy) close() {
	if c.transport != nil {
		c.transport.CloseIdleConnections()
	}
}

// Handler handles HTTP proxy requests.
type Handler struct {
	router        route.Router
	secretManager secret.Provider
	metrics       *observability.Metrics
	auditManager  *audit.Manager
	proxyCache    sync.Map
}

// NewHandler creates a new proxy handler.
func NewHandler(
	router route.Router,
	secretManager secret.Provider,
	metrics *observability.Metrics,
	auditManager *audit.Manager,
) *Handler {
	h := &Handler{
		router:        router,
		secretManager: secretManager,
		metrics:       metrics,
		auditManager:  auditManager,
	}
	return h
}

// ServeHTTP handles incoming proxy requests.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	start := time.Now()
	originalPath := r.URL.Path
	originalQuery := r.URL.RawQuery

	log.Info("incoming request",
		zap.String("method", r.Method),
		zap.String("path", r.URL.Path),
		zap.String("remote_addr", r.RemoteAddr),
	)

	// Resolve route
	matched, err := h.router.Match(r.Context(), r.URL.Path)
	if err != nil {
		if route.IsBadRequest(err) {
			log.Error("invalid request", zap.String("path", r.URL.Path), zap.Error(err))
			http.Error(w, "bad request", http.StatusBadRequest)
			h.recordMetrics(r.Method, "unknown", http.StatusBadRequest, start)
			h.recordAudit(r, start, http.StatusBadRequest, err.Error(), originalPath, originalQuery)
		} else {
			log.Error("route not found", zap.String("path", r.URL.Path), zap.Error(err))
			http.Error(w, "route not found", http.StatusNotFound)
			h.recordMetrics(r.Method, "unknown", http.StatusNotFound, start)
			h.recordAudit(r, start, http.StatusNotFound, err.Error(), originalPath, originalQuery)
		}
		return
	}

	// Get or create cached reverse proxy
	proxy, err := h.getOrCreateProxy(r.Context(), matched)
	if err != nil {
		log.Error("failed to get proxy",
			zap.String("route", matched.Name),
			zap.String("namespace", matched.Vars.Namespace),
			zap.Error(err),
		)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		h.recordMetrics(r.Method, matched.Name, http.StatusInternalServerError, start)
		h.recordAudit(r, start, http.StatusInternalServerError, err.Error(), originalPath, originalQuery)
		return
	}

	// Apply timeout if configured
	proxyReq := r
	if matched.Config.Timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel := context.WithTimeout(proxyReq.Context(), matched.Config.Timeout)
		defer cancel()
		proxyReq = proxyReq.WithContext(ctx)
	}

	// Wrap response writer to capture status code
	rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

	// Set request path to suffix path before proxying
	// This allows the default Director to use this path directly
	proxyReq.URL.Path = matched.Vars.Remaining
	proxyReq.URL.RawPath = "" // Clear encoded path

	// Only enable audit body capture on the successful forwarding path.
	if h.auditManager != nil {
		proxyReq = h.auditManager.AttachBodyCapture(proxyReq)
	}

	proxy.ServeHTTP(rw, proxyReq)

	log.Info("request completed",
		zap.String("path", r.URL.Path),
		zap.Int("status_code", rw.statusCode),
		zap.Duration("duration", time.Since(start)),
	)
	h.recordMetrics(r.Method, matched.Name, rw.statusCode, start)
	h.recordAudit(proxyReq, start, rw.statusCode, "", originalPath, originalQuery)
}

// getOrCreateProxy returns a cached reverse proxy or creates a new one.
func (h *Handler) getOrCreateProxy(ctx context.Context, matched *route.Route) (*httputil.ReverseProxy, error) {
	key := matched.BackendURL.String()
	if cached, ok := h.proxyCache.Load(key); ok {
		log.Debug("using cached proxy", zap.String("backend", key))
		return cached.(*cachedProxy).proxy, nil
	}

	// Create transport with TLS if needed
	transport, err := h.createTransport(ctx, matched.Config, matched.Vars.Namespace)
	if err != nil {
		log.Error("failed to create transport", zap.Error(err))
		return nil, err
	}

	proxy := h.buildReverseProxy(transport, matched)
	h.proxyCache.Store(key, &cachedProxy{
		proxy:     proxy,
		transport: transport,
	})
	return proxy, nil
}

func (h *Handler) createTransport(ctx context.Context, cfg *config.RouteConfig, namespace string) (*http.Transport, error) {
	transport := &http.Transport{
		IdleConnTimeout:   defaultIdleConnTimeout,
		DisableKeepAlives: true,
	}

	// Configure TLS if enabled
	if cfg.TLS.Enabled {
		if cfg.TLS.InsecureSkipVerify {
			//nolint:gosec // InsecureSkipVerify is intentional for internal proxy (e.g., TiBR)
			transport.TLSClientConfig = &tls.Config{
				InsecureSkipVerify: true,
				MinVersion:         tls.VersionTLS12,
			}
		} else if cfg.TLS.SecretRef.Name != "" {
			if h.secretManager == nil {
				return nil, fmt.Errorf("secret manager not configured")
			}
			ns := namespace
			if ns == "" {
				ns = cfg.TLS.SecretRef.Namespace
			}
			tlsConfig, err := h.secretManager.GetTLSConfig(ctx, cfg.TLS.SecretRef.Name, ns)
			if err != nil {
				return nil, err
			}
			transport.TLSClientConfig = tlsConfig
		}
	}

	return transport, nil
}

func (h *Handler) buildReverseProxy(transport http.RoundTripper, matched *route.Route) *httputil.ReverseProxy {
	proxy := httputil.NewSingleHostReverseProxy(matched.BackendURL)
	proxy.Transport = transport
	proxy.ModifyResponse = func(resp *http.Response) error {
		return setCORSHeader(resp)
	}
	// Error handler with detailed logging
	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
		log.Error("proxy error",
			zap.String("route", matched.Name),
			zap.String("backend", matched.BackendURL.String()),
			zap.String("request_path", r.URL.Path),
			zap.String("request_url", r.URL.String()),
			zap.Error(err),
		)
		if h.metrics != nil {
			h.metrics.RecordBackendError(matched.Name, "proxy_error")
		}
		http.Error(w, "bad gateway", http.StatusBadGateway)
	}

	return proxy
}

func (h *Handler) recordMetrics(method, route string, status int, start time.Time) {
	if h.metrics != nil {
		h.metrics.RecordRequest(method, route, status)
		h.metrics.RecordLatency(method, route, time.Since(start).Seconds())
	}
}

func (h *Handler) recordAudit(r *http.Request, start time.Time, statusCode int, errMsg string, originalPath string, rawQuery string) {
	if h.auditManager == nil {
		return
	}
	h.auditManager.RecordRequestAutoBody(r, audit.RequestRecordOptions{
		Start:        start,
		StatusCode:   statusCode,
		Error:        errMsg,
		OriginalPath: originalPath,
		RawQuery:     rawQuery,
	})
}

// Close gracefully shuts down the handler and releases all resources.
func (h *Handler) Close() error {
	h.proxyCache.Range(func(key, value interface{}) bool {
		cached := value.(*cachedProxy)
		cached.close()
		h.proxyCache.Delete(key)
		return true
	})
	log.Info("proxy handler closed, all cached proxies cleaned up")
	return nil
}

// responseWriter wraps http.ResponseWriter to capture status code.
type responseWriter struct {
	http.ResponseWriter
	statusCode int
	written    bool
}

func (rw *responseWriter) WriteHeader(code int) {
	if !rw.written {
		rw.statusCode = code
		rw.written = true
	}
	rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
	if !rw.written {
		rw.statusCode = http.StatusOK
		rw.written = true
	}
	return rw.ResponseWriter.Write(b)
}

// setCORSHeader sets the CORS headers on the response.
func setCORSHeader(resp *http.Response) error {
	resp.Header.Set("Access-Control-Allow-Headers", "accept, content-type, authorization")
	resp.Header.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
	resp.Header.Set("Access-Control-Allow-Origin", "*")
	return nil
}
