package aws

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"github.com/charmbracelet/bubbles/help"
	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/table"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

var (
	titleStyle = func() lipgloss.Style {
		b := lipgloss.RoundedBorder()
		b.Right = "├"
		return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
	}()

	infoStyle = func() lipgloss.Style {
		b := lipgloss.RoundedBorder()
		b.Left = "┤"
		return titleStyle.Copy().BorderStyle(b)
	}()
	magentaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("magenta"))
)

type model struct {
	preloadedData       *AllAccountData
	awsAccountsTable    table.Model
	awsAccountsViewport viewport.Model

	mainTable         table.Model
	mainTableViewport viewport.Model
	mainTableData     map[int][]table.Row

	detailsData     map[int]string
	detailsViewport viewport.Model // Use viewport for details view

	focusSelector     int
	defaultTableStyle table.Styles

	terminalWidth   int
	terminalHeight  int // Add terminal height to manage viewport size
	awsSelectedRow  int
	mainSelectedRow int // Track the currently selected row for detail view updates
	keys            keyMap
	help            help.Model
	lastKey         string
	quitting        bool
}

type CapeJSON struct {
	Account       string `json:"account"`
	Source        string `json:"source"`
	Summary       string `json:"summary"`
	Target        string `json:"target"`
	IsTargetAdmin string `json:"isTargetAdmin"`
}

type PerAccountData struct {
	FilePath     string     // Path to the JSON file
	PrivescPaths []CapeJSON // All records contained in the file
}

type AllAccountData struct {
	Files map[string]*PerAccountData // Map of file paths to their records
}

func (m model) Init() tea.Cmd {
	return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd

	s := table.DefaultStyles()
	s.Header = s.Header.
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("240")).
		BorderBottom(true).
		Bold(false)
	s.Selected = s.Selected.
		Foreground(lipgloss.Color("229")).
		Background(lipgloss.Color("57")).
		Bold(false)

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.terminalWidth = msg.Width
		m.terminalHeight = msg.Height - 7 // Adjust as needed

		// Calculate heights based on the specified percentages
		awsAccountsHeight := int(float32(m.terminalHeight) * 0.2)
		halfHeight := int(float32(m.terminalHeight) * 0.4) // For the other two viewports

		// Update dimensions for all viewports
		m.awsAccountsViewport.Width = m.terminalWidth - 4
		m.awsAccountsViewport.Height = awsAccountsHeight

		m.mainTableViewport.Width = m.terminalWidth - 4
		m.mainTableViewport.Height = halfHeight
		m.awsAccountsTable.SetHeight(awsAccountsHeight - 2)
		m.awsAccountsTable.SetWidth(m.terminalWidth - 4)
		m.awsAccountsViewport.Width = m.terminalWidth - 4

		m.detailsViewport.Width = m.terminalWidth - 4
		m.detailsViewport.Height = halfHeight
		m.mainTable.SetHeight(halfHeight - 2)

	case tea.KeyMsg:
		switch {
		case key.Matches(msg, m.keys.Up):
			m.lastKey = "↑"
		case key.Matches(msg, m.keys.Down):
			m.lastKey = "↓"
		case key.Matches(msg, m.keys.Left):
			m.lastKey = "←"
		case key.Matches(msg, m.keys.Right):
			m.lastKey = "→"
		case key.Matches(msg, m.keys.Help):
			m.help.ShowAll = !m.help.ShowAll
		case key.Matches(msg, m.keys.Tab):
			// Switch focus between the the three viewports by cycling focusSelector between 0, 1, and 2
			m.focusSelector = (m.focusSelector + 1) % 3
			//m.focusOnTable = !m.focusOnTable
		// add a case for shift and tab at the same time to to cycle the focusSelector backwards between 0, 1, and 2
		case key.Matches(msg, m.keys.ShiftTab):
			m.focusSelector = (m.focusSelector - 1) % 3

		case key.Matches(msg, m.keys.Quit):
			m.quitting = true
			return m, tea.Quit
		}

		//var detailsData map[int]string
		//var mainTable table.Model
		var err error
		var awsCurrentRow int
		var mainCurrentRow int
		switch m.focusSelector {
		case 0:
			m.awsAccountsTable, cmd = m.awsAccountsTable.Update(msg)
			m.awsAccountsViewport.SetContent(m.awsAccountsTable.View())
			awsCurrentRow = m.awsAccountsTable.Cursor()
			if m.awsSelectedRow != awsCurrentRow {
				m.awsSelectedRow = awsCurrentRow
				// Update the viewport content based on the newly selected row
				// Load the file for the selected account and show the file content in the main table
				m.mainTable, m.detailsData, err = getRecordsForAccount(m.preloadedData.Files[m.awsAccountsTable.Rows()[awsCurrentRow][0]])
				if err != nil {
					// Handle error
					break
				}
				m.mainTable.SetStyles(s)
				m.mainTableViewport.SetContent(m.mainTable.View())
				m.detailsViewport.SetContent(m.detailsData[0])

			}
		case 1:
			m.mainTable, cmd = m.mainTable.Update(msg)
			// Check if the selected row has changed
			m.mainTableViewport.SetContent(m.mainTable.View())

			mainCurrentRow = m.mainTable.Cursor()
			if m.mainSelectedRow != mainCurrentRow {
				m.mainSelectedRow = mainCurrentRow
				// Update the viewport content based on the newly selected row
				if detail, ok := m.detailsData[m.mainSelectedRow]; ok {
					m.detailsViewport.SetContent(detail)
				} else {
					m.detailsViewport.SetContent(fmt.Sprintf("No details available for main row %d (aws row %d)", m.mainSelectedRow, m.awsSelectedRow))
				}
			}
		case 2:
			m.detailsViewport, cmd = m.detailsViewport.Update(msg)
		}
	}
	return m, cmd
}

