package handlers

import (
	"context"
	stderrors "errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"unicode"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/errors"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/observability"
)

const (
	defaultActivityLimit = 20
	minActivityLimit     = 20
	maxActivityLimit     = 100
	minActivitySeverity  = -4
	maxActivitySeverity  = 8
	maxActivityFilterLen = 255
	maxActivityCursorLen = 4096
	clientClosedStatus   = 499
)

type activitySearcher interface {
	ListActivities(context.Context, uint64, string, url.Values) (*clinic_internal_api.ActivitiesResponse, error)
}

// ActivityHandler is a thin public facade over Clinic's canonical internal
// activity API. ActivityHub protocol and ownership logic remain in Clinic.
type ActivityHandler struct {
	client activitySearcher
}

func NewActivityHandler(client activitySearcher) *ActivityHandler {
	return &ActivityHandler{client: client}
}

// ListActivities returns Clinic-owned activity events through Data Proxy.
//
// @Summary      List cluster activity events
// @Tags         activity-events
// @Produce      json
// @Param        orgId path string true "TiDB Cloud/DBAAS org ID"
// @Param        clusterId path string true "Opaque TiDB Cloud cluster ID"
// @Param        begin_ts query int true "Inclusive window start in Unix seconds" minimum(0)
// @Param        end_ts query int true "Inclusive window end in Unix seconds" minimum(0)
// @Param        catalog query string false "Activity catalog"
// @Param        type query string false "Activity type"
// @Param        name query string false "Activity name"
// @Param        severity query int false "Minimum severity" default(0) minimum(-4) maximum(8)
// @Param        limit query int false "Page size" default(20) minimum(20) maximum(100)
// @Param        cursor query string false "Opaque ActivityHub pagination cursor"
// @Success      200 {object} clinic_internal_api.ActivitiesResponse
// @Failure      400 {object} errors.APIError
// @Failure      401 {object} errors.APIError
// @Failure      403 {object} errors.APIError
// @Failure      404 {object} errors.APIError
// @Failure      429 {object} errors.APIError
// @Failure      500 {object} errors.APIError
// @Failure      502 {object} errors.APIError
// @Failure      503 {object} errors.APIError
// @Failure      504 {object} errors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/activities [get]
func (h *ActivityHandler) ListActivities(c *gin.Context) {
	start, rec := beginMetadataRequest(c, "activity_list")
	rec.QueryType = "activity"
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	orgID, ok := parseOrgID(c, rec)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateResourceID(clusterID); err != nil {
		rec.ErrCode = "INVALID_CLUSTER_ID"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}
	params, err := parseActivitySearchParams(c)
	if err != nil {
		rec.ErrCode = "INVALID_ACTIVITY_QUERY"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}
	if h.client == nil {
		rec.ErrCode = "CLINIC_INTERNAL_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable", nil)
		return
	}

	response, err := h.client.ListActivities(c.Request.Context(), orgID, clusterID, params.values())
	if err != nil {
		h.writeClinicActivityError(c, rec, err, clusterID)
		return
	}
	if response == nil {
		rec.ErrCode = "CLINIC_INTERNAL_BAD_RESPONSE"
		writeResourceError(c, http.StatusBadGateway, "BadGateway", "Clinic returned an invalid activity response", nil)
		return
	}
	if response.Activities == nil {
		response.Activities = make([]*clinic_internal_api.Activity, 0)
	}
	c.JSON(http.StatusOK, response)
}

func (h *ActivityHandler) RegisterRoutes(r *gin.Engine) {
	group := r.Group("/data-proxy/api/v1")
	group.GET("/orgs/:orgId/clusters/:clusterId/activities", h.ListActivities)
	r.GET("/api/v1/orgs/:orgId/data-proxy/clusters/:clusterId/activities", h.ListActivities)
}

type activitySearchParams struct {
	BeginTS  int64
	EndTS    int64
	Catalog  string
	Type     string
	Name     string
	Severity int
	Limit    int
	Cursor   string
}

func (p activitySearchParams) values() url.Values {
	query := make(url.Values, 8)
	query.Set("begin_ts", strconv.FormatInt(p.BeginTS, 10))
	query.Set("end_ts", strconv.FormatInt(p.EndTS, 10))
	query.Set("severity", strconv.Itoa(p.Severity))
	query.Set("limit", strconv.Itoa(p.Limit))
	if p.Catalog != "" {
		query.Set("catalog", p.Catalog)
	}
	if p.Type != "" {
		query.Set("type", p.Type)
	}
	if p.Name != "" {
		query.Set("name", p.Name)
	}
	if p.Cursor != "" {
		query.Set("cursor", p.Cursor)
	}
	return query
}

