package capture

import (
	"bufio"
	"fmt"
	"io"
	"strconv"
	"strings"
	"sync"
	"time"

	"telephony-inspector/internal/sip"
	internalSSH "telephony-inspector/internal/ssh"
)

// Capturer handles SIP packet capture via SSH
type Capturer struct {
	sshClient      *internalSSH.Client
	cleanup        func() error
	running        bool
	mu             sync.Mutex
	currentNetInfo *NetInfo

	// Callbacks
	OnPacket func(*sip.Packet)
	OnError  func(error)
}

// NewCapturer creates a new capturer with the given SSH config
func NewCapturer(cfg internalSSH.Config) *Capturer {
	return &Capturer{
		sshClient: internalSSH.NewClient(cfg),
	}
}

// Connect establishes the SSH connection
func (c *Capturer) Connect() error {
	return c.sshClient.Connect()
}

// Close closes the connection and stops capture
func (c *Capturer) Close() error {
	c.Stop()
	return c.sshClient.Close()
}

// Start begins capturing SIP traffic
func (c *Capturer) Start(iface string, port int) error {
	c.mu.Lock()
	if c.running {
		c.mu.Unlock()
		return fmt.Errorf("capture already running")
	}
	c.running = true
	c.mu.Unlock()

	// Build tcpdump command
	// -l: line buffered for real-time output
	// -A: print packet payload in ASCII
	// -s 0: capture full packets
	// -nn: don't resolve hostnames or port names
	cmd := fmt.Sprintf("sudo tcpdump -l -nn -A -s 0 -i %s port %d 2>/dev/null", iface, port)

	stdout, stderr, cleanup, err := c.sshClient.StartCommand(cmd)
	if err != nil {
		c.mu.Lock()
		c.running = false
		c.mu.Unlock()
		return err
	}
	c.cleanup = cleanup

	// Process stdout in goroutine
	go c.processStream(stdout)

	// Log stderr
	go c.processErrors(stderr)

	return nil
}

// Stop stops the capture
func (c *Capturer) Stop() {
	c.mu.Lock()
	defer c.mu.Unlock()

	if !c.running {
		return
	}
	c.running = false

	if c.cleanup != nil {
		c.cleanup()
		c.cleanup = nil
	}
}

// IsRunning returns whether capture is active
func (c *Capturer) IsRunning() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.running
}

func (c *Capturer) processStream(r io.Reader) {
	scanner := bufio.NewScanner(r)
	// Increase buffer size to handle large packets
	const maxCapacity = 5 * 1024 * 1024 // 5MB
	buf := make([]byte, 0, 64*1024)
	scanner.Buffer(buf, maxCapacity)

	var buffer strings.Builder
	inSIPMessage := false
	var msgNetInfo *NetInfo
	contentLength := -1

	for scanner.Scan() {
		c.mu.Lock()
		running := c.running
		c.mu.Unlock()
		if !running {
			break
		}

		line := scanner.Text()

		// Check for tcpdump header
		if netInfo := parseTcpdumpHeader(line); netInfo != nil {
			c.currentNetInfo = netInfo
			continue
		}

		// Detect start of SIP message
		if idx := findSIPStart(line); idx != -1 {

			// Clean the line (remove prefix garbage)
			line = line[idx:]

			// If we were building a message, parse it with its OWN net info
			if buffer.Len() > 0 {
				c.parseAndEmit(buffer.String(), msgNetInfo)
				buffer.Reset()
			}

			// NOW update msgNetInfo for the new message
			if c.currentNetInfo != nil {
				info := *c.currentNetInfo
				msgNetInfo = &info
			} else {
				msgNetInfo = nil
			}

			inSIPMessage = true
			contentLength = -1 // Reset
		}

		if inSIPMessage {
			buffer.WriteString(line)
			buffer.WriteString("\r\n")

			// Check for Content-Length
			lowerLine := strings.ToLower(line)
			if strings.HasPrefix(lowerLine, "content-length:") || strings.HasPrefix(lowerLine, "l:") {
				parts := strings.Split(line, ":")
				if len(parts) >= 2 {
					val := strings.TrimSpace(parts[1])
					if val != "" {
						if cl, err := strconv.Atoi(val); err == nil {
							contentLength = cl
						}
					}
				}
			}

			// Check for end of headers (empty line)
			if line == "" {
				// If Content-Length is 0 (or not found, treating as 0)
				// Flush immediately
				if contentLength <= 0 {
					c.parseAndEmit(buffer.String(), msgNetInfo)
					buffer.Reset()
					inSIPMessage = false
					contentLength = -1
				}
			}
		}
	}

	// Parse remaining buffer
	if buffer.Len() > 0 {
		c.parseAndEmit(buffer.String(), msgNetInfo)
	}

	if err := scanner.Err(); err != nil {
		if c.OnError != nil {
			c.OnError(fmt.Errorf("scanner error: %w", err))
		}
	}
}

