package wifi

import (
	"bytes"
	"fmt"
	"net"
	"os"
	"path"

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

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

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

func allZeros(s []byte) bool {
	for _, v := range s {
		if v != 0 {
			return false
		}
	}
	return true
}

func (mod *WiFiModule) getHandshakeFileFor(ap *network.AccessPoint) string {
	shakesFileName := mod.shakesFile
	if !mod.shakesAggregate {
		parentDir := path.Dir(shakesFileName)
		// check for existing directory at "shakesFileName" for backwards compatibility
                fileInfo, err := os.Stat(shakesFileName)
                if (err == nil) && (fileInfo.IsDir()) {
			parentDir = shakesFileName
		}
		shakesFileName = path.Join(parentDir, fmt.Sprintf("%s.pcap", ap.PathFriendlyName()))
	}
	return shakesFileName
}

func (mod *WiFiModule) discoverHandshakes(radiotap *layers.RadioTap, dot11 *layers.Dot11, packet gopacket.Packet) {
	isEAPOL := false

	if ok, key, apMac, staMac := packets.Dot11ParseEAPOL(packet, dot11); ok {
		isEAPOL = true

		// first, locate the AP in our list by its BSSID
		ap, found := mod.Session.WiFi.Get(apMac.String())
		if !found {
			mod.Warning("could not find AP with BSSID %s", apMac.String())
			return
		}

		// locate the client station, if its BSSID is ours, it means we sent
		// an association request via wifi.assoc because we're trying to capture
		// the PMKID from the first EAPOL sent by the AP.
		// (Reference about PMKID https://hashcat.net/forum/thread-7717.html)
		// In this case, we need to add ourselves as a client station of the AP
		// in order to have a consistent association of AP, client and handshakes.
		staIsUs := bytes.Equal(staMac, mod.iface.HW)
		station, found := ap.Get(staMac.String())
		staAdded := false
		if !found {
			station, staAdded = ap.AddClientIfNew(staMac.String(), ap.Frequency, ap.RSSI)
		}

		rawPMKID := []byte(nil)
		if !key.Install && key.KeyACK && !key.KeyMIC {
			// [1] (ACK) AP is sending ANonce to the client
			rawPMKID = station.Handshake.AddAndGetPMKID(packet)
			PMKID := "without PMKID"
			if rawPMKID != nil {
				PMKID = "with PMKID"
			}

			mod.Debug("got frame 1/4 of the %s <-> %s handshake (%s) (anonce:%x)",
				apMac,
				staMac,
				PMKID,
				key.Nonce)

			//add the ap's station's beacon packet to be saved as part of the handshake cap file
			//https://github.com/ZerBea/hcxtools/issues/92
			//https://github.com/bettercap/bettercap/issues/592

			if ap.Station.Handshake.Beacon != nil {
				mod.Debug("adding beacon frame to handshake for %s", apMac)
				station.Handshake.AddFrame(1, ap.Station.Handshake.Beacon)
			}

		} else if !key.Install && !key.KeyACK && key.KeyMIC && !allZeros(key.Nonce) {
			// [2] (MIC) client is sending SNonce+MIC to the API
			station.Handshake.AddFrame(1, packet)

			mod.Debug("got frame 2/4 of the %s <-> %s handshake (snonce:%x mic:%x)",
				apMac,
				staMac,
				key.Nonce,
				key.MIC)
		} else if key.Install && key.KeyACK && key.KeyMIC {
			// [3]: (INSTALL+ACK+MIC) AP informs the client that the PTK is installed
			station.Handshake.AddFrame(2, packet)

			mod.Debug("got frame 3/4 of the %s <-> %s handshake (mic:%x)",
				apMac,
				staMac,
				key.MIC)
		}

		// if we have unsaved packets as part of the handshake, save them.
		numUnsaved := station.Handshake.NumUnsaved()
		shakesFileName := mod.getHandshakeFileFor(ap)
		doSave := numUnsaved > 0
		if doSave && shakesFileName != "" {
			mod.Debug("(aggregate %v) saving handshake frames to %s", mod.shakesAggregate, shakesFileName)
			if err := mod.Session.WiFi.SaveHandshakesTo(shakesFileName, mod.handle.LinkType()); err != nil {
				mod.Error("error while saving handshake frames to %s: %s", shakesFileName, err)
			}
		}

		validPMKID := rawPMKID != nil
		validHalfHandshake := !staIsUs && station.Handshake.Half()
		validFullHandshake := station.Handshake.Complete()
		// if we have unsaved packets AND
		//   if we captured a PMKID OR
		//   if we captured am half handshake which is not ours OR
		//   if we captured a full handshake
		if doSave && (validPMKID || validHalfHandshake || validFullHandshake) {
			mod.Session.Events.Add("wifi.client.handshake", HandshakeEvent{
				File:       shakesFileName,
				NewPackets: numUnsaved,
				AP:         apMac.String(),
				Station:    staMac.String(),
				PMKID:      rawPMKID,
				Half:       station.Handshake.Half(),
				Full:       station.Handshake.Complete(),
			})
			// make sure the info that we have key material for this AP
			// is persisted even after stations are pruned due to inactivity
			ap.WithKeyMaterial(true)
		}
		// if we added ourselves as a client station but we didn't get any
		// PMKID, just remove it from the list of clients of this AP.
		if staAdded || (staIsUs && rawPMKID == nil) {
			ap.RemoveClient(staMac.String())
		}
	}

	// quick and dirty heuristic, see thread here https://github.com/bettercap/bettercap/issues/810#issuecomment-805145392
	if isEAPOL || (dot11.Type.MainType() != layers.Dot11TypeData && dot11.Type.MainType() != layers.Dot11TypeCtrl) {
		target := (*network.Station)(nil)
		targetAP := (*network.AccessPoint)(nil)

		// collect target bssids
		bssids := make([]net.HardwareAddr, 0)
		for _, addr := range []net.HardwareAddr{dot11.Address1, dot11.Address2, dot11.Address3, dot11.Address4} {
			if bytes.Equal(addr, network.BroadcastHw) == false {
				bssids = append(bssids, addr)
			}
		}

		// for each AP
		mod.Session.WiFi.EachAccessPoint(func(mac string, ap *network.AccessPoint) {
			// only check APs we captured handshakes of
			if target == nil && ap.HasKeyMaterial() {
				// search client station
				ap.EachClient(func(mac string, station *network.Station) {
					// any valid key material for this station?
					if station.Handshake.Any() {
						// check if target
						for _, a := range bssids {
							if bytes.Equal(a, station.HW) {
								target = station
								targetAP = ap
								break
							}
						}
					}
				})
			}
		})

		if target != nil {
			mod.Debug("saving extra %s frame (%d bytes) for %s",
				dot11.Type.String(),
				len(packet.Data()),
				target.String())

			target.Handshake.AddExtra(packet)

			shakesFileName := mod.getHandshakeFileFor(targetAP)
			if shakesFileName != "" {
				mod.Debug("(aggregate %v) saving handshake frames to %s", mod.shakesAggregate, shakesFileName)
				if err := mod.Session.WiFi.SaveHandshakesTo(shakesFileName, mod.handle.LinkType()); err != nil {
					mod.Error("error while saving handshake frames to %s: %s", shakesFileName, err)
				}
			}
		}
	}

}