func parseActivitySearchParams(c *gin.Context) (activitySearchParams, error) {
	for _, name := range []string{"begin_ts", "end_ts", "catalog", "type", "name", "severity", "limit", "cursor"} {
		if len(c.Request.URL.Query()[name]) > 1 {
			return activitySearchParams{}, fmt.Errorf("%s must be provided at most once", name)
		}
	}

	beginTS, err := parseRequiredNonNegativeInt64(c, "begin_ts")
	if err != nil {
		return activitySearchParams{}, err
	}
	endTS, err := parseRequiredNonNegativeInt64(c, "end_ts")
	if err != nil {
		return activitySearchParams{}, err
	}
	if beginTS > endTS {
		return activitySearchParams{}, stderrors.New("begin_ts must be less than or equal to end_ts")
	}

	limit, err := parseActivityInt(c.Query("limit"), defaultActivityLimit, minActivityLimit, maxActivityLimit, "limit")
	if err != nil {
		return activitySearchParams{}, err
	}
	severity, err := parseActivityInt(c.Query("severity"), 0, minActivitySeverity, maxActivitySeverity, "severity")
	if err != nil {
		return activitySearchParams{}, err
	}

	catalog, err := validateActivityTextQuery(c.Query("catalog"), "catalog", maxActivityFilterLen, false)
	if err != nil {
		return activitySearchParams{}, err
	}
	activityType, err := validateActivityTextQuery(c.Query("type"), "type", maxActivityFilterLen, false)
	if err != nil {
		return activitySearchParams{}, err
	}
	name, err := validateActivityTextQuery(c.Query("name"), "name", maxActivityFilterLen, false)
	if err != nil {
		return activitySearchParams{}, err
	}
	cursor, err := validateActivityTextQuery(c.Query("cursor"), "cursor", maxActivityCursorLen, true)
	if err != nil {
		return activitySearchParams{}, err
	}

	return activitySearchParams{
		BeginTS:  beginTS,
		EndTS:    endTS,
		Catalog:  catalog,
		Type:     activityType,
		Name:     name,
		Severity: severity,
		Limit:    limit,
		Cursor:   cursor,
	}, nil
}

func parseRequiredNonNegativeInt64(c *gin.Context, name string) (int64, error) {
	raw, ok := c.GetQuery(name)
	if !ok || raw == "" {
		return 0, fmt.Errorf("%s is required", name)
	}
	value, err := strconv.ParseInt(raw, 10, 64)
	if err != nil || value < 0 {
		return 0, fmt.Errorf("%s must be a non-negative Unix timestamp in seconds", name)
	}
	return value, nil
}

func parseActivityInt(raw string, defaultValue, minValue, maxValue int, name string) (int, error) {
	if raw == "" {
		return defaultValue, nil
	}
	value, err := strconv.Atoi(raw)
	if err != nil || value < minValue || value > maxValue {
		return 0, fmt.Errorf("%s must be between %d and %d", name, minValue, maxValue)
	}
	return value, nil
}

func validateActivityTextQuery(value, name string, maxLength int, rejectControl bool) (string, error) {
	if len(value) > maxLength {
		return "", fmt.Errorf("%s must not exceed %d bytes", name, maxLength)
	}
	if rejectControl {
		for _, r := range value {
			if unicode.IsControl(r) {
				return "", fmt.Errorf("%s must not contain control characters", name)
			}
		}
	}
	return value, nil
}

func (h *ActivityHandler) writeClinicActivityError(c *gin.Context, rec *observability.DataProxyRecord, err error, clusterID string) {
	ctx := c.Request.Context()
	if stderrors.Is(err, context.Canceled) {
		rec.ErrCode = "CLINIC_INTERNAL_CANCELED"
		writeResourceError(c, clientClosedStatus, "Canceled", "activity request was canceled", nil)
		return
	}
	if stderrors.Is(err, context.DeadlineExceeded) {
		rec.ErrCode = "CLINIC_INTERNAL_TIMEOUT"
		writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "activity service request timed out", nil)
		return
	}
	var networkError net.Error
	if stderrors.As(err, &networkError) {
		if networkError.Timeout() {
			rec.ErrCode = "CLINIC_INTERNAL_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "activity service request timed out", nil)
			return
		}
		rec.ErrCode = "CLINIC_INTERNAL_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable", nil)
		return
	}

	var upstreamError *clinic_internal_api.UpstreamError
	if stderrors.As(err, &upstreamError) {
		log.Error(ctx, "Clinic internal activity request failed",
			zap.String("cluster_id", clusterID),
			zap.Int("upstream_status", upstreamError.StatusCode))
		switch {
		case upstreamError.StatusCode == http.StatusBadRequest:
			rec.ErrCode = "INVALID_ACTIVITY_QUERY"
			writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "invalid activity query", nil)
		case upstreamError.StatusCode == http.StatusNotFound:
			rec.ErrCode = "CLUSTER_NOT_FOUND"
			writeResourceError(c, http.StatusNotFound, "NotFound", "cluster not found", nil)
		case upstreamError.StatusCode == http.StatusTooManyRequests:
			rec.ErrCode = "ACTIVITY_RATE_LIMITED"
			writeResourceError(c, http.StatusTooManyRequests, "ResourceExhausted", "activity service rate limit exceeded", nil)
		case upstreamError.StatusCode == http.StatusRequestTimeout,
			upstreamError.StatusCode == http.StatusGatewayTimeout:
			rec.ErrCode = "CLINIC_INTERNAL_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "activity service request timed out", nil)
		case upstreamError.StatusCode == http.StatusServiceUnavailable:
			rec.ErrCode = "ACTIVITY_UNAVAILABLE"
			writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable", nil)
		case upstreamError.StatusCode == http.StatusBadGateway,
			upstreamError.StatusCode >= http.StatusInternalServerError:
			rec.ErrCode = "CLINIC_INTERNAL_ERROR"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "Clinic returned an invalid activity response", nil)
		default:
			rec.ErrCode = "CLINIC_INTERNAL_BAD_RESPONSE"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "Clinic returned an invalid activity response", nil)
		}
		return
	}

	rec.ErrCode = "CLINIC_INTERNAL_BAD_RESPONSE"
	log.Error(ctx, "Clinic internal activity response could not be processed",
		zap.String("cluster_id", clusterID),
		zap.String("error_type", fmt.Sprintf("%T", err)))
	writeResourceError(c, http.StatusBadGateway, "BadGateway", "Clinic returned an invalid activity response", nil)
}

// Keep the qualified APIError import visible to the OpenAPI comment parser.
var _ errors.APIError
