package main

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	_ "runtime"
	"strings"
	"syscall"
	"time"

	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/textarea"
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	"github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/glamour"
	"github.com/charmbracelet/lipgloss"
	"github.com/charmbracelet/log"
	"github.com/charmbracelet/ssh"
	"github.com/charmbracelet/wish"
	"github.com/charmbracelet/wish/activeterm"
	"github.com/charmbracelet/wish/bubbletea"
	"github.com/charmbracelet/wish/logging"
	"github.com/muesli/termenv"
)

type model struct {
	state       string
	menu        list.Model
	inputs      []textinput.Model
	sshKeyInput textarea.Model
	userInput   textinput.Model
	activeInput int
	viewport    viewport.Model
	rules       string
	userOutput  string
	width       int
	height      int
	statusMsg   string
	term        string
	profile     string
	bg          string
	txtStyle    lipgloss.Style
	quitStyle   lipgloss.Style
}

type item struct {
	title       string
	description string
}

// // Define a struct to hold user data
// type User struct {
// 	Username  string
// 	Name      string
// 	Address   string
// 	Zip       string
// 	Phone     string
// 	Email     string
// 	SSHKey    string
// 	githubkey string
// }

// Updated User struct to include json tags and timestamps
type User struct {
	Username   string `json:"username"`
	Name       string `json:"name"`
	Address    string `json:"address"`
	Zip        string `json:"zip"`
	Phone      string `json:"phone"`
	Email      string `json:"email"`
	SSHKey     string `json:"ssh_key"`
	GithubUser string `json:"github_user"` // Changed to exported field with proper JSON tag
	Created    string `json:"created"`     // Added to store timestamp
}

func (i item) Title() string       { return i.title }
func (i item) Description() string { return i.description }
func (i item) FilterValue() string { return i.title }

const (
	stateMenu     = "menu"
	stateRegister = "register"
	stateRules    = "rules"
	stateUser     = "user"
	// usersFile     = "users.csv"
	usersFile = "users.json"
)

var (
	focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
	blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
	statusStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("86"))
)

func checkUserExists(username string) bool {
	cmd := exec.Command("getent", "passwd", username)
	err := cmd.Run()
	return err == nil // Returns true if user exists (exit code 0)
}

func sendEmailNotification(user User) error {
	// Create the email body
	emailBody := fmt.Sprintf("Tjena gänget, nu vill en ny lirare slira in i vår förening,\n\n"+
		"Username: %s\n"+
		"Full name: %s\n"+
		"Address: %s\n"+
		"Zip: %s\n"+
		"Phone: %s\n"+
		"Email: %s\n"+
		"Github username: %s\n"+
		"Registration date: %s\n"+
		"SSH KEY: %s",
		user.Username,
		user.Name,
		user.Address,
		user.Zip,
		user.Phone,
		user.Email,
		user.GithubUser,
		user.Created,
		user.SSHKey)

	// Build the command similar to the working terminal command
	subject := fmt.Sprintf("Medlemskapsansökan: %s", user.Username)
	cmd := exec.Command("sh", "-c", fmt.Sprintf("echo '%s' | mail -s '%s' styrelsen@dflund.se", emailBody, subject))

	// Run the command
	if output, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("failed to send email: %v, output: %s", err, string(output))
	}

	return nil
}

func saveUserJSON(user User) error {
	// Set creation timestamp
	// user.Created = time.Now().Format(time.DateOnly)
	// Use ISO 8601 format (e.g., "2025-01-20T15:04:05Z")
	// user.Created = time.Now().Format(time.RFC3339)

	// Read existing users if file exists
	var users []User
	if _, err := os.Stat(usersFile); err == nil {
		fileData, err := os.ReadFile(usersFile)
		if err != nil {
			return fmt.Errorf("failed to read file: %v", err)
		}

		if len(fileData) > 0 {
			if err := json.Unmarshal(fileData, &users); err != nil {
				return fmt.Errorf("failed to parse existing data: %v", err)
			}
		}
	}

	// Append new user
	users = append(users, user)

	// Marshal with indentation
	jsonData, err := json.MarshalIndent(users, "", "    ")
	if err != nil {
		return fmt.Errorf("failed to marshal data: %v", err)
	}

	// Write to file
	if err := os.WriteFile(usersFile, jsonData, 0644); err != nil {
		return fmt.Errorf("failed to write file: %v", err)
	}

	log.Info("User saved successfully", "username", user.Username)

	return nil
}

