package tui

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"

	"gitgud.io/mike/mpv-manager/pkg/config"
	"gitgud.io/mike/mpv-manager/pkg/constants"
	"gitgud.io/mike/mpv-manager/pkg/hotkeys"
	"gitgud.io/mike/mpv-manager/pkg/installer"
	"gitgud.io/mike/mpv-manager/pkg/log"
	"gitgud.io/mike/mpv-manager/pkg/platform"
	"gitgud.io/mike/mpv-manager/pkg/version"
	tea "github.com/charmbracelet/bubbletea"
)

func executeInstallCmd(methodID, installDir string, installUOSC bool, p *platform.Platform, inst *installer.Installer) tea.Cmd {
	log.Separator("Installation Operation")
	log.Info(fmt.Sprintf("Starting installation: %s (%s)", methodID, installer.GetMethodDisplayName(methodID)))
	log.Info(fmt.Sprintf("Install directory: %s", installDir))
	log.Info(fmt.Sprintf("uOSC enabled: %v", installUOSC))
	log.Info(fmt.Sprintf("Platform: %s/%s", p.OSType, p.Arch))

	// Convert bool to uiType string
	uiType := constants.UITypeNone
	if installUOSC {
		uiType = constants.UITypeUOSC
	}

	ctx := context.Background()
	return executeWithChannels(fmt.Sprintf("Installing %s", methodID), func(cr *installer.CommandRunner) error {
		handler := installer.NewInstallationHandler(p, inst)
		return handler.ExecuteInstall(ctx, cr, methodID, uiType, false)
	})
}

func executeUninstallCmd(app *config.InstalledApp, p *platform.Platform, inst *installer.Installer) tea.Cmd {
	log.Separator("Uninstallation Operation")
	log.Info(fmt.Sprintf("Starting uninstallation: %s", app.AppName))
	log.Info(fmt.Sprintf("App type: %s", app.AppType))
	log.Info(fmt.Sprintf("Install method: %s", app.InstallMethod))
	if app.InstallPath != "" {
		log.Info(fmt.Sprintf("Install path: %s", app.InstallPath))
	}

	ctx := context.Background()
	return executeWithChannels(fmt.Sprintf("Uninstalling %s", app.AppName), func(cr *installer.CommandRunner) error {
		handler := installer.NewInstallationHandler(p, inst)
		return handler.ExecuteUninstall(ctx, cr, app)
	})
}

func executeUpdateCmd(methodID, appName string, p *platform.Platform, inst *installer.Installer) tea.Cmd {
	log.Separator("Update Operation")
	log.Info(fmt.Sprintf("Updating: %s (%s)", appName, methodID))

	ctx := context.Background()
	return executeWithChannels(fmt.Sprintf("Updating %s", appName), func(cr *installer.CommandRunner) error {
		handler := installer.NewInstallationHandler(p, inst)
		return handler.ExecuteInstall(ctx, cr, methodID, constants.DefaultUIType, true)
	})
}

func getClientName(methodID string, methodName string) string {
	return installer.GetMethodDisplayName(methodID)
}

func (m Model) platformInfoScreen() tea.Cmd {
	return func() tea.Msg {
		return struct{ content string }{content: m.platformInfoView()}
	}
}

func getUserBinDir() string {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return ""
	}

	switch runtime.GOOS {
	case "windows":
		return filepath.Join(homeDir, "bin")
	case "darwin", "linux":
		return filepath.Join(homeDir, ".local", "bin")
	default:
		return filepath.Join(homeDir, "bin")
	}
}

func getShellConfigFile(binDir string) string {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return ""
	}

	shell := os.Getenv("SHELL")
	if shell == "" {
		return ""
	}

	switch {
	case filepath.Base(shell) == "bash":
		bashrc := filepath.Join(homeDir, ".bashrc")
		if _, err := os.Stat(bashrc); err == nil {
			return bashrc
		}
		return filepath.Join(homeDir, ".bash_profile")

	case filepath.Base(shell) == "zsh":
		return filepath.Join(homeDir, ".zshrc")

	case filepath.Base(shell) == "fish":
		return filepath.Join(homeDir, ".config", "fish", "config.fish")

	case filepath.Base(shell) == "nu":
		return filepath.Join(homeDir, ".config", "nu", "config.nu")

	default:
		return ""
	}
}

