package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/kubernetes_api/agent/watch_graph"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/kubernetes_api/rpc"
)

// inspector is just an alias to shorten the type name without introducing a new type (so that it matches the GraphInspector interface).
type inspector = watch_graph.GraphInspector[watch_graph.VertexID, watch_graph.ArcType, watch_graph.VertexData, watch_graph.ArcAttrs]

type watchGraphActionEmittingObserver struct {
	log       *slog.Logger
	onAction  func(*rpc.Action)
	onWarning func(watch_graph.Warning)
}

func (o *watchGraphActionEmittingObserver) OnSetVertex(ctx context.Context, insp inspector, vid watch_graph.VertexID, vd watch_graph.VertexData) {
	o.log.Debug("emit.OnSetVertex()", vidAttr(vid))
	var objData, jsonPathData []byte
	var ok bool
	if vd.JSONPath == nil { // whole object
		objData, ok = o.toJSON(vid, vd.Object)
		if !ok {
			return
		}
	} else { // use JSON path
		results, err := vd.JSONPath.FindResults(vd.Object)
		if err != nil {
			o.onWarning(watch_graph.NewObjectProcessingWarning(vid.GVR.Value(), vid.Namespace, vid.Name,
				fmt.Sprintf("JSON path: %v", err)))
			return
		}
		if len(results) != 1 {
			o.onWarning(watch_graph.NewObjectProcessingWarning(vid.GVR.Value(), vid.Namespace, vid.Name,
				fmt.Sprintf("Expected exactly one result from JSON path, got %d", len(results))))
			return
		}
		res := make([]any, 0, len(results[0]))
		for _, result := range results[0] {
			res = append(res, result.Interface())
		}
		jsonPathData, ok = o.toJSON(vid, res)
		if !ok {
			return
		}
	}
	o.onAction(&rpc.Action{
		Action: &rpc.Action_SetVertex_{
			SetVertex: &rpc.Action_SetVertex{
				Vertex:   vid2actionVertex(vid),
				Object:   objData,
				JsonPath: jsonPathData,
			},
		},
	})
}

func (o *watchGraphActionEmittingObserver) OnDeleteVertex(ctx context.Context, insp inspector, vid watch_graph.VertexID) {
	o.log.Debug("emit.OnDeleteVertex()", vidAttr(vid))
	o.onAction(&rpc.Action{
		Action: &rpc.Action_DeleteVertex_{
			DeleteVertex: &rpc.Action_DeleteVertex{
				Vertex: vid2actionVertex(vid),
			},
		},
	})
}

func (o *watchGraphActionEmittingObserver) OnSetArc(ctx context.Context, insp inspector, from watch_graph.VertexID, to watch_graph.ArcToID[watch_graph.VertexID, watch_graph.ArcType], data watch_graph.ArcAttrs) {
	o.log.Debug("emit.OnSetArc()", arcAttr(from, to))
	var arcData []byte
	if !data.IsZero() {
		var ok bool
		arcData, ok = o.toJSON(from, data)
		if !ok {
			return
		}
	}
	o.onAction(&rpc.Action{
		Action: &rpc.Action_SetArc_{
			SetArc: &rpc.Action_SetArc{
				Source:      vid2actionVertex(from),
				Destination: vid2actionVertex(to.To),
				Type:        to.ArcType.String(),
				Attributes:  arcData,
			},
		},
	})
}

func (o *watchGraphActionEmittingObserver) OnDeleteArc(ctx context.Context, insp inspector, from watch_graph.VertexID, to watch_graph.ArcToID[watch_graph.VertexID, watch_graph.ArcType]) {
	o.log.Debug("emit.OnDeleteArc()", arcAttr(from, to))
	o.onAction(&rpc.Action{
		Action: &rpc.Action_DeleteArc_{
			DeleteArc: &rpc.Action_DeleteArc{
				Source:      vid2actionVertex(from),
				Destination: vid2actionVertex(to.To),
				Type:        to.ArcType.String(),
			},
		},
	})
}

func (o *watchGraphActionEmittingObserver) toJSON(vid watch_graph.VertexID, v any) ([]byte, bool) {
	data, err := json.Marshal(v)
	if err != nil {
		o.onWarning(watch_graph.NewObjectProcessingWarning(vid.GVR.Value(), vid.Namespace, vid.Name, fmt.Sprintf("JSON marshal error: %v", err)))
		return nil, false
	}
	return data, true
}

func vid2actionVertex(vid watch_graph.VertexID) *rpc.Action_Vertex {
	gvr := vid.GVR.Value()
	return &rpc.Action_Vertex{
		Group:     gvr.Group,
		Version:   gvr.Version,
		Resource:  gvr.Resource,
		Namespace: vid.Namespace,
		Name:      vid.Name,
	}
}
