package install

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/phishingclub/phishingclub/config"

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

var (
	titleStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#0B3D91")).
			Bold(true).
			Padding(2, 2)

	inputStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#0B3D91"))

	focusedStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#1E88E5")).
			Bold(true)

	blurredStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#0B3D91"))

	cursorStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FF9E43"))

	helpStyle = lipgloss.NewStyle().
			Italic(true).
			Foreground(lipgloss.Color("#607D8B"))

	errorStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#F44336")).
			Bold(true)

	buttonStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#43A047")).
			Bold(true).
			Padding(0, 3)

	sectionStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#6A1B9A")).
			Bold(true).
			Padding(0, 1)

	modeStyle = lipgloss.NewStyle().
			Foreground(lipgloss.Color("#FFFFFF")).
			Background(lipgloss.Color("#FF5722")).
			Bold(true).
			Padding(0, 1)
)

type InstallMode int

const (
	BasicMode InstallMode = iota
	AdvancedMode
)

// inputWithHelp extends textinput.Model to include help text
type InputWithHelp struct {
	textinput.Model
	HelpText string
}

// configModel is the model for the tea app
type ConfigModel struct {
	inputs        []InputWithHelp
	focusIndex    int
	err           error
	shouldInstall bool
	config        *config.Config
	currentMode   InstallMode
}

// init initializes the model
func (m ConfigModel) Init() tea.Cmd {
	return textinput.Blink
}

// update handles updates
func (m ConfigModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "esc":
			return m, tea.Quit

		// mode switching
		case "f1":
			if m.currentMode != BasicMode {
				m.currentMode = BasicMode
				m.focusIndex = 0
				m.inputs = m.createBasicInputs()
				return m, nil
			}
		case "f2":
			if m.currentMode != AdvancedMode {
				m.currentMode = AdvancedMode
				m.focusIndex = 0
				m.inputs = m.createAdvancedInputs()
				return m, nil
			}

		// navigate between inputs with tab/shift+tab
		case "tab", "shift+tab", "enter", "up", "down":
			s := msg.String()

			// check if user pressed enter while submit button was focused
			if s == "enter" && m.focusIndex == len(m.inputs) {
				// validate config and set shouldInstall
				var err error
				m.shouldInstall = true

				// apply input values to configuration
				err = m.applyConfig()
				if err != nil {
					m.err = err
					m.shouldInstall = false
					return m, nil
				}

				return m, tea.Quit
			}

			// cycle indexes
			if s == "up" || s == "shift+tab" {
				m.focusIndex--
			} else {
				m.focusIndex++
			}

			if m.focusIndex > len(m.inputs) {
				m.focusIndex = 0
			} else if m.focusIndex < 0 {
				m.focusIndex = len(m.inputs)
			}

			cmds := make([]tea.Cmd, len(m.inputs))
			for i := 0; i < len(m.inputs); i++ {
				if i == m.focusIndex {
					// set focused state
					cmds[i] = m.inputs[i].Focus()
					m.inputs[i].PromptStyle = focusedStyle
					m.inputs[i].TextStyle = focusedStyle
				} else {
					// remove focused state
					m.inputs[i].Blur()
					m.inputs[i].PromptStyle = blurredStyle
					m.inputs[i].TextStyle = blurredStyle
				}
			}

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

	// handle character input
	cmd := m.updateInputs(msg)
	return m, cmd
}

func (m *ConfigModel) updateInputs(msg tea.Msg) tea.Cmd {
	var cmds = make([]tea.Cmd, len(m.inputs))

	// only text inputs with Focus() set will respond
	for i := range m.inputs {
		m.inputs[i].Model, cmds[i] = m.inputs[i].Model.Update(msg)
	}

	return tea.Batch(cmds...)
}

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

	b.WriteString("\n")
	b.WriteString(titleStyle.Render(" 🐟 Phishing Club Interactive Installer "))
	b.WriteString("\n\n")

	if m.err != nil {
		b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s\n\n", m.err.Error())))
	}

	// show current mode and switching instructions
	modeText := "Basic Mode"
	if m.currentMode == AdvancedMode {
		modeText = "Advanced Mode"
	}
	b.WriteString(fmt.Sprintf("Mode: %s  ", modeStyle.Render(modeText)))
	b.WriteString(helpStyle.Render("(F1: Basic | F2: Advanced)"))
	b.WriteString("\n\n")

	// group inputs by section for advanced mode
	if m.currentMode == AdvancedMode {
		m.renderAdvancedSections(&b)
	} else {
		m.renderBasicInputs(&b)
	}

	button := blurredStyle.Copy()
	if m.focusIndex == len(m.inputs) {
		button = buttonStyle
	}
	fmt.Fprintf(&b, "\n%s\n", button.Render(" Install "))

	return b.String()
}

