package tui

import (
	"fmt"
	"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

	// 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
	// Call flow analysis
	selectedFlow int
	flowList     list.Model
	viewport     viewport.Model

	// 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
	vp := viewport.New(0, 0)
	vp.YPosition = 0

	return Model{
		currentView:   ViewDashboard,
		subView:       SubViewNone,
		networkMap:    nm,
		callFlowStore: sip.NewCallFlowStore(),
		lastPackets:   make([]string, 0, 50),
		sshConfig:     NewSSHConfigModel(),
		nodeInput:     createNodeInputs(),
		viewport:      vp,
		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 {
				// Re-render subview content effectively updates the strings, but
				// we need to set the content on viewport again if it changed.
				// This is handled in View() normally, but viewport needs SetContent.
				// Let's force a viewport update by triggering a dummy message or just re-setting it.
				// Actually, View() calls renderCallDetail which calls SetContent.
				// But View() is only called if Update returns a modified model.
				// We modified the store, so that counts.
			}
		}

		// 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
	}

	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--
			}
		case "down", "j":
			flows := m.callFlowStore.GetRecentFlows(20)
			if m.selectedFlow < len(flows)-1 {
				m.selectedFlow++
			}
		case "enter":
			m.subView = SubViewCallDetail
		}

	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
		if keyMsg, ok := msg.(tea.KeyMsg); ok {
			if keyMsg.String() == "esc" || keyMsg.String() == "q" {
				m.subView = SubViewNone
				return m, nil
			}
		}

		// Update viewport
		m.viewport, cmd = m.viewport.Update(msg)
		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
	totalWidth := m.width
	// Subtract borders/padding if any
	innerW := totalWidth - 4
	leftW := innerW / 2
	rightW := innerW - leftW

	// --- Left Pane (Details) ---
	var left strings.Builder
	left.WriteString(m.styles.Title.Render("📞 Call Detail"))
	left.WriteString("\n\n")
	left.WriteString(fmt.Sprintf("Call-ID: %s\n", flow.CallID))
	left.WriteString(fmt.Sprintf("From: %s\n", flow.From))
	left.WriteString(fmt.Sprintf("To: %s\n", flow.To))
	left.WriteString(fmt.Sprintf("State: %s\n", flow.State))

	// Calculate and display duration
	duration := flow.EndTime.Sub(flow.StartTime)
	left.WriteString(fmt.Sprintf("Duration: %s\n", duration.Round(time.Millisecond)))

	left.WriteString(fmt.Sprintf("Packets: %d\n\n", len(flow.Packets)))

	// Network Summary Section
	left.WriteString("Network Layer:\n")
	// Find first packet to get initial IPs
	if len(flow.Packets) > 0 {
		first := flow.Packets[0]

		srcLabel := m.networkMap.LabelForIP(first.SourceIP)
		if srcLabel != first.SourceIP {
			// Find node type to apply style
			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)
		}

		left.WriteString(fmt.Sprintf("  Source:      %s (%s:%d)\n", srcLabel, first.SourceIP, first.SourcePort))
		left.WriteString(fmt.Sprintf("  Destination: %s (%s:%d)\n", dstLabel, first.DestIP, first.DestPort))
	}
	left.WriteString("\n\n")
	left.WriteString(m.styles.Help.Render("Esc: Back • ↑/↓: Scroll Flow"))

	// --- Right Pane (Transaction Flow) ---
	var right strings.Builder
	// right.WriteString("Transaction Flow:\n\n") // Removed header to save space or added to viewport content

	for i, pkt := range flow.Packets {
		arrow := "→"
		arrowStyle := m.styles.ArrowOut

		// Simple direction indicator based on whether it matches initial source
		if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP {
			arrow = "←"
			arrowStyle = m.styles.ArrowIn
		}

		// Style the packet summary (Method or Status)
		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
			}
		}

		// Format timestamp
		ts := pkt.Timestamp.Format("15:04:05.000")

		// Clean packet info line (Timestamp + Arrow + Method/Status)
		right.WriteString(fmt.Sprintf("%d. [%s] %s %s\n",
			i+1,
			ts,
			arrowStyle.Render(arrow),
			summaryStyle.Render(pkt.Summary())))

		// Show SDP info if present
		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)
				}
				right.WriteString(fmt.Sprintf("      SDP Media: %s %s\n", mediaIP, label))
			}
		}
	}

	// Set content to viewport
	m.viewport.SetContent(right.String())

	// Ensure viewport size is correct (it might need update on resize msg, but being safe here)
	// We cheat a bit by updating width here during render if needed, but ideally handled in Update or Layout
	// But since this is a subview render, we just assume Update handled size or use current m.height
	// Note: You can't mutate model in Render. So we rely on Update setting viewport size.
	// However, we need to enforce sizing or styling on the rendered string.

	leftStyle := lipgloss.NewStyle().Width(leftW).Padding(1, 2)
	rightStyle := lipgloss.NewStyle().Width(rightW).Padding(0, 1).Border(lipgloss.NormalBorder(), false, false, false, true).BorderForeground(lipgloss.Color("#44475A")) // Left border

	// Render left pane
	leftRendered := leftStyle.Render(left.String())

	// Render viewport (Right pane)
	// We style the viewport string itself or wrapper?
	// The viewport.View() returns the string.
	rightRendered := rightStyle.Render(m.viewport.View())

	return lipgloss.JoinHorizontal(lipgloss.Top, leftRendered, rightRendered)
}

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+" "))
	}

	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) viewAnalysis() string {
	title := m.styles.Title.Render("📊 Analysis")

	flows := m.callFlowStore.GetRecentFlows(20)

	if len(flows) == 0 {
		return lipgloss.JoinVertical(lipgloss.Left, title,
			"No calls captured yet.",
			"",
			"Start capturing on the Capture tab to see call flows here.",
			"",
			m.styles.Help.Render("Press 2 to go to Capture"))
	}

	var lines []string
	lines = append(lines, fmt.Sprintf("Calls: %d", len(flows)))
	lines = append(lines, "")

	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))
	}

	lines = append(lines, "")
	lines = append(lines, m.styles.Help.Render("↑/↓ select • Enter details • q quit"))

	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
	}
}
