package hotkeys

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

	"gitgud.io/mike/mpv-manager/internal/assets"
	"gitgud.io/mike/mpv-manager/pkg/constants"
	"gitgud.io/mike/mpv-manager/pkg/log"
)

// inputConfFileName is the mpv input configuration file name.
const inputConfFileName = "input.conf"

// inputConfBackupPrefix is used when creating timestamped backups.
const inputConfBackupPrefix = "-input.conf.bak"

// Binding represents a single parsed line from an input.conf file.
type Binding struct {
	Key       string // Key combination (e.g. "Ctrl+RIGHT", "SPACE")
	Command   string // mpv command (e.g. "seek 5", "cycle pause")
	Comment   string // Inline comment text after the command (without "#")
	IsComment bool   // True if the line is a pure comment line
	IsSection bool   // True if the line is a section header like [default]
	IsEmpty   bool   // True if the line is blank
	RawLine   string // Original line text for round-trip preservation
}

// InputConfig holds the parsed representation of an input.conf file.
type InputConfig struct {
	Bindings []Binding
	FilePath string
}

// PresetInfo describes an available hotkey preset profile.
type PresetInfo struct {
	ID          string // Machine identifier: "default", "vlc", "mpc-hc"
	Name        string // Display name: "mpv Default", "VLC", "MPC-HC"
	Description string // Short description of the preset
}

// ParseInputConf parses input.conf content into structured bindings.
//
// The input.conf format is:
//
//	KEY COMMAND [# comment]
//	# comment line
//	[section-name]
//	(blank lines)
//
// Each binding line has a key and at least one command word, optionally followed
// by a "#" and inline comment. Comment lines, section headers, and blank lines are
// preserved for round-trip fidelity.
func ParseInputConf(content string) (*InputConfig, error) {
	config := &InputConfig{
		Bindings: []Binding{},
	}

	lines := strings.Split(content, "\n")

	for _, line := range lines {
		trimmed := strings.TrimSpace(line)

		// Blank line
		if trimmed == "" {
			config.Bindings = append(config.Bindings, Binding{
				IsEmpty: true,
				RawLine: line,
			})
			continue
		}

		// Section header (e.g. [default])
		if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
			config.Bindings = append(config.Bindings, Binding{
				IsSection: true,
				RawLine:   line,
			})
			continue
		}

		// Pure comment line
		if strings.HasPrefix(trimmed, "#") {
			config.Bindings = append(config.Bindings, Binding{
				IsComment: true,
				Comment:   strings.TrimPrefix(trimmed, "#"),
				RawLine:   line,
			})
			continue
		}

		// Binding line: KEY COMMAND [# comment]
		binding := parseBindingLine(line)
		config.Bindings = append(config.Bindings, binding)
	}

	return config, nil
}

// parseBindingLine parses a single binding line into a Binding struct.
func parseBindingLine(line string) Binding {
	binding := Binding{
		RawLine: line,
	}

	trimmed := strings.TrimSpace(line)

	// Split off inline comment if present. We look for " #" to avoid splitting
	// inside quoted command arguments that contain "#".
	var commentPart string
	if idx := strings.Index(trimmed, " #"); idx != -1 {
		commentPart = trimmed[idx+2:] // text after "# "
		trimmed = trimmed[:idx]
	}

	// First field is the key, the rest is the command
	fields := strings.Fields(trimmed)
	if len(fields) == 0 {
		binding.IsEmpty = true
		return binding
	}

	binding.Key = fields[0]
	if len(fields) > 1 {
		binding.Command = strings.Join(fields[1:], " ")
	}
	binding.Comment = commentPart

	return binding
}

// ReadInputConf reads and parses the input.conf file from disk.
// If the file does not exist it returns an empty InputConfig (no error).
func ReadInputConf(path string) (*InputConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return &InputConfig{
				Bindings: []Binding{},
				FilePath: path,
			}, nil
		}
		return nil, fmt.Errorf("failed to read input.conf: %w", err)
	}

	config, err := ParseInputConf(string(data))
	if err != nil {
		return nil, err
	}
	config.FilePath = path
	return config, nil
}

// WriteInputConf writes an InputConfig back to disk using an atomic write pattern.
// A timestamped backup of any existing file is created before overwriting.
func WriteInputConf(config *InputConfig, path string) error {
	// Create backup of existing file if it exists
	if _, err := os.Stat(path); err == nil {
		if backupErr := createInputConfBackup(path); backupErr != nil {
			log.Warn(fmt.Sprintf("Failed to create input.conf backup: %v", backupErr))
			// Continue writing even if backup fails
		}
	}

	content := SerializeInputConf(config)
	return writeFileAtomically(path, []byte(content))
}

// SerializeInputConf converts an InputConfig back to its text representation.
func SerializeInputConf(config *InputConfig) string {
	var lines []string
	for _, b := range config.Bindings {
		lines = append(lines, serializeBinding(b))
	}
	return strings.Join(lines, "\n")
}

// serializeBinding converts a single Binding back to its text form.
func serializeBinding(b Binding) string {
	if b.IsEmpty {
		return ""
	}
	if b.IsComment {
		return "#" + b.Comment
	}
	if b.IsSection {
		return b.RawLine
	}

	// Regular binding
	var sb strings.Builder
	sb.WriteString(b.Key)
	if b.Command != "" {
		sb.WriteString(" ")
		sb.WriteString(b.Command)
	}
	if b.Comment != "" {
		sb.WriteString(" #")
		sb.WriteString(b.Comment)
	}
	return sb.String()
}

