// Copyright 2025.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ui

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

	"github.com/Adembc/lazyssh/internal/core/domain"
	"github.com/mattn/go-runewidth"
)

// SSH config value constants
const (
	sshYes   = "yes"
	sshNo    = "no"
	sshForce = "force"
	sshAuto  = "auto"

	// SessionType values
	sessionTypeNone      = "none"
	sessionTypeSubsystem = "subsystem"
)

// renderTagBadgesForList renders up to two colored tag chips for the server list.
// If there are more tags, it appends a subtle gray "+N" badge. Returns an empty
// string when there are no tags to avoid cluttering the list.
func renderTagBadgesForList(tags []string) string {
	if len(tags) == 0 {
		return ""
	}
	maxTags := 2
	shown := tags
	if len(tags) > maxTags {
		shown = tags[:maxTags]
	}
	parts := make([]string, 0, len(shown)+1)
	for _, t := range shown {
		// Light blue background chip, similar to details view.
		parts = append(parts, fmt.Sprintf("[black:#5FAFFF] %s [-:-:-]", t))
	}
	if extra := len(tags) - len(shown); extra > 0 {
		parts = append(parts, fmt.Sprintf("[#8A8A8A]+%d[-]", extra))
	}
	return strings.Join(parts, " ")
}

// cellPad pads a string with spaces so its display width is at least `width` cells.
// This keeps emoji-based icons from breaking alignment in tview.
func cellPad(s string, width int) string {
	w := runewidth.StringWidth(s)
	if w >= width {
		return s
	}
	return s + strings.Repeat(" ", width-w)
}

func pinnedIcon(pinnedAt time.Time) string {
	// Use emojis for a nicer UI; combined with cellPad to keep widths consistent in tview.
	if pinnedAt.IsZero() {
		return "📡" // not pinned
	}
	return "📌" // pinned
}

func formatServerLine(s domain.Server) (primary, secondary string) {
	icon := cellPad(pinnedIcon(s.PinnedAt), 2)
	// Use a consistent color for alias; the icon reflects pinning
	primary = fmt.Sprintf("%s [white::b]%-12s[-] [#AAAAAA]%-18s[-] [#888888]Last SSH: %s[-]  %s", icon, s.Alias, s.Host, humanizeDuration(s.LastSeen), renderTagBadgesForList(s.Tags))
	secondary = ""
	return
}

func humanizeDuration(t time.Time) string {
	if t.IsZero() {
		return "never"
	}
	d := time.Since(t)
	if d < time.Minute {
		return "just now"
	}
	if d < time.Hour {
		m := int(d.Minutes())
		return fmt.Sprintf("%dm ago", m)
	}
	if d < 48*time.Hour {
		h := int(d.Hours())
		return fmt.Sprintf("%dh ago", h)
	}
	if d < 60*24*time.Hour {
		days := int(d.Hours()) / 24
		return fmt.Sprintf("%dd ago", days)
	}
	if d < 365*24*time.Hour {
		months := int(d.Hours()) / (24 * 30)
		if months < 1 {
			months = 1
		}
		return fmt.Sprintf("%dmo ago", months)
	}
	years := int(d.Hours()) / (24 * 365)
	if years < 1 {
		years = 1
	}
	return fmt.Sprintf("%dy ago", years)
}

