package node

import (
	"context"
	"encoding/json"
	"fmt"
	"oc-discovery/daemons/node/indexer"
	"oc-discovery/daemons/node/stream"
	"slices"

	oclib "cloud.o-forge.io/core/oc-lib"
	"cloud.o-forge.io/core/oc-lib/config"
	"cloud.o-forge.io/core/oc-lib/dbs"
	pp_model "cloud.o-forge.io/core/oc-lib/models/peer"
	"cloud.o-forge.io/core/oc-lib/tools"
	pp "github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/core/protocol"
)

type configPayload struct {
	PeerID string `json:"source_peer_id"`
}

type executionConsidersPayload struct {
	PeerIDs []string `json:"peer_ids"`
}

func ListenNATS(n *Node) {
	tools.NewNATSCaller().ListenNats(map[tools.NATSMethod]func(tools.NATSResponse){
		tools.PEER_BEHAVIOR_EVENT: func(resp tools.NATSResponse) { //nolint:typecheck
			handlePeerBehaviorEvent(n, resp)
		},
		// PEER_OBSERVE_EVENT is sent by oc-peer to start or stop observations
		// for a list of peer IDs, or to trigger a close-all.
		tools.PEER_OBSERVE_EVENT: func(resp tools.NATSResponse) {
			n.StreamService.HandleObserveNATSCommand(resp)
		},
		tools.PROPALGATION_EVENT: func(resp tools.NATSResponse) {
			if resp.FromApp == config.GetAppName() {
				return
			}
			p, err := oclib.GetMySelf()
			if err != nil || p == nil || p.PeerID != n.PeerID.String() {
				return
			}
			var propalgation tools.PropalgationMessage
			err = json.Unmarshal(resp.Payload, &propalgation)
			var dt *tools.DataType
			if propalgation.DataType > 0 {
				dtt := tools.DataType(propalgation.DataType)
				dt = &dtt
			}
			if err == nil {
				switch propalgation.Action {
				case tools.PB_ADMIRALTY_CONFIG, tools.PB_MINIO_CONFIG, tools.PB_SOURCE_PRESIGN:
					var m configPayload
					var proto protocol.ID = stream.ProtocolAdmiraltyConfigResource
					if propalgation.Action == tools.PB_MINIO_CONFIG {
						proto = stream.ProtocolMinioConfigResource
					} else if propalgation.Action == tools.PB_SOURCE_PRESIGN {
						proto = stream.ProtocolSourcePresignResource
					}
					if err := json.Unmarshal(propalgation.Payload, &m); err == nil {
						peers, _ := n.GetPeerRecord(context.Background(), m.PeerID)
						for _, p := range peers {
							n.StreamService.PublishCommon(&resp.Datatype, resp.User, resp.Groups,
								p.PeerID, proto, propalgation.Payload)
						}
					}
				case tools.PB_CREATE, tools.PB_UPDATE, tools.PB_DELETE:
					if slices.Contains([]tools.DataType{tools.BOOKING, tools.PURCHASE_RESOURCE}, resp.Datatype) {
						m := map[string]interface{}{}
						if err := json.Unmarshal(propalgation.Payload, &m); err == nil {
							delivered := false
							if m["peer_id"] != nil {
								_, err := n.StreamService.PublishCommon(&resp.Datatype, resp.User, resp.Groups,
									fmt.Sprintf("%v", m["peer_id"]), stream.ProtocolCreateResource, propalgation.Payload)
								delivered = err == nil
							}
							if !delivered {
								// NANO unreachable — look up its MasterID from the DB record.
								// The NANO self-attests its MASTER in its signed PeerRecord;
								// if MasterID is set we forward there, otherwise we drop silently.
								var destStruct struct {
									DestPeerID string `json:"dest_peer_id"`
								}
								if json.Unmarshal(propalgation.Payload, &destStruct) == nil && destStruct.DestPeerID != "" {
									d := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(&dbs.Filters{
										And: map[string][]dbs.Filter{
											"id": {{Operator: dbs.EQUAL.String(), Value: destStruct.DestPeerID}},
										},
									}, "", false, 0, 1)
									if len(d.Data) > 0 {
										nano := d.Data[0].(*pp_model.Peer)
										if nano.MasterID != "" {
											n.StreamService.PublishCommon(&resp.Datatype, resp.User, resp.Groups,
												nano.MasterID, stream.ProtocolCreateResource, propalgation.Payload)
										}
									}
								}
							}
						}
					} else {
						fmt.Println(n.StreamService.ToPartnerPublishEvent(
							context.Background(),
							propalgation.Action,
							dt, resp.User, resp.Groups,
							propalgation.Payload,
						))
					}
				case tools.PB_CONSIDERS:
					switch resp.Datatype {
					case tools.BOOKING, tools.PURCHASE_RESOURCE, tools.WORKFLOW_EXECUTION:
						var m executionConsidersPayload
						if err := json.Unmarshal(propalgation.Payload, &m); err == nil {
							for _, p := range m.PeerIDs {
								peers, _ := n.GetPeerRecord(context.Background(), p)
								for _, pp := range peers {
									n.StreamService.PublishCommon(&resp.Datatype, resp.User, resp.Groups,
										pp.PeerID, stream.ProtocolConsidersResource, propalgation.Payload)
								}
							}
						}
					default:
						// minio / admiralty config considers — route back to OriginID.
						var m struct {
							OriginID string `json:"origin_id"`
						}
						if err := json.Unmarshal(propalgation.Payload, &m); err == nil && m.OriginID != "" {
							peers, _ := n.GetPeerRecord(context.Background(), m.OriginID)
							for _, p := range peers {
								n.StreamService.PublishCommon(nil, resp.User, resp.Groups,
									p.PeerID, stream.ProtocolConsidersResource, propalgation.Payload)
							}
						}
					}
				case tools.PB_PLANNER:
					m := map[string]interface{}{}
					if err := json.Unmarshal(propalgation.Payload, &m); err == nil {
						b := []byte{}
						if len(m) > 1 {
							b = propalgation.Payload
						}
						if m["peer_id"] == nil { // send to every active stream
							n.StreamService.Mu.Lock()
							if n.StreamService.Streams[stream.ProtocolSendPlanner] != nil {
								for pid := range n.StreamService.Streams[stream.ProtocolSendPlanner] { // send Planner can be long lived - it's a conn
									n.StreamService.PublishCommon(nil, resp.User, resp.Groups, pid.String(), stream.ProtocolSendPlanner, b)
								}
							}
							n.StreamService.Mu.Unlock()
						} else {
							fmt.Println("REACH PLANNER")
							n.StreamService.PublishCommon(nil, resp.User, resp.Groups, fmt.Sprintf("%v", m["peer_id"]), stream.ProtocolSendPlanner, b)
						}
					}
				case tools.PB_CLOSE_PLANNER:
					m := map[string]interface{}{}
					if err := json.Unmarshal(resp.Payload, &m); err == nil {
						n.StreamService.Mu.Lock()
						if pid, err := pp.Decode(fmt.Sprintf("%v", m["peer_id"])); err == nil {
							if n.StreamService.Streams[stream.ProtocolSendPlanner] != nil && n.StreamService.Streams[stream.ProtocolSendPlanner][pid] != nil {
								n.StreamService.Streams[stream.ProtocolSendPlanner][pid].Stream.Close()
								delete(n.StreamService.Streams[stream.ProtocolSendPlanner], pid)
							}
						}
						n.StreamService.Mu.Unlock()
					}
				case tools.PB_OBSERVE:
					print("PROPALGATE OBSERVE")
					handleObserveEvent(n, propalgation)
				case tools.PB_OBSERVE_CLOSE:
					print("PROPALGATE CLOSE")
					handleObserveCloseEvent(n, propalgation)
				case tools.PB_PROPAGATE:
					// Another oc-discovery forwarded a heartbeat batch.
					// Re-emit on PEER_OBSERVE_RESPONSE_EVENT so the local oc-peer sees it.
					tools.NewNATSCaller().SetNATSPub(tools.PEER_OBSERVE_RESPONSE_EVENT, tools.NATSResponse{
						FromApp:  resp.FromApp,
						User:     resp.User,
						Datatype: tools.PEER,
						Method:   int(tools.PEER_OBSERVE_RESPONSE_EVENT),
						Payload:  propalgation.Payload,
					})
				case tools.PB_CLOSE_SEARCH:
					if propalgation.DataType == int(tools.PEER) {
						n.peerSearches.Cancel(resp.User)
					} else {
						n.StreamService.ResourceSearches.Cancel(resp.User)
					}
				case tools.PB_SEARCH:
					fmt.Println("PROPALGATE PEER")
					if propalgation.DataType == int(tools.PEER) {
						m := map[string]interface{}{}
						if err := json.Unmarshal(propalgation.Payload, &m); err == nil {
							needle := fmt.Sprintf("%v", m["search"])
							userKey := resp.User
							go n.SearchPeerRecord(userKey, needle, func(hit indexer.PeerRecord) {
								if b, err := json.Marshal(hit); err == nil {
									tools.NewNATSCaller().SetNATSPub(tools.SEARCH_EVENT, tools.NATSResponse{
										FromApp:  "oc-discovery",
										Datatype: tools.DataType(tools.PEER),
										User:     userKey,
										Method:   int(tools.SEARCH_EVENT),
										Payload:  b,
									})
								}
							})
						}

					} else {
						m := map[string]interface{}{}
						if err := json.Unmarshal(propalgation.Payload, &m); err == nil {
							fmt.Println("PB_SEARCH CATA", m, resp.User)

							n.PubSubService.SearchPublishEvent(
								context.Background(),
								dt,
								fmt.Sprintf("%v", m["type"]),
								resp.User, resp.Groups,
								fmt.Sprintf("%v", m["search"]),
							)
						}
					}
				}
			}
		},
	})
}

