package dockeragent

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	swarmtypes "github.com/docker/docker/api/types/swarm"
	systemtypes "github.com/docker/docker/api/types/system"
	agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
)

const (
	swarmScopeAuto    = "auto"
	swarmScopeNode    = "node"
	swarmScopeCluster = "cluster"
)

func normalizeSwarmScope(value string) (string, error) {
	scope := strings.ToLower(strings.TrimSpace(value))
	if scope == "" {
		return swarmScopeNode, nil
	}

	switch scope {
	case swarmScopeNode, swarmScopeCluster, swarmScopeAuto:
		return scope, nil
	default:
		return "", fmt.Errorf("invalid swarm scope %q: must be node, cluster, or auto", value)
	}
}

func (a *Agent) resolvedSwarmScope(info systemtypes.Info) string {
	switch a.cfg.SwarmScope {
	case swarmScopeAuto:
		if info.Swarm.ControlAvailable {
			return swarmScopeCluster
		}
		return swarmScopeNode
	case swarmScopeCluster, swarmScopeNode:
		return a.cfg.SwarmScope
	default:
		return swarmScopeNode
	}
}

func (a *Agent) collectSwarmData(ctx context.Context, info systemtypes.Info, containers []agentsdocker.Container) ([]agentsdocker.Service, []agentsdocker.Task, *agentsdocker.SwarmInfo) {
	if !a.supportsSwarm {
		return nil, nil, nil
	}

	if info.Swarm.NodeID == "" && string(info.Swarm.LocalNodeState) == "" && strings.TrimSpace(info.Swarm.Error) == "" {
		return nil, nil, nil
	}

	scope := a.resolvedSwarmScope(info)
	effectiveScope := scope

	nodeRole := "worker"
	if info.Swarm.ControlAvailable {
		nodeRole = "manager"
	}

	swarmInfo := &agentsdocker.SwarmInfo{
		NodeID:           info.Swarm.NodeID,
		NodeRole:         nodeRole,
		LocalState:       string(info.Swarm.LocalNodeState),
		ControlAvailable: info.Swarm.ControlAvailable,
		Error:            strings.TrimSpace(info.Swarm.Error),
		Scope:            scope,
	}

	if info.Swarm.Cluster != nil {
		swarmInfo.ClusterID = info.Swarm.Cluster.ID
		swarmInfo.ClusterName = info.Swarm.Cluster.Spec.Annotations.Name
	}

	includeServices := a.cfg.IncludeServices
	includeTasks := a.cfg.IncludeTasks

	if info.Swarm.LocalNodeState != swarmtypes.LocalNodeStateActive {
		if !includeServices {
			includeServices = false
		}
		if !includeTasks {
			includeTasks = false
		}
		return nil, nil, swarmInfo
	}

	var services []agentsdocker.Service
	var tasks []agentsdocker.Task

	containerIndex := buildContainerIndex(containers)

	if info.Swarm.ControlAvailable && (includeServices || includeTasks) {
		managerServices, managerTasks, err := a.collectSwarmDataFromManager(ctx, info, scope, containerIndex, includeServices, includeTasks)
		if err != nil {
			a.logger.Warn().Err(err).Msg("Failed to collect swarm data from manager; falling back to local inference")
		} else {
			if includeServices {
				services = managerServices
			}
			if includeTasks {
				tasks = managerTasks
			}
		}
	}

	if includeTasks && len(tasks) == 0 {
		tasks = deriveSwarmTasksFromContainers(containers, info)
		if len(tasks) > 0 {
			effectiveScope = swarmScopeNode
		}
	}

	if includeServices && len(services) == 0 {
		services = deriveSwarmServicesFromData(tasks, containers)
		if len(services) > 0 {
			effectiveScope = swarmScopeNode
		}
	}

	if includeTasks && len(tasks) > 0 {
		sort.Slice(tasks, func(i, j int) bool {
			if tasks[i].ServiceName == tasks[j].ServiceName {
				if tasks[i].Slot == tasks[j].Slot {
					return tasks[i].ID < tasks[j].ID
				}
				return tasks[i].Slot < tasks[j].Slot
			}
			return tasks[i].ServiceName < tasks[j].ServiceName
		})
	}

	if includeServices && len(services) > 0 {
		sort.Slice(services, func(i, j int) bool {
			if services[i].Name == services[j].Name {
				return services[i].ID < services[j].ID
			}
			return services[i].Name < services[j].Name
		})
	}

	swarmInfo.Scope = effectiveScope

	if !includeServices {
		services = nil
	}
	if !includeTasks {
		tasks = nil
	}

	return services, tasks, swarmInfo
}

