// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package main

import (
	"encoding/binary"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"sync"
	"syscall"
	"time"

	"git.quad4.io/Networks/Reticulum-Go/internal/config"
	"git.quad4.io/Networks/Reticulum-Go/internal/storage"
	"git.quad4.io/Networks/Reticulum-Go/pkg/buffer"
	"git.quad4.io/Networks/Reticulum-Go/pkg/channel"
	"git.quad4.io/Networks/Reticulum-Go/pkg/common"
	"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
	"git.quad4.io/Networks/Reticulum-Go/pkg/destination"
	"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
	"git.quad4.io/Networks/Reticulum-Go/pkg/interfaces"
	"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
	"git.quad4.io/Networks/Reticulum-Go/pkg/transport"
)

var (
	interceptPackets = flag.Bool("intercept-packets", false, "Enable packet interception")
	interceptOutput  = flag.String("intercept-output", "packets.log", "Output file for intercepted packets")
)

const (
	ANNOUNCE_RATE_TARGET  = 3600 // Default target time between announces (1 hour)
	ANNOUNCE_RATE_GRACE   = 3    // Number of grace announces before enforcing rate
	ANNOUNCE_RATE_PENALTY = 7200 // Additional penalty time for rate violations
	MAX_ANNOUNCE_HOPS     = 128  // Maximum number of hops for announces
	APP_NAME              = "Reticulum-Go Test Node"
	APP_ASPECT            = "node" // Always use "node" for node announces
)

type Reticulum struct {
	config            *common.ReticulumConfig
	transport         *transport.Transport
	interfaces        []interfaces.Interface
	channels          map[string]*channel.Channel
	buffers           map[string]*buffer.Buffer
	pathRequests      map[string]*common.PathRequest
	announceHistory   map[string]announceRecord
	announceHistoryMu sync.RWMutex
	identity          *identity.Identity
	destination       *destination.Destination
	storage           *storage.Manager

	// Node-specific information
	maxTransferSize int16 // Max transfer size in KB
	nodeEnabled     bool  // Whether this node is enabled
	nodeTimestamp   int64 // Last node announcement timestamp
}

type announceRecord struct {
	timestamp int64
	appData   []byte
}

