package tui

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"telephony-inspector/internal/capture"
	"telephony-inspector/internal/config"
	"telephony-inspector/internal/sip"
	internalSSH "telephony-inspector/internal/ssh"

	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// View represents the current screen in the TUI
type View int

const (
	ViewDashboard View = iota
	ViewCapture
	ViewAnalysis
	ViewNetworkMap
)

// SubView for modal states
type SubView int

const (
	SubViewNone SubView = iota
	SubViewSSHConfig
	SubViewAddNode
	SubViewCallDetail
	SubViewCaptureMenu
	SubViewFileBrowser
)

// CaptureMode defines local vs SSH capture
type CaptureMode int

const (
	CaptureModeNone CaptureMode = iota
	CaptureModeLocal
	CaptureModeSSH
)

// Model holds the application state
type Model struct {
	currentView View
	subView     SubView
	width       int
	height      int

	// Network map configuration
	networkMap    *config.NetworkMap
	captureMode   CaptureMode
	sshConfig     SSHConfigModel
	statusMessage string

	// Capture state
	capturing     bool
	connected     bool
	captureIface  string
	localCapturer *capture.LocalCapturer
	capturer      *capture.Capturer
	captureError  string
	packetCount   int
	lastPackets   []string

	packetChan chan *sip.Packet // Channel for receiving packets from callbacks

	// Data stores
	callFlowStore *sip.CallFlowStore

	// Call flow analysis
	analysisOffset      int // For pagination in analysis view
	selectedFlow        int
	selectedPacketIndex int
	flowList            list.Model
	viewport            viewport.Model
	analysisViewport    viewport.Model

	// Packet Details View
	detailsViewport    viewport.Model
	focusPacketDetails bool

	// File browser for pcap import
	fileBrowser    FileBrowserModel
	loadedPcapPath string

	// Network node input
	nodeInput      []textinput.Model
	nodeInputFocus int

	// Style definitions
	styles Styles
}

// Styles holds the lipgloss styles for the TUI
type Styles struct {
	Title     lipgloss.Style
	Subtitle  lipgloss.Style
	Active    lipgloss.Style
	Inactive  lipgloss.Style
	Help      lipgloss.Style
	StatusBar lipgloss.Style
	Error     lipgloss.Style
	Success   lipgloss.Style
	Box       lipgloss.Style
	PacketRow lipgloss.Style
	CallFlow  lipgloss.Style

	// SIP Styles
	MethodInvite   lipgloss.Style
	MethodBye      lipgloss.Style
	MethodRegister lipgloss.Style
	MethodOther    lipgloss.Style

	Status1xx lipgloss.Style
	Status2xx lipgloss.Style
	Status3xx lipgloss.Style
	Status4xx lipgloss.Style
	Status5xx lipgloss.Style
	Status6xx lipgloss.Style

	NodeLabel    lipgloss.Style
	NodePBX      lipgloss.Style
	NodeProxy    lipgloss.Style
	NodeGateway  lipgloss.Style
	NodeCarrier  lipgloss.Style
	NodeEndpoint lipgloss.Style
	NodeDefault  lipgloss.Style

	ArrowOut lipgloss.Style
	ArrowIn  lipgloss.Style
}

func defaultStyles() Styles {
	return Styles{
		Title: lipgloss.NewStyle().
			Bold(true).
			Foreground(lipgloss.Color("#7D56F4")).
			MarginBottom(1),
		Subtitle: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#AFAFAF")),
		Active: lipgloss.NewStyle().
			Bold(true).
			Foreground(lipgloss.Color("#04B575")),
		Inactive: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#626262")),
		Help: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#626262")).
			MarginTop(1),
		StatusBar: lipgloss.NewStyle().
			Background(lipgloss.Color("#7D56F4")).
			Foreground(lipgloss.Color("#FFFFFF")).
			Padding(0, 1),
		Error: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FF5555")),
		Success: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#50FA7B")),
		Box: lipgloss.NewStyle().
			Border(lipgloss.RoundedBorder()).
			BorderForeground(lipgloss.Color("#7D56F4")).
			Padding(1, 2),
		PacketRow: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#F8F8F2")),
		CallFlow: lipgloss.NewStyle().
			Border(lipgloss.NormalBorder()).
			BorderForeground(lipgloss.Color("#44475A")).
			Padding(0, 1),

		// SIP Styles Initialization
		MethodInvite:   lipgloss.NewStyle().Foreground(lipgloss.Color("#8BE9FD")).Bold(true), // Cyan
		MethodBye:      lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")).Bold(true), // Red
		MethodRegister: lipgloss.NewStyle().Foreground(lipgloss.Color("#50FA7B")).Bold(true), // Green
		MethodOther:    lipgloss.NewStyle().Foreground(lipgloss.Color("#BD93F9")).Bold(true), // Purple

		Status1xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#F1FA8C")),            // Yellow
		Status2xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#50FA7B")),            // Green
		Status3xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#8BE9FD")),            // Cyan
		Status4xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")),            // Red
		Status5xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")).Bold(true), // Red Bold
		Status6xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")).Bold(true).Underline(true),

		NodeLabel: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#000000")).
			Background(lipgloss.Color("#BD93F9")). // Purple background
			Padding(0, 1).
			Bold(true),

		// Node Styles with different background colors
		NodePBX: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#FF79C6")). // Pink
			Padding(0, 1).
			Bold(true),
		NodeProxy: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#000000")).
			Background(lipgloss.Color("#8BE9FD")). // Cyan
			Padding(0, 1).
			Bold(true),
		NodeGateway: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#000000")).
			Background(lipgloss.Color("#F1FA8C")). // Yellow
			Padding(0, 1).
			Bold(true),
		NodeCarrier: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#BD93F9")). // Purple
			Padding(0, 1).
			Bold(true),
		NodeEndpoint: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#6272A4")). // Grey/Blue
			Padding(0, 1).
			Bold(true),
		NodeDefault: lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#44475A")). // Grey
			Padding(0, 1).
			Bold(true),

		ArrowOut: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF79C6")), // Pink
		ArrowIn:  lipgloss.NewStyle().Foreground(lipgloss.Color("#F1FA8C")), // Yellow
	}
}

