package stream

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"oc-discovery/conf"
	"oc-discovery/daemons/node/common"
	"strings"
	"sync"
	"time"

	oclib "cloud.o-forge.io/core/oc-lib"
	"cloud.o-forge.io/core/oc-lib/config"
	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/models/peer"
	"cloud.o-forge.io/core/oc-lib/models/utils"
	"github.com/google/uuid"
	"github.com/libp2p/go-libp2p/core/host"
	"github.com/libp2p/go-libp2p/core/network"
	pp "github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/core/protocol"
)

const ProtocolConsidersResource = "/opencloud/resource/considers/1.0"
const ProtocolMinioConfigResource = "/opencloud/minio/config/1.0"
const ProtocolAdmiraltyConfigResource = "/opencloud/admiralty/config/1.0"

// ProtocolSourcePresignResource routes PB_SOURCE_PRESIGN to the resource-owner peer.
// The owner generates a pre-signed Minio URL and responds via PB_CONSIDERS.
const ProtocolSourcePresignResource = "/opencloud/resource/source-presign/1.0"

const ProtocolSearchResource = "/opencloud/resource/search/1.0"
const ProtocolCreateResource = "/opencloud/resource/create/1.0"
const ProtocolUpdateResource = "/opencloud/resource/update/1.0"
const ProtocolDeleteResource = "/opencloud/resource/delete/1.0"

const ProtocolSendPlanner = "/opencloud/resource/planner/1.0"
const ProtocolVerifyResource = "/opencloud/resource/verify/1.0"
const ProtocolHeartbeatPartner = "/opencloud/resource/heartbeat/partner/1.0"

var protocols = map[protocol.ID]*common.ProtocolInfo{
	ProtocolConsidersResource:       {WaitResponse: false, TTL: 3 * time.Second},
	ProtocolSendPlanner:             {WaitResponse: true, TTL: 24 * time.Hour},
	ProtocolSearchResource:          {WaitResponse: true, TTL: 1 * time.Minute},
	ProtocolVerifyResource:          {WaitResponse: true, TTL: 1 * time.Minute},
	ProtocolMinioConfigResource:     {WaitResponse: true, TTL: 1 * time.Minute},
	ProtocolAdmiraltyConfigResource: {WaitResponse: true, TTL: 1 * time.Minute},
	ProtocolSourcePresignResource:   {WaitResponse: true, TTL: 1 * time.Minute},
	ProtocolObserve:                 {WaitResponse: true, TTL: 1 * time.Minute},
}

var protocolsPartners = map[protocol.ID]*common.ProtocolInfo{
	ProtocolCreateResource: {TTL: 3 * time.Second},
	ProtocolUpdateResource: {TTL: 3 * time.Second},
	ProtocolDeleteResource: {TTL: 3 * time.Second},
}

type StreamService struct {
	Key              pp.ID
	Host             host.Host
	Node             common.DiscoveryPeer
	Streams          common.ProtocolStream
	maxNodesConn     int
	Mu               sync.RWMutex
	ResourceSearches *common.SearchTracker
	// IsPeerKnown, when set, is called at stream open for every inbound protocol.
	// Return false to reset the stream immediately. Left nil until wired by the node.
	IsPeerKnown func(pid pp.ID) bool
	// DTN is the Disconnection Network Tolerance cache for outbound streams.
	dnt *DTNCache
	// observeCache tracks running heartbeat goroutines on the OBSERVED side.
	observeCache *observeCache
	// hbBatcher accumulates incoming heartbeats (observing side) and flushes
	// them as a single NATS batch after observeBatchWindow.
	hbBatcher *heartbeatBatcher
	// drainUntil / drainMu implement the startup drain window: for 30 s after a
	// close-all, incoming ProtocolObserve requests are rejected so stale heartbeats
	// from a previous run cannot mix with fresh observations.
	drainUntil time.Time
	drainMu    sync.RWMutex
	// observeUsers tracks which users are observing which peers so streams are
	// closed only when the last observer for a peer disconnects.
	observeUsers *userPeerIndex
	// observeMetrics accumulates connection-quality data per observed peer (observing side).
	// Keys are peer_id strings; values are *PeerObserveMetrics.
	observeMetrics sync.Map
	// DTNNudge receives peer IDs for which an immediate DTN retry should be
	// attempted (e.g. when the peer just reconnected via PendingCallers).
	dntNudge chan string
}