// BuildSSHCommand constructs a ready-to-run ssh command for the given server.
// Format: ssh [options] [user@]host [command]
func BuildSSHCommand(s domain.Server) string {
	parts := []string{"ssh"}

	// Add proxy and connection options
	addProxyOptions(&parts, s)
	addConnectionTimingOptions(&parts, s)

	// Add port forwarding options
	addPortForwardingOptions(&parts, s)

	// Add authentication options
	addAuthOptions(&parts, s)

	// Add agent and forwarding options
	addForwardingOptions(&parts, s)

	// Add connection multiplexing options
	addMultiplexingOptions(&parts, s)

	// Add connection reliability options
	addConnectionOptions(&parts, s)

	// Add security options
	addSecurityOptions(&parts, s)

	// Add command execution options
	addCommandExecutionOptions(&parts, s)

	// Add environment options
	addEnvironmentOptions(&parts, s)

	// Add TTY and logging options
	addTTYAndLoggingOptions(&parts, s)

	// Port option
	if s.Port != 0 && s.Port != 22 {
		parts = append(parts, "-p", fmt.Sprintf("%d", s.Port))
	}

	// Identity file option
	if len(s.IdentityFiles) > 0 {
		for _, keyFile := range s.IdentityFiles {
			parts = append(parts, "-i", quoteIfNeeded(keyFile))
		}
	}

	// Host specification
	userHost := ""
	switch {
	case s.User != "" && s.Host != "":
		userHost = fmt.Sprintf("%s@%s", s.User, s.Host)
	case s.Host != "":
		userHost = s.Host
	default:
		userHost = s.Alias
	}
	parts = append(parts, userHost)

	// RemoteCommand (must come after the host)
	if s.RemoteCommand != "" {
		// Handle special case: RemoteCommand=none clears the command (OpenSSH 7.6+)
		if s.RemoteCommand == sessionTypeNone {
			parts = append(parts, "-o", "RemoteCommand=none")
		} else {
			parts = append(parts, quoteIfNeeded(s.RemoteCommand))
		}
	}

	return strings.Join(parts, " ")
}

// addOption adds an SSH option in the format "-o Key=Value" if value is not empty
func addOption(parts *[]string, key, value string) {
	if value != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("%s=%s", key, value))
	}
}

// addQuotedOption adds an SSH option with quoted value if needed
func addQuotedOption(parts *[]string, key, value string) {
	if value != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("%s=%s", key, quoteIfNeeded(value)))
	}
}

// addProxyOptions adds proxy-related options to the SSH command
func addProxyOptions(parts *[]string, s domain.Server) {
	if s.ProxyJump != "" {
		*parts = append(*parts, "-J", quoteIfNeeded(s.ProxyJump))
	}
	addQuotedOption(parts, "ProxyCommand", s.ProxyCommand)
}

// addConnectionTimingOptions adds connection timing options to the SSH command
func addConnectionTimingOptions(parts *[]string, s domain.Server) {
	addOption(parts, "ConnectTimeout", s.ConnectTimeout)
	addOption(parts, "ConnectionAttempts", s.ConnectionAttempts)
	if s.BindAddress != "" {
		*parts = append(*parts, "-b", s.BindAddress)
	}
	if s.BindInterface != "" {
		*parts = append(*parts, "-B", s.BindInterface)
	}
	addOption(parts, "AddressFamily", s.AddressFamily)
	addOption(parts, "IPQoS", s.IPQoS)
	// Hostname canonicalization options
	addOption(parts, "CanonicalizeHostname", s.CanonicalizeHostname)
	addOption(parts, "CanonicalDomains", s.CanonicalDomains)
	addOption(parts, "CanonicalizeFallbackLocal", s.CanonicalizeFallbackLocal)
	addOption(parts, "CanonicalizeMaxDots", s.CanonicalizeMaxDots)
	addQuotedOption(parts, "CanonicalizePermittedCNAMEs", s.CanonicalizePermittedCNAMEs)
}

// addPortForwardingOptions adds port forwarding options to the SSH command
func addPortForwardingOptions(parts *[]string, s domain.Server) {
	for _, forward := range s.LocalForward {
		*parts = append(*parts, "-L", forward)
	}
	for _, forward := range s.RemoteForward {
		*parts = append(*parts, "-R", forward)
	}
	for _, forward := range s.DynamicForward {
		*parts = append(*parts, "-D", forward)
	}
	if s.ClearAllForwardings == sshYes {
		*parts = append(*parts, "-o", "ClearAllForwardings=yes")
	}
	if s.ExitOnForwardFailure == sshYes {
		*parts = append(*parts, "-o", "ExitOnForwardFailure=yes")
	}
	if s.GatewayPorts != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("GatewayPorts=%s", s.GatewayPorts))
	}
}

