package net_recon

import (
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/bettercap/bettercap/v2/modules/syn_scan"

	"github.com/bettercap/bettercap/v2/network"
	"github.com/bettercap/bettercap/v2/packets"

	"github.com/dustin/go-humanize"

	"github.com/evilsocket/islazy/str"
	"github.com/evilsocket/islazy/tui"
)

const (
	AliveTimeInterval      = time.Duration(10) * time.Second
	PresentTimeInterval    = time.Duration(1) * time.Minute
	JustJoinedTimeInterval = time.Duration(10) * time.Second
)

type ProtoPair struct {
	Protocol string
	Hits     uint64
}

type ProtoPairList []ProtoPair

func (p ProtoPairList) Len() int           { return len(p) }
func (p ProtoPairList) Less(i, j int) bool { return p[i].Hits < p[j].Hits }
func (p ProtoPairList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func (mod *Discovery) getRow(e *network.Endpoint, withMeta bool) [][]string {
	sinceStarted := time.Since(mod.Session.StartedAt)
	sinceFirstSeen := time.Since(e.FirstSeen)

	addr := e.IpAddress
	mac := e.HwAddress
	if mod.Session.Lan.WasMissed(e.HwAddress) {
		// if endpoint was not found in ARP at least once
		addr = tui.Dim(addr)
		mac = tui.Dim(mac)
	} else if sinceStarted > (JustJoinedTimeInterval*2) && sinceFirstSeen <= JustJoinedTimeInterval {
		// if endpoint was first seen in the last 10 seconds
		addr = tui.Bold(addr)
		mac = tui.Bold(mac)
	}

	name := ""
	if e == mod.Session.Interface {
		name = e.Name()
	} else if e == mod.Session.Gateway {
		name = "gateway"
	} else if e.Alias != "" {
		name = tui.Green(e.Alias)
	} else if e.Hostname != "" {
		name = tui.Yellow(e.Hostname)
	}

	var traffic *packets.Traffic
	var found bool
	var v interface{}
	if v, found = mod.Session.Queue.Traffic.Load(e.IpAddress); !found {
		traffic = &packets.Traffic{}
	} else {
		traffic = v.(*packets.Traffic)
	}

	seen := e.LastSeen.Format("15:04:05")
	sinceLastSeen := time.Since(e.LastSeen)
	if sinceStarted > AliveTimeInterval && sinceLastSeen <= AliveTimeInterval {
		// if endpoint seen in the last 10 seconds
		seen = tui.Bold(seen)
	} else if sinceLastSeen <= PresentTimeInterval {
		// if endpoint seen in the last 60 seconds
	} else {
		// not seen in a while
		seen = tui.Dim(seen)
	}

	row := []string{
		addr,
		mac,
		name,
		tui.Dim(e.Vendor),
		humanize.Bytes(traffic.Sent),
		humanize.Bytes(traffic.Received),
		seen,
	}

	if !withMeta {
		return [][]string{row}
	} else if e.Meta.Empty() {
		return [][]string{append(row, tui.Dim("-"))}
	}

	metas := []string{}
	e.Meta.Each(func(name string, value interface{}) {
		s := ""
		if sv, ok := value.(string); ok {
			s = sv
		} else {
			s = fmt.Sprintf("%+v", value)
		}

		metas = append(metas, fmt.Sprintf("%s:%s", tui.Green(name), tui.Yellow(s)))
	})
	sort.Strings(metas)

	rows := make([][]string, 0, len(metas))
	for i, m := range metas {
		if i == 0 {
			rows = append(rows, append(row, m))
		} else {
			rows = append(rows, []string{"", "", "", "", "", "", "", m})
		}
	}

	return rows
}

func (mod *Discovery) doFilter(target *network.Endpoint) bool {
	if mod.selector.Expression == nil {
		return true
	}
	return mod.selector.Expression.MatchString(target.IpAddress) ||
		mod.selector.Expression.MatchString(target.Ip6Address) ||
		mod.selector.Expression.MatchString(target.HwAddress) ||
		mod.selector.Expression.MatchString(target.Hostname) ||
		mod.selector.Expression.MatchString(target.Alias) ||
		mod.selector.Expression.MatchString(target.Vendor)
}

func (mod *Discovery) doSelection(arg string) (err error, targets []*network.Endpoint) {
	if err = mod.selector.Update(); err != nil {
		return
	}

	if arg != "" {
		if targets, err = network.ParseEndpoints(arg, mod.Session.Lan); err != nil {
			return
		}
	} else {
		targets = mod.Session.Lan.List()
	}

	filtered := []*network.Endpoint{}
	for _, target := range targets {
		if mod.doFilter(target) {
			filtered = append(filtered, target)
		}
	}
	targets = filtered

	switch mod.selector.SortField {
	case "ip":
		sort.Sort(ByIpSorter(targets))
	case "mac":
		sort.Sort(ByMacSorter(targets))
	case "seen":
		sort.Sort(BySeenSorter(targets))
	case "sent":
		sort.Sort(BySentSorter(targets))
	case "rcvd":
		sort.Sort(ByRcvdSorter(targets))
	default:
		sort.Sort(ByAddressSorter(targets))
	}

	// default is asc
	if mod.selector.Sort == "desc" {
		// from https://github.com/golang/go/wiki/SliceTricks
		for i := len(targets)/2 - 1; i >= 0; i-- {
			opp := len(targets) - 1 - i
			targets[i], targets[opp] = targets[opp], targets[i]
		}
	}

	if mod.selector.Limit > 0 {
		limit := mod.selector.Limit
		max := len(targets)
		if limit > max {
			limit = max
		}
		targets = targets[0:limit]
	}

	return
}

func (mod *Discovery) colNames(hasMeta bool) []string {
	colNames := []string{"IP", "MAC", "Name", "Vendor", "Sent", "Recvd", "Seen"}
	if hasMeta {
		colNames = append(colNames, "Meta")
	}

	switch mod.selector.SortField {
	case "mac":
		colNames[1] += " " + mod.selector.SortSymbol
	case "sent":
		colNames[4] += " " + mod.selector.SortSymbol
	case "rcvd":
		colNames[5] += " " + mod.selector.SortSymbol
	case "seen":
		colNames[6] += " " + mod.selector.SortSymbol
	case "ip":
		colNames[0] += " " + mod.selector.SortSymbol
	}

	return colNames
}

func (mod *Discovery) showStatusBar() {
	parts := []string{
		fmt.Sprintf("%s %s", tui.Red("↑"), humanize.Bytes(mod.Session.Queue.Stats.Sent)),
		fmt.Sprintf("%s %s", tui.Green("↓"), humanize.Bytes(mod.Session.Queue.Stats.Received)),
		fmt.Sprintf("%d pkts", mod.Session.Queue.Stats.PktReceived),
	}

	if nErrors := mod.Session.Queue.Stats.Errors; nErrors > 0 {
		parts = append(parts, fmt.Sprintf("%d errs", nErrors))
	}

	mod.Printf("\n%s\n\n", strings.Join(parts, " / "))
}

func (mod *Discovery) Show(arg string) (err error) {
	var targets []*network.Endpoint
	if err, targets = mod.doSelection(arg); err != nil {
		return
	}

	pad := 1
	if mod.Session.Interface.HwAddress == mod.Session.Gateway.HwAddress {
		pad = 0
		targets = append([]*network.Endpoint{mod.Session.Interface}, targets...)
	} else {
		targets = append([]*network.Endpoint{mod.Session.Interface, mod.Session.Gateway}, targets...)
	}

	hasMeta := false
	if err, showMeta := mod.BoolParam("net.show.meta"); err != nil {
		return err
	} else if showMeta {
		for _, t := range targets {
			if !t.Meta.Empty() {
				hasMeta = true
				break
			}
		}
	}

	colNames := mod.colNames(hasMeta)
	padCols := make([]string, len(colNames))

	rows := make([][]string, 0)
	for i, t := range targets {
		rows = append(rows, mod.getRow(t, hasMeta)...)
		if i == pad {
			rows = append(rows, padCols)
		}
	}

	tui.Table(mod.Session.Events.Stdout, colNames, rows)

	mod.showStatusBar()

	mod.Session.Refresh()

	return nil
}

func (mod *Discovery) showMeta(arg string) (err error) {
	var targets []*network.Endpoint
	if err, targets = mod.doSelection(arg); err != nil {
		return
	}

	colNames := []string{"Name", "Value"}
	any := false

	for _, t := range targets {
		keys := []string{}

		t.Meta.Each(func(name string, value interface{}) {
			keys = append(keys, name)
		})

		if len(keys) > 0 {
			sort.Strings(keys)
			rows := [][]string{
				{
					tui.Green("address"),
					t.IP.String(),
				},
			}

			for _, k := range keys {
				meta := t.Meta.Get(k)
				val := ""
				if s, ok := meta.(string); ok {
					val = s
				} else if ports, ok := meta.(map[int]*syn_scan.OpenPort); ok {
					val = "ports: "
					for _, info := range ports {
						val += fmt.Sprintf("%s:%d", info.Proto, info.Port)
						if info.Service != "" {
							val += fmt.Sprintf("(%s)", info.Service)
						}
						if info.Banner != "" {
							val += fmt.Sprintf(" [%s]", info.Banner)
						}
						val += " "
					}
					val = str.Trim(val)
				} else {
					val = fmt.Sprintf("%#v", meta)
				}
				rows = append(rows, []string{
					tui.Green(k),
					tui.Yellow(val),
				})
			}

			any = true
			tui.Table(mod.Session.Events.Stdout, colNames, rows)
		}
	}

	if any {
		mod.Session.Refresh()
	}

	return nil
}