// GetInputConfPath returns the platform-aware path to the user's input.conf.
// It lives in the same directory as mpv.conf.
func GetInputConfPath() string {
	configPath, err := constants.GetMPVConfigPathWithHome()
	if err != nil {
		log.Warn(fmt.Sprintf("Failed to get MPV config path: %v", err))
		return ""
	}
	dir := filepath.Dir(configPath)
	return filepath.Join(dir, inputConfFileName)
}

// LoadPreset loads a preset's raw content from the embedded assets.
// Valid preset names are "default", "vlc", and "mpc-hc".
func LoadPreset(presetName string) (string, error) {
	data, err := assets.ReadHotkeyPreset(presetName)
	if err != nil {
		return "", fmt.Errorf("unknown hotkey preset %q: %w", presetName, err)
	}
	return string(data), nil
}

// ApplyPreset loads the named preset and writes it to configPath atomically.
// A backup of any existing input.conf at configPath is created first.
func ApplyPreset(presetName string, configPath string) error {
	content, err := LoadPreset(presetName)
	if err != nil {
		return err
	}

	// Ensure the parent directory exists
	configDir := filepath.Dir(configPath)
	if err := os.MkdirAll(configDir, constants.DirPermission); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Create backup if file exists
	if _, err := os.Stat(configPath); err == nil {
		if backupErr := createInputConfBackup(configPath); backupErr != nil {
			log.Warn(fmt.Sprintf("Failed to backup existing input.conf: %v", backupErr))
		}
	}

	return writeFileAtomically(configPath, []byte(content))
}

// GetPresetList returns information about all available preset profiles.
func GetPresetList() []PresetInfo {
	return []PresetInfo{
		{
			ID:          "default",
			Name:        "mpv Default",
			Description: "Standard mpv keybindings as shipped upstream",
		},
		{
			ID:          "vlc",
			Name:        "VLC",
			Description: "Keybindings modeled after VLC media player",
		},
		{
			ID:          "mpc-hc",
			Name:        "MPC-HC",
			Description: "Keybindings modeled after Media Player Classic Home Cinema",
		},
	}
}

// GetActiveBindings returns the parsed bindings from the user's actual input.conf.
// If the file doesn't exist, it returns an empty slice (no error) — mpv would use
// its built-in defaults in that case.
func GetActiveBindings() ([]Binding, error) {
	path := GetInputConfPath()
	if path == "" {
		return nil, nil
	}

	config, err := ReadInputConf(path)
	if err != nil {
		return nil, err
	}

	// Filter out non-binding lines for callers that only want actual key mappings
	var bindings []Binding
	for _, b := range config.Bindings {
		if b.Key != "" && b.Command != "" && !b.IsComment && !b.IsEmpty && !b.IsSection {
			bindings = append(bindings, b)
		}
	}
	return bindings, nil
}

// FindBinding returns the binding for a given key, or nil if not found.
func FindBinding(config *InputConfig, key string) *Binding {
	for i := range config.Bindings {
		if config.Bindings[i].Key == key && !config.Bindings[i].IsComment && !config.Bindings[i].IsEmpty {
			return &config.Bindings[i]
		}
	}
	return nil
}

// SetBinding adds or updates a key binding in the config.
// If the key already exists the command and comment are replaced;
// otherwise the binding is appended.
func SetBinding(config *InputConfig, key, command, comment string) {
	for i := range config.Bindings {
		if config.Bindings[i].Key == key && !config.Bindings[i].IsComment && !config.Bindings[i].IsEmpty {
			config.Bindings[i].Command = command
			config.Bindings[i].Comment = comment
			return
		}
	}
	config.Bindings = append(config.Bindings, Binding{
		Key:     key,
		Command: command,
		Comment: comment,
	})
}

// RemoveBinding removes a key binding from the config.
// Returns true if the binding was found and removed.
func RemoveBinding(config *InputConfig, key string) bool {
	for i := range config.Bindings {
		if config.Bindings[i].Key == key && !config.Bindings[i].IsComment && !config.Bindings[i].IsEmpty {
			config.Bindings = append(config.Bindings[:i], config.Bindings[i+1:]...)
			return true
		}
	}
	return false
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

// writeFileAtomically writes content to a file using an atomic rename pattern.
func writeFileAtomically(path string, content []byte) error {
	tempPath := path + ".tmp"

	if err := os.WriteFile(tempPath, content, constants.FilePermission); err != nil {
		return fmt.Errorf("failed to write temp file: %w", err)
	}

	if err := os.Rename(tempPath, path); err != nil {
		_ = os.Remove(tempPath)
		return fmt.Errorf("failed to rename temp file: %w", err)
	}

	return nil
}

// createInputConfBackup creates a timestamped backup copy of the given input.conf file.
func createInputConfBackup(filePath string) error {
	timestamp := time.Now().Format("2006-01-02_150405")
	backupPath := filePath + inputConfBackupPrefix + "." + timestamp

	data, err := os.ReadFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to read file for backup: %w", err)
	}

	if err := os.WriteFile(backupPath, data, constants.FilePermission); err != nil {
		return fmt.Errorf("failed to write backup file: %w", err)
	}

	log.Info(fmt.Sprintf("Created input.conf backup: %s", backupPath))
	return nil
}