// NewModel creates a new TUI model with default values
func NewModel() Model {
	// Try to load existing network map
	nm, err := config.LoadNetworkMap(config.DefaultNetworkMapPath())
	if err != nil {
		nm = config.NewNetworkMap()
	}

	// Initialize model with zero-sized viewport, will resize on WindowSizeMsg or view entry
	// Viewport for Transaction Flow
	vp := viewport.New(0, 0)
	vp.YPosition = 0

	// Viewport for Analysis List
	avp := viewport.New(0, 0)
	avp.YPosition = 0

	// Viewport for Packet Details
	dvp := viewport.New(0, 0)

	return Model{
		currentView:      ViewDashboard,
		subView:          SubViewNone,
		networkMap:       nm,
		callFlowStore:    sip.NewCallFlowStore(),
		lastPackets:      make([]string, 0, 50),
		sshConfig:        NewSSHConfigModel(),
		nodeInput:        createNodeInputs(),
		viewport:         vp,
		analysisViewport: avp,
		detailsViewport:  dvp,
		fileBrowser:      NewFileBrowser(".", ".pcap"),
		styles:           defaultStyles(),
	}
}

// waitForPacket waits for a packet on the channel
func waitForPacket(ch chan *sip.Packet) tea.Cmd {
	return func() tea.Msg {
		return PacketMsg{Packet: <-ch}
	}
}

func createNodeInputs() []textinput.Model {
	inputs := make([]textinput.Model, 4)

	inputs[0] = textinput.New()
	inputs[0].Placeholder = "Node Name"
	inputs[0].Prompt = "Name: "
	inputs[0].CharLimit = 64

	inputs[1] = textinput.New()
	inputs[1].Placeholder = "192.168.1.x"
	inputs[1].Prompt = "IP: "
	inputs[1].CharLimit = 45

	inputs[2] = textinput.New()
	inputs[2].Placeholder = "PBX/Proxy/MediaServer/Gateway"
	inputs[2].Prompt = "Type: "
	inputs[2].CharLimit = 20

	inputs[3] = textinput.New()
	inputs[3].Placeholder = "Optional description"
	inputs[3].Prompt = "Desc: "
	inputs[3].CharLimit = 256

	return inputs
}

// Init initializes the model
func (m Model) Init() tea.Cmd {
	return nil
}

// PacketMsg is sent when a new packet is received
type PacketMsg struct {
	Packet *sip.Packet
}

// ErrorMsg is sent when an error occurs
type ErrorMsg struct {
	Error error
}

// Update handles messages and updates the model
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd

	// GLOBAL HANDLERS: Handle signals independent of view
	switch msg := msg.(type) {
	case PacketMsg:
		if msg.Packet != nil {
			m.packetCount++
			summary := formatPacketSummary(msg.Packet, m.networkMap)
			m.lastPackets = append(m.lastPackets, summary)
			if len(m.lastPackets) > 50 {
				m.lastPackets = m.lastPackets[1:]
			}
			m.callFlowStore.AddPacket(msg.Packet)

			// If we are in Call Detail view, we might need to update the viewport content dynamically!
			if m.subView == SubViewCallDetail {
				m.updateCallDetailView()
			} else if m.currentView == ViewAnalysis {
				m.updateAnalysisView()
			}
		}

		// PROCESS NEXT PACKET - CRITICAL loop
		if m.capturing {
			cmds = append(cmds, waitForPacket(m.packetChan))
		}

		// If we processed a packet, we typically don't need to pass this msg to subviews
		// UNLESS the subview reacts to it explicitly.
		// For now, we return here to avoid double processing, BUT we must ensure
		// UI refreshes.
		return m, tea.Batch(cmds...)

	case ErrorMsg:
		m.captureError = msg.Error.Error()
		return m, nil
	}

	// Handle standard key/window messages dependent on view
	// Handle subview updates
	if m.subView != SubViewNone {
		newM, cmd := m.updateSubView(msg)
		// We need to type assert back to Model because updateSubView follows the interface but returns concrete logic
		// Actually updateSubView returns tea.Model, tea.Cmd.
		// Since we are inside Model.Update, we can cast or just return.
		realM, ok := newM.(Model)
		if ok {
			m = realM
		}
		cmds = append(cmds, cmd)
		return m, tea.Batch(cmds...)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "q", "ctrl+c":
			m.cleanup()
			return m, tea.Quit
		case "1":
			m.currentView = ViewDashboard
		case "2":
			m.currentView = ViewCapture
		case "3":
			m.currentView = ViewAnalysis
		case "4":
			m.currentView = ViewNetworkMap
		default:
			cmds = append(cmds, m.handleViewKeys(msg))
		}

	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height

		// Update viewport size for Call Detail view
		// Top nav is ~1 line, Status bar ~1 line.
		// Layout is vertical join of nav, content, status.
		// Available height for content = Height - 2 (approx).
		headerHeight := 2 // Nav + margin
		footerHeight := 1 // Status bar
		contentHeight := m.height - headerHeight - footerHeight

		if contentHeight < 0 {
			contentHeight = 0
		}

		m.viewport.Width = m.width / 2 // Right pane width
		m.viewport.Height = contentHeight

		// Correctly calculate Split Layout Dimensions for Viewports
		// Ideally we should share this logic or calculate it once
		totalWidth := m.width
		innerW := totalWidth - 2
		leftW := innerW / 2
		rightW := innerW - leftW

		leftTotalH := m.height - 4 // -4 for Nav, Status, and Help Footer
		leftTopH := leftTotalH / 3
		if leftTopH < 10 {
			leftTopH = 10
		}
		leftBotH := leftTotalH - leftTopH
		if leftBotH < 0 {
			leftBotH = 0
		}

		// Update Flow Viewport (Right)
		rvpHeight := (leftTotalH - 2) - 2 // -2 Borders, -2 Title
		if rvpHeight < 0 {
			rvpHeight = 0
		}
		m.viewport.Height = rvpHeight
		m.viewport.Width = rightW - 4

		// Update Details Viewport (Left Bottom)
		vpHeight := (leftBotH - 2) - 2 // -2 Borders, -2 Title
		if vpHeight < 0 {
			vpHeight = 0
		}
		m.detailsViewport.Height = vpHeight
		m.detailsViewport.Width = leftW - 4

		// Refresh content with new dimensions
		if m.subView == SubViewCallDetail {
			m.updateCallDetailView()
		}

		// Update Analysis Viewport size
		// We need to account for:
		// - Nav Bar: ~2 lines
		// - Status Bar: ~1 line
		// - Analysis View Header (Title + Count + Gap): ~4 lines
		// - Analysis View Footer (Gap + Help): ~2 lines
		// Total Overhead: ~9-10 lines
		analysisHeight := m.height - 10
		if analysisHeight < 0 {
			analysisHeight = 0
		}
		m.analysisViewport.Width = m.width
		m.analysisViewport.Height = analysisHeight

		if m.currentView == ViewAnalysis {
			m.updateAnalysisView()
		}
	}

	return m, tea.Batch(cmds...)
}

