package k8sresource

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/duration"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const eventListPageLimit int64 = 200

// EventView is a describe-style event summary
// sigs.k8s.io/yaml uses json struct tags for YAML field names; yaml tags are kept for readability.
type EventView struct {
	Type    string `json:"type,omitempty" yaml:"type,omitempty"`
	Reason  string `json:"reason,omitempty" yaml:"reason,omitempty"`
	Age     string `json:"age,omitempty" yaml:"age,omitempty"`
	From    string `json:"from,omitempty" yaml:"from,omitempty"`
	Message string `json:"message,omitempty" yaml:"message,omitempty"`
}

type involvedObjectEventQuery struct {
	Kind string
	Name string
	UID  string
}

type resourceWithEvents struct {
	Resource *unstructured.Unstructured `json:"resource" yaml:"resource"`
	Events   []EventView                `json:"events,omitempty" yaml:"events,omitempty"`
}

// attachEventsToResources queries events per filtered item and assembles wrappers.
// It does not sanitize resources; callers run SanitizeResourcesWithEventsForDetail separately.
func attachEventsToResources(
	ctx context.Context,
	reader client.Reader,
	gvk schema.GroupVersionKind,
	items []unstructured.Unstructured,
) ([]resourceWithEvents, int) {
	out := make([]resourceWithEvents, 0, len(items))
	failed := 0
	for i := range items {
		item := items[i].DeepCopy()
		events, err := listEventViewsByInvolvedObject(ctx, reader, involvedObjectEventQuery{
			Kind: gvk.Kind,
			Name: item.GetName(),
			UID:  string(item.GetUID()),
		})
		if err != nil {
			failed++
			events = nil
		}
		out = append(out, resourceWithEvents{
			Resource: item,
			Events:   events,
		})
	}
	return out, failed
}

func appendEventFetchHints(existing string, failed, total int) string {
	if failed == 0 || total == 0 {
		return existing
	}
	eventHint := fmt.Sprintf("events unavailable for %d/%d resources", failed, total)
	if existing == "" {
		return eventHint
	}
	return existing + "; " + eventHint
}

func listEventViewsByInvolvedObject(ctx context.Context, reader client.Reader, query involvedObjectEventQuery) ([]EventView, error) {
	events, err := listEventsByInvolvedObject(ctx, reader, query)
	if err != nil {
		return nil, err
	}

	views := make([]EventView, 0, len(events))
	for _, event := range events {
		views = append(views, convertEventToView(event))
	}
	return views, nil
}

func listEventsByInvolvedObject(ctx context.Context, reader client.Reader, query involvedObjectEventQuery) ([]corev1.Event, error) {
	if reader == nil {
		return nil, fmt.Errorf("api reader is required")
	}
	if query.Kind == "" {
		return nil, fmt.Errorf("involved object kind is required")
	}

	if query.UID != "" {
		events, err := listEventsByFieldSelector(ctx, reader, fields.AndSelectors(
			fields.OneTermEqualSelector("involvedObject.kind", query.Kind),
			fields.OneTermEqualSelector("involvedObject.uid", query.UID),
		))
		if err != nil {
			return nil, err
		}
		if len(events) > 0 || query.Name == "" {
			sortEvents(events)
			return events, nil
		}
	}

	if query.Name == "" {
		return nil, nil
	}

	events, err := listEventsByFieldSelector(ctx, reader, fields.AndSelectors(
		fields.OneTermEqualSelector("involvedObject.kind", query.Kind),
		fields.OneTermEqualSelector("involvedObject.name", query.Name),
	))
	if err != nil {
		return nil, err
	}
	sortEvents(events)
	return events, nil
}

func listEventsByFieldSelector(ctx context.Context, reader client.Reader, fieldSelector fields.Selector) ([]corev1.Event, error) {
	opts := &client.ListOptions{
		FieldSelector: fieldSelector,
		Limit:         eventListPageLimit,
	}

	events := make([]corev1.Event, 0)
	for {
		var eventList corev1.EventList
		if err := reader.List(ctx, &eventList, opts); err != nil {
			return nil, err
		}
		events = append(events, eventList.Items...)
		if eventList.Continue == "" {
			break
		}
		opts.Continue = eventList.Continue
	}
	return events, nil
}

func convertEventToView(event corev1.Event) EventView {
	return EventView{
		Type:    event.Type,
		Reason:  event.Reason,
		Age:     formatEventAge(event),
		From:    eventSource(event),
		Message: strings.TrimSpace(event.Message),
	}
}

func sortEvents(events []corev1.Event) {
	sort.SliceStable(events, func(i, j int) bool {
		left := eventSortTime(events[i])
		right := eventSortTime(events[j])
		if left.Equal(right) {
			if events[i].Reason == events[j].Reason {
				return events[i].Message < events[j].Message
			}
			return events[i].Reason < events[j].Reason
		}
		return left.Before(right)
	})
}

func eventSortTime(event corev1.Event) time.Time {
	switch {
	case event.Series != nil && !event.Series.LastObservedTime.IsZero():
		return event.Series.LastObservedTime.Time
	case !event.LastTimestamp.IsZero():
		return event.LastTimestamp.Time
	case !event.EventTime.IsZero():
		return event.EventTime.Time
	case !event.FirstTimestamp.IsZero():
		return event.FirstTimestamp.Time
	default:
		return time.Time{}
	}
}

func formatEventAge(event corev1.Event) string {
	firstTimestampSince := translateMicroTimestampSince(event.EventTime)
	if event.EventTime.IsZero() {
		firstTimestampSince = translateTimestampSince(event.FirstTimestamp)
	}

	switch {
	case event.Series != nil:
		return fmt.Sprintf("%s (x%d over %s)",
			translateMicroTimestampSince(event.Series.LastObservedTime),
			event.Series.Count,
			firstTimestampSince,
		)
	case event.Count > 1:
		return fmt.Sprintf("%s (x%d over %s)",
			translateTimestampSince(event.LastTimestamp),
			event.Count,
			firstTimestampSince,
		)
	default:
		return firstTimestampSince
	}
}

func eventSource(event corev1.Event) string {
	if event.Source.Component != "" {
		return event.Source.Component
	}
	return event.ReportingController
}

func translateMicroTimestampSince(timestamp metav1.MicroTime) string {
	if timestamp.IsZero() {
		return "<unknown>"
	}
	return duration.HumanDuration(time.Since(timestamp.Time))
}

func translateTimestampSince(timestamp metav1.Time) string {
	if timestamp.IsZero() {
		return "<unknown>"
	}
	return duration.HumanDuration(time.Since(timestamp.Time))
}