func (m ConfigModel) renderBasicInputs(b *strings.Builder) {
	for i, input := range m.inputs {
		b.WriteString(input.View())
		// display help text for the focused input
		if i == m.focusIndex {
			b.WriteString("\n  " + helpStyle.Render(input.HelpText))
		}
		b.WriteString("\n")
	}
}

func (m ConfigModel) renderAdvancedSections(b *strings.Builder) {
	sections := []struct {
		title string
		start int
		end   int
	}{
		{"Server Configuration", 0, 6},
		{"Database Configuration", 6, 8},
		{"TLS Configuration", 8, 10},
		{"Logging Configuration", 10, 12},
		{"Security Configuration", 12, len(m.inputs)},
	}

	currentSection := -1
	for i, input := range m.inputs {
		// check if we're starting a new section
		for j, section := range sections {
			if i == section.start {
				if currentSection >= 0 {
					b.WriteString("\n")
				}
				b.WriteString(sectionStyle.Render(fmt.Sprintf(" %s ", section.title)))
				b.WriteString("\n")
				currentSection = j
				break
			}
		}

		b.WriteString(input.View())
		// display help text for the focused input
		if i == m.focusIndex {
			b.WriteString("\n  " + helpStyle.Render(input.HelpText))
		}
		b.WriteString("\n")
	}
}

// createBasicInputs creates inputs for basic mode
func (m *ConfigModel) createBasicInputs() []InputWithHelp {
	var inputs []InputWithHelp
	var prompts = []struct {
		prompt       string
		defaultValue string
		placeholder  string
		description  string
	}{
		{"HTTP port", strconv.Itoa(config.DefaultProductionHTTPPhishingPort), "80", "port for HTTP phishing server"},
		{"HTTPS port", strconv.Itoa(config.DefaultProductionHTTPSPhishingPort), "443", "port for HTTPS phishing server"},
		{"Admin port", strconv.Itoa(config.DefaultProductionAdministrationPort), "0 (random port)", "admin server port - can not be the same as the ports used by the phishing server"},
		{"Admin host", config.DefaultAdminHost, "localhost", "admin server hostname - used for TLS certificate"},
		{"Use Auto TLS", config.DefaultAdminAutoTLSString, "true/false", "use automated TLS for the admin service"},
		{"ACME email", config.DefaultACMEEmail, config.DefaultACMEEmail, "email for Let's Encrypt notifications"},
	}

	for i, p := range prompts {
		inputs = append(inputs, m.createInput(i, p.prompt, p.defaultValue, p.placeholder, p.description))
	}

	return inputs
}