func (m model) View() string {
	// Define styles for active and inactive viewports
	activeBorderStyle := lipgloss.NewStyle().
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("#00FFFF")). // Cyan for active
		Padding(0)
	activeBorderStyle.PaddingLeft(1)
	activeBorderStyle.PaddingRight(1)

	inactiveBorderStyle := lipgloss.NewStyle().
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("#808080")). // Gray for inactive
		Padding(0)
	inactiveBorderStyle.PaddingLeft(1)
	inactiveBorderStyle.PaddingRight(1)

	// Conditionally apply border styles based on focus
	awsTableViewStyle := activeBorderStyle
	m.awsAccountsTable.SetStyles(m.defaultTableStyle)
	tableViewStyle := inactiveBorderStyle
	m.mainTable.SetStyles(m.defaultTableStyle)
	detailsViewStyle := inactiveBorderStyle

	switch m.focusSelector {
	case 0:
		awsTableViewStyle = activeBorderStyle
		m.awsAccountsTable.SetStyles(m.defaultTableStyle)
		tableViewStyle = inactiveBorderStyle
		detailsViewStyle = inactiveBorderStyle
	case 1:
		awsTableViewStyle = inactiveBorderStyle
		tableViewStyle = activeBorderStyle
		m.mainTable.SetStyles(m.defaultTableStyle)
		detailsViewStyle = inactiveBorderStyle
	case 2:
		awsTableViewStyle = inactiveBorderStyle
		tableViewStyle = inactiveBorderStyle
		detailsViewStyle = activeBorderStyle
	}

	// Render the table and details viewports with their styles (adjust based on focus)
	awsAccountsView := awsTableViewStyle.Render(m.awsAccountsViewport.View())
	tableView := tableViewStyle.Render(m.mainTableViewport.View())
	detailsView := detailsViewStyle.Render(m.detailsViewport.View())

	// Combine all views
	fullView := lipgloss.JoinVertical(lipgloss.Top, awsAccountsView, tableView, detailsView)

	helpView := m.help.View(m.keys)

	return fmt.Sprintf("%s\n%s", fullView, helpView)
}

func calculateMaxWidths(rows []table.Row) []int {

	var maxWidths []int
	// make sure the length of the rows is greater than 0

	if len(rows) > 0 {
		maxWidths = make([]int, len(rows[0]))
	} else {
		maxWidths = []int{30, 30, 30, 30}
	}

	for _, row := range rows {
		for i, cell := range row {
			if len(cell) > maxWidths[i] {
				maxWidths[i] = len(cell)
			}
		}
	}

	return maxWidths

}

