package node

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"oc-discovery/conf"
	"oc-discovery/daemons/node/common"
	"oc-discovery/daemons/node/indexer"
	"oc-discovery/daemons/node/location"
	"oc-discovery/daemons/node/pubsub"
	"oc-discovery/daemons/node/stream"
	"sync"
	"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"
	"github.com/google/uuid"
	"github.com/libp2p/go-libp2p"
	pubsubs "github.com/libp2p/go-libp2p-pubsub"
	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/network"
	pp "github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/core/protocol"
	"github.com/libp2p/go-libp2p/p2p/security/noise"
)

type Node struct {
	*common.LongLivedStreamRecordedService[interface{}] // change type of stream
	PS                                                  *pubsubs.PubSub
	IndexerService                                      *indexer.IndexerService
	PubSubService                                       *pubsub.PubSubService
	StreamService                                       *stream.StreamService
	PeerID                                              pp.ID
	isIndexer                                           bool
	peerRecord                                          *indexer.PeerRecord

	// peerSearches: one active peer search per user; new search cancels previous.
	peerSearches *common.SearchTracker

	Mu sync.RWMutex
}

func InitNode(isNode bool, isIndexer bool) (*Node, error) {
	if !isNode && !isIndexer {
		return nil, errors.New("wait... what ? your node need to  at least something. Retry we can't be friend in that case")
	}
	logger := oclib.GetLogger()
	logger.Info().Msg("retrieving private key...")
	priv, err := tools.LoadKeyFromFilePrivate() // your node private key
	if err != nil {
		return nil, err
	}
	logger.Info().Msg("retrieving psk file...")
	psk, err := common.LoadPSKFromFile() // network common private Network. Public OC PSK is Public Network
	if err != nil {
		return nil, nil
	}
	logger.Info().Msg("open a host...")
	gater := newOCConnectionGater(nil) // host set below after creation
	h, err := libp2p.New(
		libp2p.PrivateNetwork(psk),
		libp2p.Identity(priv),
		libp2p.Security(noise.ID, noise.New),
		libp2p.ListenAddrStrings(
			fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", conf.GetConfig().NodeEndpointPort),
		),
		libp2p.ConnectionGater(gater),
	)
	gater.host = h // wire host back into gater now that it exists
	logger.Info().Msg("Host open on " + h.ID().String())
	if err != nil {
		return nil, errors.New("no host no node")
	}
	node := &Node{
		PeerID:                         h.ID(),
		isIndexer:                      isIndexer,
		LongLivedStreamRecordedService: common.NewStreamRecordedService[interface{}](h, 1000),
		peerSearches:                   common.NewSearchTracker(),
	}
	// Register the bandwidth probe handler so any peer measuring this node's
	// throughput can open a dedicated probe stream and read the echo.
	h.SetStreamHandler(common.ProtocolBandwidthProbe, common.HandleBandwidthProbe)
	// Register the witness query handler so peers can ask this node's view of indexers.
	h.SetStreamHandler(common.ProtocolWitnessQuery, func(s network.Stream) {
		common.HandleWitnessQuery(h, s)
	})
	var ps *pubsubs.PubSub
	if isNode {
		logger.Info().Msg("generate opencloud node...")
		ps, err = pubsubs.NewGossipSub(context.Background(), node.Host)
		if err != nil {
			panic(err) // can't run your node without a propalgation pubsub, of state of node.
		}
		node.PS = ps
		// buildRecord returns a fresh signed PeerRecord as JSON, embedded in each
		// heartbeat so the receiving indexer can republish it to the DHT directly.
		// peerRecord is nil until claimInfo runs, so the first ~20s heartbeats carry
		// no record — that's fine, claimInfo publishes once synchronously at startup.
		buildRecord := func() json.RawMessage {
			if node.peerRecord == nil {
				return nil
			}
			priv, err := tools.LoadKeyFromFilePrivate()
			if err != nil {
				return nil
			}
			fresh := *node.peerRecord
			ttl := time.Duration(fresh.TTLSeconds) * time.Second
			if ttl <= 0 {
				ttl = indexer.DefaultTTLSeconds * time.Second
			}
			fresh.UpdateDate = time.Now().UTC()
			fresh.PeerRecordPayload.ExpiryDate = time.Now().UTC().Add(ttl)
			payload, _ := json.Marshal(fresh.PeerRecordPayload)
			fresh.Signature, err = priv.Sign(payload)
			if err != nil {
				return nil
			}
			b, _ := json.Marshal(fresh)
			return json.RawMessage(b)
		}
		// streamSvcRef is set after InitStream below; the heartbeat goroutine
		// first fires after 20 s so it is always non-nil by then.
		var streamSvcRef *stream.StreamService
		logger.Info().Msg("connect to indexers...")
		common.ConnectToIndexers(node.Host, conf.GetConfig().MinIndexer, conf.GetConfig().MaxIndexer,
			common.HeartbeatHooks{
				RecordFn: buildRecord,
				PendingContactFn: func() []string {
					if streamSvcRef != nil {
						return streamSvcRef.PendingContacts()
					}
					return nil
				},
				OnPendingCallers: func(callers []string) {
					if streamSvcRef != nil {
						streamSvcRef.NudgeContacts(callers)
					}
				},
			})
		logger.Info().Msg("claims my node...")
		if _, err := node.claimInfo(conf.GetConfig().Name, conf.GetConfig().Hostname); err != nil {
			panic(err)
		}
		logger.Info().Msg("run garbage collector...")
		node.StartGC(30 * time.Second)

		if node.StreamService, err = stream.InitStream(context.Background(), node.Host, node.PeerID, 1000, node); err != nil {
			panic(err)
		}
		streamSvcRef = node.StreamService
		node.StreamService.IsPeerKnown = func(pid pp.ID) bool {
			// 1. Local DB: known peer (handles blacklist).
			access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil)
			results := access.Search(&dbs.Filters{
				And: map[string][]dbs.Filter{
					"peer_id": {{Operator: dbs.EQUAL.String(), Value: pid.String()}},
				},
			}, pid.String(), false, 0, 1)
			for _, item := range results.Data {
				p, ok := item.(*peer.Peer)
				if !ok || p.PeerID != pid.String() {
					continue
				}
				return p.Relation != peer.BLACKLIST
			}
			// 2. Ask a connected indexer → DHT lookup by peer_id.
			for _, addr := range common.Indexers.GetAddrs() {
				ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
				s, err := h.NewStream(ctx, addr.Info.ID, common.ProtocolGet)
				cancel()
				if err != nil {
					continue
				}
				json.NewEncoder(s).Encode(indexer.GetValue{PeerID: pid.String()})
				var resp indexer.GetResponse
				json.NewDecoder(s).Decode(&resp)
				s.Reset()
				return resp.Found
			}
			return false
		}

		if node.PubSubService, err = pubsub.InitPubSub(context.Background(), node.Host, node.PS, node, node.StreamService); err != nil {
			panic(err)
		}
		f := func(ctx context.Context, evt common.Event, topic string) {
			m := map[string]interface{}{}
			err := json.Unmarshal(evt.Payload, &m)
			if err != nil || evt.From == node.PeerID.String() {
				return
			}
			fmt.Println("PUBSUB SendResponse bef peerrece")
			if p, err := node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 && m["search"] != nil {
				fmt.Println("PUBSUB SendResponse af peerrece", m, evt.User)
				node.StreamService.SendResponse(p[0], &evt, fmt.Sprintf("%v", m["search"]))
			}
		}
		node.AllowInbound = func(remotePeer pp.ID, isNew bool) error {
			if isNew {
				// DB blacklist check: blocks reconnection after EvictPeer + blacklist.
				if !node.isPeerKnown(remotePeer) {
					return errors.New("peer is blacklisted or unknown")
				}
				if !node.ConnGuard.Allow() {
					return errors.New("connection rate limit exceeded, retry later")
				}
			}
			return nil
		}
		logger.Info().Msg("subscribe to decentralized search flow...")
		go node.SubscribeToSearch(node.PS, &f)
		logger.Info().Msg("connect to NATS")
		go ListenNATS(node)
		logger.Info().Msg("Node is actually running.")
	}
	if isIndexer {
		logger.Info().Msg("generate opencloud indexer...")
		node.IndexerService = indexer.NewIndexerService(node.Host, ps, 500)
	}
	return node, nil
}

