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

package runtime

import (
	"encoding/json"
	"strconv"
	"strings"
	"time"

	"github.com/cosi-project/runtime/pkg/resource"
	"github.com/siderolabs/talos/pkg/machinery/api/common"
	"go.yaml.in/yaml/v4"
	"google.golang.org/protobuf/proto"
)

type protobufWrapper interface {
	GetValue() proto.Message
}

// Version implements a special uint64 type which can be unmarshalled from COSI version that can be string if it's undefined.
type Version uint64

// UnmarshalYAML implements yaml.Unmarshaler.
func (v *Version) UnmarshalYAML(value *yaml.Node) error {
	if value.Value == resource.VersionUndefined.String() {
		return nil
	}

	val, err := strconv.ParseUint(value.Value, 10, 64)
	if err != nil {
		return err
	}

	*v = Version(val)

	return nil
}

// Metadata wraps COSI or Talos metadata into a json serializable object.
type Metadata struct {
	Created     time.Time           `json:"created" yaml:"created"`
	Updated     time.Time           `json:"updated" yaml:"updated"`
	Namespace   resource.Namespace  `json:"namespace" yaml:"namespace"`
	Type        resource.Type       `json:"type" yaml:"type"`
	ID          resource.ID         `json:"id" yaml:"id"`
	Owner       resource.Owner      `json:"owner" yaml:"owner"`
	Phase       string              `json:"phase" yaml:"phase"`
	Node        string              `json:"node,omitempty" yaml:"node"`
	Labels      map[string]string   `json:"labels,omitempty" yaml:"labels,omitempty"`
	Annotations map[string]string   `json:"annotations,omitempty" yaml:"annotations,omitempty"`
	Finalizers  resource.Finalizers `json:"finalizers,omitempty" yaml:"finalizers,omitempty"`
	Version     Version             `json:"version" yaml:"version,omitempty"`
}

// Resource wraps Talos and COSI resource response to be encoded as JSON.
type Resource struct {
	Spec     any               `yaml:"spec" json:"spec"`
	Resource resource.Resource `yaml:"-" json:"-"`
	ID       string            `yaml:"-" json:"-"`
	Metadata Metadata          `yaml:"metadata" json:"metadata"`
}

// ResourceOptions describes additional resource wrapper options.
type ResourceOptions struct {
	metadata *common.Metadata
}

// ResourceOption is a single resource creation option.
type ResourceOption func(*ResourceOptions)

// WithMetadata creates resource with Talos metadata.
func WithMetadata(value *common.Metadata) ResourceOption {
	return func(o *ResourceOptions) {
		o.metadata = value
	}
}

// NewResource creates new resource.
func NewResource(r resource.Resource, options ...ResourceOption) (*Resource, error) {
	opts := &ResourceOptions{}
	for _, o := range options {
		o(opts)
	}

	s, err := resource.MarshalYAML(r)
	if err != nil {
		return nil, err
	}

	data, err := yaml.Marshal(s)
	if err != nil {
		return nil, err
	}

	parts := make([]string, 0, 3)
	if opts.metadata != nil {
		parts = append(parts, opts.metadata.Hostname)
	}

	parts = append(parts, r.Metadata().Namespace(), r.Metadata().ID())

	res := &Resource{
		ID: strings.Join(parts, "/"),
	}
	if err = yaml.Unmarshal(data, &res); err != nil {
		return nil, err
	}

	if opts.metadata != nil {
		res.Metadata.Node = opts.metadata.Hostname
	}

	if wrapped, ok := r.Spec().(protobufWrapper); ok {
		res.Spec = wrapped.GetValue()
	}

	res.Resource = r

	return res, nil
}

// protoMessage wraps proto.Message to leverage protojson encoder.
type protoMessage struct {
	Message proto.Message
}

func (r protoMessage) MarshalJSON() ([]byte, error) {
	data, err := MarshalJSON(r.Message)

	return []byte(data), err
}

// MarshalJSON overrides default marshal behavior replacing spec
// with the wrapped struct, that uses protojson encoder.
func (r *Resource) MarshalJSON() ([]byte, error) {
	m, ok := r.Spec.(proto.Message)
	if !ok {
		return json.Marshal(*r)
	}

	return json.Marshal(Resource{
		Spec:     protoMessage{Message: m},
		Metadata: r.Metadata,
		Resource: r.Resource,
		ID:       r.ID,
	})
}
