package activities

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/activityhub_client"
	model2 "github.com/pingcap-inc/clinic/pkg/db/model"
	cliniclog "github.com/pingcap-inc/clinic/pkg/log"
)

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

type activitySearcher interface {
	SearchActivities(context.Context, string, activityhub_client.SearchParams) (activityhub_client.ActivitiesResponse, error)
}

type clusterOwnershipChecker func(clusterID string, dbaasOrgID uint64) error

type activityAPIError struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	RequestID string `json:"requestId,omitempty"`
}

func ownsClusterInDBAASOrg(clusterID string, dbaasOrgID uint64) error {
	if uint64(uint(dbaasOrgID)) != dbaasOrgID {
		return gorm.ErrRecordNotFound
	}
	_, err := model2.GetClusterWithDbaasIDs(clusterID, uint(dbaasOrgID))
	return err
}

// QueryInternalActivities is the Clinic-owned Activity API consumed by Data
// Proxy. orgId is a TiDB Cloud/DBAAS org ID rather than Clinic's DB primary key.
func (s *EventManagerService) QueryInternalActivities(c *gin.Context) {
	orgID, err := strconv.ParseUint(c.Param("orgId"), 10, 64)
	if err != nil || orgID == 0 {
		writeActivityAPIError(c, http.StatusBadRequest, "InvalidArgument", "orgId must be a positive TiDB Cloud/DBAAS org ID")
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateActivityClusterID(clusterID); err != nil {
		writeActivityAPIError(c, http.StatusBadRequest, "InvalidArgument", err.Error())
		return
	}
	params, err := parseActivitySearchParams(c, c.Query("cursor"), true)
	if err != nil {
		writeActivityAPIError(c, http.StatusBadRequest, "InvalidArgument", err.Error())
		return
	}

	owns := s.owns
	if owns == nil {
		owns = ownsClusterInDBAASOrg
	}
	if err := owns(clusterID, orgID); err != nil {
		if stderrors.Is(err, gorm.ErrRecordNotFound) {
			writeActivityAPIError(c, http.StatusNotFound, "NotFound", "cluster not found")
			return
		}
		log.Error("check activity cluster ownership failed", zap.String("cluster_id", clusterID), zap.Error(err))
		writeActivityAPIError(c, http.StatusInternalServerError, "InternalError", "failed to verify cluster ownership")
		return
	}

	s.searchAndWriteActivities(c, clusterID, params)
}

func (s *EventManagerService) searchAndWriteActivities(c *gin.Context, targetID string, params activityhub_client.SearchParams) {
	searcher := s.searcher
	if searcher == nil {
		searcher = s.hubClient
	}
	if searcher == nil {
		writeActivityAPIError(c, http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable")
		return
	}

	response, err := searcher.SearchActivities(c.Request.Context(), targetID, params)
	if err != nil {
		status, code, message := classifyActivitySearchError(err)
		fields := []zap.Field{
			zap.Int("status", status),
			zap.String("error_type", fmt.Sprintf("%T", err)),
		}
		var upstreamError *activityhub_client.UpstreamError
		if stderrors.As(err, &upstreamError) {
			fields = append(fields,
				zap.Int("upstream_http_status", upstreamError.StatusCode),
				zap.Bool("upstream_protocol_error", upstreamError.ProtocolError),
				zap.String("upstream_request_id", upstreamError.RequestID),
				zap.String("upstream_aws_request_id", upstreamError.AWSRequestID),
				zap.String("upstream_api_gateway_id", upstreamError.APIGatewayID),
			)
		}
		cliniclog.WithContextL(c.Request.Context()).Error("query activities failed", fields...)
		writeActivityAPIError(c, status, code, message)
		return
	}
	if response.Activities == nil {
		response.Activities = make([]*activityhub_client.Activity, 0)
	}
	c.JSON(http.StatusOK, response)
}

func classifyActivitySearchError(err error) (int, string, string) {
	switch {
	case stderrors.Is(err, context.Canceled):
		return clientClosedStatus, "Canceled", "activity request was canceled"
	case stderrors.Is(err, context.DeadlineExceeded):
		return http.StatusGatewayTimeout, "DeadlineExceeded", "activity service request timed out"
	case stderrors.Is(err, activityhub_client.ErrUnavailable):
		return http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable"
	}

	var networkError net.Error
	if stderrors.As(err, &networkError) {
		if networkError.Timeout() {
			return http.StatusGatewayTimeout, "DeadlineExceeded", "activity service request timed out"
		}
		return http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable"
	}

	var upstreamError *activityhub_client.UpstreamError
	if stderrors.As(err, &upstreamError) {
		switch {
		case upstreamError.StatusCode == http.StatusTooManyRequests:
			return http.StatusTooManyRequests, "ResourceExhausted", "activity service rate limit exceeded"
		case upstreamError.StatusCode == http.StatusRequestTimeout,
			upstreamError.StatusCode == http.StatusGatewayTimeout:
			return http.StatusGatewayTimeout, "DeadlineExceeded", "activity service request timed out"
		case upstreamError.StatusCode == http.StatusUnauthorized,
			upstreamError.StatusCode == http.StatusForbidden,
			upstreamError.StatusCode >= http.StatusInternalServerError:
			return http.StatusServiceUnavailable, "Unavailable", "activity service is temporarily unavailable"
		default:
			return http.StatusBadGateway, "BadGateway", "activity service returned an invalid response"
		}
	}

	return http.StatusBadGateway, "BadGateway", "activity service returned an invalid response"
}

func writeActivityAPIError(c *gin.Context, status int, code, message string) {
	c.JSON(status, activityAPIError{
		Code:      code,
		Message:   message,
		RequestID: c.GetHeader("X-Trace-ID"),
	})
}

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

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

	limit, err := parseBoundedActivityInt(c.Query("limit"), defaultActivityLimit, minActivityLimit, maxActivityLimit, "limit")
	if err != nil {
		return activityhub_client.SearchParams{}, err
	}
	severity, err := parseBoundedActivityInt(c.Query("severity"), 0, minActivitySeverity, maxActivitySeverity, "severity")
	if err != nil {
		return activityhub_client.SearchParams{}, err
	}
	catalog, err := validateActivityText(c.Query("catalog"), "catalog", maxActivityFilterLen, false)
	if err != nil {
		return activityhub_client.SearchParams{}, err
	}
	activityType, err := validateActivityText(c.Query("type"), "type", maxActivityFilterLen, false)
	if err != nil {
		return activityhub_client.SearchParams{}, err
	}
	name, err := validateActivityText(c.Query("name"), "name", maxActivityFilterLen, false)
	if err != nil {
		return activityhub_client.SearchParams{}, err
	}
	cursor, err = validateActivityText(cursor, "cursor", maxActivityCursorLen, true)
	if err != nil {
		return activityhub_client.SearchParams{}, err
	}

	return activityhub_client.SearchParams{
		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 parseBoundedActivityInt(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 validateActivityText(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 validateActivityClusterID(value string) error {
	if value == "" {
		return fmt.Errorf("clusterId is required")
	}
	if len(value) > maxActivityIDLen {
		return fmt.Errorf("clusterId must not exceed %d bytes", maxActivityIDLen)
	}
	if strings.Contains(value, "/") {
		return fmt.Errorf("clusterId must not contain slash characters")
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return fmt.Errorf("clusterId must not contain control characters")
		}
	}
	return nil
}