func getShellName() string {
	shell := os.Getenv("SHELL")
	if shell == "" {
		return "unknown"
	}
	return filepath.Base(shell)
}

// writeAliasToFile safely writes an alias command to a shell config file using direct file I/O.
// This prevents shell command injection by avoiding shell interpretation.
func writeAliasToFile(aliasCommand, shellConfigFile string) error {
	// Open file in append mode, create if doesn't exist
	f, err := os.OpenFile(shellConfigFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return fmt.Errorf("failed to open shell config file: %w", err)
	}
	defer f.Close()

	// Write alias command + newline
	_, err = f.WriteString(aliasCommand + "\n")
	if err != nil {
		return fmt.Errorf("failed to write alias to shell config: %w", err)
	}

	return nil
}

func copyBinaryToDir(inst *installer.Installer, srcPath, destDir string, outputChan chan string) error {
	if _, err := os.Stat(srcPath); err != nil {
		log.Warn(fmt.Sprintf("Source binary not found: %s", srcPath))
		outputChan <- fmt.Sprintf("Warning: Source binary not found: %s", srcPath)
		return fmt.Errorf("source binary not found: %s", srcPath)
	}

	destPath := filepath.Join(destDir, constants.AppName)

	input, err := os.ReadFile(srcPath)
	if err != nil {
		log.Warn(fmt.Sprintf("Failed to read binary: %v", err))
		outputChan <- fmt.Sprintf("Warning: Failed to read binary: %v", err)
		return fmt.Errorf("failed to read binary: %w", err)
	}

	if err := os.WriteFile(destPath, input, 0755); err != nil {
		log.Warn(fmt.Sprintf("Failed to write binary: %v", err))
		outputChan <- fmt.Sprintf("Warning: Failed to write binary: %v", err)
		return fmt.Errorf("failed to write binary: %w", err)
	}

	log.Info(fmt.Sprintf("Binary copied to: %s", destPath))
	outputChan <- "Binary copied successfully"
	return nil
}