// addAuthOptions adds authentication-related options to the SSH command
func addAuthOptions(parts *[]string, s domain.Server) {
	if s.PubkeyAuthentication != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("PubkeyAuthentication=%s", s.PubkeyAuthentication))
	}
	if s.PubkeyAcceptedAlgorithms != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("PubkeyAcceptedAlgorithms=%s", s.PubkeyAcceptedAlgorithms))
	}
	if s.HostbasedAcceptedAlgorithms != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("HostbasedAcceptedAlgorithms=%s", s.HostbasedAcceptedAlgorithms))
	}
	if s.PasswordAuthentication != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("PasswordAuthentication=%s", s.PasswordAuthentication))
	}
	if s.PreferredAuthentications != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("PreferredAuthentications=%s", s.PreferredAuthentications))
	}
	if s.IdentitiesOnly != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("IdentitiesOnly=%s", s.IdentitiesOnly))
	}
	if s.AddKeysToAgent != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("AddKeysToAgent=%s", s.AddKeysToAgent))
	}
	if s.IdentityAgent != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("IdentityAgent=%s", quoteIfNeeded(s.IdentityAgent)))
	}
	if s.KbdInteractiveAuthentication != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("KbdInteractiveAuthentication=%s", s.KbdInteractiveAuthentication))
	}
	if s.NumberOfPasswordPrompts != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("NumberOfPasswordPrompts=%s", s.NumberOfPasswordPrompts))
	}
}

// addForwardingOptions adds agent and X11 forwarding options to the SSH command
func addForwardingOptions(parts *[]string, s domain.Server) {
	if s.ForwardAgent != "" {
		if s.ForwardAgent == sshYes {
			*parts = append(*parts, "-A")
		} else if s.ForwardAgent == sshNo {
			*parts = append(*parts, "-a")
		}
	}
	if s.ForwardX11 != "" {
		if s.ForwardX11 == sshYes {
			*parts = append(*parts, "-X")
		} else if s.ForwardX11 == sshNo {
			*parts = append(*parts, "-x")
		}
	}
	if s.ForwardX11Trusted == sshYes {
		*parts = append(*parts, "-Y")
	}
}

// addMultiplexingOptions adds connection multiplexing options to the SSH command
func addMultiplexingOptions(parts *[]string, s domain.Server) {
	if s.ControlMaster != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("ControlMaster=%s", s.ControlMaster))
	}
	if s.ControlPath != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("ControlPath=%s", quoteIfNeeded(s.ControlPath)))
	}
	if s.ControlPersist != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("ControlPersist=%s", s.ControlPersist))
	}
}

// addConnectionOptions adds connection reliability options to the SSH command
func addConnectionOptions(parts *[]string, s domain.Server) {
	if s.ServerAliveInterval != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("ServerAliveInterval=%s", s.ServerAliveInterval))
	}
	if s.ServerAliveCountMax != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("ServerAliveCountMax=%s", s.ServerAliveCountMax))
	}
	if s.Compression == sshYes {
		*parts = append(*parts, "-C")
	}
	if s.TCPKeepAlive != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("TCPKeepAlive=%s", s.TCPKeepAlive))
	}
	if s.BatchMode == sshYes {
		*parts = append(*parts, "-o", "BatchMode=yes")
	}
}

// addCommandExecutionOptions adds command execution options to the SSH command
func addCommandExecutionOptions(parts *[]string, s domain.Server) {
	if s.LocalCommand != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("LocalCommand=%s", quoteIfNeeded(s.LocalCommand)))
	}
	if s.PermitLocalCommand != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("PermitLocalCommand=%s", s.PermitLocalCommand))
	}
	if s.EscapeChar != "" {
		*parts = append(*parts, "-e", s.EscapeChar)
	}
}

// addEnvironmentOptions adds environment variable options to the SSH command
func addEnvironmentOptions(parts *[]string, s domain.Server) {
	for _, env := range s.SendEnv {
		*parts = append(*parts, "-o", fmt.Sprintf("SendEnv=%s", env))
	}
	for _, env := range s.SetEnv {
		*parts = append(*parts, "-o", fmt.Sprintf("SetEnv=%s", quoteIfNeeded(env)))
	}
}