func NewReticulum(cfg *common.ReticulumConfig) (*Reticulum, error) {
	if cfg == nil {
		cfg = config.DefaultConfig()
	}

	// Set default app name and aspect if not provided
	if cfg.AppName == "" {
		cfg.AppName = APP_NAME
	}
	if cfg.AppAspect == "" {
		cfg.AppAspect = APP_ASPECT // Always use "node" for node announcements
	}

	if err := initializeDirectories(); err != nil {
		return nil, fmt.Errorf("failed to initialize directories: %v", err)
	}
	debug.Log(debug.DEBUG_INFO, "Directories initialized")

	// Initialize storage manager
	storageMgr, err := storage.NewManager()
	if err != nil {
		return nil, fmt.Errorf("failed to initialize storage manager: %v", err)
	}
	debug.Log(debug.DEBUG_INFO, "Storage manager initialized")

	t := transport.NewTransport(cfg)
	debug.Log(debug.DEBUG_INFO, "Transport initialized")

	// Load or create identity
	identityPath := storageMgr.GetIdentityPath()

	var ident *identity.Identity

	if _, err := os.Stat(identityPath); err == nil {
		// Identity file exists, load it
		ident, err = identity.FromFile(identityPath)
		if err != nil {
			return nil, fmt.Errorf("failed to load identity: %v", err)
		}
		debug.Log(debug.DEBUG_ERROR, "Loaded existing identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))
	} else {
		// Create new identity
		ident, err = identity.NewIdentity()
		if err != nil {
			return nil, fmt.Errorf("failed to create identity: %v", err)
		}
		debug.Log(debug.DEBUG_ERROR, "Created new identity", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, ident.Hash()))

		// Save it to disk
		if err := ident.ToFile(identityPath); err != nil {
			debug.Log(debug.DEBUG_ERROR, "Failed to save identity to file", common.STR_ERROR, err)
		} else {
			debug.Log(debug.DEBUG_INFO, "Identity saved to file", "path", identityPath)
		}
	}

	// Create destination
	debug.Log(debug.DEBUG_INFO, "Creating destination...")
	dest, err := destination.New(
		ident,
		destination.IN,
		destination.SINGLE,
		"nomadnetwork",
		t,
		"node",
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create destination: %v", err)
	}
	debug.Log(debug.DEBUG_INFO, "Created destination with hash", common.STR_HASH, fmt.Sprintf(common.STR_FMT_HEX_LOW, dest.GetHash()))

	// Set node metadata
	nodeTimestamp := time.Now().Unix()

	r := &Reticulum{
		config:          cfg,
		transport:       t,
		interfaces:      make([]interfaces.Interface, 0),
		channels:        make(map[string]*channel.Channel),
		buffers:         make(map[string]*buffer.Buffer),
		pathRequests:    make(map[string]*common.PathRequest),
		announceHistory: make(map[string]announceRecord),
		identity:        ident,
		destination:     dest,
		storage:         storageMgr,

		// Node-specific information
		maxTransferSize: common.NUM_500, // Default 500KB
		nodeEnabled:     true,           // Enabled by default
		nodeTimestamp:   nodeTimestamp,
	}

	// Enable destination features
	dest.AcceptsLinks(true)
	// Enable ratchets and point to a file for persistence.
	// The actual path should probably be configurable.
	ratchetPath := ".git.quad4.io/Networks/Reticulum-Go/storage/ratchets/" + r.identity.GetHexHash()
	dest.EnableRatchets(ratchetPath)
	dest.SetProofStrategy(destination.PROVE_APP)

	debug.Log(debug.DEBUG_VERBOSE, "Configured destination features")

	// Initialize interfaces from config
	for name, ifaceConfig := range cfg.Interfaces {
		if !ifaceConfig.Enabled {
			continue
		}

		var iface interfaces.Interface
		var err error

		switch ifaceConfig.Type {
		case common.STR_TCP_CLIENT:
			iface, err = interfaces.NewTCPClientInterface(
				name,
				ifaceConfig.TargetHost,
				ifaceConfig.TargetPort,
				ifaceConfig.KISSFraming,
				ifaceConfig.I2PTunneled,
				ifaceConfig.Enabled,
			)
		case "UDPInterface":
			iface, err = interfaces.NewUDPInterface(
				name,
				ifaceConfig.Address,
				ifaceConfig.TargetHost,
				ifaceConfig.Enabled,
			)
		case "AutoInterface":
			iface, err = interfaces.NewAutoInterface(name, ifaceConfig)
		case "WebSocketInterface":
			wsURL := ifaceConfig.Address
			if wsURL == "" {
				wsURL = ifaceConfig.TargetHost
			}
			debug.Log(debug.DEBUG_INFO, "Creating WebSocket interface", common.STR_NAME, name, "url", wsURL, "enabled", ifaceConfig.Enabled)
			iface, err = interfaces.NewWebSocketInterface(name, wsURL, ifaceConfig.Enabled)
			if err != nil {
				debug.Log(debug.DEBUG_ERROR, "Failed to create WebSocket interface", common.STR_NAME, name, common.STR_ERROR, err)
			} else {
				debug.Log(debug.DEBUG_INFO, "WebSocket interface created successfully", common.STR_NAME, name)
			}
		case "SerialInterface":
			iface, err = interfaces.NewSerialInterface(
				name,
				ifaceConfig.Interface,
				uint32(ifaceConfig.Bitrate),
				ifaceConfig.Enabled,
			)
		case "RNodeInterface":
			// RNode usually runs over Serial
			serial, sErr := interfaces.NewSerialInterface(
				name+"_serial",
				ifaceConfig.Interface,
				uint32(ifaceConfig.Bitrate),
				ifaceConfig.Enabled,
			)
			if sErr != nil {
				err = sErr
			} else {
				iface, err = interfaces.NewRNodeInterface(
					name,
					serial,
					ifaceConfig.Frequency,
					ifaceConfig.Bandwidth,
					ifaceConfig.SF,
					ifaceConfig.CR,
					ifaceConfig.TXPower,
				)
			}
		default:
			debug.Log(debug.DEBUG_CRITICAL, "Unknown interface type", common.STR_TYPE, ifaceConfig.Type)
			continue
		}

		if err != nil {
			if cfg.PanicOnInterfaceErr {
				return nil, fmt.Errorf("failed to create interface %s: %v", name, err)
			}
			debug.Log(debug.DEBUG_CRITICAL, "Error creating interface", common.STR_NAME, name, common.STR_ERROR, err)
			continue
		}

		// Set packet callback
		iface.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
			debug.Log(debug.DEBUG_INFO, "Packet callback called for interface", common.STR_NAME, ni.GetName(), "data_len", len(data))
			if r.transport != nil {
				r.transport.HandlePacket(data, ni)
			} else {
				debug.Log(debug.DEBUG_CRITICAL, "Transport is nil in packet callback")
			}
		})

		debug.Log(debug.DEBUG_ERROR, "Configuring interface", common.STR_NAME, name, common.STR_TYPE, ifaceConfig.Type)
		r.interfaces = append(r.interfaces, iface)
		debug.Log(debug.DEBUG_INFO, "Interface started successfully", common.STR_NAME, name)
	}

	return r, nil
}