func InitStream(ctx context.Context, h host.Host, key pp.ID, maxNode int, node common.DiscoveryPeer) (*StreamService, error) {
	logger := oclib.GetLogger()
	service := &StreamService{
		Key:              key,
		Node:             node,
		Host:             h,
		Streams:          common.ProtocolStream{},
		maxNodesConn:     maxNode,
		ResourceSearches: common.NewSearchTracker(),
		dnt:              newDNTCache(),
		observeCache:     newObserveCache(),
		observeUsers:     newUserPeerIndex(),
		dntNudge:         make(chan string, 32),
	}
	service.hbBatcher = newHeartbeatBatcher(flushObserveBatch)
	for proto := range protocols {
		service.Host.SetStreamHandler(proto, service.gate(service.HandleResponse))
	}
	// ProtocolObserve uses a dedicated handler (bidirectional, long-lived).
	logger.Info().Msg("connect to partners...")
	service.connectToPartners() // we set up a stream
	go service.StartGC(8 * time.Second)
	go service.startDNTLoop()
	return service, nil
}

// PendingContacts returns the peer IDs that have at least one critical DTN
// entry pending.  Called on each heartbeat tick to populate PendingContact.
func (s *StreamService) PendingContacts() []string {
	return s.dnt.peersWithPending()
}

// NudgeContacts signals the DTN loop to retry immediately for the given peer
// IDs (typically received via HeartbeatResponse.PendingCallers).
func (s *StreamService) NudgeContacts(peerIDs []string) {
	for _, id := range peerIDs {
		select {
		case s.dntNudge <- id:
		default:
		}
	}
}

// gate wraps a stream handler with IsPeerKnown validation.
// If the peer is unknown the entire connection is closed and the handler is not called.
// IsPeerKnown is read at stream-open time so it works even when set after InitStream.
func (s *StreamService) gatePrivilege(h func(network.Stream)) func(network.Stream) {
	return func(stream network.Stream) {
		if config.GetConfig().IsNano {
			d := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(&dbs.Filters{
				And: map[string][]dbs.Filter{
					"relation": {{Operator: dbs.EQUAL.String(), Value: peer.MASTER}},
				},
			}, "", false, 0, 1)
			if len(d.Data) == 0 {
				stream.Reset()
				return
			}
			master := d.Data[0].(*peer.Peer)
			if stream.Conn().RemotePeer().String() != master.PeerID {
				logger := oclib.GetLogger()
				logger.Warn().
					Str("remote", stream.Conn().RemotePeer().String()).
					Str("master", master.PeerID).
					Msg("[gate] nano rejected stream from non-master peer")
				stream.Reset()
				return
			}
		}
		s.knowingGate(stream, h)
	}
}

// gate wraps a stream handler with IsPeerKnown validation.
// If the peer is unknown the entire connection is closed and the handler is not called.
// IsPeerKnown is read at stream-open time so it works even when set after InitStream.
func (s *StreamService) gate(h func(network.Stream)) func(network.Stream) {
	return func(stream network.Stream) {
		s.knowingGate(stream, h)
	}
}

func (s *StreamService) knowingGate(stream network.Stream, h func(network.Stream)) {
	if s.IsPeerKnown != nil && !s.IsPeerKnown(stream.Conn().RemotePeer()) {
		logger := oclib.GetLogger()
		logger.Warn().Str("peer", stream.Conn().RemotePeer().String()).Msg("[stream] unknown peer, closing connection")
		stream.Conn().Close()
		return
	}
	h(stream)
}