func executePathCmd(isRemoving bool, inst *installer.Installer) tea.Cmd {
	outputChan := make(chan string, 100)
	installDone := make(chan error, 1)

	go func() {
		var err error

		if isRemoving {
			log.Separator("PATH Removal Operation")
			log.Info("Removing mpv-manager from system PATH")

			binDir := getUserBinDir()
			destPath := filepath.Join(binDir, constants.AppName)
			altDestPath := filepath.Join(binDir, "mpv-install")

			log.Info(fmt.Sprintf("Bin directory: %s", binDir))
			log.Info(fmt.Sprintf("Binary path: %s", destPath))
			log.Info(fmt.Sprintf("Alias path: %s", altDestPath))

			outputChan <- fmt.Sprintf("Bin directory: %s", binDir)
			outputChan <- fmt.Sprintf("Removing binary: %s", destPath)
			outputChan <- fmt.Sprintf("Removing symlink: %s", altDestPath)

			if err1 := os.Remove(destPath); err1 != nil && !os.IsNotExist(err1) {
				outputChan <- fmt.Sprintf("Warning: %v", err1)
			} else {
				outputChan <- "Binary removed successfully"
			}

			if err2 := os.Remove(altDestPath); err2 != nil && !os.IsNotExist(err2) {
				outputChan <- fmt.Sprintf("Warning: %v", err2)
			} else {
				outputChan <- "Alias removed successfully"
			}

			err = nil
		} else {
			log.Separator("PATH Addition Operation")
			log.Info("Adding mpv-manager to system PATH")

			binDir := getUserBinDir()
			shellConfigFile := getShellConfigFile(binDir)
			shellConfigDir := filepath.Dir(shellConfigFile)

			if _, err := os.Stat(shellConfigDir); os.IsNotExist(err) {
				os.MkdirAll(shellConfigDir, 0755)
				log.Info(fmt.Sprintf("Created directory: %s", shellConfigDir))
			}

			destPath := filepath.Join(binDir, constants.AppName)
			srcBinary, err := os.Executable()
			if err != nil {
				log.Warn(fmt.Sprintf("Failed to get executable path: %v", err))
				outputChan <- fmt.Sprintf("Warning: Failed to get executable path: %v", err)
				installDone <- err
				close(outputChan)
				return
			}

			if _, err := os.Stat(binDir); os.IsNotExist(err) {
				outputChan <- fmt.Sprintf("Creating directory: %s", binDir)
				if err := os.MkdirAll(binDir, 0755); err != nil {
					log.Warn(fmt.Sprintf("Failed to create directory: %v", err))
					outputChan <- fmt.Sprintf("Warning: Failed to create directory: %v", err)
					installDone <- err
					close(outputChan)
					return
				}
			}

			outputChan <- fmt.Sprintf("Bin directory: %s", binDir)
			outputChan <- fmt.Sprintf("Source binary: %s", srcBinary)
			outputChan <- fmt.Sprintf("Binary path: %s", destPath)
			outputChan <- fmt.Sprintf("Shell config: %s", shellConfigFile)

			outputChan <- "Copying binary to bin directory..."
			err = copyBinaryToDir(inst, srcBinary, binDir, outputChan)
			if err != nil {
				installDone <- err
				close(outputChan)
				return
			}

			aliasPath := filepath.Join(binDir, "mpv-install")
			aliasCommand := fmt.Sprintf("alias mpv-install='%s'", destPath)

			outputChan <- "Creating symlink from mpv-install to mpv-manager..."

			if _, err := os.Stat(aliasPath); err == nil {
				log.Info("Removing existing alias file/symlink")
				os.Remove(aliasPath)
			}

			if err := os.Symlink(destPath, aliasPath); err != nil {
				log.Warn(fmt.Sprintf("Failed to create symlink: %v", err))
				outputChan <- fmt.Sprintf("Warning: Failed to create symlink: %v", err)
			} else {
				log.Info(fmt.Sprintf("Symlink created: %s -> %s", aliasPath, destPath))
				outputChan <- "Symlink created successfully"
			}

			if shellConfigFile != "" {
				log.Info(fmt.Sprintf("Adding shell alias to: %s", shellConfigFile))
				outputChan <- fmt.Sprintf("Adding shell alias to: %s", shellConfigFile)

				shell := getShellName()
				log.Info(fmt.Sprintf("Shell type: %s", shell))
				outputChan <- fmt.Sprintf("Shell type: %s", shell)

				err = writeAliasToFile(aliasCommand, shellConfigFile)
				if err != nil {
					log.Warn(fmt.Sprintf("Failed to write alias: %v", err))
					outputChan <- fmt.Sprintf("Warning: Failed to write alias to %s: %v", shellConfigFile, err)
				} else {
					log.Info(fmt.Sprintf("Alias command: %s", aliasCommand))
					log.Info("Shell alias written successfully")
					outputChan <- "Shell alias written successfully"
				}
			} else {
				log.Warn("Shell config file not found, skipping shell alias")
				outputChan <- "Warning: Shell config file not found, skipping shell alias"
			}

			outputChan <- fmt.Sprintf("You may need to restart your terminal or run: source %s", shellConfigFile)
			err = nil
		}

		installDone <- err
		close(outputChan)
	}()

	return tea.Sequence(
		func() tea.Msg {
			return cmdStartMsg{command: fmt.Sprintf("%s mpv-manager from PATH", map[bool]string{true: "Removing", false: "Adding"}[isRemoving])}
		},
		func() tea.Msg {
			return saveChannelsMsg{
				outputChan:  outputChan,
				installDone: installDone,
			}
		},
	)
}

func startStreaming(outputChan <-chan string, installDone <-chan error, progressChan <-chan downloadProgressMsg) tea.Cmd {
	return func() tea.Msg {
		select {
		case line, ok := <-outputChan:
			if ok {
				return cmdOutputMsg{line: line, isError: false}
			}
		case err, ok := <-installDone:
			if ok {
				return cmdDoneMsg{err: err}
			}
		case progress, ok := <-progressChan:
			if ok {
				return downloadProgressMsg{written: progress.written, total: progress.total}
			}
		}
		return tickMsg{}
	}
}