func (r *Reticulum) handleInterface(iface common.NetworkInterface) {
	debug.Log(debug.DEBUG_INFO, "Setting up interface", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("%T", iface))

	ch := channel.NewChannel(&transportWrapper{r.transport})
	r.channels[iface.GetName()] = ch

	rw := buffer.CreateBidirectionalBuffer(
		1,
		2,
		ch,
		func(size int) {
			data := make([]byte, size)
			debug.Log(debug.DEBUG_PACKETS, "Interface reading bytes from buffer", common.STR_NAME, iface.GetName(), "size", size)
			iface.ProcessIncoming(data)

			if len(data) > common.ZERO {
				debug.Log(debug.DEBUG_TRACE, "Interface received packet type", common.STR_NAME, iface.GetName(), common.STR_TYPE, fmt.Sprintf("0x%02x", data[0]))
				r.transport.HandlePacket(data, iface)
			}
		},
	)

	r.buffers[iface.GetName()] = &buffer.Buffer{
		ReadWriter: rw,
	}
}

func (r *Reticulum) monitorInterfaces() {
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()

	for range ticker.C {
		for _, iface := range r.interfaces {
			if tcpClient, ok := iface.(*interfaces.TCPClientInterface); ok {
				stats := fmt.Sprintf("Interface %s status - Connected: %v, TX: %d bytes (%.2f Kbps), RX: %d bytes (%.2f Kbps)",
					iface.GetName(),
					tcpClient.IsConnected(),
					tcpClient.GetTxBytes(),
					float64(tcpClient.GetTxBytes()*8)/(5*1024),
					tcpClient.GetRxBytes(),
					float64(tcpClient.GetRxBytes()*8)/(5*1024),
				)

				if runtime.GOOS != "windows" {
					stats = fmt.Sprintf("%s, RTT: %v", stats, tcpClient.GetRTT())
				}

				debug.Log(debug.DEBUG_VERBOSE, "Interface status", "stats", stats)
			}
		}
	}
}