func (m *Model) handleViewKeys(msg tea.KeyMsg) tea.Cmd {
	switch m.currentView {
	case ViewCapture:
		switch msg.String() {
		case "c":
			// Show capture mode menu if not capturing
			if !m.capturing && m.captureMode == CaptureModeNone {
				m.subView = SubViewCaptureMenu
			}
		case "l":
			// Start local capture directly
			if !m.capturing {
				m.captureMode = CaptureModeLocal
				m.captureIface = "any"
				return m.startLocalCapture()
			}
		case "r":
			// SSH remote capture
			if !m.capturing {
				m.subView = SubViewSSHConfig
				m.sshConfig = NewSSHConfigModel()
				return m.sshConfig.Init()
			}
		case "s":
			if m.capturing {
				m.stopCapture()
			} else if m.captureMode != CaptureModeNone {
				if m.captureMode == CaptureModeLocal {
					return m.startLocalCapture()
				} else if m.connected {
					return m.startSSHCapture()
				}
			}
		case "d":
			m.disconnect()
		case "p":
			if !m.capturing {
				m.fileBrowser = NewFileBrowser("", ".pcap")
				m.subView = SubViewFileBrowser
				return nil
			}
		}

	case ViewAnalysis:
		switch msg.String() {
		case "up", "k":
			if m.selectedFlow > 0 {
				m.selectedFlow--
				m.updateAnalysisView()
			}
		case "down", "j":
			flows := m.callFlowStore.GetSortedFlows()
			if m.selectedFlow < len(flows)-1 {
				m.selectedFlow++
				m.updateAnalysisView()
			}
		case "pgup":
			m.selectedFlow -= 10
			if m.selectedFlow < 0 {
				m.selectedFlow = 0
			}
			m.updateAnalysisView()
		case "pgdown":
			flows := m.callFlowStore.GetSortedFlows()
			m.selectedFlow += 10
			if m.selectedFlow >= len(flows) {
				m.selectedFlow = len(flows) - 1
			}
			m.updateAnalysisView()
		case "enter":
			m.subView = SubViewCallDetail
			m.updateCallDetailView()
		}

	case ViewNetworkMap:
		switch msg.String() {
		case "a":
			m.subView = SubViewAddNode
			m.nodeInput = createNodeInputs()
			m.nodeInput[0].Focus()
			m.nodeInputFocus = 0
		case "l":
			if nm, err := config.LoadNetworkMap(config.DefaultNetworkMapPath()); err == nil {
				m.networkMap = nm
			}
		case "s":
			config.SaveNetworkMap(m.networkMap, config.DefaultNetworkMapPath())
		case "g":
			m.networkMap = config.CreateSampleNetworkMap()
		}
	}

	return nil
}

func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch m.subView {
	case SubViewCaptureMenu:
		if keyMsg, ok := msg.(tea.KeyMsg); ok {
			switch keyMsg.String() {
			case "l", "1":
				m.captureMode = CaptureModeLocal
				m.captureIface = "any"
				m.subView = SubViewNone
				return m, m.startLocalCapture()
			case "r", "2":
				m.subView = SubViewSSHConfig
				m.sshConfig = NewSSHConfigModel()
				return m, m.sshConfig.Init()
			case "p", "3":
				// Open file browser for pcap
				m.fileBrowser = NewFileBrowser("", ".pcap")
				m.subView = SubViewFileBrowser
				return m, nil
			case "esc", "q":
				m.subView = SubViewNone
			}
		}
		return m, nil

	case SubViewFileBrowser:
		var cmd tea.Cmd
		m.fileBrowser, cmd = m.fileBrowser.Update(msg)

		if m.fileBrowser.IsSelected() {
			// Load the pcap file
			pcapPath := m.fileBrowser.GetSelectedFile()
			m.loadedPcapPath = pcapPath
			m.subView = SubViewNone

			// Load packets from pcap
			reader := capture.NewPcapReader(pcapPath)
			packets, err := reader.ReadAll()
			if err != nil {
				m.captureError = err.Error()
			} else {
				m.captureError = ""
				m.packetCount = len(packets)
				for _, pkt := range packets {
					m.callFlowStore.AddPacket(pkt)
					summary := formatPacketSummary(pkt, m.networkMap)
					m.lastPackets = append(m.lastPackets, summary)
				}
			}
			return m, nil
		} else if m.fileBrowser.IsCancelled() {
			m.subView = SubViewNone
		}
		return m, cmd

	case SubViewSSHConfig:
		var cmd tea.Cmd
		m.sshConfig, cmd = m.sshConfig.Update(msg)

		if m.sshConfig.IsSubmitted() {
			host, port, user, password := m.sshConfig.GetConfig()
			portInt, _ := strconv.Atoi(port)
			if portInt == 0 {
				portInt = 22
			}

			cfg := internalSSH.Config{
				Host:     host,
				Port:     portInt,
				User:     user,
				Password: password,
			}

			m.capturer = capture.NewCapturer(cfg)
			if err := m.capturer.Connect(); err != nil {
				m.captureError = err.Error()
			} else {
				m.connected = true
				m.captureMode = CaptureModeSSH
				m.captureError = ""
			}
			m.subView = SubViewNone
		} else if m.sshConfig.IsCancelled() {
			m.subView = SubViewNone
		}
		return m, cmd

	case SubViewAddNode:
		return m.updateNodeInput(msg)

	case SubViewCallDetail:
		var cmd tea.Cmd

		// Handle Focus Switching
		if keyMsg, ok := msg.(tea.KeyMsg); ok {
			switch keyMsg.String() {
			case "esc", "q":
				m.subView = SubViewNone
				m.selectedPacketIndex = 0 // Reset selection
				m.focusPacketDetails = false
				return m, nil
			case "tab":
				m.focusPacketDetails = !m.focusPacketDetails
				return m, nil
			case "e":
				// Export Call Log
				flows := m.callFlowStore.GetRecentFlows(20)
				if m.selectedFlow < len(flows) {
					flow := flows[m.selectedFlow]
					// Sanitize Call-ID for filename
					safeCallID := strings.ReplaceAll(flow.CallID, "/", "_")
					safeCallID = strings.ReplaceAll(safeCallID, ":", "_")
					safeCallID = strings.ReplaceAll(safeCallID, "\\", "_")

					timestamp := time.Now().Format("20060102_150405")
					filename := fmt.Sprintf("export_%s_%s.log", timestamp, safeCallID)
					err := m.exportCallToLog(flow, filename)
					if err != nil {
						m.captureError = fmt.Sprintf("Export failed: %v", err)
						m.statusMessage = ""
					} else {
						m.captureError = ""
						m.statusMessage = fmt.Sprintf("Exported to %s", filename)
						// Clear success message after 3 seconds (handled loosely by UI updates)
						go func() {
							time.Sleep(3 * time.Second)
							// This is unsafe in bubbletea without a Msg, but valid for a quick hack
							// Better to use a command, but let's stick to simple field for now
						}()
					}
				}
				return m, nil
			}
		}

		// Route keys based on focus
		if m.focusPacketDetails {
			// Forward keys to Details Viewport for scrolling
			m.detailsViewport, cmd = m.detailsViewport.Update(msg)
			return m, cmd
		} else {
			// Handle Flow Navigation
			if keyMsg, ok := msg.(tea.KeyMsg); ok {
				switch keyMsg.String() {
				case "up", "k":
					if m.selectedPacketIndex > 0 {
						m.selectedPacketIndex--
						// Sync viewport
						if m.selectedPacketIndex < m.viewport.YOffset {
							m.viewport.SetYOffset(m.selectedPacketIndex)
						}
						// Force update of details content happens in View currently, but for state consistency
						// we should probably do it here or let View handle it.
						// Since View is pure function of state, it's fine.
						// But detailsViewport scroll position should reset if packet changes?
						m.detailsViewport.GotoTop()
						m.updateCallDetailView()
					}
				case "down", "j":
					flows := m.callFlowStore.GetRecentFlows(20)
					if m.selectedFlow < len(flows) {
						flow := flows[m.selectedFlow]
						if m.selectedPacketIndex < len(flow.Packets)-1 {
							m.selectedPacketIndex++
							// Sync viewport
							if m.selectedPacketIndex >= m.viewport.YOffset+m.viewport.Height {
								m.viewport.SetYOffset(m.selectedPacketIndex - m.viewport.Height + 1)
							}
							m.detailsViewport.GotoTop()
							m.updateCallDetailView()
						}
					}
				}
			}
		}

		return m, cmd
	}

	return m, nil
}

