// sftp_service.go
package sftpd

import (
	"context"
	"fmt"
	"io"
	"net"
	"os"
	"path/filepath"
	"time"

	"github.com/pkg/sftp"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb"
	"github.com/seaweedfs/seaweedfs/weed/sftpd/auth"
	"github.com/seaweedfs/seaweedfs/weed/sftpd/user"
	"golang.org/x/crypto/ssh"
	"google.golang.org/grpc"
)

// SFTPService holds configuration for the SFTP service.
type SFTPService struct {
	options     SFTPServiceOptions
	userStore   user.Store
	authManager *auth.Manager
}

// SFTPServiceOptions contains all configuration options for the SFTP service.
type SFTPServiceOptions struct {
	GrpcDialOption grpc.DialOption
	DataCenter     string
	FilerGroup     string
	Filer          pb.ServerAddress

	// SSH Configuration
	SshPrivateKey  string        // Legacy single host key
	HostKeysFolder string        // Multiple host keys for different algorithms
	AuthMethods    []string      // Enabled auth methods: "password", "publickey", "certificate"
	MaxAuthTries   int           // Limit authentication attempts
	BannerMessage  string        // Pre-auth banner message
	LoginGraceTime time.Duration // Timeout for authentication

	// Connection Management
	ClientAliveInterval time.Duration // Keep-alive check interval
	ClientAliveCountMax int           // Max missed keep-alives before disconnect

	// User Management
	UserStoreFile string // Path to user store file

	// Certificate Authentication
	TrustedUserCAKeysFile string // Path to file with trusted user CA public keys (OpenSSH authorized_keys format)

	// JWT Configuration for Filer
	FilerSigningKey          []byte // JWT signing key for filer uploads
	FilerSigningExpiresAfter int    // JWT token expiration time in seconds
}

// NewSFTPService creates a new service instance.
func NewSFTPService(options *SFTPServiceOptions) *SFTPService {
	service := SFTPService{options: *options}

	// Initialize user store
	userStore, err := user.NewFileStore(options.UserStoreFile)
	if err != nil {
		glog.Fatalf("Failed to initialize user store: %v", err)
	}
	service.userStore = userStore

	// Initialize auth manager
	authManager, err := auth.NewManager(userStore, options.AuthMethods, options.TrustedUserCAKeysFile)
	if err != nil {
		glog.Fatalf("Failed to initialize auth manager: %v", err)
	}
	service.authManager = authManager

	return &service
}

// Serve accepts incoming connections on the provided listener and handles them.
func (s *SFTPService) Serve(listener net.Listener) error {
	// Build SSH server config
	sshConfig, err := s.buildSSHConfig()
	if err != nil {
		return fmt.Errorf("failed to create SSH config: %w", err)
	}

	glog.V(0).Infof("Starting Seaweed SFTP service on %s", listener.Addr().String())

	for {
		conn, err := listener.Accept()
		if err != nil {
			return fmt.Errorf("failed to accept incoming connection: %w", err)
		}
		go s.handleSSHConnection(conn, sshConfig)
	}
}

// buildSSHConfig creates the SSH server configuration with proper authentication.
func (s *SFTPService) buildSSHConfig() (*ssh.ServerConfig, error) {
	// Get base config from auth manager
	config := s.authManager.GetSSHServerConfig()

	// Set additional options
	config.MaxAuthTries = s.options.MaxAuthTries
	config.BannerCallback = func(conn ssh.ConnMetadata) string {
		return s.options.BannerMessage
	}
	config.ServerVersion = "SSH-2.0-SeaweedFS-SFTP" // Custom server version

	hostKeysAdded := 0
	// Add legacy host key if specified
	if s.options.SshPrivateKey != "" {
		if err := s.addHostKey(config, s.options.SshPrivateKey); err != nil {
			return nil, err
		}
		hostKeysAdded++
	}

	// Add all host keys from the specified folder
	if s.options.HostKeysFolder != "" {
		files, err := os.ReadDir(s.options.HostKeysFolder)
		if err != nil {
			return nil, fmt.Errorf("failed to read host keys folder: %w", err)
		}
		for _, file := range files {
			if file.IsDir() {
				continue // Skip directories
			}

			keyPath := filepath.Join(s.options.HostKeysFolder, file.Name())
			if err := s.addHostKey(config, keyPath); err != nil {
				// Log the error but continue with other keys
				glog.V(0).Info(fmt.Sprintf("Failed to add host key %s: %v", keyPath, err))
				continue
			}
			hostKeysAdded++
		}

		if hostKeysAdded == 0 {
			glog.V(0).Info(fmt.Sprintf("Warning: no valid host keys found in folder %s", s.options.HostKeysFolder))
		}
	}

	// Ensure we have at least one host key
	if hostKeysAdded == 0 {
		return nil, fmt.Errorf("no host keys provided")
	}
	return config, nil
}