func executeWithChannels(cmdStartMsgText string, fn func(*installer.CommandRunner) error) tea.Cmd {
	outputChan := make(chan string, constants.OutputChanBufferSize)
	installDone := make(chan error, 1)

	go func() {
		cr := installer.NewCommandRunner(outputChan, nil)
		err := fn(cr)
		installDone <- err
		close(outputChan)
	}()

	return tea.Sequence(
		func() tea.Msg {
			return cmdStartMsg{command: cmdStartMsgText}
		},
		func() tea.Msg {
			return saveChannelsMsg{
				outputChan:  outputChan,
				installDone: installDone,
			}
		},
	)
}

func isInstallMPVInPATH() bool {
	binDir := getUserBinDir()
	if binDir == "" {
		return false
	}

	destPath := filepath.Join(binDir, constants.AppName)
	if _, err := os.Stat(destPath); err == nil {
		return true
	}
	return false
}

func checkForUpdateCmd() tea.Cmd {
	return func() tea.Msg {
		result := version.CheckForUpdate()
		return versionCheckMsg{result: result}
	}
}

func checkForUpdatesCmd(inst *installer.Installer) tea.Cmd {
	return func() tea.Msg {
		installedApps := config.GetInstalledApps()
		result := version.CheckForAppUpdates(&inst.ReleaseInfo, installedApps)
		return updateCheckResultMsg{result: result}
	}
}

func runUpdatesCheckCmd(inst *installer.Installer) tea.Cmd {
	return tea.Sequence(
		func() tea.Msg {
			result := version.CheckForUpdate()
			return versionCheckMsg{result: result}
		},
		func() tea.Msg {
			installedApps := config.GetInstalledApps()
			result := version.CheckForAppUpdates(&inst.ReleaseInfo, installedApps)
			return updateCheckResultMsg{result: result}
		},
	)
}

func refreshUninstallList() tea.Cmd {
	return func() tea.Msg {
		apps := config.GetInstalledApps()
		return uninstallAppsLoadedMsg{apps: apps}
	}
}

// uiSelectedMsg is sent when a UI overlay is selected
type uiSelectedMsg struct {
	methodID string
	uiType   string
}

// executeInstallCmdWithUI executes installation with a specific UI type
func executeInstallCmdWithUI(methodID, installDir, uiType string, p *platform.Platform, inst *installer.Installer) tea.Cmd {
	log.Separator("Installation Operation")
	log.Info(fmt.Sprintf("Starting installation: %s (%s)", methodID, installer.GetMethodDisplayName(methodID)))
	log.Info(fmt.Sprintf("Install directory: %s", installDir))
	log.Info(fmt.Sprintf("UI type: %s", uiType))
	log.Info(fmt.Sprintf("Platform: %s/%s", p.OSType, p.Arch))

	ctx := context.Background()
	return executeWithChannels(fmt.Sprintf("Installing %s", methodID), func(cr *installer.CommandRunner) error {
		handler := installer.NewInstallationHandler(p, inst)
		return handler.ExecuteInstall(ctx, cr, methodID, uiType, false)
	})
}

// runApplyPresetCmd applies a hotkey preset and returns a presetApplyMsg
func (m *Model) runApplyPresetCmd(presetID string) tea.Cmd {
	// Find the preset name for display
	var presetName string
	for _, p := range hotkeys.GetPresetList() {
		if p.ID == presetID {
			presetName = p.Name
			break
		}
	}
	if presetName == "" {
		presetName = presetID
	}

	return func() tea.Msg {
		configPath := hotkeys.GetInputConfPath()
		if configPath == "" {
			return presetApplyMsg{presetID: presetID, preset: presetName, err: fmt.Errorf("could not determine input.conf path")}
		}

		err := hotkeys.ApplyPreset(presetID, configPath)
		if err != nil {
			log.Error(fmt.Sprintf("Failed to apply preset %s: %v", presetID, err))
			return presetApplyMsg{presetID: presetID, preset: presetName, err: err}
		}

		log.Info(fmt.Sprintf("Applied hotkey preset: %s", presetName))
		return presetApplyMsg{presetID: presetID, preset: presetName}
	}
}