func (m *Model) updateNodeInput(msg tea.Msg) (tea.Model, tea.Cmd) {
	if keyMsg, ok := msg.(tea.KeyMsg); ok {
		switch keyMsg.String() {
		case "esc":
			m.subView = SubViewNone
			return m, nil
		case "tab", "down":
			m.nodeInput[m.nodeInputFocus].Blur()
			m.nodeInputFocus = (m.nodeInputFocus + 1) % len(m.nodeInput)
			return m, m.nodeInput[m.nodeInputFocus].Focus()
		case "shift+tab", "up":
			m.nodeInput[m.nodeInputFocus].Blur()
			m.nodeInputFocus--
			if m.nodeInputFocus < 0 {
				m.nodeInputFocus = len(m.nodeInput) - 1
			}
			return m, m.nodeInput[m.nodeInputFocus].Focus()
		case "enter":
			if m.nodeInputFocus == len(m.nodeInput)-1 {
				// Submit
				name := m.nodeInput[0].Value()
				ip := m.nodeInput[1].Value()
				nodeType := m.nodeInput[2].Value()
				desc := m.nodeInput[3].Value()

				if name != "" && ip != "" {
					m.networkMap.AddNode(config.NetworkNode{
						Name:        name,
						IP:          ip,
						Type:        config.NodeType(nodeType),
						Description: desc,
					})
				}
				m.subView = SubViewNone
				return m, nil
			}
			m.nodeInput[m.nodeInputFocus].Blur()
			m.nodeInputFocus++
			return m, m.nodeInput[m.nodeInputFocus].Focus()
		}
	}

	var cmd tea.Cmd
	m.nodeInput[m.nodeInputFocus], cmd = m.nodeInput[m.nodeInputFocus].Update(msg)
	return m, cmd
}

func (m *Model) startLocalCapture() tea.Cmd {
	m.capturing = true
	m.captureError = ""
	m.packetCount = 0
	m.lastPackets = m.lastPackets[:0]
	m.captureMode = CaptureModeLocal

	// Create a buffered channel for packets
	m.packetChan = make(chan *sip.Packet, 100)

	m.localCapturer = capture.NewLocalCapturer()
	m.localCapturer.OnPacket = func(p *sip.Packet) {
		if m.capturing {
			m.packetChan <- p
		}
	}
	m.localCapturer.OnError = func(err error) {
		m.captureError = err.Error()
	}

	iface := m.captureIface
	if iface == "" {
		iface = "any"
	}

	if err := m.localCapturer.Start(iface, 5060); err != nil {
		m.captureError = err.Error()
		m.capturing = false
		return nil
	}

	return waitForPacket(m.packetChan)
}

func (m *Model) startSSHCapture() tea.Cmd {
	m.capturing = true
	m.captureError = ""
	m.packetCount = 0
	m.lastPackets = m.lastPackets[:0]

	// Create a buffered channel for packets
	m.packetChan = make(chan *sip.Packet, 100)

	m.capturer.OnPacket = func(p *sip.Packet) {
		if m.capturing {
			m.packetChan <- p
		}
	}
	m.capturer.OnError = func(err error) {
		m.captureError = err.Error()
	}

	if err := m.capturer.Start("any", 5060); err != nil {
		m.captureError = err.Error()
		m.capturing = false
		return nil
	}

	return waitForPacket(m.packetChan)
}

func (m *Model) stopCapture() {
	if m.localCapturer != nil {
		m.localCapturer.Stop()
	}
	if m.capturer != nil {
		m.capturer.Stop()
	}

	wasCapturing := m.capturing
	m.capturing = false

	// Unblock any waiting waitForPacket command
	if wasCapturing && m.packetChan != nil {
		go func() {
			m.packetChan <- nil
		}()
	}
}

func (m *Model) disconnect() {
	m.stopCapture()
	if m.localCapturer != nil {
		m.localCapturer.Close()
		m.localCapturer = nil
	}
	if m.capturer != nil {
		m.capturer.Close()
		m.capturer = nil
	}
	m.connected = false
	m.captureMode = CaptureModeNone
}

func (m *Model) cleanup() {
	m.disconnect()
}

// View renders the TUI
func (m Model) View() string {
	// Handle subview modals
	if m.subView != SubViewNone {
		return m.renderSubView()
	}

	var content string

	switch m.currentView {
	case ViewDashboard:
		content = m.viewDashboard()
	case ViewCapture:
		content = m.viewCapture()
	case ViewAnalysis:
		content = m.viewAnalysis()
	case ViewNetworkMap:
		content = m.viewNetworkMap()
	}

	nav := m.renderNav()
	status := m.renderStatusBar()

	return lipgloss.JoinVertical(lipgloss.Left, nav, content, status)
}