// createAdvancedInputs creates inputs for advanced mode
func (m *ConfigModel) createAdvancedInputs() []InputWithHelp {
	var inputs []InputWithHelp
	var prompts = []struct {
		prompt       string
		defaultValue string
		placeholder  string
		description  string
	}{
		// server configuration
		{"HTTP port", strconv.Itoa(config.DefaultProductionHTTPPhishingPort), "80", "port for HTTP phishing server"},
		{"HTTPS port", strconv.Itoa(config.DefaultProductionHTTPSPhishingPort), "443", "port for HTTPS phishing server"},
		{"Admin port", strconv.Itoa(config.DefaultProductionAdministrationPort), "0 (random port)", "admin server port - can not be the same as the ports used by the phishing server"},
		{"Admin host", config.DefaultAdminHost, "localhost", "admin server hostname - used for TLS certificate"},
		{"Use Auto TLS", config.DefaultAdminAutoTLSString, "true/false", "use automated TLS for the admin service"},
		{"ACME email", config.DefaultACMEEmail, config.DefaultACMEEmail, "email for Let's Encrypt notifications"},

		// database configuration
		{"Database engine", config.DefaultDatabase, "sqlite3/postgres", "database engine to use (sqlite3 or postgres)"},
		{"Database DSN", config.DefaultAdministrationDSN, "file:./db.sqlite3", "database connection string"},

		// tls configuration
		{"TLS cert path", "", "/path/to/cert.pem", "path to TLS certificate file (leave empty for auto TLS)"},
		{"TLS key path", "", "/path/to/key.pem", "path to TLS private key file (leave empty for auto TLS)"},

		// logging configuration
		{"Log file path", config.DefaultLogFilePath, "", "path to log file (empty for stdout)"},
		{"Error log path", config.DefaultErrLogFilePath, "", "path to error log file (empty for stderr)"},

		// security configuration
		{"Admin allowed IPs", "", "192.168.1.0/24,10.0.0.1", "comma-separated list of IP/CIDR ranges allowed to access admin (empty for all)"},
		{"Trusted proxies", "", "192.168.1.1,10.0.0.1", "comma-separated list of trusted proxy IPs/CIDR ranges"},
		{"Trusted IP header", config.DefaultTrustedIPHeader, "X-Real-IP", "header name to check for real client IP from trusted proxies"},
	}

	for i, p := range prompts {
		inputs = append(inputs, m.createInput(i, p.prompt, p.defaultValue, p.placeholder, p.description))
	}

	return inputs
}

// createInput creates a single input field
func (m *ConfigModel) createInput(index int, prompt, defaultValue, placeholder, description string) InputWithHelp {
	t := textinput.New()
	t.Cursor.Style = cursorStyle
	t.CharLimit = 256

	// configure each input
	t.Placeholder = placeholder
	t.PromptStyle = blurredStyle
	t.TextStyle = blurredStyle

	// the first input is focused
	if index == 0 {
		t.PromptStyle = focusedStyle
		t.TextStyle = focusedStyle
		t.Focus()
	}

	// set the prompt with the default value displayed
	t.Prompt = fmt.Sprintf("%s [%s]: ", prompt, defaultValue)

	// create custom input with help text
	return InputWithHelp{
		Model:    t,
		HelpText: description,
	}
}

// initialModel creates the initial model for the tea app
func InitialModel(currentConfig *config.Config) ConfigModel {
	model := ConfigModel{
		config:      currentConfig,
		currentMode: BasicMode,
		focusIndex:  0,
	}
	model.inputs = model.createBasicInputs()
	return model
}

// applyConfig takes the input values and applies them to the config
func (m *ConfigModel) applyConfig() error {
	if m.currentMode == BasicMode {
		return m.applyBasicConfig()
	}
	return m.applyAdvancedConfig()
}

// applyBasicConfig applies basic configuration
func (m *ConfigModel) applyBasicConfig() error {
	// get the input values or use defaults if empty
	httpPort := getValueOrDefault(m.inputs[0].Value(), strconv.Itoa(config.DefaultProductionHTTPPhishingPort))
	httpsPort := getValueOrDefault(m.inputs[1].Value(), strconv.Itoa(config.DefaultProductionHTTPSPhishingPort))
	adminPort := getValueOrDefault(m.inputs[2].Value(), strconv.Itoa(config.DefaultProductionAdministrationPort))
	adminHost := getValueOrDefault(m.inputs[3].Value(), config.DefaultAdminHost)
	autoTLS := getValueOrDefault(m.inputs[4].Value(), config.DefaultAdminAutoTLSString)
	acmeEmail := getValueOrDefault(m.inputs[5].Value(), config.DefaultACMEEmail)

	return m.applyServerConfig(httpPort, httpsPort, adminPort, adminHost, autoTLS, acmeEmail)
}

