package deployer

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"strconv"
	"time"

	"golang.org/x/crypto/ssh"
)

// LineCallback is called for each line of deploy output.
type LineCallback func(line string)

// FileMapping maps a local file path to a remote server path.
type FileMapping struct {
	Local  string `json:"local"`
	Remote string `json:"remote"`
}

// Config holds SSH connection parameters.
type Config struct {
	Host       string
	Port       int
	Username   string
	AuthMethod string // "key" or "password"
	SSHKey     string // private key content (PEM)
	Password   string
}

// DeployResult holds the outcome of a deployment.
type DeployResult struct {
	Success bool
	Output  string
}

// Service handles SSH deployment operations.
type Service struct{}

// New creates a new deployer Service.
func New() *Service {
	return &Service{}
}

// Deploy performs a full deployment: connect, pre-commands, upload files, post-commands, health check.
// Output is streamed via the onLine callback.
func (s *Service) Deploy(ctx context.Context, cfg *Config, deployDir string,
	mappings []FileMapping, preCmd, postCmd, healthCheckURL string,
	healthCheckTimeout int, onLine LineCallback) (*DeployResult, error) {

	log := func(format string, args ...any) {
		onLine(fmt.Sprintf(format, args...))
	}

	log("=== Deploy started at %s ===", time.Now().Format(time.RFC3339))
	log("Target: %s@%s:%d", cfg.Username, cfg.Host, cfg.Port)
	log("Deploy directory: %s", deployDir)
	log("Auth method: %s", cfg.AuthMethod)

	// 1. Connect to SSH server
	client, err := s.dial(cfg)
	if err != nil {
		log("ERROR: SSH connection failed: %v", err)
		return &DeployResult{Success: false, Output: fmt.Sprintf("SSH connection failed: %v", err)}, err
	}
	defer client.Close()
	log("SSH connection established")

	// 2. Ensure remote deploy directory exists
	if err := s.runCommand(client, fmt.Sprintf("mkdir -p %s", deployDir), onLine); err != nil {
		log("ERROR: Failed to create deploy directory: %v", err)
		return &DeployResult{Success: false, Output: fmt.Sprintf("mkdir failed: %v", err)}, err
	}
	log("Remote directory ensured: %s", deployDir)

	// 3. Execute pre-deploy commands
	if preCmd != "" {
		log("=== Executing pre-deploy command ===")
		log("$ %s", preCmd)
		if err := s.runCommand(client, preCmd, onLine); err != nil {
			log("ERROR: Pre-deploy command failed: %v", err)
			return &DeployResult{Success: false, Output: fmt.Sprintf("pre-deploy failed: %v", err)}, err
		}
		log("Pre-deploy command completed")
	}

	// 4. Upload files
	if len(mappings) > 0 {
		log("=== Uploading %d file(s) ===", len(mappings))
	}
	for _, m := range mappings {
		if err := s.uploadFile(client, m.Local, m.Remote, onLine); err != nil {
			log("ERROR: Failed to upload %s: %v", m.Local, err)
			return &DeployResult{Success: false, Output: fmt.Sprintf("upload %s failed: %v", m.Local, err)}, err
		}
	}

	// 5. Execute post-deploy commands
	if postCmd != "" {
		log("=== Executing post-deploy command ===")
		log("$ %s", postCmd)
		if err := s.runCommand(client, postCmd, onLine); err != nil {
			log("ERROR: Post-deploy command failed: %v", err)
			return &DeployResult{Success: false, Output: fmt.Sprintf("post-deploy failed: %v", err)}, err
		}
		log("Post-deploy command completed")
	}

	// 6. Health check
	if healthCheckURL != "" {
		log("=== Health check ===")
		log("Checking %s (timeout: %ds)", healthCheckURL, healthCheckTimeout)

		timeout := time.Duration(healthCheckTimeout) * time.Second
		if err := s.checkHealth(healthCheckURL, timeout); err != nil {
			log("WARNING: Health check failed: %v", err)
			// Health check failure is a warning, not a deploy failure
		} else {
			log("Health check passed")
		}
	}

	log("=== Deploy completed successfully at %s ===", time.Now().Format(time.RFC3339))
	return &DeployResult{Success: true, Output: "Deployment succeeded"}, nil
}

// dial establishes an SSH connection.
func (s *Service) dial(cfg *Config) (*ssh.Client, error) {
	auth, err := s.makeAuth(cfg)
	if err != nil {
		return nil, fmt.Errorf("make auth: %w", err)
	}

	sshCfg := &ssh.ClientConfig{
		User:            cfg.Username,
		Auth:            []ssh.AuthMethod{auth},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         10 * time.Second,
	}

	addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))
	return ssh.Dial("tcp", addr, sshCfg)
}

