package capture

import (
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"net/netip"
	"strconv"
	"strings"
	"time"

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	"github.com/miekg/dns"

	nbdns "github.com/netbirdio/netbird/dns"
)

// TextWriter writes human-readable one-line-per-packet summaries.
// It is not safe for concurrent use; callers must serialize access.
type TextWriter struct {
	w       io.Writer
	verbose bool
	ascii   bool
	flows   map[dirKey]uint32
}

type dirKey struct {
	src netip.AddrPort
	dst netip.AddrPort
}

// NewTextWriter creates a text formatter that writes to w.
func NewTextWriter(w io.Writer, verbose, ascii bool) *TextWriter {
	return &TextWriter{
		w:       w,
		verbose: verbose,
		ascii:   ascii,
		flows:   make(map[dirKey]uint32),
	}
}

// tag formats the fixed-width "[DIR PROTO]" prefix with right-aligned protocol.
func tag(dir Direction, proto string) string {
	return fmt.Sprintf("[%-3s %4s]", dir, proto)
}

// WritePacket formats and writes a single packet line.
func (tw *TextWriter) WritePacket(ts time.Time, data []byte, dir Direction) error {
	ts = ts.Local()
	info, ok := parsePacketInfo(data)
	if !ok {
		_, err := fmt.Fprintf(tw.w, "%s [%-3s    ?] ??? len=%d\n",
			ts.Format("15:04:05.000000"), dir, len(data))
		return err
	}

	timeStr := ts.Format("15:04:05.000000")

	var err error
	switch info.proto {
	case protoTCP:
		err = tw.writeTCP(timeStr, dir, &info, data)
	case protoUDP:
		err = tw.writeUDP(timeStr, dir, &info, data)
	case protoICMP:
		err = tw.writeICMPv4(timeStr, dir, &info, data)
	case protoICMPv6:
		err = tw.writeICMPv6(timeStr, dir, &info, data)
	default:
		var verbose string
		if tw.verbose {
			verbose = tw.verboseIP(data, info.family)
		}
		_, err = fmt.Fprintf(tw.w, "%s %s %s > %s length %d%s\n",
			timeStr, tag(dir, fmt.Sprintf("P%d", info.proto)),
			info.srcIP, info.dstIP, len(data)-info.hdrLen, verbose)
	}
	return err
}

func (tw *TextWriter) writeTCP(timeStr string, dir Direction, info *packetInfo, data []byte) error {
	tcp := &layers.TCP{}
	if err := tcp.DecodeFromBytes(data[info.hdrLen:], gopacket.NilDecodeFeedback); err != nil {
		return tw.writeFallback(timeStr, dir, "TCP", info, data)
	}

	flags := tcpFlagsStr(tcp)
	plen := len(tcp.Payload)

	// Protocol annotation
	var annotation string
	if plen > 0 {
		annotation = annotatePayload(tcp.Payload)
	}

	if !tw.verbose {
		_, err := fmt.Fprintf(tw.w, "%s %s %s > %s [%s] length %d%s\n",
			timeStr, tag(dir, "TCP"),
			net.JoinHostPort(info.srcIP.String(), strconv.Itoa(int(info.srcPort))), net.JoinHostPort(info.dstIP.String(), strconv.Itoa(int(info.dstPort))),
			flags, plen, annotation)
		if err != nil {
			return err
		}
		if tw.ascii && plen > 0 {
			return tw.writeASCII(tcp.Payload)
		}
		return nil
	}

	relSeq, relAck := tw.relativeSeqAck(info, tcp.Seq, tcp.Ack)

	var seqStr string
	if plen > 0 {
		seqStr = fmt.Sprintf(", seq %d:%d", relSeq, relSeq+uint32(plen))
	} else {
		seqStr = fmt.Sprintf(", seq %d", relSeq)
	}

	var ackStr string
	if tcp.ACK {
		ackStr = fmt.Sprintf(", ack %d", relAck)
	}

	var opts string
	if s := formatTCPOptions(tcp.Options); s != "" {
		opts = ", options [" + s + "]"
	}

	verbose := tw.verboseIP(data, info.family)

	_, err := fmt.Fprintf(tw.w, "%s %s %s > %s [%s]%s%s, win %d%s, length %d%s%s\n",
		timeStr, tag(dir, "TCP"),
		net.JoinHostPort(info.srcIP.String(), strconv.Itoa(int(info.srcPort))), net.JoinHostPort(info.dstIP.String(), strconv.Itoa(int(info.dstPort))),
		flags, seqStr, ackStr, tcp.Window, opts, plen, annotation, verbose)
	if err != nil {
		return err
	}
	if tw.ascii && plen > 0 {
		return tw.writeASCII(tcp.Payload)
	}
	return nil
}