func main() {
	flag.Parse()
	debug.Init()
	debug.Log(debug.DEBUG_CRITICAL, "Initializing Reticulum", "debug_level", debug.GetDebugLevel())

	cfg, err := config.InitConfig()
	if err != nil {
		debug.GetLogger().Error("Failed to initialize config", common.STR_ERROR, err)
		os.Exit(1)
	}
	debug.Log(debug.DEBUG_ERROR, "Configuration loaded", "path", cfg.ConfigPath)

	r, err := NewReticulum(cfg)
	if err != nil {
		debug.GetLogger().Error("Failed to create Reticulum instance", common.STR_ERROR, err)
		os.Exit(1)
	}

	// Start monitoring interfaces
	go r.monitorInterfaces()

	// Register announce handler
	handler := NewAnnounceHandler(r, []string{"*"})
	r.transport.RegisterAnnounceHandler(handler)

	// Start Reticulum
	if err := r.Start(); err != nil {
		debug.GetLogger().Error("Failed to start Reticulum", common.STR_ERROR, err)
		os.Exit(1)
	}

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan

	debug.Log(debug.DEBUG_CRITICAL, "Shutting down...")
	if err := r.Stop(); err != nil {
		debug.Log(debug.DEBUG_CRITICAL, "Error during shutdown", common.STR_ERROR, err)
	}
	debug.Log(debug.DEBUG_CRITICAL, "Goodbye!")
}

type transportWrapper struct {
	*transport.Transport
}

func (tw *transportWrapper) GetRTT() float64 {
	return 0.1
}

func (tw *transportWrapper) RTT() float64 {
	return tw.GetRTT()
}

func (tw *transportWrapper) GetStatus() byte {
	return transport.STATUS_ACTIVE
}

func (tw *transportWrapper) Send(data []byte) interface{} {
	p := &packet.Packet{
		PacketType: packet.PacketTypeData,
		Hops:       0,
		Data:       data,
		HeaderType: packet.HeaderType1,
	}

	err := tw.Transport.SendPacket(p)
	if err != nil {
		return nil
	}
	return p
}

func (tw *transportWrapper) Resend(p interface{}) error {
	if pkt, ok := p.(*packet.Packet); ok {
		return tw.Transport.SendPacket(pkt)
	}
	return fmt.Errorf("invalid packet type")
}

func (tw *transportWrapper) SetPacketTimeout(packet interface{}, callback func(interface{}), timeout time.Duration) {
	time.AfterFunc(timeout, func() {
		callback(packet)
	})
}

func (tw *transportWrapper) SetPacketDelivered(packet interface{}, callback func(interface{})) {
	callback(packet)
}

func (tw *transportWrapper) GetLinkID() []byte {
	return nil
}

func (tw *transportWrapper) HandleInbound(pkt *packet.Packet) error {
	return nil
}

func (tw *transportWrapper) ValidateLinkProof(pkt *packet.Packet, networkIface common.NetworkInterface) error {
	return nil
}

func initializeDirectories() error {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("failed to get home directory: %v", err)
	}

	basePath := filepath.Join(homeDir, ".reticulum-go")
	dirs := []string{
		basePath,
		filepath.Join(basePath, common.STR_STORAGE),
		filepath.Join(basePath, common.STR_STORAGE, "destinations"),
		filepath.Join(basePath, common.STR_STORAGE, "identities"),
		filepath.Join(basePath, common.STR_STORAGE, "ratchets"),
		filepath.Join(basePath, common.STR_STORAGE, "cache"),
		filepath.Join(basePath, common.STR_STORAGE, "cache", "announces"),
		filepath.Join(basePath, common.STR_STORAGE, "resources"),
	}

	for _, dir := range dirs {
		if err := os.MkdirAll(dir, common.NUM_0700); err != nil { // #nosec G301
			return fmt.Errorf("failed to create directory %s: %v", dir, err)
		}
	}
	return nil
}