// applyAdvancedConfig applies advanced configuration
func (m *ConfigModel) applyAdvancedConfig() error {
	// server configuration
	httpPort := getValueOrDefault(m.inputs[0].Value(), strconv.Itoa(config.DefaultProductionHTTPPhishingPort))
	httpsPort := getValueOrDefault(m.inputs[1].Value(), strconv.Itoa(config.DefaultProductionHTTPSPhishingPort))
	adminPort := getValueOrDefault(m.inputs[2].Value(), strconv.Itoa(config.DefaultProductionAdministrationPort))
	adminHost := getValueOrDefault(m.inputs[3].Value(), config.DefaultAdminHost)
	autoTLS := getValueOrDefault(m.inputs[4].Value(), config.DefaultAdminAutoTLSString)
	acmeEmail := getValueOrDefault(m.inputs[5].Value(), config.DefaultACMEEmail)

	// apply server config first
	if err := m.applyServerConfig(httpPort, httpsPort, adminPort, adminHost, autoTLS, acmeEmail); err != nil {
		return err
	}

	// database configuration
	dbEngine := getValueOrDefault(m.inputs[6].Value(), config.DefaultDatabase)
	dbDSN := getValueOrDefault(m.inputs[7].Value(), config.DefaultAdministrationDSN)

	// validate database engine
	if dbEngine != config.DatabaseUsePostgres && dbEngine != config.DefaultAdministrationUseSqlite {
		return fmt.Errorf("invalid database engine: %s (must be 'postgres' or 'sqlite3')", dbEngine)
	}

	// set database config
	m.config.SetDatabaseEngine(dbEngine)
	m.config.SetDatabaseDSN(dbDSN)

	// tls configuration (if not using auto TLS)
	tlsCertPath := m.inputs[8].Value()
	tlsKeyPath := m.inputs[9].Value()
	if tlsCertPath != "" {
		m.config.SetTLSCertPath(tlsCertPath)
	}
	if tlsKeyPath != "" {
		m.config.SetTLSKeyPath(tlsKeyPath)
	}

	// logging configuration
	logPath := m.inputs[10].Value()
	errLogPath := m.inputs[11].Value()
	m.config.SetLogPath(logPath)
	m.config.SetErrLogPath(errLogPath)

	// security configuration
	adminAllowed := m.inputs[12].Value()
	trustedProxies := m.inputs[13].Value()
	trustedIPHeader := m.inputs[14].Value()

	// parse comma-separated IP lists
	adminAllowedList := []string{}
	trustedProxiesList := []string{}

	if adminAllowed != "" {
		adminAllowedList = strings.Split(strings.ReplaceAll(adminAllowed, " ", ""), ",")
	}
	if trustedProxies != "" {
		trustedProxiesList = strings.Split(strings.ReplaceAll(trustedProxies, " ", ""), ",")
	}

	// set security config
	m.config.IPSecurity.AdminAllowed = adminAllowedList
	m.config.IPSecurity.TrustedProxies = trustedProxiesList
	m.config.IPSecurity.TrustedIPHeader = trustedIPHeader

	return nil
}

// applyServerConfig applies server configuration (common to both modes)
func (m *ConfigModel) applyServerConfig(httpPort, httpsPort, adminPort, adminHost, autoTLS, acmeEmail string) error {
	// convert ports to integers
	httpPortInt, err := strconv.Atoi(httpPort)
	if err != nil {
		return fmt.Errorf("invalid HTTP port: %w", err)
	}

	httpsPortInt, err := strconv.Atoi(httpsPort)
	if err != nil {
		return fmt.Errorf("invalid HTTPS port: %w", err)
	}

	adminPortInt, err := strconv.Atoi(adminPort)
	if err != nil {
		return fmt.Errorf("invalid admin port: %w", err)
	}

	// validate port values
	if httpPortInt <= 0 || httpPortInt > 65535 {
		return fmt.Errorf("HTTP port must be between 1 and 65535")
	}
	if httpsPortInt <= 0 || httpsPortInt > 65535 {
		return fmt.Errorf("HTTPS port must be between 1 and 65535")
	}
	if adminPortInt < 0 || adminPortInt > 65535 {
		return fmt.Errorf("admin port must be between 0 and 65535")
	}

	// check for port conflicts
	if httpPortInt == httpsPortInt {
		return fmt.Errorf("HTTP and HTTPS ports cannot be the same")
	}
	if adminPortInt != 0 && (adminPortInt == httpPortInt || adminPortInt == httpsPortInt) {
		return fmt.Errorf("admin port cannot be the same as HTTP or HTTPS ports")
	}

	// convert autoTLS to boolean
	autoTLSBool := false
	if strings.ToLower(autoTLS) == config.DefaultAdminAutoTLSString {
		autoTLSBool = true
	}

	// set values in config
	err = m.config.SetPhishingHTTPNetAddress(fmt.Sprintf("0.0.0.0:%d", httpPortInt))
	if err != nil {
		return fmt.Errorf("failed to set HTTP address: %w", err)
	}

	err = m.config.SetPhishingHTTPSNetAddress(fmt.Sprintf("0.0.0.0:%d", httpsPortInt))
	if err != nil {
		return fmt.Errorf("failed to set HTTPS address: %w", err)
	}

	err = m.config.SetAdminNetAddress(fmt.Sprintf("0.0.0.0:%d", adminPortInt))
	if err != nil {
		return fmt.Errorf("failed to set admin address: %w", err)
	}

	m.config.SetTLSHost(adminHost)
	m.config.SetTLSAuto(autoTLSBool)
	m.config.SetACMEEmail(acmeEmail)

	return nil
}