func (m Model) renderSubView() string {
	switch m.subView {
	case SubViewCaptureMenu:
		return m.styles.Box.Render(m.renderCaptureMenu())
	case SubViewFileBrowser:
		return m.styles.Box.Render(m.fileBrowser.View())
	case SubViewSSHConfig:
		return m.styles.Box.Render(m.sshConfig.View())
	case SubViewAddNode:
		return m.styles.Box.Render(m.renderAddNodeForm())
	case SubViewCallDetail:
		return m.renderCallDetail()
	}
	return ""
}

func (m Model) renderCaptureMenu() string {
	var b strings.Builder
	b.WriteString(m.styles.Title.Render("📡 Select Capture Mode"))
	b.WriteString("\n\n")
	b.WriteString("  [1] [L]ocal  - Capture on this machine (requires tcpdump)\n")
	b.WriteString("  [2] [R]emote - Capture via SSH on remote server\n")
	b.WriteString("  [3] [P]cap   - Import pcap file from disk\n")
	b.WriteString("\n")
	b.WriteString(m.styles.Help.Render("Press 1/L, 2/R, or 3/P to select • Esc to cancel"))
	return b.String()
}

func (m Model) renderAddNodeForm() string {
	var b strings.Builder
	b.WriteString(m.styles.Title.Render("➕ Add Network Node"))
	b.WriteString("\n\n")
	for _, input := range m.nodeInput {
		b.WriteString(input.View())
		b.WriteString("\n")
	}
	b.WriteString("\n")
	b.WriteString(m.styles.Help.Render("Node Types: PBX, Proxy, SBC, Carrier, Handset, Firewall"))
	b.WriteString("\n")
	b.WriteString(m.styles.Help.Render("Tab navigate • Enter submit • Esc cancel"))
	return b.String()
}

func (m Model) renderCallDetail() string {
	flows := m.callFlowStore.GetRecentFlows(20)
	if m.selectedFlow >= len(flows) || len(flows) == 0 {
		return m.styles.Box.Render("No call selected\n\nPress Esc to go back")
	}

	flow := flows[m.selectedFlow]

	// Split Widths and Heights
	totalWidth := m.width
	// Subtract borders/padding if any
	innerW := totalWidth - 2 // Outer margin?
	leftW := innerW / 2
	rightW := innerW - leftW

	// Left Pane Heights
	leftTotalH := m.height - 4 // Nav + Status + Help Footer
	// Adjust for borders: we have two boxes stacked.
	// Let's say we split space 33% / 66%.
	leftTopH := leftTotalH / 3
	leftBotH := leftTotalH - leftTopH

	if leftTopH < 10 {
		leftTopH = 10
	}

	// Determine Border Colors based on Focus
	detailsBorderColor := lipgloss.Color("#44475A")
	flowBorderColor := lipgloss.Color("#44475A")
	infoBorderColor := lipgloss.Color("#44475A") // Call info usually static focus

	if m.focusPacketDetails {
		detailsBorderColor = lipgloss.Color("#bd93f9") // Active Purple
	} else {
		flowBorderColor = lipgloss.Color("#bd93f9")
	}

	// Definition of Styles
	// We want Borders on ALL sides now since they are distinct boxes
	baseStyle := lipgloss.NewStyle().Padding(0, 1).Border(lipgloss.RoundedBorder())

	leftTopStyle := baseStyle.Copy().
		Width(leftW - 2).     // -2 for borders
		Height(leftTopH - 2). // -2 for borders
		BorderForeground(infoBorderColor)

	leftBotStyle := baseStyle.Copy().
		Width(leftW - 2).
		Height(leftBotH - 2).
		BorderForeground(detailsBorderColor)

	rightStyle := baseStyle.Copy().
		Width(rightW - 2).
		Height(leftTotalH - 2).
		BorderForeground(flowBorderColor)

	// --- Left Pane TOP (Call Info) ---
	var leftTop strings.Builder
	// Title
	leftTop.WriteString(m.styles.Title.Render("📞 Call Detail"))
	leftTop.WriteString("\n\n")

	// Content
	leftTop.WriteString(fmt.Sprintf("Call-ID: %s\n", flow.CallID))
	leftTop.WriteString(fmt.Sprintf("From: %s\n", flow.From))
	leftTop.WriteString(fmt.Sprintf("To: %s\n", flow.To))
	leftTop.WriteString(fmt.Sprintf("State: %s\n", flow.State))

	duration := flow.EndTime.Sub(flow.StartTime)
	leftTop.WriteString(fmt.Sprintf("Duration: %s\n", duration.Round(time.Millisecond)))
	leftTop.WriteString(fmt.Sprintf("Packets: %d\n\n", len(flow.Packets)))

	leftTop.WriteString("Network Layer:\n")
	if len(flow.Packets) > 0 {
		first := flow.Packets[0]
		srcLabel := m.networkMap.LabelForIP(first.SourceIP)
		if srcLabel != first.SourceIP {
			node := m.networkMap.FindByIP(first.SourceIP)
			srcLabel = m.styleForNode(node).Render(srcLabel)
		}
		dstLabel := m.networkMap.LabelForIP(first.DestIP)
		if dstLabel != first.DestIP {
			node := m.networkMap.FindByIP(first.DestIP)
			dstLabel = m.styleForNode(node).Render(dstLabel)
		}
		leftTop.WriteString(fmt.Sprintf("  Source:      %s (%s:%d)\n", srcLabel, first.SourceIP, first.SourcePort))
		leftTop.WriteString(fmt.Sprintf("  Destination: %s (%s:%d)\n", dstLabel, first.DestIP, first.DestPort))
	}

	// --- Left Pane BOTTOM (Selected Packet Details) ---
	// Title
	detailsTitle := m.styles.Title.Render("📦 Packet Details")

	// Update details viewport
	// Calculate available height: Pane Height - Borders(2) - Header(2 approx)
	// Actually styles handle borders on the container.
	// Viewport height should be container InnerHeight - TitleHeight - Padding.
	// leftBotStyle.GetHeight() returns outer height.
	// We set style height explicitly, so we know it.

	vpHeight := (leftBotH - 2) - 2 // -2 Borders, -2 Title/Margin
	if vpHeight < 0 {
		vpHeight = 0
	}

	m.detailsViewport.Width = leftW - 4 // Margin/Padding
	m.detailsViewport.Height = vpHeight
	// Content is set in Update now

	// --- Right Pane (Transaction Flow) ---
	// Title
	flowTitle := m.styles.Title.Render("🚀 Transaction Flow")

	// Right Pane Height logic
	rvpHeight := (leftTotalH - 2) - 2 // -2 Borders, -2 Title/Margin
	if rvpHeight < 0 {
		rvpHeight = 0
	}
	m.viewport.Height = rvpHeight
	m.viewport.Width = rightW - 4

	// Render Final Layout

	// Left Top: Just content
	leftTopRendered := leftTopStyle.Render(leftTop.String())

	// Left Bot: Title + Viewport
	leftBotContent := lipgloss.JoinVertical(lipgloss.Left, detailsTitle, "\n", m.detailsViewport.View())
	leftBotRendered := leftBotStyle.Render(leftBotContent)

	// Left Col
	leftCol := lipgloss.JoinVertical(lipgloss.Left, leftTopRendered, leftBotRendered)

	// Right: Title + Viewport
	rightContent := lipgloss.JoinVertical(lipgloss.Left, flowTitle, "\n", m.viewport.View())
	rightRendered := rightStyle.Render(rightContent)

	content := lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightRendered)

	help := m.styles.Help.Render("[Tab] Switch Pane • [↑/↓] Scroll • [e] Export Log • [Esc] Back")
	return lipgloss.JoinVertical(lipgloss.Left, content, help)
}

