// SPDX-License-Identifier: GPL-3.0-or-later

// Package tor provides Tor connectivity for the Private Archive backend.
//
// It automatically handles Tor setup with zero user configuration:
// 1. If ExternalSOCKS is configured, uses an external SOCKS5 proxy
// 2. Otherwise, downloads the official Tor Expert Bundle and runs Tor as a subprocess
//
// The Tor Expert Bundle is downloaded from torproject.org and cached locally.
// This approach provides truly zero-dependency Tor support across all platforms.
package tor

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

	"golang.org/x/net/proxy"
)

// ConnectionState represents the current state of the Tor connection.
type ConnectionState string

const (
	StateDisconnected  ConnectionState = "disconnected"
	StateConnecting    ConnectionState = "connecting"
	StateBootstrapping ConnectionState = "bootstrapping"
	StateConnected     ConnectionState = "connected"
	StateError         ConnectionState = "error"
)

// ProgressCallback is called during Tor connection establishment.
type ProgressCallback func(state ConnectionState, progress int, message string)

// Dialer is the interface for making network connections through Tor.
type Dialer interface {
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

// Config holds embedded Tor configuration.
type Config struct {
	// DataDir for Tor cached state (speeds up reconnection)
	DataDir string

	// ConnectTimeout for Tor circuit establishment
	ConnectTimeout time.Duration

	// CircuitTimeout for idle circuit timeout
	CircuitTimeout time.Duration

	// MaxRetries is the maximum number of connection attempts (default: 3)
	MaxRetries int

	// RetryDelay is the delay between retry attempts (default: 5 seconds)
	RetryDelay time.Duration

	// ClearStateOnRetry clears Tor cached state before retrying (default: true)
	ClearStateOnRetry bool

	// ProgressCallback is called during connection progress
	ProgressCallback ProgressCallback

	// ExternalSOCKS is the address of an external SOCKS5 proxy (e.g., "127.0.0.1:9050")
	// If set, uses the external proxy instead of starting an embedded Tor process.
	ExternalSOCKS string
}

// DefaultConfig returns a default Tor configuration.
func DefaultConfig() *Config {
	return &Config{
		ConnectTimeout:    2 * time.Minute,
		CircuitTimeout:    10 * time.Minute,
		MaxRetries:        3,
		RetryDelay:        5 * time.Second,
		ClearStateOnRetry: true,
	}
}

// Client wraps a bundled Tor process or external SOCKS proxy.
type Client struct {
	config      *Config
	process     *Process // Bundled Tor subprocess
	dialer      proxy.ContextDialer
	state       ConnectionState
	useExternal bool // true if using external SOCKS proxy
	mu          sync.RWMutex

	closeCh       chan struct{} // closed by Close() to stop the supervisor
	closeOnce     sync.Once
	superviseOnce sync.Once // starts the Tor supervisor exactly once
}

// NewClient creates a new embedded Tor client.
func NewClient(config *Config) *Client {
	if config == nil {
		config = DefaultConfig()
	}
	return &Client{
		config:  config,
		state:   StateDisconnected,
		closeCh: make(chan struct{}),
	}
}

// Connect starts the embedded Tor process or connects to external SOCKS proxy.
// It will retry up to MaxRetries times if the connection fails.
func (c *Client) Connect(ctx context.Context) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	if c.state == StateConnected {
		return nil // Already connected
	}

	// Check if we should use external SOCKS proxy
	if c.config.ExternalSOCKS != "" {
		return c.connectExternal(ctx)
	}

	return c.connectEmbeddedWithRetry(ctx)
}