func saveUserCSV(user User) error {
	// Create file if it doesn't exist
	file, err := os.OpenFile(usersFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to open file: %v", err)
	}
	defer file.Close()

	writer := csv.NewWriter(file)
	defer writer.Flush()

	// Write user data with timestamp
	record := []string{
		user.Username,
		user.Name,
		user.Address,
		user.Zip,
		user.Phone,
		user.Email,
		user.SSHKey,
		time.Now().Format(time.DateOnly),
	}

	if err := writer.Write(record); err != nil {
		return fmt.Errorf("failed to write record: %v", err)
	}

	return nil
}

func initialModel() model {
	// Initialize menu items
	items := []list.Item{
		item{title: "Registrering", description: "Ansök om medlemskap"},
		item{title: "Stadgar", description: "Läs föreningens stadgar"},
		item{title: "Hitta användare", description: "Kolla upp info om medlemmar (finger)"},
		item{title: "Avsluta", description: "Avsluta programmet"},
	}

	ui := textinput.New()
	ui.Placeholder = "Knappa in användarnamn att söka efter"
	ui.Focus()

	// Initialize menu
	menu := list.New(items, list.NewDefaultDelegate(), 0, 0)
	menu.Title = "Datorföreningen vid LU & LTH medlemskapsansökan"
	menu.SetShowStatusBar(false)
	// menu.SetShowTitle(false)

	placeholders := []string{"Användarnamn", "Namn", "Adress", "Postkod", "Telefon", "Epost", "Github användarnamn"}
	inputs := make([]textinput.Model, len(placeholders))

	for i, placeholder := range placeholders {
		t := textinput.New()
		t.CharLimit = 50
		t.Width = 40
		t.Placeholder = placeholder // Set placeholder from the slice
		inputs[i] = t
	}
	inputs[0].Focus()

	// Initialize SSH key textarea
	ti := textarea.New()
	ti.Placeholder = "Klistra in din SSH-nyckel här..."
	ti.ShowLineNumbers = true
	ti.MaxWidth = 60
	ti.MaxHeight = 10

	// Initialize viewport for rules
	vp := viewport.New(80, 20)
	vp.Style = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())

	return model{
		state:       stateMenu,
		menu:        menu,
		inputs:      inputs,
		sshKeyInput: ti,
		userInput:   ui,
		viewport:    vp,
		activeInput: 0,
	}
}

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

