// Package ssh implements ports.CommandExecutor over internal/actuator:
// the dial pool, host-key handling, and streaming/combined execution.
package ssh

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"sync"
	"time"

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

	"github.com/dtoro/oikos/internal/actuator"
	"github.com/dtoro/oikos/internal/core/ports"
)

const (
	defaultExecTimeout = 10 * time.Minute
	defaultKeyPathEnv  = "OIKOS_SSH_KEY_PATH"
	defaultKeyPath     = "/etc/oikos/ssh_key"
)

// SignerSource supplies the SSH signer used for all dials. The secrets
// adapter provides one backed by Infisical/SOPS; tests inject a static one.
type SignerSource func(ctx context.Context) (cryptossh.Signer, error)

// FileSignerSource reads an OpenSSH private key from disk once and parses
// it (path from env OIKOS_SSH_KEY_PATH, default /etc/oikos/ssh_key — the
// same resolution the httpapi path used before the extraction).
func FileSignerSource() SignerSource {
	var (
		once   sync.Once
		signer cryptossh.Signer
		err    error
	)
	return func(context.Context) (cryptossh.Signer, error) {
		once.Do(func() {
			path := os.Getenv(defaultKeyPathEnv)
			if path == "" {
				path = defaultKeyPath
			}
			key, rerr := os.ReadFile(path)
			if rerr != nil {
				err = fmt.Errorf("read ssh key %s: %w", path, rerr)
				return
			}
			signer, err = actuator.LoadSignerFromBytes(key)
		})
		return signer, err
	}
}

// Executor runs commands over SSH through a dial pool.
type Executor struct {
	signer SignerSource
	pool   *actuator.DialPool
}

var _ ports.CommandExecutor = (*Executor)(nil)

// NewExecutor builds an executor. The dial pool reuses connections per
// host/user for the given TTL.
func NewExecutor(signer SignerSource, poolTTL time.Duration) *Executor {
	return &Executor{
		signer: signer,
		pool:   actuator.NewDialPool(poolTTL),
	}
}

// Close releases pooled connections.
func (e *Executor) Close() { e.pool.Close() }

// Run dials the target (via the pool), wraps the command for transport when
// the target needs it (pct/qm guests), executes with streaming output, and
// maps the outcome onto ports.ExecResult.
func (e *Executor) Run(ctx context.Context, target ports.Target, command string, opts ports.ExecOpts) ports.ExecResult {
	start := time.Now()

	signer, err := e.signer(ctx)
	if err != nil {
		return ports.ExecResult{Err: err, Duration: time.Since(start)}
	}

	client, err := e.pool.Get(ctx, actuator.DialOptions{
		Host:   target.Host,
		User:   target.User,
		Signer: signer,
	})
	if err != nil {
		return ports.ExecResult{Err: err, Duration: time.Since(start)}
	}
	// Pooled client: do not close here; the pool evicts on TTL.

	if target.Wrap != nil {
		command = target.Wrap(command)
	}

	timeout := opts.Timeout
	if timeout <= 0 {
		timeout = defaultExecTimeout
	}

	output, runErr := actuator.RunStreaming(ctx, client, command, opts.Sink, timeout)
	if runErr != nil {
		slog.Debug("ssh exec: command failed", "host", target.Host, "error", runErr)
	}
	return ports.ExecResult{
		Output:   output,
		Duration: time.Since(start),
		Err:      runErr,
	}
}