func (tw *TextWriter) writeUDP(timeStr string, dir Direction, info *packetInfo, data []byte) error {
	udp := &layers.UDP{}
	if err := udp.DecodeFromBytes(data[info.hdrLen:], gopacket.NilDecodeFeedback); err != nil {
		return tw.writeFallback(timeStr, dir, "UDP", info, data)
	}

	plen := len(udp.Payload)

	// DNS replaces the entire line format
	if plen > 0 && (isDNSPort(info.srcPort) || isDNSPort(info.dstPort)) {
		if s := formatDNSPayload(udp.Payload); s != "" {
			var verbose string
			if tw.verbose {
				verbose = tw.verboseIP(data, info.family)
			}
			_, err := fmt.Fprintf(tw.w, "%s %s %s > %s %s%s\n",
				timeStr, tag(dir, "UDP"),
				net.JoinHostPort(info.srcIP.String(), strconv.Itoa(int(info.srcPort))), net.JoinHostPort(info.dstIP.String(), strconv.Itoa(int(info.dstPort))),
				s, verbose)
			return err
		}
	}

	var verbose string
	if tw.verbose {
		verbose = tw.verboseIP(data, info.family)
	}
	_, err := fmt.Fprintf(tw.w, "%s %s %s > %s length %d%s\n",
		timeStr, tag(dir, "UDP"),
		net.JoinHostPort(info.srcIP.String(), strconv.Itoa(int(info.srcPort))), net.JoinHostPort(info.dstIP.String(), strconv.Itoa(int(info.dstPort))),
		plen, verbose)
	if err != nil {
		return err
	}
	if tw.ascii && plen > 0 {
		return tw.writeASCII(udp.Payload)
	}
	return nil
}

func (tw *TextWriter) writeICMPv4(timeStr string, dir Direction, info *packetInfo, data []byte) error {
	icmp := &layers.ICMPv4{}
	if err := icmp.DecodeFromBytes(data[info.hdrLen:], gopacket.NilDecodeFeedback); err != nil {
		return tw.writeFallback(timeStr, dir, "ICMP", info, data)
	}

	var detail string
	if icmp.TypeCode.Type() == layers.ICMPv4TypeEchoRequest || icmp.TypeCode.Type() == layers.ICMPv4TypeEchoReply {
		detail = fmt.Sprintf("%s, id %d, seq %d", icmp.TypeCode.String(), icmp.Id, icmp.Seq)
	} else {
		detail = icmp.TypeCode.String()
	}

	var verbose string
	if tw.verbose {
		verbose = tw.verboseIP(data, info.family)
	}
	_, err := fmt.Fprintf(tw.w, "%s %s %s > %s %s, length %d%s\n",
		timeStr, tag(dir, "ICMP"), info.srcIP, info.dstIP, detail, len(data)-info.hdrLen, verbose)
	return err
}

func (tw *TextWriter) writeICMPv6(timeStr string, dir Direction, info *packetInfo, data []byte) error {
	icmp := &layers.ICMPv6{}
	if err := icmp.DecodeFromBytes(data[info.hdrLen:], gopacket.NilDecodeFeedback); err != nil {
		return tw.writeFallback(timeStr, dir, "ICMP", info, data)
	}

	var verbose string
	if tw.verbose {
		verbose = tw.verboseIP(data, info.family)
	}
	_, err := fmt.Fprintf(tw.w, "%s %s %s > %s %s, length %d%s\n",
		timeStr, tag(dir, "ICMP"), info.srcIP, info.dstIP, icmp.TypeCode.String(), len(data)-info.hdrLen, verbose)
	return err
}

func (tw *TextWriter) writeFallback(timeStr string, dir Direction, proto string, info *packetInfo, data []byte) error {
	_, err := fmt.Fprintf(tw.w, "%s %s %s > %s length %d\n",
		timeStr, tag(dir, proto),
		net.JoinHostPort(info.srcIP.String(), strconv.Itoa(int(info.srcPort))), net.JoinHostPort(info.dstIP.String(), strconv.Itoa(int(info.dstPort))),
		len(data)-info.hdrLen)
	return err
}