// connectEmbeddedWithRetry attempts to connect using embedded Tor with automatic retries.
func (c *Client) connectEmbeddedWithRetry(ctx context.Context) error {
	maxRetries := c.config.MaxRetries
	if maxRetries < 1 {
		maxRetries = 1
	}

	var lastErr error
	for attempt := 1; attempt <= maxRetries; attempt++ {
		// Check if context is cancelled
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		if attempt > 1 {
			c.setState(StateConnecting, 0, fmt.Sprintf("Retrying connection (attempt %d/%d)...", attempt, maxRetries))

			// Clear Tor state before retry if configured
			if c.config.ClearStateOnRetry && c.config.DataDir != "" {
				torDataDir := filepath.Join(c.config.DataDir, "data")
				if err := os.RemoveAll(torDataDir); err != nil {
					// Log but don't fail - state clearing is best-effort
					c.setState(StateConnecting, 0, fmt.Sprintf("Warning: failed to clear Tor state: %v", err))
				}
			}

			// Wait before retry
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(c.config.RetryDelay):
			}
		}

		err := c.connectEmbedded(ctx)
		if err == nil {
			return nil // Success!
		}

		lastErr = err

		// Don't retry if context was cancelled
		if ctx.Err() != nil {
			return ctx.Err()
		}

		// Log the failure
		if attempt < maxRetries {
			c.setState(StateError, 0, fmt.Sprintf("Connection attempt %d/%d failed: %v", attempt, maxRetries, err))
		}
	}

	return fmt.Errorf("failed to connect to Tor after %d attempts: %w", maxRetries, lastErr)
}

// connectExternal connects using an external SOCKS5 proxy.
func (c *Client) connectExternal(ctx context.Context) error {
	c.setState(StateConnecting, 0, "Connecting to external SOCKS proxy...")

	// Create a SOCKS5 dialer
	socksDialer, err := proxy.SOCKS5("tcp", c.config.ExternalSOCKS, nil, proxy.Direct)
	if err != nil {
		c.setState(StateError, 0, fmt.Sprintf("Failed to create SOCKS5 dialer: %v", err))
		return fmt.Errorf("failed to create SOCKS5 dialer: %w", err)
	}

	// Verify the proxy is reachable by doing a test connection
	c.setState(StateBootstrapping, 50, "Testing SOCKS5 proxy...")
	testConn, err := socksDialer.Dial("tcp", c.config.ExternalSOCKS)
	if err == nil {
		testConn.Close()
	}
	// Note: Connection to the proxy address itself may fail, but that's okay
	// The proxy just needs to be listening

	// Cast to ContextDialer if possible, otherwise wrap it
	if contextDialer, ok := socksDialer.(proxy.ContextDialer); ok {
		c.dialer = contextDialer
	} else {
		// Wrap in a simple context dialer
		c.dialer = &simpleContextDialer{dialer: socksDialer}
	}

	c.useExternal = true
	c.setState(StateConnected, 100, "Connected to external SOCKS proxy")

	return nil
}

// simpleContextDialer wraps a proxy.Dialer to implement proxy.ContextDialer.
type simpleContextDialer struct {
	dialer proxy.Dialer
}

func (d *simpleContextDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	// Check if context is already cancelled
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	// Create a channel to receive the connection result
	type dialResult struct {
		conn net.Conn
		err  error
	}
	resultCh := make(chan dialResult, 1)

	go func() {
		conn, err := d.dialer.Dial(network, address)
		resultCh <- dialResult{conn: conn, err: err}
	}()

	// Wait for either the connection or context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case result := <-resultCh:
		return result.conn, result.err
	}
}