func (r *Reticulum) Start() error {
	debug.Log(debug.DEBUG_ERROR, "Starting Reticulum...")

	if err := r.transport.Start(); err != nil {
		return fmt.Errorf("failed to start transport: %v", err)
	}
	debug.Log(debug.DEBUG_INFO, "Transport started successfully")

	// Start interfaces
	for _, iface := range r.interfaces {
		debug.Log(debug.DEBUG_ERROR, "Starting interface", "name", iface.GetName())
		if err := iface.Start(); err != nil {
			if r.config.PanicOnInterfaceErr {
				return fmt.Errorf("failed to start interface %s: %v", iface.GetName(), err)
			}
			debug.Log(debug.DEBUG_CRITICAL, "Error starting interface", "name", iface.GetName(), "error", err)
			continue
		}

		if netIface, ok := iface.(common.NetworkInterface); ok {
			// Register interface with transport
			if err := r.transport.RegisterInterface(iface.GetName(), netIface); err != nil {
				debug.Log(debug.DEBUG_CRITICAL, "Failed to register interface with transport", "name", iface.GetName(), "error", err)
			} else {
				debug.Log(debug.DEBUG_INFO, "Registered interface with transport", "name", iface.GetName())
			}
			r.handleInterface(netIface)
		}
		debug.Log(debug.DEBUG_INFO, "Interface started successfully", "name", iface.GetName())
	}

	// Wait for interfaces to initialize
	time.Sleep(2 * time.Second)

	// Send initial announce
	debug.Log(debug.DEBUG_ERROR, "Sending initial announce")
	nodeName := "Reticulum-Go Test Node"
	r.destination.SetDefaultAppData([]byte(nodeName))
	if err := r.destination.Announce(false, nil, nil); err != nil {
		debug.Log(debug.DEBUG_CRITICAL, "Failed to send initial announce", "error", err)
	}

	// Start periodic announce goroutine
	go func() {
		// Wait a bit before the first announce
		time.Sleep(5 * time.Second)

		for {
			debug.Log(debug.DEBUG_INFO, "Announcing destination...")
			err := r.destination.Announce(false, nil, nil)
			if err != nil {
				debug.Log(debug.DEBUG_CRITICAL, "Could not send announce", "error", err)
			}

			time.Sleep(60 * time.Second)
		}
	}()

	go r.monitorInterfaces()

	debug.Log(debug.DEBUG_ERROR, "Reticulum started successfully")
	return nil
}

func (r *Reticulum) Stop() error {
	debug.Log(debug.DEBUG_ERROR, "Stopping Reticulum...")

	for _, buf := range r.buffers {
		if err := buf.Close(); err != nil {
			debug.Log(debug.DEBUG_CRITICAL, "Error closing buffer", "error", err)
		}
	}

	for _, ch := range r.channels {
		if err := ch.Close(); err != nil {
			debug.Log(debug.DEBUG_CRITICAL, "Error closing channel", "error", err)
		}
	}

	for _, iface := range r.interfaces {
		if err := iface.Stop(); err != nil {
			debug.Log(debug.DEBUG_CRITICAL, "Error stopping interface", "name", iface.GetName(), "error", err)
		}
	}

	if err := r.transport.Close(); err != nil {
		return fmt.Errorf("failed to close transport: %v", err)
	}

	debug.Log(debug.DEBUG_ERROR, "Reticulum stopped successfully")
	return nil
}

type AnnounceHandler struct {
	aspectFilter []string
	reticulum    *Reticulum
}

func NewAnnounceHandler(r *Reticulum, aspectFilter []string) *AnnounceHandler {
	return &AnnounceHandler{
		aspectFilter: aspectFilter,
		reticulum:    r,
	}
}

func (h *AnnounceHandler) AspectFilter() []string {
	return h.aspectFilter
}

