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

// Package tor provides Tor connectivity for the Private Archive client backend.
// This file handles launching and managing the Tor subprocess.
package tor

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Process manages a Tor subprocess.
type Process struct {
	// DataDir is the Tor data directory (state, cache, etc.)
	DataDir string

	// BundleDir is where the Tor binary is stored
	BundleDir string

	// ProgressCallback is called during startup with progress updates
	ProgressCallback func(progress int, message string)

	cmd         *exec.Cmd
	socksPort   int
	controlPort int
	mu          sync.Mutex
	running     bool
}

// NewProcess creates a new Tor process manager.
func NewProcess(dataDir, bundleDir string) *Process {
	return &Process{
		DataDir:   dataDir,
		BundleDir: bundleDir,
	}
}

// killStaleTor removes any Tor left orphaned by a previous UNCLEAN shutdown (crash,
// SIGKILL, or the desktop app force-killing the backend sidecar on close). Such a
// Tor is reparented but keeps running and holds the DataDirectory lock, which stops
// a fresh Tor from binding its SOCKS port — the "stale Tor" first-run failure. We
// kill it (verified as our own Tor, so a reused PID is never touched) and clear the
// leftover lock so this startup is clean.
func (p *Process) killStaleTor() {
	pidPath := filepath.Join(p.DataDir, "tor.pid")
	if data, err := os.ReadFile(pidPath); err == nil {
		if pid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil && pid > 1 {
			if isOurTorProcess(pid, p.DataDir) {
				if proc, err := os.FindProcess(pid); err == nil {
					_ = proc.Kill()
					time.Sleep(200 * time.Millisecond) // let the OS release the flock
				}
			}
		}
		_ = os.Remove(pidPath)
	}
	// A dead Tor's flock is auto-released, but the lock FILE can still linger.
	// Removing it is safe once any live holder is gone.
	_ = os.Remove(filepath.Join(p.DataDir, "lock"))
}

// isOurTorProcess reports whether pid is a live Tor launched for our DataDir. On
// Linux it inspects /proc/<pid>/cmdline so an unrelated process that reused the PID
// is never killed; on other platforms it returns false (we still clear the lock
// file, which resolves the common case there).
func isOurTorProcess(pid int, dataDir string) bool {
	cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
	if err != nil {
		return false
	}
	s := strings.ReplaceAll(string(cmdline), "\x00", " ")
	return strings.Contains(s, "tor") && strings.Contains(s, dataDir)
}

// Start launches the Tor subprocess.
func (p *Process) Start(ctx context.Context) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.running {
		return nil // Already running
	}

	// Reap any Tor orphaned by a previous unclean shutdown before starting a fresh
	// one; otherwise its lingering DataDirectory lock blocks the new Tor's SOCKS
	// port from binding (the "stale Tor" first-run failure).
	p.killStaleTor()

	// Get the Tor binary path, downloading if necessary
	bundle := NewBundleManager(p.BundleDir)
	bundle.ProgressCallback = p.ProgressCallback

	torPath, err := bundle.GetTorPath(ctx)
	if err != nil {
		return fmt.Errorf("failed to get Tor binary: %w", err)
	}

	// Find ports for SOCKS and control. On a RESTART after the Tor process died,
	// reuse the previous ports (now freed) so the SOCKS address stays stable — the
	// restic engine and any dialers captured it and must keep working across a
	// supervisor restart without being rebuilt.
	socksPort := p.socksPort
	if socksPort == 0 {
		socksPort, err = findAvailablePort()
		if err != nil {
			return fmt.Errorf("failed to find SOCKS port: %w", err)
		}
	}
	controlPort := p.controlPort
	if controlPort == 0 {
		controlPort, err = findAvailablePort()
		if err != nil {
			return fmt.Errorf("failed to find control port: %w", err)
		}
	}

	// Create data directory
	if err := os.MkdirAll(p.DataDir, 0700); err != nil {
		return fmt.Errorf("failed to create data directory: %w", err)
	}

	// Generate torrc - note: we log to a file instead of stderr to avoid pipe issues
	torrcPath := filepath.Join(p.DataDir, "torrc")
	torLogPath := filepath.Join(p.DataDir, "tor.log")
	// Bundle data dir contains geoip files
	bundleDataDir := filepath.Join(p.BundleDir, "data")
	if err := p.writeTorrc(torrcPath, socksPort, controlPort, bundleDataDir, torLogPath); err != nil {
		return fmt.Errorf("failed to write torrc: %w", err)
	}

	p.progress(10, "Starting Tor daemon...")

	// Build command - use exec.Command (not CommandContext) because the Tor process
	// should outlive the context used for bootstrap. The context is only used for
	// timeout during bootstrap phase.
	cmd := exec.Command(torPath,
		"-f", torrcPath,
		"--DataDirectory", p.DataDir,
	)

	// Set LD_LIBRARY_PATH to include the tor directory (for shared libs)
	// This is needed because Tor Expert Bundle includes libcrypto, libssl, libevent
	torDir := filepath.Dir(torPath)
	cmd.Env = append(os.Environ(),
		fmt.Sprintf("LD_LIBRARY_PATH=%s", torDir),
	)

	// Discard stdout/stderr to avoid blocking - we'll read progress from log file
	cmd.Stdout = nil
	cmd.Stderr = nil

	// Start process
	if err := cmd.Start(); err != nil {
		return fmt.Errorf("failed to start Tor: %w", err)
	}

	p.cmd = cmd
	p.socksPort = socksPort
	p.controlPort = controlPort

	// Record the Tor PID so a later startup can reap it if we die uncleanly.
	_ = os.WriteFile(filepath.Join(p.DataDir, "tor.pid"),
		[]byte(strconv.Itoa(cmd.Process.Pid)), 0600)

	// Wait for bootstrap by monitoring the log file
	if err := p.waitForBootstrapFromLog(ctx, torLogPath); err != nil {
		cmd.Process.Kill()
		return err
	}

	// Wait for SOCKS port to be ready
	if err := waitForSOCKSReady(socksPort, 30*time.Second); err != nil {
		cmd.Process.Kill()
		return fmt.Errorf("SOCKS port not ready: %w", err)
	}

	p.running = true
	p.progress(100, "Tor daemon ready")

	// Start a goroutine to wait for the process to exit
	go func() {
		cmd.Wait()
		p.mu.Lock()
		p.running = false
		p.mu.Unlock()
	}()

	return nil
}

