package checker

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"time"

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

	"github.com/tidbcloud/service-proxy/pkg/audit"
	"github.com/tidbcloud/service-proxy/pkg/checker/core"

	// Register all checker types
	_ "github.com/tidbcloud/service-proxy/pkg/checker/storage"
	_ "github.com/tidbcloud/service-proxy/pkg/checker/streaming"
)

const (
	defaultCheckTimeout = 10 * time.Second

	// maxCheckRequestSize is the maximum allowed check request body size.
	// Check requests should be small JSON payloads. Anything larger is rejected with 413.
	maxCheckRequestSize = 64 * 1024 // 64KB
)

// ErrorResponse represents an error response.
type ErrorResponse struct {
	Error string `json:"error"`
}

// Handler handles connectivity check HTTP requests.
type Handler struct {
	registry     *core.Registry
	timeout      time.Duration
	auditManager *audit.Manager
}

// NewHandler creates a new check handler.
func NewHandler(registry *core.Registry, auditManager *audit.Manager) *Handler {
	if registry == nil {
		registry = core.DefaultRegistry
	}
	return &Handler{
		registry:     registry,
		timeout:      defaultCheckTimeout,
		auditManager: auditManager,
	}
}

// ServeHTTP handles POST /check requests.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	start := time.Now()

	log.Debug("received connectivity check request",
		zap.String("method", r.Method),
		zap.String("path", r.URL.Path),
		zap.String("remote_addr", r.RemoteAddr),
	)

	if h.auditManager != nil {
		r = h.auditManager.AttachBodyCapture(r)
	}

	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		h.recordAudit(r, start, http.StatusMethodNotAllowed, "method not allowed")
		return
	}

	// Apply business limit: reject requests larger than maxCheckRequestSize with 413
	r.Body = http.MaxBytesReader(w, r.Body, maxCheckRequestSize)

	var req core.CheckRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		// Distinguish "body too large" from "invalid JSON"
		var maxBytesErr *http.MaxBytesError
		if errors.As(err, &maxBytesErr) {
			log.Warn("check request body too large", zap.Int64("limit", maxCheckRequestSize))
			h.writeError(w, http.StatusRequestEntityTooLarge, "request body too large")
			h.recordAudit(r, start, http.StatusRequestEntityTooLarge, "request body too large")
			return
		}
		log.Warn("failed to decode check request body", zap.Error(err))
		h.writeError(w, http.StatusBadRequest, "invalid request body")
		h.recordAudit(r, start, http.StatusBadRequest, "invalid request body")
		return
	}

	checker, err := h.registry.Create(&req)
	if err != nil {
		log.Warn("failed to create checker",
			zap.String("type", req.TargetType),
			zap.Error(err),
		)
		h.writeError(w, http.StatusBadRequest, err.Error())
		h.recordAudit(r, start, http.StatusBadRequest, err.Error())
		return
	}

	// Apply timeout
	ctx, cancel := context.WithTimeout(r.Context(), h.timeout)
	defer cancel()

	// Perform check
	if err := checker.Check(ctx); err != nil {
		log.Warn("connectivity check failed",
			zap.String("type", req.TargetType),
			zap.Error(err),
		)

		statusCode := http.StatusBadRequest
		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
			statusCode = http.StatusRequestTimeout
		}

		h.writeError(w, statusCode, err.Error())
		h.recordAudit(r, start, statusCode, err.Error())
		return
	}

	log.Info("connectivity check succeeded", zap.String("type", req.TargetType))
	w.WriteHeader(http.StatusOK)
	h.recordAudit(r, start, http.StatusOK, "")
}

func (h *Handler) writeError(w http.ResponseWriter, statusCode int, message string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	_ = json.NewEncoder(w).Encode(ErrorResponse{
		Error: message,
	})
}

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