package wifi

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

	"github.com/bettercap/bettercap/v2/modules/net_recon"
	"github.com/bettercap/bettercap/v2/network"
	"github.com/bettercap/bettercap/v2/session"

	"github.com/dustin/go-humanize"

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

func (mod *WiFiModule) isApSelected() bool {
	return mod.ap != nil
}

func (mod *WiFiModule) getRow(station *network.Station) ([]string, bool) {
	rssi := network.ColorRSSI(int(station.RSSI))
	bssid := station.HwAddress
	sinceStarted := time.Since(mod.Session.StartedAt)
	sinceFirstSeen := time.Since(station.FirstSeen)
	if sinceStarted > (net_recon.JustJoinedTimeInterval*2) && sinceFirstSeen <= net_recon.JustJoinedTimeInterval {
		// if endpoint was first seen in the last 10 seconds
		bssid = tui.Bold(bssid)
	}

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

	ssid := ops.Ternary(station.ESSID() == "<hidden>", tui.Dim(station.ESSID()), station.ESSID()).(string)

	encryption := station.Encryption
	if len(station.Cipher) > 0 {
		encryption = fmt.Sprintf("%s (%s, %s)", station.Encryption, station.Cipher, station.Authentication)
	}

	if encryption == "OPEN" || encryption == "" {
		encryption = tui.Green("OPEN")
		ssid = tui.Green(ssid)
		bssid = tui.Green(bssid)
	} else {
		// this is ugly, but necessary in order to have this
		// method handle both access point and clients
		// transparently
		if ap, found := mod.Session.WiFi.Get(station.HwAddress); found && ap.HasKeyMaterial() {
			encryption = tui.Red(encryption)
		}
	}

	sent := ops.Ternary(station.Sent > 0, humanize.Bytes(station.Sent), "").(string)
	recvd := ops.Ternary(station.Received > 0, humanize.Bytes(station.Received), "").(string)

	include := false
	if mod.source == "" {
		for _, frequencies := range mod.frequencies {
			if frequencies == station.Frequency {
				include = true
				break
			}
		}
	} else {
		include = true
	}

	if int(station.RSSI) < mod.minRSSI {
		include = false
	}

	if mod.isApSelected() {
		if mod.showManuf {
			return []string{
				rssi,
				bssid,
				tui.Dim(station.Vendor),
				strconv.Itoa(station.Channel),
				sent,
				recvd,
				seen,
			}, include
		} else {
			return []string{
				rssi,
				bssid,
				strconv.Itoa(station.Channel),
				sent,
				recvd,
				seen,
			}, include
		}
	} else {
		// this is ugly, but necessary in order to have this
		// method handle both access point and clients
		// transparently
		clients := ""
		if ap, found := mod.Session.WiFi.Get(station.HwAddress); found {
			if ap.NumClients() > 0 {
				clients = strconv.Itoa(ap.NumClients())
			}
		}

		wps := ""
		if station.HasWPS() {
			if ver, found := station.WPS["Version"]; found {
				wps = ver
			} else {
				wps = "✔"
			}

			if state, found := station.WPS["State"]; found {
				if state == "Not Configured" {
					wps += " (not configured)"
				}
			}

			wps = tui.Dim(tui.Yellow(wps))
		}

		if mod.showManuf {
			return []string{
				rssi,
				bssid,
				tui.Dim(station.Vendor),
				ssid,
				encryption,
				wps,
				strconv.Itoa(station.Channel),
				clients,
				sent,
				recvd,
				seen,
			}, include
		} else {
			return []string{
				rssi,
				bssid,
				ssid,
				encryption,
				wps,
				strconv.Itoa(station.Channel),
				clients,
				sent,
				recvd,
				seen,
			}, include
		}
	}
}

func (mod *WiFiModule) doFilter(station *network.Station) bool {
	if mod.selector.Expression == nil {
		return true
	}
	return mod.selector.Expression.MatchString(station.BSSID()) ||
		mod.selector.Expression.MatchString(station.ESSID()) ||
		mod.selector.Expression.MatchString(station.Alias) ||
		mod.selector.Expression.MatchString(station.Vendor) ||
		mod.selector.Expression.MatchString(station.Encryption)
}

func (mod *WiFiModule) doSelection() (err error, stations []*network.Station) {
	if err = mod.selector.Update(); err != nil {
		return
	}

	apSelected := mod.isApSelected()
	if apSelected {
		if ap, found := mod.Session.WiFi.Get(mod.ap.HwAddress); found {
			stations = ap.Clients()
		} else {
			err = fmt.Errorf("could not find station %s", mod.ap.HwAddress)
			return
		}
	} else {
		stations = mod.Session.WiFi.Stations()
	}

	filtered := []*network.Station{}
	for _, station := range stations {
		if mod.doFilter(station) {
			filtered = append(filtered, station)
		}
	}
	stations = filtered

	switch mod.selector.SortField {
	case "seen":
		sort.Sort(ByWiFiSeenSorter(stations))
	case "essid":
		sort.Sort(ByEssidSorter(stations))
	case "bssid":
		sort.Sort(ByBssidSorter(stations))
	case "channel":
		sort.Sort(ByChannelSorter(stations))
	case "clients":
		sort.Sort(ByClientsSorter(stations))
	case "encryption":
		sort.Sort(ByEncryptionSorter(stations))
	case "sent":
		sort.Sort(ByWiFiSentSorter(stations))
	case "rcvd":
		sort.Sort(ByWiFiRcvdSorter(stations))
	case "rssi":
		sort.Sort(ByRSSISorter(stations))
	default:
		sort.Sort(ByRSSISorter(stations))
	}

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

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

	return
}

