package libbox

import (
	"slices"
	"strings"
	"time"

	"github.com/sagernet/sing-box/daemon"
	M "github.com/sagernet/sing/common/metadata"
)

type StatusMessage struct {
	Memory           int64
	Goroutines       int32
	ConnectionsIn    int32
	ConnectionsOut   int32
	TrafficAvailable bool
	Uplink           int64
	Downlink         int64
	UplinkTotal      int64
	DownlinkTotal    int64
}

type SystemProxyStatus struct {
	Available bool
	Enabled   bool
}

type OutboundGroup struct {
	Tag        string
	Type       string
	Selectable bool
	Selected   string
	IsExpand   bool
	itemList   []*OutboundGroupItem
}

func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
	return newIterator(g.itemList)
}

type OutboundGroupIterator interface {
	Next() *OutboundGroup
	HasNext() bool
}

type OutboundGroupItem struct {
	Tag          string
	Type         string
	URLTestTime  int64
	URLTestDelay int32
}

type OutboundGroupItemIterator interface {
	Next() *OutboundGroupItem
	HasNext() bool
}

const (
	ConnectionStateAll = iota
	ConnectionStateActive
	ConnectionStateClosed
)

const (
	ConnectionEventNew = iota
	ConnectionEventUpdate
	ConnectionEventClosed
)

const (
	closedConnectionMaxAge = int64((5 * time.Minute) / time.Millisecond)
)

type ConnectionEvent struct {
	Type          int32
	ID            string
	Connection    *Connection
	UplinkDelta   int64
	DownlinkDelta int64
	ClosedAt      int64
}

type ConnectionEvents struct {
	Reset  bool
	events []*ConnectionEvent
}

func (c *ConnectionEvents) Iterator() ConnectionEventIterator {
	return newIterator(c.events)
}

type ConnectionEventIterator interface {
	Next() *ConnectionEvent
	HasNext() bool
}

type Connections struct {
	connectionMap map[string]*Connection
	input         []Connection
	filtered      []Connection
	filterState   int32
	filterApplied bool
}

func NewConnections() *Connections {
	return &Connections{
		connectionMap: make(map[string]*Connection),
	}
}

func (c *Connections) ApplyEvents(events *ConnectionEvents) {
	if events == nil {
		return
	}
	if events.Reset {
		c.connectionMap = make(map[string]*Connection)
	}

	for _, event := range events.events {
		switch event.Type {
		case ConnectionEventNew:
			if event.Connection != nil {
				conn := *event.Connection
				c.connectionMap[event.ID] = &conn
			}
		case ConnectionEventUpdate:
			if conn, ok := c.connectionMap[event.ID]; ok {
				conn.Uplink = event.UplinkDelta
				conn.Downlink = event.DownlinkDelta
				conn.UplinkTotal += event.UplinkDelta
				conn.DownlinkTotal += event.DownlinkDelta
			}
		case ConnectionEventClosed:
			if event.Connection != nil {
				conn := *event.Connection
				conn.ClosedAt = event.ClosedAt
				conn.Uplink = 0
				conn.Downlink = 0
				c.connectionMap[event.ID] = &conn
				continue
			}
			if conn, ok := c.connectionMap[event.ID]; ok {
				conn.ClosedAt = event.ClosedAt
				conn.Uplink = 0
				conn.Downlink = 0
			}
		}
	}

	c.evictClosedConnections(time.Now().UnixMilli())
	c.input = c.input[:0]
	for _, conn := range c.connectionMap {
		c.input = append(c.input, *conn)
	}
	if c.filterApplied {
		c.FilterState(c.filterState)
	} else {
		c.filtered = c.filtered[:0]
		c.filtered = append(c.filtered, c.input...)
	}
}

func (c *Connections) evictClosedConnections(nowMilliseconds int64) {
	for id, conn := range c.connectionMap {
		if conn.ClosedAt == 0 {
			continue
		}
		if nowMilliseconds-conn.ClosedAt > closedConnectionMaxAge {
			delete(c.connectionMap, id)
		}
	}
}

