package endpoints

import (
	"net/http"
	"sort"
	"strings"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/dataservices"
	"github.com/portainer/portainer/api/http/security"
	"github.com/portainer/portainer/api/internal/endpointutils"
	httperror "github.com/portainer/portainer/pkg/libhttp/error"
	"github.com/portainer/portainer/pkg/libhttp/response"
	"github.com/rs/zerolog/log"

	"golang.org/x/mod/semver"
)

type groupCount struct {
	GroupID   int    `json:"groupID"`
	GroupName string `json:"groupName"`
	Count     int    `json:"count"`
}

type platformCounts struct {
	Docker     int `json:"docker"`
	Kubernetes int `json:"kubernetes"`
	Azure      int `json:"azure"`
	Podman     int `json:"podman"`
}

type healthCounts struct {
	Down      int `json:"down"`
	Outdated  int `json:"outdated"`
	Up        int `json:"up"`
	Heartbeat int `json:"heartbeat"`
}

type EnvironmentSummaryCountsResponse struct {
	Total          int            `json:"total"`
	Up             int            `json:"up"`
	Down           int            `json:"down"`
	Outdated       int            `json:"outdated"`
	Unassigned     int            `json:"unassigned"`
	ByGroup        []groupCount   `json:"byGroup"`
	ByPlatformType platformCounts `json:"byPlatformType"`
	ByHealth       healthCounts   `json:"byHealth"`
}

const UnassignedGroupID = portainer.EndpointGroupID(1)

// @id EndpointSummaryCounts
// @summary Get environment summary counts
// @description Returns counts of environments by status (up, down) and ungrouped environments (unassigned), plus breakdowns by group, type, and health.
// @description **Access policy**: restricted
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {object} EnvironmentSummaryCountsResponse "Environment summary counts"
// @failure 500 "Server error"
// @router /endpoints/summary [get]
func (handler *Handler) endpointSummaryCounts(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
	var endpointGroups []portainer.EndpointGroup
	var endpoints []portainer.Endpoint
	var settings *portainer.Settings

	if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
		var err error

		endpointGroups, err = tx.EndpointGroup().ReadAll()
		if err != nil {
			return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
		}

		endpoints, err = tx.Endpoint().Endpoints()
		if err != nil {
			return httperror.InternalServerError("Unable to retrieve environments from the database", err)
		}

		settings, err = tx.Settings().Settings()
		if err != nil {
			return httperror.InternalServerError("Unable to retrieve settings from the database", err)
		}

		return nil
	}); err != nil {
		return response.TxErrorResponse(err)
	}

	// Refresh LastCheckInDate from the in-memory heartbeats map. Edge agents
	// use ETag-based response caching: cache hits update the map but do not
	// write LastCheckInDate back to the database, so the tx value grows stale.
	// The tx path cannot access the in-memory map; this non-tx access is
	// intentional.
	endpointSvc := handler.DataStore.Endpoint()
	for i := range endpoints {
		if t, ok := endpointSvc.Heartbeat(endpoints[i].ID); ok {
			endpoints[i].LastCheckInDate = t
		}
	}

	securityContext, err := security.RetrieveRestrictedRequestContext(r)
	if err != nil {
		return httperror.InternalServerError("Unable to retrieve info from request context", err)
	}

	filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)

	trustedEndpoints := make([]portainer.Endpoint, 0, len(filteredEndpoints))
	for i := range filteredEndpoints {
		ep := &filteredEndpoints[i]
		if endpointutils.IsEdgeEndpoint(ep) && !ep.UserTrusted {
			continue
		}
		trustedEndpoints = append(trustedEndpoints, filteredEndpoints[i])
	}

	counts := EnvironmentSummaryCountsResponse{
		Total: len(trustedEndpoints),
	}

	groupCounts := make(map[portainer.EndpointGroupID]int)
	platformCounts := platformCounts{}
	healthCounts := healthCounts{}

	for i := range trustedEndpoints {
		endpoint := &trustedEndpoints[i]

		switch endpointutils.EndpointPlatformType(endpoint) {
		case portainer.DockerPlatformType:
			platformCounts.Docker++
		case portainer.KubernetesPlatformType:
			platformCounts.Kubernetes++
		case portainer.AzurePlatformType:
			platformCounts.Azure++
		case portainer.PodmanPlatformType:
			platformCounts.Podman++
		case portainer.UnknownPlatformType:
			log.Error().Int("endpoint_id", int(endpoint.ID)).Msg("Unknown platform type")
		}

		groupCounts[endpoint.GroupID]++

		if endpoint.GroupID == UnassignedGroupID {
			counts.Unassigned++
		}

		outdated := isOutdated(endpoint)
		status := resolveEndpointStatus(endpoint, settings)

		if outdated {
			counts.Outdated++
			healthCounts.Outdated++
		}

		switch status {
		case statusHeartbeat:
			healthCounts.Heartbeat++
			healthCounts.Up++
			counts.Up++
		case statusUp:
			healthCounts.Up++
			counts.Up++
		case statusDown:
			healthCounts.Down++
			counts.Down++
		}
	}

	counts.ByGroup = parseGroupCounts(groupCounts, endpointGroups)
	counts.ByPlatformType = platformCounts
	counts.ByHealth = healthCounts

	return response.JSON(w, counts)
}

// iota order overlaps with portainer.EndpointStatus (Up=1, Down=2) so non-edge
// endpoints can pass their Status straight through. statusHeartbeat (0) is
// edge-only.
const (
	statusHeartbeat = iota
	statusUp
	statusDown
)

func resolveEndpointStatus(endpoint *portainer.Endpoint, settings *portainer.Settings) int {
	if endpointutils.IsEdgeEndpoint(endpoint) {
		if endpointutils.GetHeartbeatStatus(endpoint, settings) {
			return statusHeartbeat
		}
		return statusDown
	}
	return int(endpoint.Status)
}

func parseGroupCounts(counts map[portainer.EndpointGroupID]int, endpointGroups []portainer.EndpointGroup) []groupCount {
	parsedGroupCounts := []groupCount{}

	// Build group name lookup
	groupNameByID := make(map[portainer.EndpointGroupID]string, len(endpointGroups))
	for _, g := range endpointGroups {
		groupNameByID[g.ID] = g.Name
	}

	for groupID, count := range counts {

		parsedGroupCounts = append(parsedGroupCounts,
			groupCount{
				GroupID:   int(groupID),
				GroupName: groupNameByID[groupID],
				Count:     count,
			})
	}

	sort.Slice(parsedGroupCounts, func(i, j int) bool {
		return parsedGroupCounts[i].GroupID < parsedGroupCounts[j].GroupID
	})

	return parsedGroupCounts
}

// canonicalizeSemver ensures v has a "v" prefix as required by golang.org/x/mod/semver.
func canonicalizeSemver(v string) string {
	v = strings.TrimSpace(v)
	if v == "" || strings.HasPrefix(v, "v") {
		return v
	}
	return "v" + v
}

func isOutdated(endpoint *portainer.Endpoint) bool {
	if !endpointutils.IsAgentEndpoint(endpoint) {
		return false
	}

	if endpoint.Agent.Version == "" {
		edgeHasCheckedInWithoutVersion := endpointutils.IsEdgeEndpoint(endpoint) && endpoint.LastCheckInDate > 0
		return edgeHasCheckedInWithoutVersion
	}

	latestVersion := canonicalizeSemver(portainer.APIVersion)
	agentVersion := canonicalizeSemver(endpoint.Agent.Version)

	return semver.Compare(agentVersion, latestVersion) < 0
}