// addHostKey adds a host key to the SSH server configuration.
func (s *SFTPService) addHostKey(config *ssh.ServerConfig, keyPath string) error {
	keyBytes, err := os.ReadFile(keyPath)
	if err != nil {
		return fmt.Errorf("failed to read host key %s: %v", keyPath, err)
	}

	// Try parsing as private key
	signer, err := ssh.ParsePrivateKey(keyBytes)
	if err != nil {
		// Try parsing with passphrase if available
		if passphraseErr, ok := err.(*ssh.PassphraseMissingError); ok {
			return fmt.Errorf("host key %s requires passphrase: %v", keyPath, passphraseErr)
		}
		return fmt.Errorf("failed to parse host key %s: %v", keyPath, err)
	}
	config.AddHostKey(signer)
	glog.V(0).Infof("Added host key %s (%s)", keyPath, signer.PublicKey().Type())
	return nil
}

// handleSSHConnection handles an incoming SSH connection.
func (s *SFTPService) handleSSHConnection(conn net.Conn, config *ssh.ServerConfig) {
	// Set connection deadline for handshake
	_ = conn.SetDeadline(time.Now().Add(s.options.LoginGraceTime))

	// Perform SSH handshake
	sshConn, chans, reqs, err := ssh.NewServerConn(conn, config)
	if err != nil {
		glog.Errorf("Failed to handshake: %v", err)
		conn.Close()
		return
	}

	// Clear deadline after successful handshake
	_ = conn.SetDeadline(time.Time{})

	// Set up connection monitoring
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Start keep-alive monitoring
	go s.monitorConnection(ctx, sshConn)

	username := sshConn.Permissions.Extensions["username"]
	glog.V(0).Infof("New SSH connection from %s (%s) as user %s",
		sshConn.RemoteAddr(), sshConn.ClientVersion(), username)

	// Get user from store
	sftpUser, err := s.authManager.GetUser(username)
	if err != nil {
		glog.Errorf("Failed to retrieve user %s: %v", username, err)
		sshConn.Close()
		return
	}

	// Create user-specific filesystem
	userFs := NewSftpServer(
		s.options.Filer,
		s.options.GrpcDialOption,
		s.options.DataCenter,
		s.options.FilerGroup,
		sftpUser,
		s.options.FilerSigningKey,
		s.options.FilerSigningExpiresAfter,
	)

	// Ensure home directory exists with proper permissions
	if err := userFs.EnsureHomeDirectory(); err != nil {
		glog.Errorf("Failed to ensure home directory for user %s: %v", username, err)
		// We don't close the connection here, as the user might still be able to access other directories
	}

	// Handle SSH requests and channels
	go ssh.DiscardRequests(reqs)
	for newChannel := range chans {
		go s.handleChannel(newChannel, &userFs)
	}
}

// monitorConnection monitors an SSH connection with keep-alives.
func (s *SFTPService) monitorConnection(ctx context.Context, sshConn *ssh.ServerConn) {
	if s.options.ClientAliveInterval <= 0 {
		return
	}

	ticker := time.NewTicker(s.options.ClientAliveInterval)
	defer ticker.Stop()

	missedCount := 0

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			// Send keep-alive request
			_, _, err := sshConn.SendRequest("keepalive@openssh.com", true, nil)
			if err != nil {
				missedCount++
				glog.V(0).Infof("Keep-alive missed for %s: %v (%d/%d)",
					sshConn.RemoteAddr(), err, missedCount, s.options.ClientAliveCountMax)

				if missedCount >= s.options.ClientAliveCountMax {
					glog.Warningf("Closing unresponsive connection from %s", sshConn.RemoteAddr())
					sshConn.Close()
					return
				}
			} else {
				missedCount = 0
			}
		}
	}
}

// handleChannel handles a single SSH channel.
func (s *SFTPService) handleChannel(newChannel ssh.NewChannel, fs *SftpServer) {
	if newChannel.ChannelType() != "session" {
		_ = newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
		return
	}

	channel, requests, err := newChannel.Accept()
	if err != nil {
		glog.Errorf("Could not accept channel: %v", err)
		return
	}

	go func(in <-chan *ssh.Request) {
		for req := range in {
			switch req.Type {
			case "subsystem":
				// Check that the subsystem is "sftp".
				if string(req.Payload[4:]) == "sftp" {
					_ = req.Reply(true, nil)
					s.handleSFTP(channel, fs)
				} else {
					_ = req.Reply(false, nil)
				}
			default:
				_ = req.Reply(false, nil)
			}
		}
	}(requests)
}

// handleSFTP starts the SFTP server on the SSH channel.
func (s *SFTPService) handleSFTP(channel ssh.Channel, fs *SftpServer) {
	// Start at virtual root "/" - toAbsolutePath translates this to the user's HomeDir
	serverOptions := sftp.WithStartDirectory("/")
	server := sftp.NewRequestServer(channel, sftp.Handlers{
		FileGet:  fs,
		FilePut:  fs,
		FileCmd:  fs,
		FileList: fs,
	}, serverOptions)

	if err := server.Serve(); err == io.EOF {
		server.Close()
		glog.V(0).Info("SFTP client exited session.")
	} else if err != nil {
		glog.Errorf("SFTP server finished with error: %v", err)
	}
}

// Reload reloads the user store from disk, useful for HUP signal handling
func (s *SFTPService) Reload() {
	glog.V(0).Info("Reload SFTP server...")
	if fileStore, ok := s.userStore.(*user.FileStore); ok {
		if err := fileStore.Reload(); err != nil {
			glog.Errorf("Failed to reload user store: %v", err)
		} else {
			glog.V(0).Info("Successfully reloaded SFTP user store")
		}
	}
}