func (c *Connections) FilterState(state int32) {
	c.filterApplied = true
	c.filterState = state
	c.filtered = c.filtered[:0]
	switch state {
	case ConnectionStateAll:
		c.filtered = append(c.filtered, c.input...)
	case ConnectionStateActive:
		for _, connection := range c.input {
			if connection.ClosedAt == 0 {
				c.filtered = append(c.filtered, connection)
			}
		}
	case ConnectionStateClosed:
		for _, connection := range c.input {
			if connection.ClosedAt != 0 {
				c.filtered = append(c.filtered, connection)
			}
		}
	}
}

func (c *Connections) SortByDate() {
	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
		if x.CreatedAt < y.CreatedAt {
			return 1
		} else if x.CreatedAt > y.CreatedAt {
			return -1
		} else {
			return strings.Compare(y.ID, x.ID)
		}
	})
}

func (c *Connections) SortByTraffic() {
	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
		xTraffic := x.Uplink + x.Downlink
		yTraffic := y.Uplink + y.Downlink
		if xTraffic < yTraffic {
			return 1
		} else if xTraffic > yTraffic {
			return -1
		} else {
			return strings.Compare(y.ID, x.ID)
		}
	})
}

func (c *Connections) SortByTrafficTotal() {
	slices.SortStableFunc(c.filtered, func(x, y Connection) int {
		xTraffic := x.UplinkTotal + x.DownlinkTotal
		yTraffic := y.UplinkTotal + y.DownlinkTotal
		if xTraffic < yTraffic {
			return 1
		} else if xTraffic > yTraffic {
			return -1
		} else {
			return strings.Compare(y.ID, x.ID)
		}
	})
}

func (c *Connections) Iterator() ConnectionIterator {
	return newPtrIterator(c.filtered)
}

type ProcessInfo struct {
	ProcessID    int64
	UserID       int32
	UserName     string
	ProcessPath  string
	packageNames []string
}

func (p *ProcessInfo) PackageNames() StringIterator {
	return newIterator(p.packageNames)
}

type Connection struct {
	ID            string
	Inbound       string
	InboundType   string
	IPVersion     int32
	Network       string
	Source        string
	Destination   string
	Domain        string
	Protocol      string
	User          string
	FromOutbound  string
	CreatedAt     int64
	ClosedAt      int64
	Uplink        int64
	Downlink      int64
	UplinkTotal   int64
	DownlinkTotal int64
	Rule          string
	Outbound      string
	OutboundType  string
	chainList     []string
	ProcessInfo   *ProcessInfo
}

func (c *Connection) Chain() StringIterator {
	return newIterator(c.chainList)
}

func (c *Connection) DisplayDestination() string {
	destination := M.ParseSocksaddr(c.Destination)
	if destination.IsIP() && c.Domain != "" {
		destination = M.Socksaddr{
			Fqdn: c.Domain,
			Port: destination.Port,
		}
		return destination.String()
	}
	return c.Destination
}

type ConnectionIterator interface {
	Next() *Connection
	HasNext() bool
}

func statusMessageFromGRPC(status *daemon.Status) *StatusMessage {
	if status == nil {
		return nil
	}
	return &StatusMessage{
		Memory:           int64(status.Memory),
		Goroutines:       status.Goroutines,
		ConnectionsIn:    status.ConnectionsIn,
		ConnectionsOut:   status.ConnectionsOut,
		TrafficAvailable: status.TrafficAvailable,
		Uplink:           status.Uplink,
		Downlink:         status.Downlink,
		UplinkTotal:      status.UplinkTotal,
		DownlinkTotal:    status.DownlinkTotal,
	}
}

func outboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator {
	if groups == nil || len(groups.Group) == 0 {
		return newIterator([]*OutboundGroup{})
	}
	var libboxGroups []*OutboundGroup
	for _, g := range groups.Group {
		libboxGroup := &OutboundGroup{
			Tag:        g.Tag,
			Type:       g.Type,
			Selectable: g.Selectable,
			Selected:   g.Selected,
			IsExpand:   g.IsExpand,
		}
		for _, item := range g.Items {
			libboxGroup.itemList = append(libboxGroup.itemList, &OutboundGroupItem{
				Tag:          item.Tag,
				Type:         item.Type,
				URLTestTime:  item.UrlTestTime,
				URLTestDelay: item.UrlTestDelay,
			})
		}
		libboxGroups = append(libboxGroups, libboxGroup)
	}
	return newIterator(libboxGroups)
}

func outboundGroupItemListFromGRPC(list *daemon.OutboundList) OutboundGroupItemIterator {
	if list == nil || len(list.Outbounds) == 0 {
		return newIterator([]*OutboundGroupItem{})
	}
	var items []*OutboundGroupItem
	for _, ob := range list.Outbounds {
		items = append(items, &OutboundGroupItem{
			Tag:          ob.Tag,
			Type:         ob.Type,
			URLTestTime:  ob.UrlTestTime,
			URLTestDelay: ob.UrlTestDelay,
		})
	}
	return newIterator(items)
}

func connectionFromGRPC(conn *daemon.Connection) Connection {
	var processInfo *ProcessInfo
	if conn.ProcessInfo != nil {
		processInfo = &ProcessInfo{
			ProcessID:    int64(conn.ProcessInfo.ProcessId),
			UserID:       conn.ProcessInfo.UserId,
			UserName:     conn.ProcessInfo.UserName,
			ProcessPath:  conn.ProcessInfo.ProcessPath,
			packageNames: conn.ProcessInfo.PackageNames,
		}
	}
	return Connection{
		ID:            conn.Id,
		Inbound:       conn.Inbound,
		InboundType:   conn.InboundType,
		IPVersion:     conn.IpVersion,
		Network:       conn.Network,
		Source:        conn.Source,
		Destination:   conn.Destination,
		Domain:        conn.Domain,
		Protocol:      conn.Protocol,
		User:          conn.User,
		FromOutbound:  conn.FromOutbound,
		CreatedAt:     conn.CreatedAt,
		ClosedAt:      conn.ClosedAt,
		Uplink:        conn.Uplink,
		Downlink:      conn.Downlink,
		UplinkTotal:   conn.UplinkTotal,
		DownlinkTotal: conn.DownlinkTotal,
		Rule:          conn.Rule,
		Outbound:      conn.Outbound,
		OutboundType:  conn.OutboundType,
		chainList:     conn.ChainList,
		ProcessInfo:   processInfo,
	}
}

func connectionEventFromGRPC(event *daemon.ConnectionEvent) *ConnectionEvent {
	if event == nil {
		return nil
	}
	libboxEvent := &ConnectionEvent{
		Type:          int32(event.Type),
		ID:            event.Id,
		UplinkDelta:   event.UplinkDelta,
		DownlinkDelta: event.DownlinkDelta,
		ClosedAt:      event.ClosedAt,
	}
	if event.Connection != nil {
		conn := connectionFromGRPC(event.Connection)
		libboxEvent.Connection = &conn
	}
	return libboxEvent
}

func connectionEventsFromGRPC(events *daemon.ConnectionEvents) *ConnectionEvents {
	if events == nil {
		return nil
	}
	libboxEvents := &ConnectionEvents{
		Reset: events.Reset_,
	}
	for _, event := range events.Events {
		if libboxEvent := connectionEventFromGRPC(event); libboxEvent != nil {
			libboxEvents.events = append(libboxEvents.events, libboxEvent)
		}
	}
	return libboxEvents
}

func systemProxyStatusFromGRPC(status *daemon.SystemProxyStatus) *SystemProxyStatus {
	if status == nil {
		return nil
	}
	return &SystemProxyStatus{
		Available: status.Available,
		Enabled:   status.Enabled,
	}
}

func systemProxyStatusToGRPC(status *SystemProxyStatus) *daemon.SystemProxyStatus {
	if status == nil {
		return nil
	}
	return &daemon.SystemProxyStatus{
		Available: status.Available,
		Enabled:   status.Enabled,
	}
}