func (s *StreamService) HandleResponse(stream network.Stream) {
	s.Mu.Lock()
	defer s.Mu.Unlock()
	stream.Protocol()
	if s.Streams[stream.Protocol()] == nil {
		s.Streams[stream.Protocol()] = map[pp.ID]*common.Stream{}
	}
	expiry := 1 * time.Minute

	if protocols[stream.Protocol()] != nil {
		expiry = protocols[stream.Protocol()].TTL
	} else if protocolsPartners[stream.Protocol()] != nil {
		expiry = protocolsPartners[stream.Protocol()].TTL
	}

	s.Streams[stream.Protocol()][stream.Conn().RemotePeer()] = &common.Stream{
		Stream: stream,
		Expiry: time.Now().UTC().Add(expiry + 1*time.Minute),
	}

	// ProtocolObserve uses a bidirectional long-lived stream: the remote writes
	// heartbeats back on the same stream, and may later send a close event.
	// Use a persistent readLoop so we can receive both heartbeats and close events.
	protoInfo := protocols[stream.Protocol()]
	if stream.Protocol() == ProtocolObserve {
		protoInfo = &common.ProtocolInfo{PersistantStream: true}
	}
	go s.readLoop(s.Streams[stream.Protocol()][stream.Conn().RemotePeer()],
		stream.Conn().RemotePeer(),
		stream.Protocol(), protoInfo)
}

func (s *StreamService) connectToPartners() error {
	logger := oclib.GetLogger()
	// Register handlers for partner resource protocols (create/update/delete).
	// Connections to partners happen on-demand via TempStream when needed.
	for proto, info := range protocolsPartners {
		f := func(ss network.Stream) {
			if s.Streams[proto] == nil {
				s.Streams[proto] = map[pp.ID]*common.Stream{}
			}
			s.Streams[proto][ss.Conn().RemotePeer()] = &common.Stream{
				Stream: ss,
				Expiry: time.Now().UTC().Add(10 * time.Second),
			}
			go s.readLoop(s.Streams[proto][ss.Conn().RemotePeer()], ss.Conn().RemotePeer(), proto, info)
		}
		logger.Info().Msg("SetStreamHandler " + string(proto))
		s.Host.SetStreamHandler(proto, s.gatePrivilege(f))
	}
	return nil
}

func (s *StreamService) searchPeer(search string) ([]*peer.Peer, error) {
	ps := []*peer.Peer{}
	if conf.GetConfig().NanoIDS != "" {
		for _, peerID := range strings.Split(conf.GetConfig().NanoIDS, ",") {
			ppID := strings.Split(peerID, "/")
			ps = append(ps, &peer.Peer{
				AbstractObject: utils.AbstractObject{
					UUID: uuid.New().String(),
					Name: ppID[1],
				},
				PeerID:        ppID[len(ppID)-1],
				StreamAddress: peerID,
				Relation:      peer.NANO,
			})
		}
	}
	if conf.GetConfig().PeerIDS != "" {
		for _, peerID := range strings.Split(conf.GetConfig().PeerIDS, ",") {
			ppID := strings.Split(peerID, "/")
			ps = append(ps, &peer.Peer{
				AbstractObject: utils.AbstractObject{
					UUID: uuid.New().String(),
					Name: ppID[1],
				},
				PeerID:        ppID[len(ppID)-1],
				StreamAddress: peerID,
				Relation:      peer.PARTNER,
			})
		}
	}
	access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil)
	peers := access.Search(nil, search, false, 0, 0)
	for _, p := range peers.Data {
		ps = append(ps, p.(*peer.Peer))
	}
	return ps, nil
}

func (ix *StreamService) Close() {
	for _, s := range ix.Streams {
		for _, ss := range s {
			ss.Stream.Close()
		}
	}
}