// waitForBootstrapFromLog monitors Tor's log file for bootstrap progress.
func (p *Process) waitForBootstrapFromLog(ctx context.Context, logPath string) error {
	bootstrapRegex := regexp.MustCompile(`Bootstrapped (\d+)%`)
	timeout := time.After(3 * time.Minute)
	ticker := time.NewTicker(200 * time.Millisecond)
	defer ticker.Stop()

	var lastPos int64 = 0

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-timeout:
			return fmt.Errorf("Tor bootstrap timed out")
		case <-ticker.C:
			// Check if process is still running
			if p.cmd.ProcessState != nil {
				return fmt.Errorf("Tor process exited unexpectedly")
			}

			// Read new content from log file
			file, err := os.Open(logPath)
			if err != nil {
				// Log file might not exist yet
				continue
			}

			// Seek to where we left off
			file.Seek(lastPos, 0)

			scanner := bufio.NewScanner(file)
			for scanner.Scan() {
				line := scanner.Text()

				// Check for bootstrap progress
				if matches := bootstrapRegex.FindStringSubmatch(line); len(matches) > 1 {
					pct, _ := strconv.Atoi(matches[1])
					// Map 0-100% bootstrap to 10-90% of our progress
					progress := 10 + (pct * 80 / 100)
					p.progress(progress, fmt.Sprintf("Bootstrapping Tor: %d%%", pct))

					if pct >= 100 {
						file.Close()
						return nil // Bootstrap complete
					}
				}

				// Check for fatal errors
				if strings.Contains(line, "[err]") {
					file.Close()
					return fmt.Errorf("Tor error: %s", line)
				}
			}

			// Remember position for next read
			lastPos, _ = file.Seek(0, io.SeekCurrent)
			file.Close()
		}
	}
}

// Stop terminates the Tor subprocess.
func (p *Process) Stop() error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if !p.running || p.cmd == nil || p.cmd.Process == nil {
		return nil
	}

	// Send SIGTERM for graceful shutdown
	if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
		// If interrupt fails, force kill
		p.cmd.Process.Kill()
	}

	// Wait for process to exit (with timeout)
	done := make(chan error, 1)
	go func() {
		done <- p.cmd.Wait()
	}()

	select {
	case <-time.After(5 * time.Second):
		p.cmd.Process.Kill()
	case <-done:
	}

	p.running = false
	return nil
}

// SOCKSPort returns the SOCKS5 proxy port.
func (p *Process) SOCKSPort() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.socksPort
}

// SOCKSAddress returns the full SOCKS5 proxy address.
func (p *Process) SOCKSAddress() string {
	return fmt.Sprintf("127.0.0.1:%d", p.SOCKSPort())
}

// IsRunning returns true if the Tor process is running.
func (p *Process) IsRunning() bool {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.running
}

// writeTorrc generates a minimal torrc configuration.
func (p *Process) writeTorrc(path string, socksPort, controlPort int, bundleDataDir, logPath string) error {
	config := fmt.Sprintf(`# Auto-generated torrc for Private Archive
# Do not edit - this file is regenerated on each start

# SOCKS5 proxy for client connections
SocksPort %d

# Control port (for status checking)
ControlPort %d

# GeoIP files from the Tor Expert Bundle
GeoIPFile %s/geoip
GeoIPv6File %s/geoip6

# Disable exit node functionality (we're just a client)
ExitRelay 0
ExitPolicy reject *:*

# Optimize for client usage
ClientOnly 1

# Reduce disk writes
AvoidDiskWrites 1

# Safe logging (no sensitive data)
SafeLogging 1

# Log to file (not stderr - avoids pipe issues)
Log notice file %s

# Use bridges if direct connections fail (can be configured later)
# UseBridges 0

# Connection limits for resource-constrained environments
MaxClientCircuitsPending 32
`, socksPort, controlPort, bundleDataDir, bundleDataDir, logPath)

	return os.WriteFile(path, []byte(config), 0600)
}

// progress calls the progress callback if set.
func (p *Process) progress(pct int, msg string) {
	if p.ProgressCallback != nil {
		p.ProgressCallback(pct, msg)
	}
}

// findAvailablePort finds an available TCP port.
func findAvailablePort() (int, error) {
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return 0, err
	}
	defer listener.Close()

	addr := listener.Addr().(*net.TCPAddr)
	return addr.Port, nil
}

// waitForSOCKSReady waits for the SOCKS port to be ready.
func waitForSOCKSReady(port int, timeout time.Duration) error {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second)
		if err == nil {
			conn.Close()
			return nil
		}
		time.Sleep(100 * time.Millisecond)
	}
	return fmt.Errorf("SOCKS port %d not ready after %v", port, timeout)
}