func (m Model) renderNav() string {
	tabs := []string{"[1] Dashboard", "[2] Capture", "[3] Analysis", "[4] Network Map"}

	// Calculate tab width for even distribution
	tabWidth := m.width / len(tabs)
	if tabWidth < 15 {
		tabWidth = 15
	}

	// Style for active and inactive tabs with fixed width
	activeStyle := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("#FFFFFF")).
		Background(lipgloss.Color("#7D56F4")).
		Width(tabWidth).
		Align(lipgloss.Center)

	inactiveStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("#AFAFAF")).
		Background(lipgloss.Color("#282A36")).
		Width(tabWidth).
		Align(lipgloss.Center)

	var rendered []string
	for i, tab := range tabs {
		if View(i) == m.currentView {
			rendered = append(rendered, activeStyle.Render(tab))
		} else {
			rendered = append(rendered, inactiveStyle.Render(tab))
		}
	}

	// Join tabs and add background bar
	navBar := lipgloss.JoinHorizontal(lipgloss.Top, rendered...)

	// Fill remaining width with background
	barStyle := lipgloss.NewStyle().
		Background(lipgloss.Color("#282A36")).
		Width(m.width)

	return barStyle.Render(navBar) + "\n"
}

func (m Model) renderStatusBar() string {
	var parts []string
	parts = append(parts, " Telephony Inspector v0.1.0 ")

	if m.connected {
		parts = append(parts, m.styles.Success.Render(" SSH: Connected "))
	}
	if m.capturing {
		parts = append(parts, m.styles.Active.Render(fmt.Sprintf(" Capturing: %d pkts ", m.packetCount)))
	}
	if m.captureError != "" {
		parts = append(parts, m.styles.Error.Render(" Error: "+m.captureError+" "))
	} else if m.statusMessage != "" {
		parts = append(parts, m.styles.Success.Render(" "+m.statusMessage+" "))
	}

	return m.styles.StatusBar.Render(strings.Join(parts, "|"))
}

func (m Model) viewDashboard() string {
	title := m.styles.Title.Render("📞 Dashboard")

	var stats []string
	stats = append(stats, m.styles.Subtitle.Render("SIP Telephony Inspector"))
	stats = append(stats, "")

	if m.connected {
		stats = append(stats, m.styles.Success.Render("✓ SSH Connected"))
	} else {
		stats = append(stats, m.styles.Inactive.Render("○ SSH Disconnected"))
	}

	stats = append(stats, fmt.Sprintf("Network Nodes: %d", len(m.networkMap.Nodes)))
	stats = append(stats, fmt.Sprintf("Active Calls: %d", m.callFlowStore.Count()))
	stats = append(stats, fmt.Sprintf("Packets Captured: %d", m.packetCount))
	stats = append(stats, "")
	stats = append(stats, "Quick Start:")
	stats = append(stats, "  1. Go to [2] Capture → Press 'c' to connect SSH")
	stats = append(stats, "  2. Press 's' to start capturing SIP traffic")
	stats = append(stats, "  3. Go to [3] Analysis to view call flows")
	stats = append(stats, "  4. Go to [4] Network Map to label IPs")
	stats = append(stats, "")
	stats = append(stats, m.styles.Help.Render("Press 1-4 to navigate, q to quit"))

	return lipgloss.JoinVertical(lipgloss.Left, title, lipgloss.JoinVertical(lipgloss.Left, stats...))
}

func (m Model) viewCapture() string {
	title := m.styles.Title.Render("🔍 Capture")

	var lines []string

	// Mode indicator
	switch m.captureMode {
	case CaptureModeLocal:
		lines = append(lines, m.styles.Success.Render("● Mode: Local capture"))
	case CaptureModeSSH:
		if m.connected {
			lines = append(lines, m.styles.Success.Render("● Mode: SSH (connected)"))
		} else {
			lines = append(lines, m.styles.Inactive.Render("○ Mode: SSH (disconnected)"))
		}
	default:
		lines = append(lines, m.styles.Inactive.Render("○ No capture mode selected"))
	}

	// Capture status
	if m.capturing {
		mode := "local"
		if m.captureMode == CaptureModeSSH {
			mode = "SSH"
		}
		lines = append(lines, m.styles.Active.Render(fmt.Sprintf("● Capturing on port 5060 (%s)", mode)))
	} else if m.captureMode != CaptureModeNone {
		lines = append(lines, m.styles.Inactive.Render("○ Capture stopped"))
	}

	lines = append(lines, fmt.Sprintf("Packets: %d", m.packetCount))
	lines = append(lines, "")

	// Last packets
	if len(m.lastPackets) > 0 {
		lines = append(lines, "Recent Packets:")
		start := 0
		if len(m.lastPackets) > 15 {
			start = len(m.lastPackets) - 15
		}
		for _, pkt := range m.lastPackets[start:] {
			lines = append(lines, "  "+pkt)
		}
	}

	lines = append(lines, "")

	// Help
	var help string
	if m.captureMode == CaptureModeNone {
		help = "[c] Choose mode  [l] Local  [r] Remote SSH  [p] Pcap Import  [q] Quit"
	} else if !m.capturing {
		help = "[s] Start  [c] Change mode  [d] Disconnect  [q] Quit"
	} else {
		help = "[s] Stop  [d] Disconnect  [q] Quit"
	}
	lines = append(lines, m.styles.Help.Render(help))

	return lipgloss.JoinVertical(lipgloss.Left, title, lipgloss.JoinVertical(lipgloss.Left, lines...))
}