func (s *StreamService) StartGC(interval time.Duration) {
	go func() {
		t := time.NewTicker(interval)
		defer t.Stop()
		for range t.C {
			s.gc()
		}
	}()
}

func (s *StreamService) gc() {
	s.Mu.Lock()
	defer s.Mu.Unlock()
	now := time.Now().UTC()

	for pid, rec := range s.Streams[ProtocolHeartbeatPartner] {
		if now.After(rec.Expiry) {
			for _, sstreams := range s.Streams {
				if sstreams[pid] != nil {
					sstreams[pid].Stream.Close()
					delete(sstreams, pid)
				}
			}
		}
	}
}

func (ps *StreamService) readLoop(s *common.Stream, id pp.ID, proto protocol.ID, protocolInfo *common.ProtocolInfo) {
	defer s.Stream.Close()
	defer func() {
		ps.Mu.Lock()
		defer ps.Mu.Unlock()
		delete(ps.Streams[proto], id)
	}()
	loop := true
	if !protocolInfo.PersistantStream && !protocolInfo.WaitResponse { // 2 sec is enough... to wait a response
		time.AfterFunc(2*time.Second, func() {
			loop = false
		})
	}
	for {
		if !loop {
			break
		}
		var evt common.Event
		if err := json.NewDecoder(s.Stream).Decode(&evt); err != nil {
			// Any decode error (EOF, reset, malformed JSON) terminates the loop;
			// continuing on a dead/closed stream creates an infinite spin.
			if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) ||
				strings.Contains(err.Error(), "reset") ||
				strings.Contains(err.Error(), "closed") ||
				strings.Contains(err.Error(), "too many connections") {
				return
			}
			continue
		}
		ps.handleEvent(evt.Type, &evt, s.Stream)
		if protocolInfo.WaitResponse && !protocolInfo.PersistantStream {
			break
		}
	}
}

func (abs *StreamService) FilterPeer(peerID string, groups []string, search string) *dbs.Filters {
	p, err := oclib.GetMySelf()
	if err != nil {
		return nil
	}
	groups = append(groups, "*")
	filter := map[string][]dbs.Filter{
		"abstractinstanciatedresource.abstractresource.abstractobject.creator_id": {{Operator: dbs.EQUAL.String(), Value: p.GetID()}}, // is my resource...
		"": {{Operator: dbs.OR.String(), Value: &dbs.Filters{
			Or: map[string][]dbs.Filter{
				"abstractinstanciatedresource.abstractresource.abstractobject.access_mode": {{Operator: dbs.EQUAL.String(), Value: 1}}, // if public
				"abstractinstanciatedresource.instances": {{Operator: dbs.ELEMMATCH.String(), Value: &dbs.Filters{ // or got a partners instances
					And: map[string][]dbs.Filter{
						"resourceinstance.partnerships": {{Operator: dbs.ELEMMATCH.String(), Value: &dbs.Filters{
							And: map[string][]dbs.Filter{
								"resourcepartnership.peer_groups." + peerID: {{Operator: dbs.IN.String(), Value: groups}},
							},
						}}},
					},
				}}},
			},
		}}},
	}

	if search != "" {
		filter[" "] = []dbs.Filter{{Operator: dbs.OR.String(), Value: &dbs.Filters{
			Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided
				"abstractinstanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
				"abstractinstanciatedresource.abstractresource.type":                {{Operator: dbs.LIKE.String(), Value: search}},
				"abstractinstanciatedresource.abstractresource.short_description":   {{Operator: dbs.LIKE.String(), Value: search}},
				"abstractinstanciatedresource.abstractresource.description":         {{Operator: dbs.LIKE.String(), Value: search}},
				"abstractinstanciatedresource.abstractresource.owners.name":         {{Operator: dbs.LIKE.String(), Value: search}},
			},
		}}}
	}
	return &dbs.Filters{
		And: filter,
	}
}