// handlePeerBehaviorEvent applies a PeerBehaviorReport received from a trusted
// service (oc-scheduler, oc-datacenter, …). It:
//  1. Loads the target peer from the local DB.
//  2. Deducts the trust penalty and appends a BehaviorWarning.
//  3. Auto-blacklists and evicts the peer stream when TrustScore ≤ threshold.
//
// oc-discovery does NOT re-emit a PROPALGATION_EVENT: propagation is strictly
// inbound (oc-catalog → oc-discovery). The blacklist takes effect locally at
// the next isPeerKnown() call, and immediately via EvictPeer().
func handlePeerBehaviorEvent(n *Node, resp tools.NATSResponse) {
	var report tools.PeerBehaviorReport
	if err := json.Unmarshal(resp.Payload, &report); err != nil {
		fmt.Println("handlePeerBehaviorEvent: unmarshal error:", err)
		return
	}
	if report.TargetPeerID == "" {
		return
	}

	access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil)
	data := access.LoadOne(report.TargetPeerID)
	if data.Data == nil {
		fmt.Println("handlePeerBehaviorEvent: peer not found:", report.TargetPeerID)
		return
	}
	p := data.ToPeer()
	if p == nil {
		return
	}
	// Self-protection: never penalise ourselves.
	if self, err := oclib.GetMySelf(); err == nil && self != nil && self.GetID() == p.GetID() {
		return
	}

	shouldBlacklist := p.ApplyBehaviorReport(report)
	if shouldBlacklist && p.Relation != pp_model.BLACKLIST {
		p.Relation = pp_model.BLACKLIST
		fmt.Printf("handlePeerBehaviorEvent: auto-blacklisting peer %s — reason: %s\n",
			p.PeerID, p.BlacklistReason)
		// Immediately evict any active stream so the peer can no longer heartbeat.
		if n.IndexerService != nil {
			n.IndexerService.EvictPeer(p.PeerID)
		}
	}

	// Persist updated trust score + relation locally.
	if updated := access.UpdateOne(p.Serialize(p), p.GetID()); updated.Err != "" {
		fmt.Println("handlePeerBehaviorEvent: could not update peer:", updated.Err)
		return
	}

	// Notify oc-peer (and any other local NATS consumer) of the updated peer record
	// via CREATE_RESOURCE so they can synchronise their own state.
	if b, err := json.Marshal(p.Serialize(p)); err == nil {
		tools.NewNATSCaller().SetNATSPub(tools.CREATE_RESOURCE, tools.NATSResponse{
			FromApp:  "oc-discovery",
			User:     resp.User,
			Datatype: tools.PEER,
			Method:   int(tools.CREATE_RESOURCE),
			Payload:  b,
		})
	}
}