func (m model) footerView() string {
	info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.detailsViewport.ScrollPercent()*100))
	line := strings.Repeat("─", max(0, m.detailsViewport.Width-lipgloss.Width(info)))
	return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func loadFileRecords(filePath string) (*PerAccountData, error) {
	// Open the file
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	// Decode the JSON data
	var records []CapeJSON
	decoder := json.NewDecoder(file)
	if err := decoder.Decode(&records); err != nil {
		return nil, err
	}

	// Return a new FileRecords instance
	return &PerAccountData{
		FilePath:     filePath,
		PrivescPaths: records,
	}, nil
}

func preloadData(filePaths []string) (*AllAccountData, error) {
	appData := &AllAccountData{
		Files: make(map[string]*PerAccountData),
	}

	for _, filePath := range filePaths {
		fileRecords, err := loadFileRecords(filePath)
		if err != nil {			
			return nil, err
		}
		appData.Files[filePath] = fileRecords
	}

	return appData, nil
}

func getRecordsForAccount(preloadedData *PerAccountData) (table.Model, map[int]string, error) {
	// lets load the records for the first file in the list

	records := preloadedData.PrivescPaths
	if len(records) < 1 {
		mainTable := table.New(
			table.WithColumns([]table.Column{
				{Title: "Account", Width: 30},
				{Title: "Source", Width: 30},
				{Title: "Target", Width: 30},
				{Title: "isTargetAdmin", Width: 30},
			}),
			table.WithRows([]table.Row{{"No records found", "", "", ""}}),
			table.WithFocused(true),
		)
		return mainTable, nil, nil

	}

	// Prepare rows for the table and data for the right view
	rows := make([]table.Row, 0, len(records)-1)
	detailsData := make(map[int]string)  // Initialize the map for the fourth column's data
	for i, record := range records[1:] { // Skip the header row
		rows = append(rows, table.Row{record.Account, record.Source, record.Target, record.IsTargetAdmin})
		detailsData[i] = expandDetailsData(record.Summary)
	}

	// Calculate max widths
	maxWidths := calculateMaxWidths(rows)

	// Define columns with calculated widths
	columns := make([]table.Column, len(maxWidths))
	colNames := []string{}
	for _, header := range []string{"Account", "Source", "Target", "isTargetAdmin"} {
		colNames = append(colNames, header)
	}

	for i, width := range maxWidths {
		colName := colNames[i]
		// if column name width is greater than the calculated width, use the column name width
		if len(colName) > width {
			width = len(colName)
		}
		columns[i] = table.Column{
			Title: colName,
			Width: width,
		}
	}

	mainTable := table.New(
		table.WithColumns(columns),
		table.WithRows(rows),
		table.WithFocused(true),
	)
	return mainTable, detailsData, nil
}

func CapeTUI(outputFiles []string) {

	preloadData, err := preloadData(outputFiles)
	if err != nil {
		fmt.Printf("Error preloading data: %s\n", err)
		fmt.Println("Either remove this profile from the list of profiles, or make sure cape can run successfully for this profile")
		os.Exit(1)
	}

	var awsAccountsRows []table.Row
	for _, file := range preloadData.Files {
		// Extract a user-friendly account name from the file path if needed
		// For simplicity, here we use the file path itself
		awsAccountsRows = append(awsAccountsRows, table.Row{file.FilePath})
	}

	mainTable, detailsData, err := getRecordsForAccount(preloadData.Files[outputFiles[0]])

	s := table.DefaultStyles()
	s.Header = s.Header.
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("240")).
		BorderBottom(true).
		Bold(false)
	s.Selected = s.Selected.
		Foreground(lipgloss.Color("229")).
		Background(lipgloss.Color("57")).
		Bold(false)

	awsAccountsWidth := calculateMaxWidths(awsAccountsRows)
	awsAccountsTable := table.New(
		table.WithColumns([]table.Column{
			{Title: "AWS Accounts", Width: awsAccountsWidth[0]},
		}),
		table.WithRows(awsAccountsRows),
		table.WithFocused(true), // Initially unfocused
	)

	awsAccountsTable.SetStyles(s)
	awsAccountsViewport := viewport.New(0, 0)               // Initialize with 0 size; will be updated
	awsAccountsViewport.SetContent(awsAccountsTable.View()) // Set initial content

	mainTable.SetStyles(s)
	mainTableViewport := viewport.New(0, 0)        // Initialize with 0 size; it will be updated
	mainTableViewport.SetContent(mainTable.View()) // Set the initial content of the table viewport

	// Initialize viewport for details view
	detailsViewportModel := viewport.New(0, 0) // Size will be set based on terminal size in Update
	// show the first row's details by default
	detailsViewportModel.SetContent(detailsData[0])

	m := model{
		preloadedData:       preloadData,
		awsAccountsTable:    awsAccountsTable,
		awsAccountsViewport: awsAccountsViewport,
		mainTable:           mainTable,
		mainTableViewport:   mainTableViewport,
		detailsData:         detailsData,
		detailsViewport:     detailsViewportModel,

		//focusOnTable:  true,
		focusSelector:     0,
		mainSelectedRow:   0, // Initialize selectedRow with an invalid index
		awsSelectedRow:    0,
		defaultTableStyle: s,
		keys:              keys,
		help:              help.New(),
	}

	p := tea.NewProgram(m)
	if err := p.Start(); err != nil {
		fmt.Printf("Error starting program: %s\n", err)
		os.Exit(1)
	}
}

