package wifi

import (
	"bytes"
	"net"
	"time"

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

	"github.com/gopacket/gopacket"
	"github.com/gopacket/gopacket/layers"
)

func (mod *WiFiModule) stationPruner() {
	mod.reads.Add(1)
	defer mod.reads.Done()

	maxApTTL := time.Duration(mod.apTTL) * time.Second
	maxStaTTL := time.Duration(mod.staTTL) * time.Second

	mod.Debug("wifi stations pruner started (ap.ttl:%v sta.ttl:%v).", maxApTTL, maxStaTTL)
	for mod.Running() {
		// loop every AP
		for _, ap := range mod.Session.WiFi.List() {
			sinceLastSeen := time.Since(ap.LastSeen)
			if sinceLastSeen > maxApTTL {
				mod.Debug("station %s not seen in %s, removing.", ap.BSSID(), sinceLastSeen)
				mod.Session.WiFi.Remove(ap.BSSID())
				continue
			}
			// loop every AP client
			for _, c := range ap.Clients() {
				sinceLastSeen := time.Since(c.LastSeen)
				if sinceLastSeen > maxStaTTL {
					mod.Debug("client %s of station %s not seen in %s, removing.", c.String(), ap.BSSID(), sinceLastSeen)
					ap.RemoveClient(c.BSSID())

					mod.Session.Events.Add("wifi.client.lost", ClientEvent{
						AP:     ap,
						Client: c,
					})
				}
			}
		}
		time.Sleep(1 * time.Second)
		// refresh
		maxApTTL = time.Duration(mod.apTTL) * time.Second
		maxStaTTL = time.Duration(mod.staTTL) * time.Second
	}
}

func (mod *WiFiModule) discoverAccessPoints(radiotap *layers.RadioTap, dot11 *layers.Dot11, packet gopacket.Packet) {
	// search for Dot11InformationElementIDSSID
	if ok, ssid := packets.Dot11ParseIDSSID(packet); ok {
		from := dot11.Address3

		// skip stuff we're sending
		if mod.apRunning && bytes.Equal(from, mod.apConfig.BSSID) {
			return
		}

		if !network.IsZeroMac(from) && !network.IsBroadcastMac(from) {
			if int(radiotap.DBMAntennaSignal) >= mod.minRSSI {
				var frequency int
				bssid := from.String()

				if found, channel := packets.Dot11ParseDSSet(packet); found {
					frequency = network.Dot11Chan2Freq(channel)
				} else {
					frequency = int(radiotap.ChannelFrequency)
				}

				if ap, isNew := mod.Session.WiFi.AddIfNew(ssid, bssid, frequency, radiotap.DBMAntennaSignal); !isNew {
					//set beacon packet on the access point station.
					//This is for it to be included in the saved handshake file for wifi.assoc
					ap.Station.Handshake.Beacon = packet
					ap.EachClient(func(mac string, station *network.Station) {
						station.Handshake.SetBeacon(packet)
					})
				}
			} else {
				mod.Debug("skipping %s with %d dBm", from.String(), radiotap.DBMAntennaSignal)
			}
		}
	}
}

func (mod *WiFiModule) startProbing(staMac net.HardwareAddr, ssid string) error {
	// if not already running, temporarily enable the pcap handle
	// for packet injection
	if !mod.Running() {
		if err := mod.Configure(); err != nil {
			return err
		}
		defer mod.handle.Close()
	}

	for seq := uint16(0); seq < 5 && mod.Running(); seq++ {
		if err, pkt := packets.NewDot11ProbeRequest(staMac, seq, ssid, network.GetInterfaceChannel(mod.iface.Name())); err != nil {
			mod.Error("could not create probe packet: %s", err)
			continue
		} else {
			mod.injectPacket(pkt)
		}
	}

	mod.Debug("sent probe frames")

	return nil
}

func (mod *WiFiModule) discoverProbes(radiotap *layers.RadioTap, dot11 *layers.Dot11, packet gopacket.Packet) {
	if dot11.Type != layers.Dot11TypeMgmtProbeReq {
		return
	}

	reqLayer := packet.Layer(layers.LayerTypeDot11MgmtProbeReq)
	if reqLayer == nil {
		return
	}

	req, ok := reqLayer.(*layers.Dot11MgmtProbeReq)
	if !ok {
		return
	}

	// skip stuff we're sending
	if bytes.Equal(mod.probeMac, dot11.Address2) {
		return
	}

	clientSTA := network.NormalizeMac(dot11.Address2.String())
	if mod.filterProbeSTA != nil && !mod.filterProbeSTA.MatchString(clientSTA) {
		return
	}

	tot := len(req.Contents)
	if tot < 3 {
		return
	}

	avail := uint32(tot - 2)
	if avail < 2 {
		return
	}
	size := uint32(req.Contents[1])
	if size == 0 || size > avail {
		return
	}

	apSSID := string(req.Contents[2 : 2+size])
	if mod.filterProbeAP != nil && !mod.filterProbeAP.MatchString(apSSID) {
		return
	}

	mod.Session.Events.Add("wifi.client.probe", ProbeEvent{
		FromAddr:   clientSTA,
		FromVendor: network.ManufLookup(clientSTA),
		FromAlias:  mod.Session.Lan.GetAlias(clientSTA),
		SSID:       apSSID,
		RSSI:       radiotap.DBMAntennaSignal,
	})
}

func (mod *WiFiModule) discoverClients(radiotap *layers.RadioTap, dot11 *layers.Dot11, packet gopacket.Packet) {
	mod.Session.WiFi.EachAccessPoint(func(bssid string, ap *network.AccessPoint) {
		// packet going to this specific BSSID?
		if packets.Dot11IsDataFor(dot11, ap.HW) {
			bssid := dot11.Address2.String()
			freq := int(radiotap.ChannelFrequency)
			rssi := radiotap.DBMAntennaSignal

			if station, isNew := ap.AddClientIfNew(bssid, freq, rssi); isNew {
				mod.Session.Events.Add("wifi.client.new", ClientEvent{
					AP:     ap,
					Client: station,
				})
			}
		}
	})
}

func (mod *WiFiModule) discoverDeauths(radiotap *layers.RadioTap, dot11 *layers.Dot11, packet gopacket.Packet) {
	if dot11.Type != layers.Dot11TypeMgmtDeauthentication {
		return
	}

	// ignore deauth frames that we sent
	if radiotap.ChannelFrequency == 0 {
		return
	}

	deauthLayer := packet.Layer(layers.LayerTypeDot11MgmtDeauthentication)
	if deauthLayer == nil {
		return
	}

	deauth, ok := deauthLayer.(*layers.Dot11MgmtDeauthentication)
	reason := "?"
	if ok {
		reason = deauth.Reason.String()
	}

	// trigger events only if the deauth is coming from an AP we know of
	source := dot11.Address1.String()
	ap, found := mod.Session.WiFi.Get(source)
	if !found {
		mod.Debug("skipping deauth frame from %s", source)
		return
	}

	mod.Debug("deauth radio %#v", radiotap)

	mod.Session.Events.Add("wifi.deauthentication", DeauthEvent{
		RSSI:     radiotap.DBMAntennaSignal,
		AP:       ap,
		Address1: source,
		Address2: dot11.Address2.String(),
		Address3: dot11.Address3.String(),
		Reason:   reason,
	})
}
