package stream

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

	oclib "cloud.o-forge.io/core/oc-lib"
	"cloud.o-forge.io/core/oc-lib/dbs"
	"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"
)

func (ps *StreamService) PublishesCommon(dt *tools.DataType, user string, groups []string, filter *dbs.Filters, resource []byte, protos ...protocol.ID) error {
	access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil)
	var p oclib.LibDataShallow
	if filter == nil {
		p = access.LoadAll(false, 0, 10000)
	} else {
		p = access.Search(filter, "", false, 0, 10000)
	}
	for _, pes := range p.Data {
		for _, proto := range protos {
			if _, err := ps.PublishCommon(dt, user, groups, pes.(*peer.Peer).PeerID, proto, resource); err != nil {
				continue
			}
		}
	}
	return nil
}

func (ps *StreamService) PublishCommon(dt *tools.DataType, user string, groups []string, toPeerID string, proto protocol.ID, resource []byte) (*common.Stream, error) {
	fmt.Println("PublishCommon", toPeerID)
	if toPeerID == ps.Key.String() {
		fmt.Println("Can't send to ourself !")
		return nil, errors.New("Can't send to ourself !")
	}

	access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil)
	p := access.Search(&dbs.Filters{
		And: map[string][]dbs.Filter{ // search by name if no filters are provided
			"peer_id": {{Operator: dbs.EQUAL.String(), Value: toPeerID}},
		},
	}, toPeerID, false, 0, 1)
	var pe *peer.Peer
	if len(p.Data) > 0 && p.Data[0].(*peer.Peer).Relation != peer.BLACKLIST {
		pe = p.Data[0].(*peer.Peer)
	} else if pps, err := ps.Node.GetPeerRecord(context.Background(), toPeerID); err == nil && len(pps) > 0 {
		pe = pps[0]
	}
	if pe != nil {
		ad, err := pp.AddrInfoFromString(pe.StreamAddress)
		if err != nil {
			return nil, err
		}
		stream, err := ps.write(toPeerID, ad, dt, user, resource, proto)
		if err != nil {
			if _, ok := DTNProtocols[proto]; ok {
				ps.dnt.enqueue(&DTNEntry{
					did:           toPeerID,
					resourceID:    extractResourceID(resource),
					forceCritical: pe.Relation == peer.NANO,
					addr:          *ad,
					dt:            dt,
					user:          user,
					payload:       resource,
					proto:         proto,
					addedAt:       time.Now().UTC(),
				})
			}
			return nil, err
		}
		return stream, nil
	}
	return nil, errors.New("peer unvalid " + toPeerID)
}

func (ps *StreamService) ToPartnerPublishEvent(
	ctx context.Context, action tools.PubSubAction, dt *tools.DataType, user string, groups []string, payload []byte) error {
	var proto protocol.ID
	proto = ProtocolCreateResource
	switch action {
	case tools.PB_DELETE:
		proto = ProtocolDeleteResource
	case tools.PB_UPDATE:
		proto = ProtocolUpdateResource
	}
	if *dt == tools.PEER {
		var p peer.Peer
		if err := json.Unmarshal(payload, &p); err != nil {
			return err
		}
		if _, err := pp.Decode(p.PeerID); err != nil {
			return err
		}

		if pe, err := oclib.GetMySelf(); err != nil {
			return err
		} else if pe.GetID() == p.GetID() {
			return fmt.Errorf("can't send to ourself")
		} else {
			pe.Relation = p.Relation
			pe.Verify = false
			if b2, err := json.Marshal(pe); err == nil {
				if _, err := ps.PublishCommon(dt, user, groups, p.PeerID, ProtocolUpdateResource, b2); err != nil {
					return err
				}

			}
		}
		var per peer.Peer
		if err := json.Unmarshal(payload, &per); err == nil && !strings.Contains(per.Relation.String(), "master") && !strings.Contains(per.Relation.String(), "nano") {
			for _, rel := range []peer.PeerRelation{peer.MASTER, peer.NANO} {
				ps.PublishesCommon(dt, user, groups, &dbs.Filters{
					And: map[string][]dbs.Filter{
						"relation": {{Operator: dbs.EQUAL.String(), Value: rel}},
					},
				}, payload, proto)
			}
		}

		return nil
	}
	// Extract creator_id to route to the correct nano.
	// A master must only forward a resource to the nano that owns it.
	var creatorID string
	var minPayload struct {
		CreatorID string `json:"creator_id"`
	}
	if json.Unmarshal(payload, &minPayload) == nil {
		creatorID = minPayload.CreatorID
	}

	// PARTNER and MASTER receive every resource unconditionally.
	for _, rel := range []peer.PeerRelation{peer.PARTNER, peer.MASTER} {
		ps.PublishesCommon(dt, user, groups, &dbs.Filters{
			And: map[string][]dbs.Filter{
				"relation": {{Operator: dbs.EQUAL.String(), Value: rel}},
			},
		}, payload, proto)
	}

	// NANO: only send to the nano whose UUID matches the resource creator.
	if creatorID != "" {
		ps.PublishesCommon(dt, user, groups, &dbs.Filters{
			And: map[string][]dbs.Filter{
				"relation": {{Operator: dbs.EQUAL.String(), Value: peer.NANO}},
				"id":       {{Operator: dbs.EQUAL.String(), Value: creatorID}},
			},
		}, payload, proto)
	}
	return nil
}

// forwardToNano sends a booking mutation directly to a known NANO peer.
// The NANO peer is already resolved by the caller (resolveBookingNano).
// DTN critical is applied automatically by PublishCommon (Relation == NANO).
func (abs *StreamService) forwardToNano(nano *peer.Peer, evt *common.Event, proto string) {
	dt := tools.DataType(evt.DataType)
	abs.PublishCommon(&dt, evt.User, evt.Groups, nano.PeerID, protocol.ID(proto), evt.Payload)
}

func (s *StreamService) write(
	did string,
	peerID *pp.AddrInfo,
	dt *tools.DataType,
	user string,
	payload []byte,
	proto protocol.ID) (*common.Stream, error) {
	logger := oclib.GetLogger()
	var err error
	pts := map[protocol.ID]*common.ProtocolInfo{}
	for k, v := range protocols {
		pts[k] = v
	}
	for k, v := range protocolsPartners {
		pts[k] = v
	}
	// should create a very temp stream
	if s.Streams, err = common.TempStream(s.Host, *peerID, proto, did, s.Streams, pts, &s.Mu); err != nil {
		fmt.Println("TempStream", err)
		return nil, errors.New("no stream available for protocol " + fmt.Sprintf("%v", proto) + " from PID " + peerID.ID.String())
	}

	stream := s.Streams[proto][peerID.ID]
	evt := common.NewEvent(string(proto), s.Host.ID().String(), dt, user, payload)
	fmt.Println("SEND EVENT ", peerID, proto, evt.From, evt.DataType, evt.Timestamp)
	if err := json.NewEncoder(stream.Stream).Encode(evt); err != nil {
		stream.Stream.Close()
		logger.Err(err)
		return nil, err
	}
	if protocolInfo, ok := protocols[proto]; ok && protocolInfo.WaitResponse {
		go s.readLoop(stream, peerID.ID, proto, &common.ProtocolInfo{PersistantStream: true})
	}
	return stream, nil
}