// keyMap defines a set of keybindings. To work for help it must satisfy
// key.Map. It could also very easily be a map[string]key.Binding.
type keyMap struct {
	Up       key.Binding
	Down     key.Binding
	Left     key.Binding
	Right    key.Binding
	Help     key.Binding
	Quit     key.Binding
	Tab      key.Binding
	ShiftTab key.Binding
}

// ShortHelp returns keybindings to be shown in the mini help view. It's part
// of the key.Map interface.
func (k keyMap) ShortHelp() []key.Binding {
	return []key.Binding{k.Tab, k.ShiftTab, k.Up, k.Down, k.Help, k.Quit}
}

// FullHelp returns keybindings for the expanded help view. It's part of the
// key.Map interface.
func (k keyMap) FullHelp() [][]key.Binding {
	return [][]key.Binding{
		{k.Up, k.Down, k.Left, k.Right},     // first column
		{k.Tab, k.ShiftTab, k.Help, k.Quit}, // second column
	}
}

var keys = keyMap{
	Up: key.NewBinding(
		key.WithKeys("up", "k"),
		key.WithHelp("↑/k", "move up"),
	),
	Down: key.NewBinding(
		key.WithKeys("down", "j"),
		key.WithHelp("↓/j", "move down"),
	),
	Left: key.NewBinding(
		key.WithKeys("left", "h"),
		key.WithHelp("←/h", "move left"),
	),
	Right: key.NewBinding(
		key.WithKeys("right", "l"),
		key.WithHelp("→/l", "move right"),
	),
	Help: key.NewBinding(
		key.WithKeys("?"),
		key.WithHelp("?", "toggle help"),
	),
	Quit: key.NewBinding(
		key.WithKeys("q", "esc", "ctrl+c"),
		key.WithHelp("q", "quit"),
	),
	Tab: key.NewBinding(
		key.WithKeys("tab"),
		key.WithHelp("tab", "Switch window focus"),
	),
	ShiftTab: key.NewBinding(
		key.WithKeys("shift+tab"),
		key.WithHelp("shift+tab", "Switch window focus"),
	),
}

func expandDetailsData(input string) string {
	output := ""
	lines := strings.Split(input, "\n")

	for _, line := range lines {
		var hop, option int
		var rest string

		// Use Sscanf to extract hop and option values
		n, err := fmt.Sscanf(line, "[Hop: %d] [Option: %d]", &hop, &option)
		if err != nil || n != 2 {
			fmt.Printf("Error parsing line, expected 2 got %d: %v\n", n, err)
			continue
		}

		// Extract the rest of the line manually
		restIndex := strings.Index(line, "]") + 1
		restIndex = strings.Index(line[restIndex:], "]") + restIndex + 1
		if restIndex > 1 && restIndex < len(line) {
			rest = line[restIndex+1:] // +1 to skip the space after the second ]
		}

		// Construct the formatted output
		formattedLine := fmt.Sprintf("[Hop: %d][Option: %d]\n\t%s\n\n", hop, option, rest)
		output += formattedLine
	}

	return output

}
