// Package tor provides Tor hidden service integration for the ORLY relay.
// It spawns a tor subprocess with automatic configuration and manages
// the hidden service lifecycle.
package tor

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

	"github.com/gorilla/websocket"
	"lol.mleku.dev/chk"
	"lol.mleku.dev/log"
)

// Config holds Tor subprocess configuration.
type Config struct {
	// Port is the internal port for the hidden service
	Port int
	// DataDir is the directory for Tor data (torrc, keys, hostname, etc.)
	DataDir string
	// Binary is the path to the tor executable
	Binary string
	// SOCKSPort is the port for outbound SOCKS connections (0 = disabled)
	SOCKSPort int
	// Handler is the HTTP handler to serve (typically the main relay handler)
	Handler http.Handler
}

// Service manages the Tor subprocess and hidden service listener.
type Service struct {
	cfg      *Config
	listener net.Listener
	server   *http.Server

	// Tor subprocess
	cmd    *exec.Cmd
	stdout io.ReadCloser
	stderr io.ReadCloser

	// onionAddress is the detected .onion address
	onionAddress string

	// hostname watcher
	hostnameWatcher *HostnameWatcher

	ctx    context.Context
	cancel context.CancelFunc
	wg     sync.WaitGroup
	mu     sync.RWMutex
}

// New creates a new Tor service with the given configuration.
// Returns an error if the tor binary is not found.
func New(cfg *Config) (*Service, error) {
	if cfg.Port == 0 {
		cfg.Port = 3336
	}

	// Find tor binary
	binary := cfg.Binary
	if binary == "" {
		binary = "tor"
	}

	torPath, err := exec.LookPath(binary)
	if err != nil {
		return nil, fmt.Errorf("tor binary not found: %w (install tor or set ORLY_TOR_ENABLED=false)", err)
	}
	cfg.Binary = torPath

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

	ctx, cancel := context.WithCancel(context.Background())

	s := &Service{
		cfg:    cfg,
		ctx:    ctx,
		cancel: cancel,
	}

	return s, nil
}

// generateTorrc creates the torrc configuration file.
func (s *Service) generateTorrc() (string, error) {
	torrcPath := filepath.Join(s.cfg.DataDir, "torrc")
	hsDir := filepath.Join(s.cfg.DataDir, "hidden_service")

	// Ensure hidden service directory exists with correct permissions
	if err := os.MkdirAll(hsDir, 0700); err != nil {
		return "", fmt.Errorf("failed to create hidden service directory: %w", err)
	}

	var sb strings.Builder
	sb.WriteString("# ORLY Tor hidden service configuration\n")
	sb.WriteString("# Auto-generated - do not edit\n\n")

	// Data directory
	sb.WriteString(fmt.Sprintf("DataDirectory %s/data\n", s.cfg.DataDir))

	// Hidden service configuration
	sb.WriteString(fmt.Sprintf("HiddenServiceDir %s\n", hsDir))
	sb.WriteString(fmt.Sprintf("HiddenServicePort 80 127.0.0.1:%d\n", s.cfg.Port))

	// Optional SOCKS port for outbound connections
	if s.cfg.SOCKSPort > 0 {
		sb.WriteString(fmt.Sprintf("SocksPort %d\n", s.cfg.SOCKSPort))
	} else {
		sb.WriteString("SocksPort 0\n")
	}

	// Disable unused features to reduce resource usage
	sb.WriteString("ControlPort 0\n")
	sb.WriteString("Log notice stdout\n")

	// Write torrc
	if err := os.WriteFile(torrcPath, []byte(sb.String()), 0600); err != nil {
		return "", fmt.Errorf("failed to write torrc: %w", err)
	}

	// Create data subdirectory
	if err := os.MkdirAll(filepath.Join(s.cfg.DataDir, "data"), 0700); err != nil {
		return "", fmt.Errorf("failed to create Tor data subdirectory: %w", err)
	}

	return torrcPath, nil
}