// addSecurityOptions adds security-related options to the SSH command
func addSecurityOptions(parts *[]string, s domain.Server) {
	if s.StrictHostKeyChecking != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("StrictHostKeyChecking=%s", s.StrictHostKeyChecking))
	}
	if s.CheckHostIP != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("CheckHostIP=%s", s.CheckHostIP))
	}
	if s.FingerprintHash != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("FingerprintHash=%s", s.FingerprintHash))
	}
	if s.UserKnownHostsFile != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("UserKnownHostsFile=%s", quoteIfNeeded(s.UserKnownHostsFile)))
	}
	if s.HostKeyAlgorithms != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("HostKeyAlgorithms=%s", s.HostKeyAlgorithms))
	}
	if s.MACs != "" {
		*parts = append(*parts, "-m", s.MACs)
	}
	if s.Ciphers != "" {
		*parts = append(*parts, "-c", s.Ciphers)
	}
	if s.KexAlgorithms != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("KexAlgorithms=%s", s.KexAlgorithms))
	}
	if s.VerifyHostKeyDNS != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("VerifyHostKeyDNS=%s", s.VerifyHostKeyDNS))
	}
	if s.UpdateHostKeys != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("UpdateHostKeys=%s", s.UpdateHostKeys))
	}
	if s.HashKnownHosts != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("HashKnownHosts=%s", s.HashKnownHosts))
	}
	if s.VisualHostKey != "" {
		*parts = append(*parts, "-o", fmt.Sprintf("VisualHostKey=%s", s.VisualHostKey))
	}
}

// addTTYAndLoggingOptions adds TTY and logging options to the SSH command
func addTTYAndLoggingOptions(parts *[]string, s domain.Server) {
	// RequestTTY option
	if s.RequestTTY != "" {
		switch s.RequestTTY {
		case sshYes:
			*parts = append(*parts, "-t")
		case sshNo:
			*parts = append(*parts, "-T")
		case sshForce:
			*parts = append(*parts, "-tt")
		case sshAuto:
			// auto is the default behavior, no flag needed
		default:
			// For any other value, pass it as-is via -o
			*parts = append(*parts, "-o", fmt.Sprintf("RequestTTY=%s", s.RequestTTY))
		}
	}

	// LogLevel option
	if s.LogLevel != "" {
		switch strings.ToLower(s.LogLevel) {
		case "quiet":
			*parts = append(*parts, "-q")
		case "verbose":
			*parts = append(*parts, "-v")
		case "debug", "debug1":
			*parts = append(*parts, "-v")
		case "debug2":
			*parts = append(*parts, "-vv")
		case "debug3":
			*parts = append(*parts, "-vvv")
		}
	}

	// SessionType option (OpenSSH 8.7+)
	// "none" is equivalent to -N flag
	if s.SessionType != "" {
		switch s.SessionType {
		case sessionTypeNone:
			// Use -N flag for better compatibility
			*parts = append(*parts, "-N")
		case sessionTypeSubsystem:
			// Use -s flag for subsystem
			*parts = append(*parts, "-s")
		default:
			// For other values, use -o SessionType=
			*parts = append(*parts, "-o", fmt.Sprintf("SessionType=%s", s.SessionType))
		}
	}
}

// quoteIfNeeded returns the value quoted if it contains spaces.
func quoteIfNeeded(val string) string {
	if strings.ContainsAny(val, " \t") {
		return fmt.Sprintf("%q", val)
	}
	return val
}

