// Copyright (c) 2026 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

// Package runtime implements connectors to various runtimes.
package runtime

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

	cosiresource "github.com/cosi-project/runtime/pkg/resource"
	"github.com/cosi-project/runtime/pkg/resource/typed"
	"github.com/siderolabs/gen/xslices"

	"github.com/siderolabs/omni/client/api/omni/resources"
	"github.com/siderolabs/omni/client/pkg/runtime"
)

// Runtime is an abstraction for the data access.
type Runtime interface {
	Watch(context.Context, chan<- WatchResponse, ...QueryOption) error
	Get(context.Context, ...QueryOption) (any, error)
	List(context.Context, ...QueryOption) (ListResult, error)
	Create(context.Context, cosiresource.Resource, ...QueryOption) error
	Update(context.Context, cosiresource.Resource, ...QueryOption) error
	Delete(context.Context, ...QueryOption) error
}

// WatchResponse is a wrapper for the resources.WatchResponse.
type WatchResponse interface {
	ID() string
	Namespace() string
	Field(name string) (string, bool)
	Match(searchFor string) bool
	Unwrap() *resources.WatchResponse
}

// EventType returns event type for the response.
func EventType(resp WatchResponse) resources.EventType { return resp.Unwrap().Event.EventType }

// ListResult is a wrapper for the list result.
type ListResult struct {
	Items []runtime.ListItem
	Total int
}

// ListComparator is a comparator for the list items.
type ListComparator func(a, b runtime.ListItem) (int, error)

// SortInPlace sorts the list result.
func (l *ListResult) SortInPlace(cmp ListComparator) error { return unsafeSort(l.Items, cmp) }

// Slice returns a slice of the list result.
func (l *ListResult) Slice(offset, count int) ListResult {
	if offset >= len(l.Items) {
		return ListResult{
			Total: l.Total,
		}
	}

	items := l.Items[offset:]

	if count > 0 && count < len(items) {
		items = items[:count]
	}

	return ListResult{
		Items: items,
		Total: l.Total,
	}
}

// Filter filters Items using the provided predicate.
func (l *ListResult) Filter(match func(m runtime.ListItem) bool) ListResult {
	result := xslices.Filter(l.Items, match)

	return ListResult{
		Items: result,
		Total: len(result),
	}
}

// NewProxyRuntime wraps a Runtime with sorting, filtering, and pagination support.
func NewProxyRuntime(r Runtime) Runtime { //nolint:ireturn
	return &proxyRuntime{r}
}

// NewBasicResponse creates a new basic response.
func NewBasicResponse(id string, namespace string, resp *resources.WatchResponse) BasicResponse {
	mustNotNil(resp, "nil response")
	mustNotNil(resp.Event, "nil response event")

	return BasicResponse{
		BasicItem: BasicItem[*resources.WatchResponse]{
			id: id,
			ns: namespace,
			v:  resp,
		},
	}
}

// BasicResponse is a basic implementation of the WatchResponse.
type BasicResponse struct {
	BasicItem[*resources.WatchResponse]
}

// Field implements WatchResponse. name can be "id", "namespace" or "event_type".
// If name is empty, it returns ID. If name is unknown, it returns false.
func (b *BasicResponse) Field(name string) (string, bool) {
	field, ok := b.BasicItem.Field(name)
	if ok {
		return field, true
	}

	if name == "event_type" {
		return string(EventType(b)), true
	}

	return "", false
}

// Match looks for a specific string in item data.
func (b *BasicResponse) Match(searchFor string) bool {
	return EventType(b) == resources.EventType_BOOTSTRAPPED || b.BasicItem.Match(searchFor)
}

// String implements fmt.Stringer.
func (b *BasicResponse) String() string {
	return fmt.Sprintf("{id=%q, namespace=%q, event_type=%q, total=%d}", b.id, b.ns, EventType(b), b.Unwrap().GetTotal())
}

// MakeBasicItem creates a new basic item.
func MakeBasicItem[T any](id string, ns string, v T) BasicItem[T] {
	return BasicItem[T]{id: id, ns: ns, v: v}
}

// BasicItem is a basic building block for the WatchResponse and ListItem.
type BasicItem[T any] struct {
	v  T
	id string
	ns string
}

// ID implements WatchResponse.
func (bi BasicItem[T]) ID() string { return bi.id }

// Namespace implements WatchResponse.
func (bi BasicItem[T]) Namespace() string { return bi.ns }

// Unwrap implements WatchResponse.
func (bi BasicItem[T]) Unwrap() T { return bi.v }

// Field implements WatchResponse. name can be "id", "namespace" or "event_type".
// If name is empty, it returns ID. If name is unknown, it returns false.
func (bi BasicItem[T]) Field(name string) (string, bool) {
	switch name {
	case "id", "":
		return bi.id, true
	case "namespace":
		return bi.ns, true
	default:
		return "", false
	}
}

// Match looks for a specific string in item data.
func (bi BasicItem[T]) Match(searchFor string) bool {
	return strings.Contains(bi.ID(), searchFor) ||
		strings.Contains(bi.Namespace(), searchFor)
}

// String implements fmt.Stringer.
func (bi BasicItem[T]) String() string {
	return fmt.Sprintf("{id=%q, namespace=%q}", bi.id, bi.ns)
}

func mustNotNil[T any](v *T, msg string) {
	if v == nil {
		panic(msg)
	}
}

// ResourceField returns a metadata field value.
func ResourceField(res cosiresource.Resource, name string) (string, bool) {
	if res == nil {
		return "", false
	}

	switch name {
	case "created":
		return timeToString(res.Metadata().Created()), true
	case "updated":
		return timeToString(res.Metadata().Updated()), true
	}

	fielder, ok := typed.LookupExtension[runtime.Fielder](res)
	if !ok {
		return "", false
	}

	return fielder.Field(name)
}

func timeToString(t time.Time) string {
	return t.UTC().Format("2006-01-02 15:04:05.000000000")
}

// MatchResource returns true if searchFor is in any field of metadata.
func MatchResource(res cosiresource.Resource, searchFor string) bool {
	if res == nil {
		return false
	}

	if strings.Contains(timeToString(res.Metadata().Created()), searchFor) ||
		strings.Contains(timeToString(res.Metadata().Updated()), searchFor) {
		return true
	}

	matcher, ok := typed.LookupExtension[runtime.Matcher](res)
	if !ok {
		return false
	}

	return matcher.Match(searchFor)
}