// connectEmbedded connects using the bundled Tor process.
func (c *Client) connectEmbedded(ctx context.Context) error {
	c.setState(StateConnecting, 0, "Initializing Tor...")

	// Determine data directory for Tor
	dataDir := c.config.DataDir
	if dataDir == "" {
		// Use default location in user's config directory
		configDir, err := os.UserConfigDir()
		if err != nil {
			configDir = os.TempDir()
		}
		dataDir = filepath.Join(configDir, "privatebackup", "tor")
	}

	// Ensure directories exist
	torDataDir := filepath.Join(dataDir, "data")
	bundleDir := filepath.Join(dataDir, "bundle")
	if err := os.MkdirAll(torDataDir, 0700); err != nil {
		c.setState(StateError, 0, fmt.Sprintf("Failed to create data directory: %v", err))
		return fmt.Errorf("failed to create Tor data directory: %w", err)
	}

	c.setState(StateBootstrapping, 5, "Checking Tor bundle...")

	// Create Tor process manager
	// The process manager handles downloading the bundle if needed
	c.process = NewProcess(torDataDir, bundleDir)

	// Set up progress callback to forward to client state
	c.process.ProgressCallback = func(progress int, message string) {
		// Map process progress (0-100) to client progress (5-95)
		clientProgress := 5 + (progress * 90 / 100)
		c.setState(StateBootstrapping, clientProgress, message)
	}

	// Start Tor process
	// IMPORTANT: We use context.Background() here, NOT the caller's context.
	// The Tor process should run for the lifetime of the Client, not just
	// for the duration of the Connect() call. Using the caller's context
	// would kill Tor when Connect() returns (via defer cancel()).
	// The bootstrap phase uses the caller's context for timeout.
	if err := c.process.Start(context.Background()); err != nil {
		// Check if the original context was cancelled during startup
		if ctx.Err() != nil {
			c.process.Stop()
			c.setState(StateError, 0, "Connection cancelled")
			return ctx.Err()
		}
		c.setState(StateError, 0, fmt.Sprintf("Failed to start Tor: %v", err))
		return fmt.Errorf("failed to start Tor process: %w", err)
	}

	c.setState(StateBootstrapping, 95, "Creating dialer...")

	// Create SOCKS5 dialer to Tor process
	socksAddr := c.process.SOCKSAddress()
	socksDialer, err := proxy.SOCKS5("tcp", socksAddr, nil, proxy.Direct)
	if err != nil {
		c.process.Stop()
		c.setState(StateError, 0, fmt.Sprintf("Failed to create SOCKS dialer: %v", err))
		return fmt.Errorf("failed to create SOCKS5 dialer: %w", err)
	}

	// Cast to ContextDialer if possible, otherwise wrap it
	if contextDialer, ok := socksDialer.(proxy.ContextDialer); ok {
		c.dialer = contextDialer
	} else {
		c.dialer = &simpleContextDialer{dialer: socksDialer}
	}

	c.setState(StateConnected, 100, "Connected to Tor network")

	// Keep Tor alive for the life of the daemon: if the subprocess later dies,
	// the supervisor restarts it (reusing the SOCKS port). Without this a
	// long-running daemon silently loses Tor and reports the server unreachable.
	c.superviseOnce.Do(func() { go c.superviseProcess() })

	return nil
}

// superviseProcess keeps the bundled Tor alive. If the subprocess dies while we
// still believe we are connected, it restarts Tor and rebuilds the dialer. It
// runs until Close().
func (c *Client) superviseProcess() {
	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-c.closeCh:
			return
		case <-ticker.C:
			c.mu.RLock()
			ext, proc := c.useExternal, c.process
			c.mu.RUnlock()
			if ext || proc == nil || proc.IsRunning() {
				continue
			}
			c.restartTor()
		}
	}
}

// restartTor respawns a Tor subprocess that has died and rebuilds the dialer. It
// does NOT hold c.mu across the (slow) bootstrap, so in-flight callers aren't
// blocked. The SOCKS port is reused, so the dialer address is unchanged.
func (c *Client) restartTor() {
	c.mu.Lock()
	proc := c.process
	if c.useExternal || proc == nil || proc.IsRunning() {
		c.mu.Unlock()
		return
	}
	c.setState(StateBootstrapping, 10, "Tor process exited — restarting…")
	c.mu.Unlock()

	if err := proc.Start(context.Background()); err != nil {
		c.mu.Lock()
		c.setState(StateError, 0, fmt.Sprintf("Tor restart failed: %v", err))
		c.mu.Unlock()
		return
	}

	socksDialer, derr := proxy.SOCKS5("tcp", proc.SOCKSAddress(), nil, proxy.Direct)

	c.mu.Lock()
	defer c.mu.Unlock()
	// Close() may have replaced/cleared the process while we were restarting —
	// if so, tear down the one we just started rather than adopt it.
	if c.process != proc {
		proc.Stop()
		return
	}
	if derr != nil {
		c.setState(StateError, 0, fmt.Sprintf("dialer rebuild failed: %v", derr))
		return
	}
	if cd, ok := socksDialer.(proxy.ContextDialer); ok {
		c.dialer = cd
	} else {
		c.dialer = &simpleContextDialer{dialer: socksDialer}
	}
	c.setState(StateConnected, 100, "Reconnected to Tor network")
}