func (tw *TextWriter) verboseIP(data []byte, family uint8) string {
	return fmt.Sprintf(", ttl %d, id %d, iplen %d",
		ipTTL(data, family), ipID(data, family), len(data))
}

// relativeSeqAck returns seq/ack relative to the first seen value per direction.
func (tw *TextWriter) relativeSeqAck(info *packetInfo, seq, ack uint32) (relSeq, relAck uint32) {
	fwd := dirKey{
		src: netip.AddrPortFrom(info.srcIP, info.srcPort),
		dst: netip.AddrPortFrom(info.dstIP, info.dstPort),
	}
	rev := dirKey{
		src: netip.AddrPortFrom(info.dstIP, info.dstPort),
		dst: netip.AddrPortFrom(info.srcIP, info.srcPort),
	}

	if isn, ok := tw.flows[fwd]; ok {
		relSeq = seq - isn
	} else {
		tw.flows[fwd] = seq
	}

	if isn, ok := tw.flows[rev]; ok {
		relAck = ack - isn
	} else {
		relAck = ack
	}

	return relSeq, relAck
}

// writeASCII prints payload bytes as printable ASCII.
func (tw *TextWriter) writeASCII(payload []byte) error {
	if len(payload) == 0 {
		return nil
	}
	buf := make([]byte, len(payload))
	for i, b := range payload {
		switch {
		case b >= 0x20 && b < 0x7f:
			buf[i] = b
		case b == '\n' || b == '\r' || b == '\t':
			buf[i] = b
		default:
			buf[i] = '.'
		}
	}
	_, err := fmt.Fprintf(tw.w, "%s\n", buf)
	return err
}

// --- TCP helpers ---

func ipTTL(data []byte, family uint8) uint8 {
	if family == 4 && len(data) > 8 {
		return data[8]
	}
	if family == 6 && len(data) > 7 {
		return data[7]
	}
	return 0
}

func ipID(data []byte, family uint8) uint16 {
	if family == 4 && len(data) >= 6 {
		return binary.BigEndian.Uint16(data[4:6])
	}
	return 0
}

func tcpFlagsStr(tcp *layers.TCP) string {
	var buf [6]byte
	n := 0
	if tcp.SYN {
		buf[n] = 'S'
		n++
	}
	if tcp.FIN {
		buf[n] = 'F'
		n++
	}
	if tcp.RST {
		buf[n] = 'R'
		n++
	}
	if tcp.PSH {
		buf[n] = 'P'
		n++
	}
	if tcp.ACK {
		buf[n] = '.'
		n++
	}
	if tcp.URG {
		buf[n] = 'U'
		n++
	}
	if n == 0 {
		return "none"
	}
	return string(buf[:n])
}

func formatTCPOptions(opts []layers.TCPOption) string {
	var parts []string
	for _, opt := range opts {
		switch opt.OptionType {
		case layers.TCPOptionKindEndList:
			return strings.Join(parts, ",")
		case layers.TCPOptionKindNop:
			parts = append(parts, "nop")
		case layers.TCPOptionKindMSS:
			if len(opt.OptionData) == 2 {
				parts = append(parts, fmt.Sprintf("mss %d", binary.BigEndian.Uint16(opt.OptionData)))
			}
		case layers.TCPOptionKindWindowScale:
			if len(opt.OptionData) == 1 {
				parts = append(parts, fmt.Sprintf("wscale %d", opt.OptionData[0]))
			}
		case layers.TCPOptionKindSACKPermitted:
			parts = append(parts, "sackOK")
		case layers.TCPOptionKindSACK:
			blocks := len(opt.OptionData) / 8
			parts = append(parts, fmt.Sprintf("sack %d", blocks))
		case layers.TCPOptionKindTimestamps:
			if len(opt.OptionData) == 8 {
				tsval := binary.BigEndian.Uint32(opt.OptionData[0:4])
				tsecr := binary.BigEndian.Uint32(opt.OptionData[4:8])
				parts = append(parts, fmt.Sprintf("TS val %d ecr %d", tsval, tsecr))
			}
		}
	}
	return strings.Join(parts, ",")
}

// --- Protocol annotation ---