func (h *AnnounceHandler) ReceivedAnnounce(destHash []byte, id interface{}, appData []byte, hops uint8) error {
	debug.Log(debug.DEBUG_INFO, "Received announce", "hash", fmt.Sprintf("%x", destHash), "hops", hops)
	debug.Log(debug.DEBUG_PACKETS, "Raw announce data", "data", fmt.Sprintf("%x", appData))
	debug.Log(debug.DEBUG_INFO, "MAIN HANDLER: Received announce", "hash", fmt.Sprintf("%x", destHash), "appData_len", len(appData), "hops", hops)

	var isNode bool
	var nodeEnabled bool
	var nodeTimestamp int64
	var nodeMaxSize int16

	// Parse msgpack appData from transport announce format
	if len(appData) > common.ZERO {
		// appData is msgpack array [name, customData]
		if appData[0] == common.HEX_0x92 { // array of 2 elements
			// Skip array header and first element (name)
			pos := common.ONE
			if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
				nameLen := int(appData[pos+1])
				pos += common.TWO + nameLen
				if pos < len(appData) && appData[pos] == common.HEX_0xC4 { // bin 8
					dataLen := int(appData[pos+1])
					if pos+2+dataLen <= len(appData) {
						customData := appData[pos+2 : pos+2+dataLen]
						nodeName := string(customData)
						debug.Log(debug.DEBUG_INFO, "Parsed node name", "name", nodeName)
						debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
					}
				}
			}
		} else {
			// Fallback: treat as raw node name
			nodeName := string(appData)
			debug.Log(debug.DEBUG_INFO, "Raw node name", "name", nodeName)
			debug.Log(debug.DEBUG_INFO, "Announced node", "name", nodeName)
		}
	} else {
		debug.Log(debug.DEBUG_INFO, "No appData (empty announce)")
	}

	// Type assert and log identity details
	if identity, ok := id.(*identity.Identity); ok {
		debug.Log(debug.DEBUG_ALL, "Identity details")
		debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", identity.GetHexHash())
		debug.Log(debug.DEBUG_ALL, "Identity public key", "key", fmt.Sprintf("%x", identity.GetPublicKey()))

		ratchets := identity.GetRatchets()
		debug.Log(debug.DEBUG_ALL, "Active ratchets", "count", len(ratchets))

		if len(ratchets) > 0 {
			ratchetKey := identity.GetCurrentRatchetKey()
			if ratchetKey != nil {
				ratchetID := identity.GetRatchetID(ratchetKey)
				debug.Log(debug.DEBUG_ALL, "Current ratchet ID", "id", fmt.Sprintf("%x", ratchetID))
			}
		}

		// Create a better record with more info
		recordType := "peer"
		if isNode {
			recordType = "node"
			debug.Log(debug.DEBUG_INFO, "Storing node in announce history", "enabled", nodeEnabled, "timestamp", nodeTimestamp, "maxsize", fmt.Sprintf("%dKB", nodeMaxSize))
		}

		h.reticulum.announceHistoryMu.Lock()
		h.reticulum.announceHistory[identity.GetHexHash()] = announceRecord{
			timestamp: time.Now().Unix(),
			appData:   appData,
		}
		h.reticulum.announceHistoryMu.Unlock()

		debug.Log(debug.DEBUG_VERBOSE, "Stored announce in history", "type", recordType, "identity", identity.GetHexHash())
	}

	return nil
}

func (h *AnnounceHandler) ReceivePathResponses() bool {
	return true
}

func (r *Reticulum) GetDestination() *destination.Destination {
	return r.destination
}

func (r *Reticulum) createNodeAppData() []byte {
	// Create a msgpack array with 3 elements
	// [Bool, Int32, Int16] for [enable, timestamp, max_transfer_size]
	appData := []byte{common.HEX_0x93} // Array with 3 elements

	// Element 0: Boolean for enable/disable peer
	if r.nodeEnabled {
		appData = append(appData, common.HEX_0xC3) // true
	} else {
		appData = append(appData, common.HEX_0xC2) // false
	}

	// Element 1: Int32 timestamp (current time)
	r.nodeTimestamp = time.Now().Unix()
	appData = append(appData, common.HEX_0xD2) // int32 format
	timeBytes := make([]byte, common.FOUR)
	binary.BigEndian.PutUint32(timeBytes, uint32(r.nodeTimestamp)) // #nosec G115
	appData = append(appData, timeBytes...)

	// Element 2: Int16 max transfer size in KB
	appData = append(appData, common.HEX_0xD1) // int16 format
	sizeBytes := make([]byte, common.TWO)
	binary.BigEndian.PutUint16(sizeBytes, uint16(r.maxTransferSize)) // #nosec G115
	appData = append(appData, sizeBytes...)

	debug.Log(debug.DEBUG_ALL, "Created node appData", "enable", r.nodeEnabled, "timestamp", r.nodeTimestamp, "maxsize", r.maxTransferSize, "data", fmt.Sprintf("%x", appData))
	return appData
}