// isPeerKnown is the stream-level gate: returns true if pid is allowed.
// Check order (fast → slow):
//  1. In-memory stream records — currently heartbeating to this indexer.
//  2. Local DB by peer_id — known peer, blacklist enforced here.
//  3. DHT /pid/{peerID} → /node/{DID} — registered on any indexer.
//
// ProtocolHeartbeat and ProtocolPublish handlers do NOT call this — they are
// the streams through which a node first makes itself known.
func (d *Node) isPeerKnown(pid pp.ID) bool {
	// 1. Fast path: active heartbeat session.
	d.StreamMU.RLock()
	_, active := d.StreamRecords[common.ProtocolHeartbeat][pid]
	d.StreamMU.RUnlock()
	if active {
		return true
	}
	// 2. Local DB: known peer (handles blacklist).
	access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil)
	results := access.Search(&dbs.Filters{
		And: map[string][]dbs.Filter{
			"peer_id": {{Operator: dbs.EQUAL.String(), Value: pid.String()}},
		},
	}, pid.String(), false, 0, 1)
	for _, item := range results.Data {
		p, ok := item.(*peer.Peer)
		if !ok || p.PeerID != pid.String() {
			continue
		}
		return p.Relation != peer.BLACKLIST
	}
	return true
}

func (d *Node) Close() {
	if d.isIndexer && d.IndexerService != nil {
		d.IndexerService.Close()
	}
	d.PubSubService.Close()
	d.StreamService.Close()
	d.Host.Close()
}

