/*
Copyright © 2021 Ben Buxton <bbuxton@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

//go:build pcap

package cmd

import (
	"encoding/hex"
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/buxtronix/phev2mqtt/protocol"
	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	"github.com/google/gopacket/pcap"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

// pcapCmd represents the pcap command
var pcapCmd = &cobra.Command{
	Use:   "pcap",
	Short: "Decode packets from a PCAP traffic dump",
	Long: `Reads packets from a PCAP file generated by a packet sniffer
such as Wireshark.

The decoder filters TCP packets to and from port 8080.

This can also read files from a TCP connection. Specify
"tcp:<address>:<port>". This can be useful for realtime monitoring
via Android (https://wladimir-tm4pda.github.io/porting/tcpdump.html)
`,
	Args: cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		var handle *pcap.Handle
		var err error
		if parts := strings.Split(args[0], ":"); len(parts) == 3 && parts[0] == "tcp" {
			conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", parts[1], parts[2]))
			if err != nil {
				log.Fatal(err)
			}
			if tcpconn, ok := conn.(*net.TCPConn); ok {
				if tc, err := tcpconn.File(); err == nil {
					handle, err = pcap.OpenOfflineFile(tc)
					if err != nil {
						log.Fatal(err)
					}
				}
			}
		} else {
			handle, err = pcap.OpenOffline(args[0])
		}
		if err != nil {
			log.Fatal(err)
		}
		defer handle.Close()
		securityKey = &protocol.SecurityKey{}
		pings, _ = cmd.Flags().GetBool("pings")

		packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
		var currentTime time.Time
		pNum := 0
		for packet := range packetSource.Packets() {
			if m := packet.Metadata(); m != nil {
				if pNum > 0 {
					if r, _ := cmd.Flags().GetBool("latency"); r {
						time.Sleep(m.CaptureInfo.Timestamp.Sub(currentTime))
					}
					currentTime = m.CaptureInfo.Timestamp
				} else {
					currentTime = m.CaptureInfo.Timestamp
				}
			}
			pNum += 1
			decodePacket(cmd, packet)
		}
	},
}

func decodePacket(cmd *cobra.Command, packet gopacket.Packet) {
	dir := "?"
	tcpLayer := packet.Layer(layers.LayerTypeTCP)
	if tcpLayer != nil {
		tcp, _ := tcpLayer.(*layers.TCP)
		if tcp.SrcPort == 8080 {
			dir = "in  "
		} else if tcp.DstPort == 8080 {
			dir = "out "
		}
		if tcp.SYN && tcp.ACK {
			log.Infof("TCP-SYN-ACK\n")
		}
		if tcp.FIN {
			log.Infof("TCP-FIN\n")
		}
		if tcp.RST {
			log.Infof("TCP-RST\n")
		}
	}

	d, _ := cmd.Flags().GetString("direction")
	if dir == "in  " && d == "out" {
		return
	}
	if dir == "out " && d == "in" {
		return
	}

	al := packet.ApplicationLayer()
	if al != nil {
		processPayload(cmd, al.Payload(), dir)
	}

	if err := packet.ErrorLayer(); err != nil {
		log.Errorf("error decoding packet:", err)
	}
}

func processPayload(cmd *cobra.Command, data []byte, dir string) {
	log.Tracef("%%PHEV_PCAP_RAW_%s%%: %s\n", strings.ToUpper(dir), hex.EncodeToString(data))
	msgs := protocol.NewFromBytes(data, securityKey)
	for _, msg := range msgs {
		if r, _ := cmd.Flags().GetBool("registers"); r {
			handleRegisters(msg)
			return
		}
		if !pings && (msg.Type == protocol.CmdOutPingReq || msg.Type == protocol.CmdInPingResp) {
			return
		}
		log.Infof("%s [%02x] %s", dir, msg.Xor, msg.ShortForm())
	}
}

var regs = map[byte]string{}

var pings bool

func handleRegisters(m *protocol.PhevMessage) {
	if m.Type == protocol.CmdInResp {
		data := hex.EncodeToString(m.Data)
		if d := regs[m.Register]; d != data {
			if m.Reg != nil {
				log.Infof("UPDATEREG 0x%02x: %s -> %s (%s)\n", m.Register, d, data, m.Reg.String())
			} else {
				log.Infof("UPDATEREG 0x%02x: %s -> %s\n", m.Register, d, data)
			}
			regs[m.Register] = data
		}
	}
}

func init() {
	decodeCmd.AddCommand(pcapCmd)

	// Here you will define your flags and configuration settings.

	// Cobra supports Persistent Flags which will work for this command
	// and all subcommands, e.g.:
	// pcapCmd.PersistentFlags().String("foo", "", "A help for foo")

	// Cobra supports local flags which will only run when this command
	// is called directly, e.g.:
	// pcapCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
	pcapCmd.Flags().StringP("direction", "d", "both", "Direction to decode")
	pcapCmd.Flags().BoolP("latency", "l", false, "Replay with original network latency")
	pcapCmd.Flags().BoolP("registers", "R", false, "Show register updates")
	pcapCmd.Flags().BoolP("pings", "P", false, "Show ping requests and responses")
}