func (a *Agent) collectSwarmDataFromManager(ctx context.Context, info systemtypes.Info, scope string, containers map[string]agentsdocker.Container, includeServices, includeTasks bool) ([]agentsdocker.Service, []agentsdocker.Task, error) {
	serviceList, err := a.docker.ServiceList(ctx, types.ServiceListOptions{Status: true})
	if err != nil {
		return nil, nil, err
	}

	servicePointers := make(map[string]*swarmtypes.Service, len(serviceList))
	for i := range serviceList {
		servicePointers[serviceList[i].ID] = &serviceList[i]
	}

	var services []agentsdocker.Service
	if includeServices {
		services = make([]agentsdocker.Service, 0, len(serviceList))
		for i := range serviceList {
			services = append(services, mapSwarmService(&serviceList[i]))
		}
	}

	var tasks []agentsdocker.Task
	if includeTasks {
		taskFilters := filters.NewArgs()
		if scope != swarmScopeCluster && info.Swarm.NodeID != "" {
			taskFilters.Add("node", info.Swarm.NodeID)
		}

		taskList, err := a.docker.TaskList(ctx, types.TaskListOptions{Filters: taskFilters})
		if err != nil {
			return services, nil, err
		}

		tasks = make([]agentsdocker.Task, 0, len(taskList))
		for i := range taskList {
			var svc *swarmtypes.Service
			if ptr, ok := servicePointers[taskList[i].ServiceID]; ok {
				svc = ptr
			}
			tasks = append(tasks, mapSwarmTask(&taskList[i], svc, containers))
		}

		if scope == swarmScopeNode && includeServices && len(services) > 0 {
			used := make(map[string]struct{}, len(tasks))
			for _, task := range tasks {
				if task.ServiceID != "" {
					used[task.ServiceID] = struct{}{}
				}
			}

			filtered := services[:0]
			for _, svc := range services {
				if _, ok := used[svc.ID]; ok || len(used) == 0 {
					filtered = append(filtered, svc)
				}
			}
			services = filtered
		}
	}

	return services, tasks, nil
}

func mapSwarmService(svc *swarmtypes.Service) agentsdocker.Service {
	service := agentsdocker.Service{
		ID:   svc.ID,
		Name: svc.Spec.Annotations.Name,
		Mode: serviceMode(svc.Spec.Mode),
	}

	if svc.Spec.TaskTemplate.ContainerSpec != nil {
		service.Image = svc.Spec.TaskTemplate.ContainerSpec.Image
	}

	if svc.Spec.Annotations.Labels != nil {
		service.Labels = copyStringMap(svc.Spec.Annotations.Labels)
		if stack, ok := svc.Spec.Annotations.Labels["com.docker.stack.namespace"]; ok {
			service.Stack = stack
		}
	}

	if svc.ServiceStatus != nil {
		service.DesiredTasks = int(svc.ServiceStatus.DesiredTasks)
		service.RunningTasks = int(svc.ServiceStatus.RunningTasks)
		service.CompletedTasks = int(svc.ServiceStatus.CompletedTasks)
	}

	if svc.UpdateStatus != nil {
		update := &agentsdocker.ServiceUpdate{
			State:   string(svc.UpdateStatus.State),
			Message: svc.UpdateStatus.Message,
		}
		if svc.UpdateStatus.CompletedAt != nil {
			completed := *svc.UpdateStatus.CompletedAt
			if !completed.IsZero() {
				update.CompletedAt = &completed
			}
		}
		service.UpdateStatus = update
	}

	if len(svc.Endpoint.Ports) > 0 {
		service.EndpointPorts = make([]agentsdocker.ServicePort, len(svc.Endpoint.Ports))
		for i, port := range svc.Endpoint.Ports {
			service.EndpointPorts[i] = agentsdocker.ServicePort{
				Name:          port.Name,
				Protocol:      string(port.Protocol),
				TargetPort:    port.TargetPort,
				PublishedPort: port.PublishedPort,
				PublishMode:   string(port.PublishMode),
			}
		}
	}

	if !svc.Meta.CreatedAt.IsZero() {
		created := svc.Meta.CreatedAt
		service.CreatedAt = &created
	}
	if !svc.Meta.UpdatedAt.IsZero() {
		updated := svc.Meta.UpdatedAt
		service.UpdatedAt = &updated
	}

	return service
}