func sanitizeField(input string) string {
	return strings.ReplaceAll(strings.TrimSpace(input), ",", ".")
}

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

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height
		m.menu.SetWidth(msg.Width)
		m.menu.SetHeight(msg.Height - 4)
		m.viewport.Width = msg.Width - 4
		m.viewport.Height = msg.Height - 4
		m.sshKeyInput.SetWidth(msg.Width - 4)
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "escape":
			if m.state == stateMenu {
				return m, tea.Quit
			}
			m.state = stateMenu
			return m, nil

		case "up":
			if m.state == stateRegister {
				if m.sshKeyInput.Focused() {
					m.sshKeyInput.Blur()
					m.inputs[len(m.inputs)-1].Focus()
					m.activeInput = len(m.inputs) - 1
				} else if m.activeInput > 0 {
					m.inputs[m.activeInput].Blur()
					m.activeInput--
					m.inputs[m.activeInput].Focus()
				}
			}

		case "down", "tab":
			if m.state == stateRegister {
				if m.activeInput < len(m.inputs)-1 {
					m.inputs[m.activeInput].Blur()
					m.activeInput++
					m.inputs[m.activeInput].Focus()
				} else if !m.sshKeyInput.Focused() {
					m.inputs[m.activeInput].Blur()
					m.sshKeyInput.Focus()
				}
			}

		case "enter":
			if m.state == stateMenu {
				selected := m.menu.SelectedItem()
				if selected == nil {
					return m, nil // No item selected, do nothing
				}

				selectedItem, ok := selected.(item)
				if !ok {
					return m, nil // Type assertion failed, do nothing
				}

				switch selectedItem.title {
				case "Registrering":
					m.state = stateRegister
					m.statusMsg = ""
					// Reset inputs and focus the first one
					for i := range m.inputs {
						m.inputs[i].Reset()
						m.inputs[i].Blur()
					}
					m.activeInput = 0
					m.inputs[0].Focus()
				case "Stadgar":
					m.state = stateRules
					// Read rules from file
					rulesContent, err := os.ReadFile("stadgar.md")
					if err != nil {
						m.viewport.SetContent(fmt.Sprintf("Error reading rules file: %v", err))
						return m, nil
					}

					renderer, _ := glamour.NewTermRenderer(
						glamour.WithAutoStyle(),
						glamour.WithWordWrap(m.viewport.Width),
					)
					rendered, _ := renderer.Render(string(rulesContent))
					m.viewport.SetContent(rendered)
				case "Hitta användare":
					m.state = stateUser
				case "Avsluta":
					return m, tea.Quit
				}
			} else if m.state == stateRegister {
				if m.activeInput == len(m.inputs)-1 {

					// Get fields with sanitization added
					username := sanitizeField(m.inputs[0].Value())
					name := sanitizeField(m.inputs[1].Value())
					address := sanitizeField(m.inputs[2].Value())
					zip := sanitizeField(m.inputs[3].Value())
					phone := sanitizeField(m.inputs[4].Value())
					email := sanitizeField(m.inputs[5].Value())
					sshKey := sanitizeField(m.sshKeyInput.Value())
					githubUsername := sanitizeField(m.inputs[6].Value())
					createDate := time.Now().Format(time.RFC3339)
					
					if checkUserExists(username) {
						m.statusMsg = "Error: User already exists with that username"
						log.Error("User attempted to register with existing username", "username", username)
						return m, nil
					}

					// now we need to curl github.com/<username>.keys and save the output to a file and if the http status code is not 404, save the github username to the user struct
					if githubUsername != "" {

						resp, err := http.Get(fmt.Sprintf("https://github.com/%s.keys", githubUsername))
						if err != nil {
							m.statusMsg = fmt.Sprintf("Error: %v", err)
							return m, nil
						}
						defer resp.Body.Close()

						if resp.StatusCode != http.StatusNotFound {
							// Read the response body
							keys, _ := io.ReadAll(resp.Body)
							sshKey = string(keys)
						} else {
							m.statusMsg = "Error: GitHub user not found"
							return m, nil
						}
					} // no github username specified, use the ssh key from the textarea

					user := User{
						Username:   username,
						Name:       name,
						Address:    address,
						Zip:        zip,
						Phone:      phone,
						Email:      email,
						SSHKey:     sshKey,
						GithubUser: githubUsername,
						Created:   createDate,
					}

					// Basic validation
					if username == "" || name == "" || zip == "" || phone == "" || address == "" || email == "" {
						m.statusMsg = "Error: All fields are required"
						return m, nil
					}

					if err := saveUserJSON(user); err != nil {
						m.statusMsg = fmt.Sprintf("Error saving user: %v", err)
						return m, nil
					}

					// Add the email notification right after saving:
					if err := sendEmailNotification(user); err != nil {
						m.statusMsg = fmt.Sprintf("Error sending notification: %v", err)
						return m, nil
					}

					log.Info("User registered", "username", username)

					// Clear inputs and return to menu
					for i := range m.inputs {
						m.inputs[i].Reset()
					}
					m.sshKeyInput.Reset()
					m.activeInput = 0
					m.inputs[0].Focus()
					m.state = stateMenu
					m.statusMsg = "User registered successfully!"
				} else {
					m.activeInput++
					m.inputs[m.activeInput].Focus()
					m.inputs[m.activeInput-1].Blur()
				}
			} else if m.state == stateUser {
				// Execute finger command with user input
				username := strings.TrimSpace(m.userInput.Value())
				if username != "" {
					cmd := exec.Command("finger", "-m", username)
					output, err := cmd.Output()
					if err != nil {
						m.userOutput = fmt.Sprintf("Error: %v", err)
					} else {
						m.userOutput = string(output)
					}
					m.userInput.Reset()
				}
			}
		}
	}

	switch m.state {
	case stateMenu:
		m.menu, cmd = m.menu.Update(msg)
		cmds = append(cmds, cmd)
	case stateRegister:
		if m.sshKeyInput.Focused() {
			m.sshKeyInput, cmd = m.sshKeyInput.Update(msg)
			cmds = append(cmds, cmd)
		} else {
			for i := range m.inputs {
				m.inputs[i], cmd = m.inputs[i].Update(msg)
				cmds = append(cmds, cmd)
			}
		}
	case stateRules:
		m.viewport, cmd = m.viewport.Update(msg)
		cmds = append(cmds, cmd)
	case stateUser:
		m.userInput, cmd = m.userInput.Update(msg)
		cmds = append(cmds, cmd)
	}

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