func (d *Node) publishPeerRecord(
	rec *indexer.PeerRecord,
) error {
	priv, err := tools.LoadKeyFromFilePrivate() // your node private key
	if err != nil {
		return err
	}

	for _, ad := range common.Indexers.GetAddrs() {
		var err error
		if common.Indexers.Streams, err = common.TempStream(d.Host, *ad.Info, common.ProtocolPublish, "", common.Indexers.Streams, map[protocol.ID]*common.ProtocolInfo{},
			&common.Indexers.MuStream); err != nil {
			continue
		}
		stream := common.Indexers.Streams.GetPerID(common.ProtocolPublish, ad.Info.ID)
		ttl := time.Duration(rec.TTLSeconds) * time.Second
		if ttl <= 0 {
			ttl = indexer.DefaultTTLSeconds * time.Second
		}
		rec.ExpiryDate = time.Now().UTC().Add(ttl)
		payload, _ := json.Marshal(rec.PeerRecordPayload)
		rec.Signature, err = priv.Sign(payload)
		if err := json.NewEncoder(stream.Stream).Encode(&rec); err != nil { // then publish on stream
			return err
		}
	}
	return nil
}

// SearchPeerRecord starts a distributed peer search via ProtocolSearchPeer.
// A new call for the same userKey cancels any previous search.
// Results are pushed to onResult as they arrive; the function returns when
// the stream closes (idle timeout, explicit cancel, or indexer unreachable).
func (d *Node) SearchPeerRecord(userKey, needle string, onResult func(indexer.PeerRecord)) {
	logger := oclib.GetLogger()

	idleTimeout := common.SearchIdleTimeout()
	ctx, cancel := context.WithCancel(context.Background())
	// Register cancels any previous search for userKey and starts the idle timer.
	// The composite key doubles as QueryID so the indexer echoes it back.
	searchKey := d.peerSearches.Register(userKey, cancel, idleTimeout)
	defer d.peerSearches.Cancel(searchKey)

	req := common.SearchPeerRequest{QueryID: searchKey}
	if pid, err := pp.Decode(needle); err == nil {
		req.PeerID = pid.String()
	} else if _, err := uuid.Parse(needle); err == nil {
		req.DID = needle
	} else {
		req.Name = needle
	}
	fmt.Println("PROPALGATE PEER", needle, common.Indexers.GetAddrs())
	for _, ad := range common.Indexers.GetAddrs() {
		if ad.Info == nil {
			continue
		}
		dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second)
		s, err := d.Host.NewStream(dialCtx, ad.Info.ID, common.ProtocolSearchPeer)
		dialCancel()
		if err != nil {
			continue
		}
		if err := json.NewEncoder(s).Encode(req); err != nil {
			s.Reset()
			continue
		}
		// Interrupt the blocking Decode as soon as the context is cancelled
		// (idle timer, explicit PB_CLOSE_SEARCH, or replacement search).
		go func() {
			<-ctx.Done()
			s.SetReadDeadline(time.Now())
		}()
		seen := map[string]struct{}{}
		dec := json.NewDecoder(s)
		for {
			var result indexer.SearchPeerResult
			if err := dec.Decode(&result); err != nil {
				break
			}
			if result.QueryID != searchKey || !d.peerSearches.IsActive(searchKey) {
				break
			}
			d.peerSearches.ResetIdle(searchKey)
			for _, hit := range result.Records {
				key := hit.PeerID
				if key == "" {
					key = hit.DID
				}
				if _, already := seen[key]; already {
					continue
				}
				seen[key] = struct{}{}
				onResult(hit)
			}
		}
		s.Reset()
		return
	}
	logger.Warn().Str("user", userKey).Msg("[search] no reachable indexer for peer search")
}