func mapSwarmTask(task *swarmtypes.Task, svc *swarmtypes.Service, containers map[string]agentsdocker.Container) agentsdocker.Task {
	result := agentsdocker.Task{
		ID:           task.ID,
		ServiceID:    task.ServiceID,
		Slot:         task.Slot,
		NodeID:       task.NodeID,
		DesiredState: string(task.DesiredState),
		CurrentState: string(task.Status.State),
		Error:        task.Status.Err,
		Message:      task.Status.Message,
		CreatedAt:    task.Meta.CreatedAt,
	}

	if svc != nil {
		result.ServiceName = svc.Spec.Annotations.Name
	}

	if !task.Meta.UpdatedAt.IsZero() {
		updated := task.Meta.UpdatedAt
		result.UpdatedAt = &updated
	}

	if ts := task.Status.Timestamp; !ts.IsZero() {
		if task.Status.State == swarmtypes.TaskStateRunning {
			started := ts
			result.StartedAt = &started
		} else if isTaskCompletedState(string(task.Status.State)) {
			completed := ts
			result.CompletedAt = &completed
		}
	}

	if cs := task.Status.ContainerStatus; cs != nil {
		if cs.ContainerID != "" {
			result.ContainerID = cs.ContainerID
			if container, ok := lookupContainer(containers, cs.ContainerID); ok {
				result.ContainerID = container.ID
				result.ContainerName = container.Name
				if container.StartedAt != nil && !container.StartedAt.IsZero() && result.StartedAt == nil {
					started := *container.StartedAt
					result.StartedAt = &started
				}
				if container.FinishedAt != nil && !container.FinishedAt.IsZero() && result.CompletedAt == nil {
					finished := *container.FinishedAt
					result.CompletedAt = &finished
				}
			}
		}
	}

	return result
}

func serviceMode(mode swarmtypes.ServiceMode) string {
	switch {
	case mode.Global != nil:
		return "global"
	case mode.ReplicatedJob != nil:
		return "replicated-job"
	case mode.GlobalJob != nil:
		return "global-job"
	case mode.Replicated != nil:
		return "replicated"
	default:
		return ""
	}
}

func buildContainerIndex(containers []agentsdocker.Container) map[string]agentsdocker.Container {
	if len(containers) == 0 {
		return nil
	}

	index := make(map[string]agentsdocker.Container, len(containers)*2)
	for _, c := range containers {
		index[c.ID] = c
		if len(c.ID) >= 12 {
			index[c.ID[:12]] = c
		}
	}
	return index
}

func lookupContainer(index map[string]agentsdocker.Container, id string) (agentsdocker.Container, bool) {
	if len(index) == 0 {
		return agentsdocker.Container{}, false
	}

	if container, ok := index[id]; ok {
		return container, true
	}
	if len(id) > 12 {
		if container, ok := index[id[:12]]; ok {
			return container, true
		}
	}
	return agentsdocker.Container{}, false
}