func (m Model) viewNetworkMap() string {
	title := m.styles.Title.Render("🗺️ Network Map")

	if len(m.networkMap.Nodes) == 0 {
		return lipgloss.JoinVertical(lipgloss.Left, title,
			"No network nodes configured.",
			"",
			"Add nodes to label IPs in your SIP infrastructure.",
			"",
			m.styles.Help.Render("[a] Add node  [l] Load file  [g] Generate sample"))
	}

	var lines []string
	for _, node := range m.networkMap.Nodes {
		icon := "○"
		switch node.Type {
		case config.NodeTypePBX:
			icon = "☎"
		case config.NodeTypeProxy:
			icon = "⇄"
		case config.NodeTypeMediaServer:
			icon = "♪"
		case config.NodeTypeGateway:
			icon = "⬚"
		}
		line := fmt.Sprintf("  %s %s (%s): %s", icon, node.Name, node.Type, node.IP)
		if node.Description != "" {
			line += " - " + node.Description
		}
		lines = append(lines, line)
	}

	lines = append(lines, "")
	lines = append(lines, m.styles.Help.Render("[a] Add  [s] Save  [l] Load  [g] Sample  [q] Quit"))

	return lipgloss.JoinVertical(lipgloss.Left, title, lipgloss.JoinVertical(lipgloss.Left, lines...))
}

// Helper functions

func formatPacketSummary(p *sip.Packet, nm *config.NetworkMap) string {
	src := nm.LabelForIP(p.SourceIP)
	dst := nm.LabelForIP(p.DestIP)

	if p.IsRequest {
		return fmt.Sprintf("%s → %s: %s", src, dst, p.Method)
	}
	return fmt.Sprintf("%s → %s: %d %s", src, dst, p.StatusCode, p.StatusText)
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max-1] + "…"
}

func extractUser(sipAddr string) string {
	// Extract user from "Display Name" <sip:user@host>
	if idx := strings.Index(sipAddr, "<sip:"); idx >= 0 {
		start := idx + 5
		end := strings.Index(sipAddr[start:], "@")
		if end > 0 {
			return sipAddr[start : start+end]
		}
	}
	if idx := strings.Index(sipAddr, "sip:"); idx >= 0 {
		start := idx + 4
		end := strings.Index(sipAddr[start:], "@")
		if end > 0 {
			return sipAddr[start : start+end]
		}
	}
	return sipAddr
}

// styleForNode returns the style for a given node type
func (m Model) styleForNode(node *config.NetworkNode) lipgloss.Style {
	if node == nil {
		return m.styles.NodeDefault
	}
	switch node.Type {
	case config.NodeTypePBX:
		return m.styles.NodePBX
	case config.NodeTypeProxy:
		return m.styles.NodeProxy
	case config.NodeTypeGateway:
		return m.styles.NodeGateway
	case config.NodeTypeMediaServer:
		// Reusing Proxy style for MediaServer if no specific one defined
		return m.styles.NodeProxy
	case config.NodeTypeEndpoint:
		return m.styles.NodeEndpoint
	case config.NodeTypeUnknown:
		return m.styles.NodeDefault
	default:
		// Attempt to match by name if type is custom or carrier
		lowerName := strings.ToLower(node.Name)
		if strings.Contains(lowerName, "carrier") || strings.Contains(lowerName, "provider") {
			return m.styles.NodeCarrier
		}
		return m.styles.NodeDefault
	}
}

func (m *Model) updateCallDetailView() {
	flows := m.callFlowStore.GetRecentFlows(20)
	if m.selectedFlow >= len(flows) || len(flows) == 0 {
		m.detailsViewport.SetContent("No call selected")
		m.viewport.SetContent("")
		return
	}

	flow := flows[m.selectedFlow]

	// --- Selected Packet Details ---
	var detailsContent string
	if m.selectedPacketIndex < len(flow.Packets) {
		pkt := flow.Packets[m.selectedPacketIndex]

		srcLabel := m.networkMap.LabelForIP(pkt.SourceIP)
		if srcLabel != pkt.SourceIP {
			srcLabel = fmt.Sprintf("%s (%s)", srcLabel, pkt.SourceIP)
		}

		dstLabel := m.networkMap.LabelForIP(pkt.DestIP)
		if dstLabel != pkt.DestIP {
			dstLabel = fmt.Sprintf("%s (%s)", dstLabel, pkt.DestIP)
		}

		detailsContent = fmt.Sprintf("Time: %s\nIP: %s -> %s\n\n%s",
			pkt.Timestamp.Format("15:04:05.000"),
			srcLabel, dstLabel,
			pkt.Raw)
	} else {
		detailsContent = "No packet selected"
	}

	m.detailsViewport.SetContent(detailsContent)

	// --- Transaction Flow ---
	var right strings.Builder
	// Calculate padding needed
	digits := len(fmt.Sprintf("%d", len(flow.Packets)))
	if digits < 2 {
		digits = 2
	}
	// Pad format string e.g. "%03d."
	numFmt := fmt.Sprintf("%%0%dd.", digits)

	for i, pkt := range flow.Packets {
		arrow := "→"
		arrowStyle := m.styles.ArrowOut
		if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP {
			arrow = "←"
			arrowStyle = m.styles.ArrowIn
		}

		var summaryStyle lipgloss.Style
		if pkt.IsRequest {
			switch pkt.Method {
			case sip.MethodINVITE:
				summaryStyle = m.styles.MethodInvite
			case sip.MethodBYE, sip.MethodCANCEL:
				summaryStyle = m.styles.MethodBye
			case sip.MethodREGISTER:
				summaryStyle = m.styles.MethodRegister
			default:
				summaryStyle = m.styles.MethodOther
			}
		} else {
			switch {
			case pkt.StatusCode >= 100 && pkt.StatusCode < 200:
				summaryStyle = m.styles.Status1xx
			case pkt.StatusCode >= 200 && pkt.StatusCode < 300:
				summaryStyle = m.styles.Status2xx
			case pkt.StatusCode >= 300 && pkt.StatusCode < 400:
				summaryStyle = m.styles.Status3xx
			case pkt.StatusCode >= 400 && pkt.StatusCode < 500:
				summaryStyle = m.styles.Status4xx
			case pkt.StatusCode >= 500 && pkt.StatusCode < 600:
				summaryStyle = m.styles.Status5xx
			default:
				summaryStyle = m.styles.Status6xx
			}
		}

		ts := pkt.Timestamp.Format("15:04:05.000")
		// Use pre-calculated format
		numStr := fmt.Sprintf(numFmt, i+1)
		lineStr := fmt.Sprintf("%s [%s] %s %s", numStr, ts, arrowStyle.Render(arrow), summaryStyle.Render(pkt.Summary()))

		if pkt.SDP != nil {
			mediaIP := pkt.SDP.GetSDPMediaIP()
			if mediaIP != "" {
				label := m.networkMap.LabelForIP(mediaIP)
				if label != mediaIP {
					node := m.networkMap.FindByIP(mediaIP)
					label = m.styleForNode(node).Render(label)
				}
				lineStr += fmt.Sprintf(" (SDP: %s %s)", mediaIP, label)
			}
		}

		if i == m.selectedPacketIndex {
			lineStr = m.styles.Active.Render("> " + lineStr)
		} else {
			lineStr = "  " + lineStr
		}
		right.WriteString(lineStr + "\n")
	}

	m.viewport.SetContent(right.String())
}