func (d *Node) GetPeerRecord(
	ctx context.Context,
	pidOrdid string,
) ([]*peer.Peer, error) {
	var err error
	var info map[string]indexer.PeerRecord
	// Build the GetValue request: if pidOrdid is neither a UUID DID nor a libp2p
	// PeerID, treat it as a human-readable name and let the indexer resolve it.
	// GetPeerRecord resolves by PeerID or DID only.
	// Name-based search goes through SearchPeerRecord (ProtocolSearchPeer).
	getReq := indexer.GetValue{Key: pidOrdid}
	if pidR, pidErr := pp.Decode(pidOrdid); pidErr == nil {
		getReq.PeerID = pidR.String()
		getReq.Key = ""
	}
	for _, ad := range common.Indexers.GetAddrs() {
		if common.Indexers.Streams, err = common.TempStream(d.Host, *ad.Info, common.ProtocolGet, "",
			common.Indexers.Streams, map[protocol.ID]*common.ProtocolInfo{}, &common.Indexers.MuStream); err != nil {
			continue
		}
		stream := common.Indexers.Streams.GetPerID(common.ProtocolGet, ad.Info.ID)
		if err := json.NewEncoder(stream.Stream).Encode(getReq); err != nil {
			continue
		}
		var resp indexer.GetResponse
		if err := json.NewDecoder(stream.Stream).Decode(&resp); err != nil {
			continue
		}
		if resp.Found {
			info = resp.Records
		}
		break
	}
	var ps []*peer.Peer
	for _, pr := range info {
		if pk, err := pr.Verify(); err != nil {
			return nil, err
		} else if _, p, err := pr.ExtractPeer(d.PeerID.String(), pr.PeerID, pk); err != nil {
			return nil, err
		} else {
			ps = append(ps, p)
		}
	}

	return ps, err
}