// annotatePayload returns a protocol annotation string for known application protocols.
func annotatePayload(payload []byte) string {
	if len(payload) < 4 {
		return ""
	}

	s := string(payload)

	// SSH banner: "SSH-2.0-OpenSSH_9.6\r\n"
	if strings.HasPrefix(s, "SSH-") {
		if end := strings.IndexByte(s, '\r'); end > 0 && end < 256 {
			return ": " + s[:end]
		}
	}

	// TLS records
	if ann := annotateTLS(payload); ann != "" {
		return ": " + ann
	}

	// HTTP request or response
	for _, method := range [...]string{"GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "PATCH ", "OPTIONS ", "CONNECT "} {
		if strings.HasPrefix(s, method) {
			if end := strings.IndexByte(s, '\r'); end > 0 && end < 200 {
				return ": " + s[:end]
			}
		}
	}
	if strings.HasPrefix(s, "HTTP/") {
		if end := strings.IndexByte(s, '\r'); end > 0 && end < 200 {
			return ": " + s[:end]
		}
	}

	return ""
}

// annotateTLS returns a description for TLS handshake and alert records.
func annotateTLS(data []byte) string {
	if len(data) < 6 {
		return ""
	}

	switch data[0] {
	case 0x16:
		return annotateTLSHandshake(data)
	case 0x15:
		return annotateTLSAlert(data)
	}
	return ""
}

func annotateTLSHandshake(data []byte) string {
	if len(data) < 10 {
		return ""
	}
	switch data[5] {
	case 0x01:
		if sni := extractSNI(data); sni != "" {
			return "TLS ClientHello SNI=" + sni
		}
		return "TLS ClientHello"
	case 0x02:
		return "TLS ServerHello"
	}
	return ""
}

func annotateTLSAlert(data []byte) string {
	if len(data) < 7 {
		return ""
	}
	severity := "warning"
	if data[5] == 2 {
		severity = "fatal"
	}
	return fmt.Sprintf("TLS Alert %s %s", severity, tlsAlertDesc(data[6]))
}

func tlsAlertDesc(code byte) string {
	switch code {
	case 0:
		return "close_notify"
	case 10:
		return "unexpected_message"
	case 40:
		return "handshake_failure"
	case 42:
		return "bad_certificate"
	case 43:
		return "unsupported_certificate"
	case 44:
		return "certificate_revoked"
	case 45:
		return "certificate_expired"
	case 48:
		return "unknown_ca"
	case 49:
		return "access_denied"
	case 50:
		return "decode_error"
	case 70:
		return "protocol_version"
	case 80:
		return "internal_error"
	case 86:
		return "inappropriate_fallback"
	case 90:
		return "user_canceled"
	case 112:
		return "unrecognized_name"
	default:
		return fmt.Sprintf("alert(%d)", code)
	}
}

// extractSNI parses a TLS ClientHello and returns the SNI server name.
func extractSNI(data []byte) string {
	if len(data) < 6 || data[0] != 0x16 {
		return ""
	}
	recordLen := int(binary.BigEndian.Uint16(data[3:5]))
	handshake := data[5:]
	if len(handshake) > recordLen {
		handshake = handshake[:recordLen]
	}

	if len(handshake) < 4 || handshake[0] != 0x01 {
		return ""
	}
	hsLen := int(handshake[1])<<16 | int(handshake[2])<<8 | int(handshake[3])
	body := handshake[4:]
	if len(body) > hsLen {
		body = body[:hsLen]
	}

	extPos := clientHelloExtensionsOffset(body)
	if extPos < 0 {
		return ""
	}
	return findSNIExtension(body, extPos)
}

// clientHelloExtensionsOffset returns the byte offset where extensions begin
// within the ClientHello body, or -1 if the body is too short.
func clientHelloExtensionsOffset(body []byte) int {
	if len(body) < 38 {
		return -1
	}
	pos := 34

	if pos >= len(body) {
		return -1
	}
	pos += 1 + int(body[pos]) // session ID

	if pos+2 > len(body) {
		return -1
	}
	pos += 2 + int(binary.BigEndian.Uint16(body[pos:pos+2])) // cipher suites

	if pos >= len(body) {
		return -1
	}
	pos += 1 + int(body[pos]) // compression methods

	if pos+2 > len(body) {
		return -1
	}
	return pos
}

func findSNIExtension(body []byte, pos int) string {
	extLen := int(binary.BigEndian.Uint16(body[pos : pos+2]))
	pos += 2
	extEnd := pos + extLen
	if extEnd > len(body) {
		extEnd = len(body)
	}

	for pos+4 <= extEnd {
		extType := binary.BigEndian.Uint16(body[pos : pos+2])
		eLen := int(binary.BigEndian.Uint16(body[pos+2 : pos+4]))
		pos += 4
		if pos+eLen > extEnd {
			break
		}
		if extType == 0 && eLen >= 5 {
			nameLen := int(binary.BigEndian.Uint16(body[pos+3 : pos+5]))
			if pos+5+nameLen <= extEnd {
				return string(body[pos+5 : pos+5+nameLen])
			}
		}
		pos += eLen
	}
	return ""
}