func (m model) View() string {
	var b strings.Builder

	switch m.state {
	case stateMenu:

		b.WriteString(m.menu.View())
		if m.statusMsg != "" {
			b.WriteString("\n\n")
			b.WriteString(statusStyle.Render(m.statusMsg))
		}
	case stateRegister:
		b.WriteString("Medlemskapsansökan\n\n")
		for i := range m.inputs {
			if i == m.activeInput {
				b.WriteString(focusedStyle.Render(m.inputs[i].View()))
			} else {
				b.WriteString(blurredStyle.Render(m.inputs[i].View()))
			}
			b.WriteString("\n")
		}
		b.WriteString(blurredStyle.Render("^Hämta ssh nyckel från github (valfritt)\n"))
		b.WriteString("\nSSH Nyckel: (valfritt)\n")
		b.WriteString(m.sshKeyInput.View())
		b.WriteString("\n\n")
		b.WriteString(focusedStyle.Render("Att skicka in ansökan innebär att du godkänner föreningens stadgar.\n\n"))

		if m.statusMsg != "" {
			b.WriteString("\n")
			b.WriteString(statusStyle.Render(m.statusMsg))
		}

		b.WriteString(blurredStyle.Render("\n\nTryck Enter för att skicka in ansökan • Esc för att avbryta"))
		// b.WriteString("\n\nPress Enter to submit • Esc to cancel")
	case stateRules:
		b.WriteString(m.viewport.View())
		b.WriteString("\nPress Esc to return to menu")
	case stateUser:
		b.WriteString("Find User\n\n")
		b.WriteString(m.userInput.View())
		b.WriteString("\n\nPress Enter to search • Esc to return to menu\n\n")
		if m.userOutput != "" {
			b.WriteString(m.userOutput)
		}
	}
	return b.String()
}

const (
	host = "localhost"
	// host = "signup.dflund.se"
	port = "1337"
)

func main() {
	s, err := wish.NewServer(
		wish.WithAddress(net.JoinHostPort(host, port)),
		wish.WithHostKeyPath(".ssh/id_ed25519"),
		wish.WithMiddleware(
			bubbletea.Middleware(teaHandler),
			activeterm.Middleware(), // Bubble Tea apps usually require a PTY.
			logging.Middleware(),
		),
	)
	if err != nil {
		log.Error("Could not start server", "error", err)
	}

	done := make(chan os.Signal, 1)
	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	log.Info("Starting SSH server", "host", host, "port", port)
	go func() {
		if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
			log.Error("Could not start server", "error", err)
			done <- nil
		}
	}()

	<-done
	log.Info("Stopping SSH server")
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer func() { cancel() }()
	if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
		log.Error("Could not stop server", "error", err)
	}
}

// You can wire any Bubble Tea model up to the middleware with a function that
// handles the incoming ssh.Session. Here we just grab the terminal info and
// pass it to the new model. You can also return tea.ProgramOptions (such as
// tea.WithAltScreen) on a session by session basis.
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
	// This should never fail, as we are using the activeterm middleware.
	pty, _, _ := s.Pty()

	// When running a Bubble Tea app over SSH, you shouldn't use the default
	// lipgloss.NewStyle function.
	// That function will use the color profile from the os.Stdin, which is the
	// server, not the client.
	// We provide a MakeRenderer function in the bubbletea middleware package,
	// so you can easily get the correct renderer for the current session, and
	// use it to create the styles.
	// The recommended way to use these styles is to then pass them down to
	// your Bubble Tea model.
	renderer := bubbletea.MakeRenderer(s)
	txtStyle := renderer.NewStyle().Foreground(lipgloss.Color("10"))
	quitStyle := renderer.NewStyle().Foreground(lipgloss.Color("8"))

	bg := "dark"

	m := initialModel()
	m.term = pty.Term
	m.profile = renderer.ColorProfile().Name()
	m.width = pty.Window.Width
	m.height = pty.Window.Height
	m.bg = bg
	m.txtStyle = txtStyle
	m.quitStyle = quitStyle
	lipgloss.SetColorProfile(termenv.TrueColor)
	return m, []tea.ProgramOption{tea.WithAltScreen()}
}