// makeAuth builds the ssh.AuthMethod from config.
func (s *Service) makeAuth(cfg *Config) (ssh.AuthMethod, error) {
	switch cfg.AuthMethod {
	case "password":
		return ssh.Password(cfg.Password), nil
	case "key":
		// Try to parse the private key
		key, err := ssh.ParsePrivateKey([]byte(cfg.SSHKey))
		if err != nil {
			// Try parsing with passphrase (empty passphrase)
			key, err2 := ssh.ParsePrivateKeyWithPassphrase([]byte(cfg.SSHKey), []byte{})
			if err2 != nil {
				return nil, fmt.Errorf("parse private key: %w (original: %v)", err2, err)
			}
			return ssh.PublicKeys(key), nil
		}
		return ssh.PublicKeys(key), nil
	default:
		return nil, fmt.Errorf("unsupported auth method: %s", cfg.AuthMethod)
	}
}

// runCommand executes a command on the remote host and streams output line by line.
func (s *Service) runCommand(client *ssh.Client, cmd string, onLine LineCallback) error {
	session, err := client.NewSession()
	if err != nil {
		return fmt.Errorf("new session: %w", err)
	}
	defer session.Close()

	// Combine stdout and stderr
	stdout, err := session.StdoutPipe()
	if err != nil {
		return fmt.Errorf("stdout pipe: %w", err)
	}
	stderr, err := session.StderrPipe()
	if err != nil {
		return fmt.Errorf("stderr pipe: %w", err)
	}

	if err := session.Start(cmd); err != nil {
		return fmt.Errorf("start command: %w", err)
	}

	// Stream stdout lines
	go streamLines(stdout, onLine)
	// Stream stderr lines
	go streamLines(stderr, onLine)

	err = session.Wait()
	if err != nil {
		if exitErr, ok := err.(*ssh.ExitError); ok {
			return fmt.Errorf("command exited with %d", exitErr.ExitStatus())
		}
		return fmt.Errorf("command error: %w", err)
	}
	return nil
}

// uploadFile copies a local file to a remote path via SSH session.
// Uses "cat > remotePath" which is universally available on Linux.
func (s *Service) uploadFile(client *ssh.Client, localPath, remotePath string, onLine LineCallback) error {
	// Get file info
	info, err := os.Stat(localPath)
	if err != nil {
		return fmt.Errorf("stat local file: %w", err)
	}

	fileSize := info.Size()
	onLine(fmt.Sprintf("Uploading %s (%d bytes) -> %s", localPath, fileSize, remotePath))

	// Open local file
	f, err := os.Open(localPath)
	if err != nil {
		return fmt.Errorf("open local file: %w", err)
	}
	defer f.Close()

	session, err := client.NewSession()
	if err != nil {
		return fmt.Errorf("new session: %w", err)
	}
	defer session.Close()

	// Pipe file content to cat > remotePath on the server
	session.Stdin = f

	// Capture stderr for error reporting
	stderr, err := session.StderrPipe()
	if err != nil {
		return fmt.Errorf("stderr pipe: %w", err)
	}

	// Also create parent directories if needed
	cmd := fmt.Sprintf("mkdir -p $(dirname %s) && cat > %s", remotePath, remotePath)
	if err := session.Start(cmd); err != nil {
		return fmt.Errorf("start upload: %w", err)
	}

	// Read stderr in background
	errBuf := make([]byte, 4096)
	n, _ := stderr.Read(errBuf)

	err = session.Wait()
	if err != nil {
		if n > 0 {
			return fmt.Errorf("upload failed: %s: %w", string(errBuf[:n]), err)
		}
		return fmt.Errorf("upload failed: %w", err)
	}

	onLine(fmt.Sprintf("Uploaded %s successfully", localPath))
	return nil
}

// checkHealth performs an HTTP GET to the health check URL.
func (s *Service) checkHealth(url string, timeout time.Duration) error {
	client := &http.Client{
		Timeout: timeout,
	}

	resp, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("health check request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
		return nil
	}
	return fmt.Errorf("health check returned status %d", resp.StatusCode)
}

// streamLines reads lines from a reader and calls onLine for each.
func streamLines(r io.Reader, onLine LineCallback) {
	buf := make([]byte, 4096)
	line := make([]byte, 0)
	for {
		n, err := r.Read(buf)
		if n > 0 {
			for _, b := range buf[:n] {
				if b == '\n' {
					onLine(string(line))
					line = line[:0]
				} else if b != '\r' {
					line = append(line, b)
				}
			}
		}
		if err != nil {
			if len(line) > 0 {
				onLine(string(line))
			}
			return
		}
	}
}