// getValueOrDefault returns the value or the default if value is empty
func getValueOrDefault(value, defaultValue string) string {
	if value == "" {
		return defaultValue
	}
	return value
}

func RunInteractiveInstall() error {
	// first check if we're running as root
	if os.Geteuid() != 0 {
		return fmt.Errorf("installation must be run as root")
	}

	// create installation directories first
	if err := createDirectories(); err != nil {
		return fmt.Errorf("failed to create install directories: %w", err)
	}

	// get default configuration
	conf := config.NewProductionDefaultConfig()

	// run the tea program
	p := tea.NewProgram(InitialModel(conf))
	model, err := p.Run()
	if err != nil {
		return fmt.Errorf("error running interactive installer: %w", err)
	}

	// get the final model
	finalModel := model.(ConfigModel)
	if !finalModel.shouldInstall {
		return fmt.Errorf("installation cancelled")
	}

	// save the config to the installation directory
	configPath := filepath.Join(installDir, "config.json")
	err = finalModel.config.WriteToFile(configPath)
	if err != nil {
		return fmt.Errorf("failed to save configuration to %s: %w", configPath, err)
	}
	if err := os.Chmod(configPath, 0600); err != nil {
		return fmt.Errorf("failed to set config file permissions: %w", err)
	}

	fmt.Printf("Configuration saved to %s\n", configPath)

	// now run the actual installation
	err = InstallWithConfig(finalModel.config)
	if err != nil {
		return err
	}
	return nil
}

// runInteractiveConfigOnly runs the interactive installer and saves config without installing
func RunInteractiveConfigOnly(configPath string) error {
	fmt.Println("📝 Running in CONFIG-ONLY mode - no actual installation will be performed")
	fmt.Println()

	// get default configuration (no root check needed for config-only mode)
	conf := config.NewProductionDefaultConfig()

	// run the tea program
	p := tea.NewProgram(InitialModel(conf))
	model, err := p.Run()
	if err != nil {
		return fmt.Errorf("error running interactive installer: %w", err)
	}

	// get the final model
	finalModel := model.(ConfigModel)
	if !finalModel.shouldInstall {
		fmt.Println("Installation cancelled by user")
		return nil
	}

	// save the config to the specified path
	err = finalModel.config.WriteToFile(configPath)
	if err != nil {
		return fmt.Errorf("failed to save configuration to %s: %w", configPath, err)
	}

	fmt.Printf("✅ Configuration saved to %s\n", configPath)
	fmt.Println("💡 Review the config file and run without --config-only flag as root to install")

	return nil
}

// installWithConfig handles the installation using the provided configuration
func InstallWithConfig(conf *config.Config) error {
	steps := []struct {
		name string
		fn   func() error
	}{
		{"check sqlite dependency", checkSQLiteDependency},
		{"create user and group", createUserAndGroup},
		{"create directories", createDirectories},
		{"install binary", installBinary},
		{"install systemd service", installSystemdService},
		{"set permissions", setPermissions},
		{"enable service", enableService},
		{"start service", startService},
		{"print info", outputCredentialsAndInfo},
	}

	for _, step := range steps {
		fmt.Printf("Step: %s\n", step.name)
		if err := step.fn(); err != nil {
			return fmt.Errorf("%s: %w", step.name, err)
		}
	}

	fmt.Println()
	fmt.Println("Installer completed successfully! 🐟")
	fmt.Println()
	fmt.Println("# Tips")
	fmt.Println("'journalctl -u phishingclub.service -f' to see logs")
	fmt.Println("'systemctl status phishingclub' to check status of the service")
	fmt.Println("")

	return nil
}