// GetDialer returns the Tor SOCKS5 dialer for making connections.
func (c *Client) GetDialer() Dialer {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.dialer
}

// DialContext dials a connection through Tor.
func (c *Client) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	c.mu.RLock()
	d := c.dialer
	c.mu.RUnlock()

	if d == nil {
		return nil, fmt.Errorf("Tor not connected")
	}

	return d.DialContext(ctx, network, address)
}

// State returns the current connection state.
func (c *Client) State() ConnectionState {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.state
}

// IsConnected returns true if Tor is connected. For the bundled Tor it also
// checks the subprocess is actually alive, so a dead Tor is never reported as
// connected (the supervisor will restart it shortly).
func (c *Client) IsConnected() bool {
	c.mu.RLock()
	defer c.mu.RUnlock()
	if c.state != StateConnected {
		return false
	}
	if c.useExternal {
		return true
	}
	return c.process != nil && c.process.IsRunning()
}

// Close shuts down the Tor process.
func (c *Client) Close() error {
	// Stop the supervisor first so it doesn't race to restart the process we're
	// about to tear down.
	c.closeOnce.Do(func() { close(c.closeCh) })

	c.mu.Lock()
	defer c.mu.Unlock()

	if c.useExternal {
		// External SOCKS proxy doesn't need cleanup
		c.dialer = nil
		c.state = StateDisconnected
		c.useExternal = false
		return nil
	}

	if c.process != nil {
		err := c.process.Stop()
		c.process = nil
		c.dialer = nil
		c.state = StateDisconnected
		return err
	}

	return nil
}

// IsUsingExternalSOCKS returns true if using an external SOCKS proxy.
func (c *Client) IsUsingExternalSOCKS() bool {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.useExternal
}

// SOCKSAddress returns the SOCKS5 proxy address.
// Returns the external SOCKS address if using external, or the embedded Tor address.
func (c *Client) SOCKSAddress() string {
	c.mu.RLock()
	defer c.mu.RUnlock()

	if c.useExternal {
		return c.config.ExternalSOCKS
	}
	if c.process != nil {
		return c.process.SOCKSAddress()
	}
	return ""
}

// SOCKSProxyURL returns the SOCKS5 proxy URL suitable for HTTP_PROXY etc.
// Returns empty string if not connected.
func (c *Client) SOCKSProxyURL() string {
	addr := c.SOCKSAddress()
	if addr == "" {
		return ""
	}
	return "socks5://" + addr
}

// IsBundledTorAvailable returns true if the bundled Tor approach is available.
// This is true on all supported platforms (Linux, macOS, Windows).
func IsBundledTorAvailable() bool {
	_, err := GetBundleInfo()
	return err == nil
}

// setState updates the state and calls the progress callback if set.
func (c *Client) setState(state ConnectionState, progress int, message string) {
	c.state = state
	if c.config.ProgressCallback != nil {
		c.config.ProgressCallback(state, progress, message)
	}
}

// WaitForConnection waits until Tor is connected or the context is cancelled.
func (c *Client) WaitForConnection(ctx context.Context) error {
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
			if c.IsConnected() {
				return nil
			}
			state := c.State()
			if state == StateError {
				return fmt.Errorf("Tor connection failed")
			}
		}
	}
}