func deriveSwarmTasksFromContainers(containers []agentsdocker.Container, info systemtypes.Info) []agentsdocker.Task {
	if len(containers) == 0 {
		return nil
	}

	tasks := make([]agentsdocker.Task, 0, len(containers))

	for _, container := range containers {
		if len(container.Labels) == 0 {
			continue
		}

		serviceID := container.Labels["com.docker.swarm.service.id"]
		serviceName := container.Labels["com.docker.swarm.service.name"]
		if serviceID == "" && serviceName == "" {
			continue
		}

		taskID := container.Labels["com.docker.swarm.task.id"]
		if taskID == "" {
			taskID = container.ID
		}

		task := agentsdocker.Task{
			ID:            taskID,
			ServiceID:     serviceID,
			ServiceName:   serviceName,
			ContainerID:   container.ID,
			ContainerName: container.Name,
			NodeID:        container.Labels["com.docker.swarm.node.id"],
			NodeName:      container.Labels["com.docker.swarm.node.name"],
			DesiredState:  container.Labels["com.docker.swarm.task.desired-state"],
			CurrentState:  strings.ToLower(strings.TrimSpace(container.State)),
			CreatedAt:     container.CreatedAt,
		}

		if task.NodeID == "" {
			task.NodeID = info.Swarm.NodeID
		}

		if slotStr := container.Labels["com.docker.swarm.task.slot"]; slotStr != "" {
			if slot, err := strconv.Atoi(slotStr); err == nil {
				task.Slot = slot
			}
		}

		if msg := container.Labels["com.docker.swarm.task.message"]; msg != "" {
			task.Message = msg
		}
		if errMsg := container.Labels["com.docker.swarm.task.error"]; errMsg != "" {
			task.Error = errMsg
		}

		if container.StartedAt != nil && !container.StartedAt.IsZero() {
			started := *container.StartedAt
			task.StartedAt = &started
		}
		if container.FinishedAt != nil && !container.FinishedAt.IsZero() {
			finished := *container.FinishedAt
			task.CompletedAt = &finished
		}

		tasks = append(tasks, task)
	}

	return tasks
}

func deriveSwarmServicesFromData(tasks []agentsdocker.Task, containers []agentsdocker.Container) []agentsdocker.Service {
	if len(tasks) == 0 {
		return nil
	}

	type aggregate struct {
		service   agentsdocker.Service
		total     int
		running   int
		completed int
	}

	aggregates := make(map[string]*aggregate)
	for _, task := range tasks {
		key := task.ServiceID
		if key == "" {
			key = task.ServiceName
		}
		if key == "" {
			continue
		}

		agg, ok := aggregates[key]
		if !ok {
			serviceID := task.ServiceID
			if serviceID == "" {
				serviceID = key
			}
			agg = &aggregate{
				service: agentsdocker.Service{
					ID:   serviceID,
					Name: task.ServiceName,
				},
			}
			aggregates[key] = agg
		}

		if task.ServiceName != "" {
			agg.service.Name = task.ServiceName
		}

		agg.total++
		if strings.EqualFold(task.CurrentState, "running") {
			agg.running++
		}
		if isTaskCompletedState(task.CurrentState) {
			agg.completed++
		}
	}

	if len(aggregates) == 0 {
		return nil
	}

	for _, container := range containers {
		if len(container.Labels) == 0 {
			continue
		}
		key := container.Labels["com.docker.swarm.service.id"]
		if key == "" {
			key = container.Labels["com.docker.swarm.service.name"]
		}
		if key == "" {
			continue
		}
		agg, ok := aggregates[key]
		if !ok {
			continue
		}
		if agg.service.Image == "" {
			agg.service.Image = container.Image
		}
		if stack := container.Labels["com.docker.stack.namespace"]; stack != "" {
			if agg.service.Stack == "" {
				agg.service.Stack = stack
			}
			if agg.service.Labels == nil {
				agg.service.Labels = map[string]string{}
			}
			agg.service.Labels["com.docker.stack.namespace"] = stack
		}
	}

	services := make([]agentsdocker.Service, 0, len(aggregates))
	for _, agg := range aggregates {
		agg.service.DesiredTasks = agg.total
		agg.service.RunningTasks = agg.running
		agg.service.CompletedTasks = agg.completed
		if len(agg.service.Labels) == 0 {
			agg.service.Labels = nil
		}
		services = append(services, agg.service)
	}

	return services
}

func copyStringMap(source map[string]string) map[string]string {
	if len(source) == 0 {
		return nil
	}
	result := make(map[string]string, len(source))
	for k, v := range source {
		result[k] = v
	}
	return result
}

func isTaskCompletedState(state string) bool {
	switch strings.ToLower(state) {
	case "completed", "complete", "shutdown", "failed", "rejected":
		return true
	default:
		return false
	}
}