func (m *Model) updateAnalysisView() {
	flows := m.callFlowStore.GetSortedFlows()

	if len(flows) == 0 {
		m.analysisViewport.SetContent("No calls captured yet.\n\nStart capturing on the Capture tab to see call flows here.")
		return
	}

	var lines []string
	for i, flow := range flows {
		prefix := "  "
		style := m.styles.Inactive
		if i == m.selectedFlow {
			prefix = "> "
			style = m.styles.Active
		}

		stateIcon := "○"
		switch flow.State {
		case sip.CallStateRinging:
			stateIcon = "◐"
		case sip.CallStateConnected:
			stateIcon = "●"
		case sip.CallStateTerminated:
			stateIcon = "◯"
		case sip.CallStateFailed:
			stateIcon = "✕"
		}

		summary := fmt.Sprintf("%s%s %s → %s [%d pkts]",
			prefix, stateIcon,
			truncate(extractUser(flow.From), 15),
			truncate(extractUser(flow.To), 15),
			len(flow.Packets))

		lines = append(lines, style.Render(summary))
	}

	m.analysisViewport.SetContent(strings.Join(lines, "\n"))

	// Sync viewport scrolling
	// Similar to transaction flow: keep selected line visible
	if m.selectedFlow < m.analysisViewport.YOffset {
		m.analysisViewport.SetYOffset(m.selectedFlow)
	} else if m.selectedFlow >= m.analysisViewport.YOffset+m.analysisViewport.Height {
		m.analysisViewport.SetYOffset(m.selectedFlow - m.analysisViewport.Height + 1)
	}
}

func (m Model) viewAnalysis() string {
	title := m.styles.Title.Render("📊 Analysis")
	flows := m.callFlowStore.GetSortedFlows()

	countStr := fmt.Sprintf("Calls: %d", len(flows))

	return lipgloss.JoinVertical(lipgloss.Left,
		title,
		countStr,
		"",
		m.analysisViewport.View(),
		"",
		m.styles.Help.Render("↑/↓ select • Enter details • q quit"))
}

func (m *Model) exportCallToLog(flow *sip.CallFlow, filename string) error {
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	// Header
	fmt.Fprintf(f, "Call Detail Export\n")
	fmt.Fprintf(f, "==================\n")
	fmt.Fprintf(f, "Date: %s\n", time.Now().Format(time.RFC1123))
	fmt.Fprintf(f, "Call-ID: %s\n", flow.CallID)
	fmt.Fprintf(f, "From: %s\n", flow.From)
	fmt.Fprintf(f, "To: %s\n", flow.To)
	fmt.Fprintf(f, "State: %s\n", flow.State)
	fmt.Fprintf(f, "Duration: %s\n", flow.EndTime.Sub(flow.StartTime).String())
	fmt.Fprintf(f, "Packets: %d\n\n", len(flow.Packets))

	// Network Layer
	fmt.Fprintf(f, "Network Layer:\n")
	if len(flow.Packets) > 0 {
		first := flow.Packets[0]
		srcLabel := m.networkMap.LabelForIP(first.SourceIP)
		dstLabel := m.networkMap.LabelForIP(first.DestIP)
		fmt.Fprintf(f, "  Source:      %s (%s:%d)\n", srcLabel, first.SourceIP, first.SourcePort)
		fmt.Fprintf(f, "  Destination: %s (%s:%d)\n\n", dstLabel, first.DestIP, first.DestPort)
	}

	// Network Map Context
	if len(m.networkMap.Nodes) > 0 {
		fmt.Fprintf(f, "Network Context:\n")
		for _, node := range m.networkMap.Nodes {
			fmt.Fprintf(f, "  - %s (%s): %s\n", node.Name, node.Type, node.IP)
		}
		fmt.Fprintf(f, "\n")
	}

	// Transaction Flow
	fmt.Fprintf(f, "Transaction Flow:\n")
	fmt.Fprintf(f, "-----------------\n")

	// Calculate padding needed
	digits := len(fmt.Sprintf("%d", len(flow.Packets)))
	if digits < 2 {
		digits = 2
	}
	numFmt := fmt.Sprintf("%%0%dd.", digits)

	for i, pkt := range flow.Packets {
		arrow := "->"
		if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP {
			arrow = "<-"
		}

		summary := pkt.Summary() // e.g. INVITE sip:options...

		// Resolve IPs
		src := m.networkMap.LabelForIP(pkt.SourceIP)
		if src != pkt.SourceIP {
			src = fmt.Sprintf("%s (%s)", src, pkt.SourceIP)
		}

		dst := m.networkMap.LabelForIP(pkt.DestIP)
		if dst != pkt.DestIP {
			dst = fmt.Sprintf("%s (%s)", dst, pkt.DestIP)
		}

		// Use pre-calculated format
		numStr := fmt.Sprintf(numFmt, i+1)
		fmt.Fprintf(f, "%s [%s] %s %s %s (%s line %d)\n",
			numStr,
			pkt.Timestamp.Format("15:04:05.000"),
			src, arrow, dst,
			summary,
			len(pkt.Raw))
	}

	fmt.Fprintf(f, "\n\nPacket Details:\n")
	fmt.Fprintf(f, "---------------\n")

	for i, pkt := range flow.Packets {
		srcLabel := m.networkMap.LabelForIP(pkt.SourceIP)
		if srcLabel != pkt.SourceIP {
			srcLabel = fmt.Sprintf("%s (%s)", srcLabel, pkt.SourceIP)
		}

		dstLabel := m.networkMap.LabelForIP(pkt.DestIP)
		if dstLabel != pkt.DestIP {
			dstLabel = fmt.Sprintf("%s (%s)", dstLabel, pkt.DestIP)
		}

		fmt.Fprintf(f, "\n--- Packet %d [%s] ---\n", i+1, pkt.Timestamp.Format("15:04:05.000"))
		fmt.Fprintf(f, "%s -> %s\n", srcLabel, dstLabel)
		fmt.Fprintf(f, "%s\n", pkt.Raw)
	}

	return nil
}