func (mod *WiFiModule) colDecorate(colNames []string, name string, dir string) {
	for i, c := range colNames {
		if c == name {
			colNames[i] += " " + dir
			break
		}
	}
}

func (mod *WiFiModule) colNames(nrows int) []string {
	columns := []string(nil)

	if !mod.isApSelected() {
		if mod.showManuf {
			columns = []string{"RSSI", "BSSID", "Manufacturer", "SSID", "Encryption", "WPS", "Ch", "Clients", "Sent", "Recvd", "Seen"}
		} else {
			columns = []string{"RSSI", "BSSID", "SSID", "Encryption", "WPS", "Ch", "Clients", "Sent", "Recvd", "Seen"}
		}
	} else if nrows > 0 {
		if mod.showManuf {
			columns = []string{"RSSI", "BSSID", "Manufacturer", "Ch", "Sent", "Recvd", "Seen"}
		} else {
			columns = []string{"RSSI", "BSSID", "Ch", "Sent", "Recvd", "Seen"}
		}
		mod.Printf("\n%s clients:\n", mod.ap.HwAddress)
	} else {
		mod.Printf("\nNo authenticated clients detected for %s.\n", mod.ap.HwAddress)
	}

	if columns != nil {
		switch mod.selector.SortField {
		case "seen":
			mod.colDecorate(columns, "Seen", mod.selector.SortSymbol)
		case "essid":
			mod.colDecorate(columns, "SSID", mod.selector.SortSymbol)
		case "bssid":
			mod.colDecorate(columns, "BSSID", mod.selector.SortSymbol)
		case "channel":
			mod.colDecorate(columns, "Ch", mod.selector.SortSymbol)
		case "clients":
			mod.colDecorate(columns, "Clients", mod.selector.SortSymbol)
		case "encryption":
			mod.colDecorate(columns, "Encryption", mod.selector.SortSymbol)
		case "sent":
			mod.colDecorate(columns, "Sent", mod.selector.SortSymbol)
		case "rcvd":
			mod.colDecorate(columns, "Recvd", mod.selector.SortSymbol)
		case "rssi":
			mod.colDecorate(columns, "RSSI", mod.selector.SortSymbol)
		}
	}

	return columns
}

func (mod *WiFiModule) showStatusBar() {
	parts := []string{
		fmt.Sprintf("%s (ch. %d)", mod.iface.Name(), network.GetInterfaceChannel(mod.iface.Name())),
		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))
	}

	if nHandshakes := mod.Session.WiFi.NumHandshakes(); nHandshakes > 0 {
		parts = append(parts, fmt.Sprintf("%d handshakes", nHandshakes))
	}

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

func (mod *WiFiModule) Show() (err error) {
	// module has not been started yet
	if mod.iface == nil {
		if err := mod.Configure(); err != nil {
			return err
		}
	}

	var stations []*network.Station
	if err, stations = mod.doSelection(); err != nil {
		return
	}

	if err, mod.showManuf = mod.BoolParam("wifi.show.manufacturer"); err != nil {
		return err
	}

	rows := make([][]string, 0)
	for _, s := range stations {
		if row, include := mod.getRow(s); include {
			rows = append(rows, row)
		}
	}
	nrows := len(rows)
	if nrows > 0 {
		tui.Table(mod.Session.Events.Stdout, mod.colNames(nrows), rows)
	}

	mod.showStatusBar()

	mod.Session.Refresh()

	return nil
}

func (mod *WiFiModule) ShowWPS(bssid string) (err error) {
	if mod.Running() == false {
		return session.ErrAlreadyStopped(mod.Name())
	}

	toShow := []*network.Station{}
	if bssid == network.BroadcastMac {
		for _, station := range mod.Session.WiFi.List() {
			if station.HasWPS() {
				toShow = append(toShow, station.Station)
			}
		}
	} else {
		if station, found := mod.Session.WiFi.Get(bssid); found {
			if station.HasWPS() {
				toShow = append(toShow, station.Station)
			}
		}
	}

	if len(toShow) == 0 {
		return fmt.Errorf("no WPS enabled access points matched the criteria")
	}

	sort.Sort(ByBssidSorter(toShow))

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

	for _, station := range toShow {
		ssid := ops.Ternary(station.ESSID() == "<hidden>", tui.Dim(station.ESSID()), station.ESSID()).(string)

		rows := [][]string{
			{tui.Green("essid"), ssid},
			{tui.Green("bssid"), station.BSSID()},
		}

		keys := []string{}
		for name := range station.WPS {
			keys = append(keys, name)
		}
		sort.Strings(keys)

		for _, name := range keys {
			rows = append(rows, []string{
				tui.Green(name),
				tui.Yellow(station.WPS[name]),
			})
		}

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

	return nil
}