// Start spawns the Tor subprocess and initializes the listener.
func (s *Service) Start() error {
	// Generate torrc
	torrcPath, err := s.generateTorrc()
	if err != nil {
		return err
	}

	log.I.F("starting Tor subprocess with config: %s", torrcPath)

	// Start tor subprocess
	s.cmd = exec.CommandContext(s.ctx, s.cfg.Binary, "-f", torrcPath)

	// Capture stdout/stderr for logging
	s.stdout, err = s.cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("failed to get Tor stdout: %w", err)
	}
	s.stderr, err = s.cmd.StderrPipe()
	if err != nil {
		return fmt.Errorf("failed to get Tor stderr: %w", err)
	}

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

	log.I.F("Tor subprocess started (PID %d)", s.cmd.Process.Pid)

	// Log Tor output
	s.wg.Add(2)
	go s.logOutput("tor", s.stdout)
	go s.logOutput("tor", s.stderr)

	// Monitor subprocess
	s.wg.Add(1)
	go s.monitorProcess()

	// Start hostname watcher
	hsDir := filepath.Join(s.cfg.DataDir, "hidden_service")
	s.hostnameWatcher = NewHostnameWatcher(hsDir)
	s.hostnameWatcher.OnChange(func(addr string) {
		s.mu.Lock()
		s.onionAddress = addr
		s.mu.Unlock()
		log.I.F("Tor hidden service address: %s", addr)
	})
	if err := s.hostnameWatcher.Start(); err != nil {
		log.W.F("failed to start hostname watcher: %v", err)
	} else {
		// Get initial address
		if addr := s.hostnameWatcher.Address(); addr != "" {
			s.mu.Lock()
			s.onionAddress = addr
			s.mu.Unlock()
		}
	}

	// Create listener for the hidden service port
	addr := fmt.Sprintf("127.0.0.1:%d", s.cfg.Port)
	s.listener, err = net.Listen("tcp", addr)
	if chk.E(err) {
		s.Stop()
		return fmt.Errorf("failed to listen on %s: %w", addr, err)
	}

	// Create HTTP server with the provided handler
	s.server = &http.Server{
		Handler:      s.cfg.Handler,
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 30 * time.Second,
		IdleTimeout:  120 * time.Second,
	}

	// Start serving
	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		log.I.F("Tor hidden service listener started on %s", addr)
		if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed {
			log.E.F("Tor server error: %v", err)
		}
	}()

	return nil
}

// logOutput reads from a pipe and logs each line.
func (s *Service) logOutput(prefix string, r io.ReadCloser) {
	defer s.wg.Done()
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		// Filter out common noise
		if strings.Contains(line, "Bootstrapped") {
			log.I.F("[%s] %s", prefix, line)
		} else if strings.Contains(line, "[warn]") || strings.Contains(line, "[err]") {
			log.W.F("[%s] %s", prefix, line)
		} else {
			log.D.F("[%s] %s", prefix, line)
		}
	}
}

// monitorProcess watches the Tor subprocess and logs when it exits.
func (s *Service) monitorProcess() {
	defer s.wg.Done()
	err := s.cmd.Wait()
	if err != nil {
		select {
		case <-s.ctx.Done():
			// Expected shutdown
			log.D.F("Tor subprocess exited (shutdown)")
		default:
			log.E.F("Tor subprocess exited unexpectedly: %v", err)
		}
	} else {
		log.I.F("Tor subprocess exited cleanly")
	}
}

// Stop gracefully shuts down the Tor service.
func (s *Service) Stop() error {
	s.cancel()

	// Stop hostname watcher
	if s.hostnameWatcher != nil {
		s.hostnameWatcher.Stop()
	}

	// Shutdown HTTP server
	if s.server != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := s.server.Shutdown(ctx); chk.E(err) {
			// Continue shutdown anyway
		}
	}

	// Close listener
	if s.listener != nil {
		s.listener.Close()
	}

	// Terminate Tor subprocess
	if s.cmd != nil && s.cmd.Process != nil {
		log.D.F("sending SIGTERM to Tor subprocess (PID %d)", s.cmd.Process.Pid)
		s.cmd.Process.Signal(os.Interrupt)

		// Give it a few seconds to exit gracefully
		done := make(chan struct{})
		go func() {
			s.cmd.Wait()
			close(done)
		}()

		select {
		case <-done:
			log.D.F("Tor subprocess exited gracefully")
		case <-time.After(5 * time.Second):
			log.W.F("Tor subprocess did not exit, killing")
			s.cmd.Process.Kill()
		}
	}

	s.wg.Wait()
	log.I.F("Tor service stopped")
	return nil
}

// OnionAddress returns the current .onion address.
func (s *Service) OnionAddress() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.onionAddress
}

// OnionWSAddress returns the full WebSocket URL for the hidden service.
// Format: ws://<address>.onion/
func (s *Service) OnionWSAddress() string {
	addr := s.OnionAddress()
	if addr == "" {
		return ""
	}
	// Ensure address ends with .onion
	if len(addr) >= 6 && addr[len(addr)-6:] != ".onion" {
		addr = addr + ".onion"
	}
	return "ws://" + addr + "/"
}

// IsRunning returns whether the Tor service is currently running.
func (s *Service) IsRunning() bool {
	return s.listener != nil && s.cmd != nil && s.cmd.Process != nil
}

// Upgrader returns a WebSocket upgrader configured for Tor connections.
// Tor connections don't send Origin headers, so we skip origin check.
func (s *Service) Upgrader() *websocket.Upgrader {
	return &websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			return true // Allow all origins for Tor
		},
	}
}

// DataDir returns the Tor data directory path.
func (s *Service) DataDir() string {
	return s.cfg.DataDir
}

// HiddenServiceDir returns the hidden service directory path.
func (s *Service) HiddenServiceDir() string {
	return filepath.Join(s.cfg.DataDir, "hidden_service")
}