func (c *Capturer) processErrors(r io.Reader) {
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		if c.OnError != nil {
			c.OnError(fmt.Errorf("tcpdump: %s", scanner.Text()))
		}
	}
}

func (c *Capturer) parseAndEmit(raw string, netInfo *NetInfo) {
	packet, err := sip.Parse(raw)
	if err != nil {
		if c.OnError != nil {
			c.OnError(err)
		}
		return
	}
	if packet != nil {
		// Attach network info if available
		if netInfo != nil {
			packet.Timestamp = netInfo.Timestamp
			packet.SourceIP = netInfo.SourceIP
			packet.SourcePort = netInfo.SourcePort
			packet.DestIP = netInfo.DestIP
			packet.DestPort = netInfo.DestPort
		}

		if c.OnPacket != nil {
			c.OnPacket(packet)
		}
	}
}

// findSIPStart returns the index of the start of a SIP message, or -1 if not found
func findSIPStart(line string) int {
	sipMethods := []string{"INVITE", "ACK", "BYE", "CANCEL", "REGISTER", "OPTIONS", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER", "MESSAGE", "UPDATE"}

	// Check for Response "SIP/2.0"
	if idx := strings.Index(line, "SIP/2.0 "); idx != -1 {
		// Verify it's not part of a header like Via or Record-Route
		// We look at what comes before. If it's the start of the line or preceded by garbage (nulls etc), it's likely a start.
		// If it is preceded by "Via: " or "Route: ", it is a header.
		prefix := strings.ToUpper(line[:idx])
		if !strings.HasSuffix(prefix, "VIA: ") &&
			!strings.HasSuffix(prefix, "ROUTE: ") &&
			!strings.HasSuffix(prefix, "VIA:") { // Handle varying spacing
			return idx
		}
	}

	// Check for Request "METHOD "
	for _, m := range sipMethods {
		target := m + " "
		if idx := strings.Index(line, target); idx != -1 {
			// Verify it's not CSeq, Allow, Rack, etc.
			prefix := strings.ToUpper(line[:idx])
			if !strings.HasSuffix(prefix, "CSEQ: ") &&
				!strings.HasSuffix(prefix, "ALLOW: ") &&
				!strings.HasSuffix(prefix, "RACK: ") &&
				!strings.HasSuffix(prefix, "SUPPORTED: ") {
				return idx
			}
		}
	}

	return -1
}

// NetInfo stores network layer information from tcpdump headers
type NetInfo struct {
	Timestamp  time.Time
	SourceIP   string
	SourcePort int
	DestIP     string
	DestPort   int
}

func parseTcpdumpHeader(line string) *NetInfo {
	// Robust parsing
	// Look for " IP " or " IP6 "
	parts := strings.Fields(line)
	// Need at least: Time IP Src > Dst:
	if len(parts) < 5 {
		return nil
	}

	// Find the direction arrow ">"
	arrowIdx := -1
	for i, p := range parts {
		if p == ">" {
			arrowIdx = i
			break
		}
	}

	if arrowIdx == -1 || arrowIdx < 2 {
		return nil
	}

	// Verify "IP" or "IP6" exists before the source (usually parts[1] or parts[2])
	// Example: 15:35... IP src > dst: ...
	// Example: 15:35... IP6 src > dst: ...
	// Example (verbose): 15:35... IP (tos 0x0...) src > dst: ...
	// We'll trust the arrow for now, but double check IPs.

	// Assume SRC is before arrow, DST is after
	srcStr := parts[arrowIdx-1]
	dstStr := parts[arrowIdx+1]

	// Find timestamp (usually first field)
	now := time.Now()
	t, err := time.Parse("15:04:05.000000", parts[0])
	if err == nil {
		t = time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), now.Location())
	} else {
		// Try without microseconds
		t, err = time.Parse("15:04:05", parts[0])
		if err == nil {
			t = time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), t.Second(), 0, now.Location())
		} else {
			t = now
		}
	}

	// Helper to extract IP and Port
	parseIPPort := func(s string) (string, int) {
		lastDot := strings.LastIndex(s, ".")
		if lastDot == -1 {
			// IPv6 might use different separation or just be IP
			return s, 0
		}
		ip := s[:lastDot]
		portStr := s[lastDot+1:]
		// Remove trailing colon if present (dest)
		portStr = strings.TrimSuffix(portStr, ":")
		// Remove trailing comma if present
		portStr = strings.TrimSuffix(portStr, ",")

		var port int
		fmt.Sscanf(portStr, "%d", &port)
		return ip, port
	}

	srcIP, srcPort := parseIPPort(srcStr)
	dstIP, dstPort := parseIPPort(dstStr)

	return &NetInfo{
		Timestamp:  t,
		SourceIP:   srcIP,
		SourcePort: srcPort,
		DestIP:     dstIP,
		DestPort:   dstPort,
	}
}