// handleObserveEvent processes a PB_OBSERVE PropalgationMessage from another
// oc-discovery node, starting observation for the listed peers.
func handleObserveEvent(n *Node, p tools.PropalgationMessage) {
	var cmd stream.ObserveCommand
	if err := json.Unmarshal(p.Payload, &cmd); err != nil {
		fmt.Println("handleObserveEvent: unmarshal error:", err)
		return
	}
	for _, sp := range cmd.Peers {
		if err := n.StreamService.OpenObserveStream(sp); err != nil {
			fmt.Println("handleObserveEvent: OpenObserveStream failed for", sp.PeerID, ":", err)
		}
	}
}

// handleObserveCloseEvent processes a PB_OBSERVE_CLOSE PropalgationMessage from
// another oc-discovery node, stopping observation for the listed peer IDs.
func handleObserveCloseEvent(n *Node, p tools.PropalgationMessage) {
	var cmd stream.ObserveCommand
	if err := json.Unmarshal(p.Payload, &cmd); err != nil {
		fmt.Println("handleObserveCloseEvent: unmarshal error:", err)
		return
	}
	if cmd.CloseAll {
		n.StreamService.CloseAllObserves()
		return
	}
	for _, peerID := range cmd.PeerIDs {
		if err := n.StreamService.CloseObserveStream(peerID); err != nil {
			fmt.Println("handleObserveCloseEvent: CloseObserveStream failed for", peerID, ":", err)
		}
	}
}