func isDNSPort(p uint16) bool {
	switch p {
	case nbdns.DefaultDNSPort, nbdns.ForwarderClientPort, nbdns.ForwarderServerPort:
		return true
	}
	return false
}

// formatDNSPayload parses DNS and returns a tcpdump-style summary.
func formatDNSPayload(payload []byte) string {
	d := &layers.DNS{}
	if err := d.DecodeFromBytes(payload, gopacket.NilDecodeFeedback); err != nil {
		return ""
	}

	rd := ""
	if d.RD {
		rd = "+"
	}

	if !d.QR {
		return formatDNSQuery(d, rd, len(payload))
	}
	return formatDNSResponse(d, rd, len(payload))
}

func formatDNSQuery(d *layers.DNS, rd string, plen int) string {
	if len(d.Questions) == 0 {
		return fmt.Sprintf("%04x%s (%d)", d.ID, rd, plen)
	}
	q := d.Questions[0]
	return fmt.Sprintf("%04x%s %s? %s. (%d)", d.ID, rd, q.Type, q.Name, plen)
}

func formatDNSResponse(d *layers.DNS, rd string, plen int) string {
	anCount := d.ANCount
	nsCount := d.NSCount
	arCount := d.ARCount
	ede := formatEDE(d)

	if d.ResponseCode != layers.DNSResponseCodeNoErr {
		return fmt.Sprintf("%04x %d/%d/%d %s%s (%d)", d.ID, anCount, nsCount, arCount, d.ResponseCode, ede, plen)
	}

	if anCount > 0 && len(d.Answers) > 0 {
		rr := d.Answers[0]
		if rdata := shortRData(&rr); rdata != "" {
			return fmt.Sprintf("%04x %d/%d/%d %s %s%s (%d)", d.ID, anCount, nsCount, arCount, rr.Type, rdata, ede, plen)
		}
	}

	return fmt.Sprintf("%04x %d/%d/%d%s (%d)", d.ID, anCount, nsCount, arCount, ede, plen)
}

// dnsOPTCodeEDE is the EDNS0 option code for Extended DNS Errors (RFC 8914).
const dnsOPTCodeEDE layers.DNSOptionCode = layers.DNSOptionCode(dns.EDNS0EDE)

// formatEDE returns " EDE=Name" for the first Extended DNS Error option
// found in the response, or empty string if none is present.
func formatEDE(d *layers.DNS) string {
	for _, rr := range d.Additionals {
		if rr.Type != layers.DNSTypeOPT {
			continue
		}
		for _, opt := range rr.OPT {
			if opt.Code != dnsOPTCodeEDE || len(opt.Data) < 2 {
				continue
			}
			info := binary.BigEndian.Uint16(opt.Data[:2])
			name, ok := dns.ExtendedErrorCodeToString[info]
			if !ok {
				name = fmt.Sprintf("%d", info)
			}
			return " EDE=" + name
		}
	}
	return ""
}

func shortRData(rr *layers.DNSResourceRecord) string {
	switch rr.Type {
	case layers.DNSTypeA, layers.DNSTypeAAAA:
		if rr.IP != nil {
			return rr.IP.String()
		}
	case layers.DNSTypeCNAME:
		if len(rr.CNAME) > 0 {
			return string(rr.CNAME) + "."
		}
	case layers.DNSTypePTR:
		if len(rr.PTR) > 0 {
			return string(rr.PTR) + "."
		}
	case layers.DNSTypeNS:
		if len(rr.NS) > 0 {
			return string(rr.NS) + "."
		}
	case layers.DNSTypeMX:
		return fmt.Sprintf("%d %s.", rr.MX.Preference, rr.MX.Name)
	case layers.DNSTypeTXT:
		if len(rr.TXTs) > 0 {
			return fmt.Sprintf("%q", string(rr.TXTs[0]))
		}
	case layers.DNSTypeSRV:
		return fmt.Sprintf("%d %d %d %s.", rr.SRV.Priority, rr.SRV.Weight, rr.SRV.Port, rr.SRV.Name)
	}
	return ""
}