func (d *Node) claimInfo(
	name string,
	endPoint string, // TODO : endpoint is not necesserry StreamAddress
) (*peer.Peer, error) {
	if endPoint == "" {
		return nil, errors.New("no endpoint found for peer")
	}
	did := uuid.New().String()

	peers := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(&dbs.Filters{
		And: map[string][]dbs.Filter{ // search by name if no filters are provided
			"peer_id": {{Operator: dbs.EQUAL.String(), Value: d.Host.ID().String()}},
		},
	}, "", false, 0, 1)
	var masterID string
	if len(peers.Data) > 0 {
		did = peers.Data[0].GetID() // if already existing set up did as made
		masterID = peers.Data[0].(*peer.Peer).MasterID
	}
	priv, err := tools.LoadKeyFromFilePrivate()
	if err != nil {
		return nil, err
	}
	pub, err := tools.LoadKeyFromFilePublic()
	if err != nil {
		return nil, err
	}
	pubBytes, err := crypto.MarshalPublicKey(pub)
	if err != nil {
		return nil, err
	}

	now := time.Now().UTC()
	pRec := indexer.PeerRecordPayload{
		Name:   name,
		DID:    did, // REAL PEER ID
		PubKey: pubBytes,

		IsNano:     oclib.GetConfig().IsNano,
		MasterID:   masterID,
		TTLSeconds: indexer.DefaultTTLSeconds,
		ExpiryDate: now.Add(indexer.DefaultTTLSeconds * time.Second),
	}
	d.PeerID = d.Host.ID()
	payload, _ := json.Marshal(pRec)

	rec := &indexer.PeerRecord{
		PeerRecordPayload: pRec,
	}
	rec.CreationDate = time.Now().UTC()
	rec.UpdateDate = time.Now().UTC()
	rec.Signature, err = priv.Sign(payload)
	if err != nil {
		return nil, err
	}
	rec.PeerID = d.Host.ID().String()
	rec.APIUrl = endPoint
	rec.StreamAddress = "/ip4/" + conf.GetConfig().Hostname + "/tcp/" + fmt.Sprintf("%v", conf.GetConfig().NodeEndpointPort) + "/p2p/" + rec.PeerID
	rec.NATSAddress = oclib.GetConfig().NATSUrl
	rec.WalletAddress = "my-wallet"
	rec.Location = location.Geolocate(conf.GetConfig().LocationGranularity)

	if err := d.publishPeerRecord(rec); err != nil {
		return nil, err
	}
	d.peerRecord = rec
	if _, err := rec.Verify(); err != nil {
		return nil, err
	} else {
		_, p, err := rec.ExtractPeer(did, did, pub)
		b, err := json.Marshal(p)
		if err != nil {
			return p, err
		}
		go tools.NewNATSCaller().SetNATSPub(tools.CREATE_RESOURCE, tools.NATSResponse{
			FromApp:    "oc-discovery",
			Datatype:   tools.PEER,
			Method:     int(tools.CREATE_RESOURCE),
			SearchAttr: "peer_id",
			Payload:    b,
		})
		return p, err
	}
}

// DeleteRecord broadcasts a signed tombstone to all connected indexers, signalling
// that this node is voluntarily leaving the network.
// Each indexer verifies the signature, stores the tombstone in the DHT (replacing
// the live record), and evicts the peer from its active pool.
// After a successful call, d.peerRecord is set to nil.
func (d *Node) DeleteRecord() error {
	if d.peerRecord == nil {
		return errors.New("no peer record to delete")
	}
	priv, err := tools.LoadKeyFromFilePrivate()
	if err != nil {
		return err
	}
	pubBytes, err := crypto.MarshalPublicKey(priv.GetPublic())
	if err != nil {
		return err
	}
	tp := indexer.TombstonePayload{
		DID:       d.peerRecord.DID,
		PeerID:    d.PeerID.String(),
		DeletedAt: time.Now().UTC(),
	}
	payloadBytes, _ := json.Marshal(tp)
	sig, err := priv.Sign(payloadBytes)
	if err != nil {
		return err
	}
	ts := &indexer.TombstoneRecord{
		TombstonePayload: tp,
		PubKey:           pubBytes,
		Tombstone:        true,
		Signature:        sig,
	}
	data, _ := json.Marshal(ts)
	for _, ad := range common.Indexers.GetAddrs() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		s, err := d.Host.NewStream(ctx, ad.Info.ID, common.ProtocolDelete)
		cancel()
		if err != nil {
			continue
		}
		s.SetDeadline(time.Now().Add(5 * time.Second))
		s.Write(data)
		s.Close()
	}
	d.peerRecord = nil
	return nil
}

/*
TODO:
- Le booking est un flow neuf décentralisé :
 On check on attend une réponse, on valide, il passe par discovery, on relais.
- Le shared workspace est une affaire de décentralisation,
	on communique avec les shared les mouvements
- Un shared remplace la notion de partnership à l'échelle de partnershipping
	-> quand on share un workspace on devient partenaire temporaire
		qu'on le soit originellement ou non.
	-> on a alors les mêmes privilèges.
- Les orchestrations admiralty ont le même fonctionnement.
	Un evenement provoque alors une création de clé de service.

On doit pouvoir crud avec verification de signature un DBobject.
*/