// GetAvailableSSHKeys returns a list of available SSH private key files in the user's .ssh directory.
// It safely handles file permission issues and only returns readable key files.
func GetAvailableSSHKeys() []string {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return []string{}
	}

	sshDir := filepath.Join(homeDir, ".ssh")
	keys := []string{}

	// Common SSH key filenames to look for
	commonKeyFiles := []string{
		"id_ed25519",
		"id_rsa",
		"id_ecdsa",
		"id_dsa",
		"id_ed25519_sk",
		"id_ecdsa_sk",
	}

	// First, add common key files if they exist and are readable
	for _, keyName := range commonKeyFiles {
		keyPath := filepath.Join(sshDir, keyName)
		if info, err := os.Stat(keyPath); err == nil && !info.IsDir() {
			// Check if file is readable
			// #nosec G304 - keyPath is constructed from known safe values
			if file, err := os.Open(keyPath); err == nil {
				_ = file.Close()
				// Use tilde notation for user-friendly display
				keys = append(keys, "~/.ssh/"+keyName)
			}
		}
	}

	// Additionally, scan for any other files that look like private keys
	// (files without .pub extension and with appropriate permissions)
	entries, err := os.ReadDir(sshDir)
	if err != nil {
		return keys // Return what we have so far
	}

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		name := entry.Name()
		// Skip public keys, known_hosts, config, and authorized_keys
		if strings.HasSuffix(name, ".pub") ||
			name == "known_hosts" ||
			name == "config" ||
			name == "authorized_keys" ||
			strings.HasPrefix(name, ".") {
			continue
		}

		// Skip if already added as common key
		isCommon := false
		for _, commonKey := range commonKeyFiles {
			if name == commonKey {
				isCommon = true
				break
			}
		}
		if isCommon {
			continue
		}

		// Check if file is readable and looks like a private key
		keyPath := filepath.Join(sshDir, name)
		// #nosec G304 - keyPath is constructed from safe directory enumeration
		if file, err := os.Open(keyPath); err == nil {
			// Read first few bytes to check if it might be a private key
			buffer := make([]byte, 100)
			n, readErr := file.Read(buffer)
			_ = file.Close()
			if readErr == nil && n > 0 {
				content := string(buffer[:n])
				if strings.Contains(content, "PRIVATE KEY") ||
					strings.Contains(content, "SSH2 ENCRYPTED PRIVATE KEY") {
					keys = append(keys, "~/.ssh/"+name)
				}
			}
		}
	}

	return keys
}

// GetAvailableKnownHostsFiles returns a list of available known_hosts files in common locations.
// It safely handles file permission issues and only returns readable files.
func GetAvailableKnownHostsFiles() []string {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return []string{}
	}

	files := []string{}

	// Common known_hosts file locations to check
	commonPaths := []string{
		filepath.Join(homeDir, ".ssh", "known_hosts"),
		filepath.Join(homeDir, ".ssh", "known_hosts2"),
		filepath.Join(homeDir, ".ssh", "known_hosts.old"),
		"/etc/ssh/ssh_known_hosts",
		"/etc/ssh/ssh_known_hosts2",
	}

	// Check each common location
	for _, path := range commonPaths {
		if info, err := os.Stat(path); err == nil && !info.IsDir() {
			// Check if file is readable
			// #nosec G304 - path is constructed from known safe values
			if file, err := os.Open(path); err == nil {
				_ = file.Close()
				// Convert to user-friendly path with tilde notation
				if strings.HasPrefix(path, homeDir) {
					relPath := strings.TrimPrefix(path, homeDir)
					files = append(files, "~"+relPath)
				} else {
					files = append(files, path)
				}
			}
		}
	}

	// Also check for any other files in .ssh directory that might be known_hosts files
	sshDir := filepath.Join(homeDir, ".ssh")
	entries, err := os.ReadDir(sshDir)
	if err == nil {
		for _, entry := range entries {
			if entry.IsDir() {
				continue
			}

			name := entry.Name()
			// Look for files that might be known_hosts variants
			if strings.Contains(name, "known_hosts") && !strings.HasSuffix(name, ".pub") {
				// Skip if already added from common paths
				fullPath := filepath.Join(sshDir, name)
				tildeNotation := "~/.ssh/" + name

				alreadyAdded := false
				for _, existing := range files {
					if existing == tildeNotation {
						alreadyAdded = true
						break
					}
				}

				if !alreadyAdded {
					// Check if readable
					// #nosec G304 - fullPath is constructed from safe directory enumeration
					if file, err := os.Open(fullPath); err == nil {
						_ = file.Close()
						files = append(files, tildeNotation)
					}
				}
			}
		}
	}

	return files
}
