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

// Package tor provides Tor connectivity for the Private Archive client backend.
// This file handles automatic download and management of the Tor Expert Bundle.
package tor

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

// BundleInfo contains information about a Tor Expert Bundle.
type BundleInfo struct {
	URL      string // Download URL
	SHA256   string // Expected SHA256 checksum
	FileName string // Name of the tor executable inside the bundle
}

// BundleVersion is the current Tor Expert Bundle version we use.
// Update this when newer versions are released.
const BundleVersion = "15.0.2"
const TorVersion = "0.4.8.21"

// GetBundleInfo returns the bundle info for the current platform.
func GetBundleInfo() (*BundleInfo, error) {
	goos := runtime.GOOS
	goarch := runtime.GOARCH

	// Map Go arch names to Tor bundle arch names
	archMap := map[string]string{
		"amd64": "x86_64",
		"386":   "i686",
		"arm64": "aarch64",
		"arm":   "armv7",
	}

	torArch, ok := archMap[goarch]
	if !ok {
		return nil, fmt.Errorf("unsupported architecture: %s", goarch)
	}

	// Build download URL based on platform
	// Format: https://archive.torproject.org/tor-package-archive/torbrowser/15.0.2/tor-expert-bundle-linux-x86_64-15.0.2.tar.gz
	var osName string
	var ext string

	switch goos {
	case "linux":
		osName = "linux"
		ext = "tar.gz"
	case "darwin":
		osName = "macos"
		ext = "tar.gz"
	case "windows":
		osName = "windows"
		ext = "tar.gz"
	default:
		return nil, fmt.Errorf("unsupported operating system: %s", goos)
	}

	fileName := fmt.Sprintf("tor-expert-bundle-%s-%s-%s.%s", osName, torArch, BundleVersion, ext)
	url := fmt.Sprintf("https://archive.torproject.org/tor-package-archive/torbrowser/%s/%s", BundleVersion, fileName)

	// Checksums for Tor Expert Bundle 15.0.2
	// SECURITY: Empty checksums allow download but skip verification - this is a security risk!
	// TODO: Populate all checksums before production release. Get checksums from:
	// https://www.torproject.org/download/tor/ or
	// https://archive.torproject.org/tor-package-archive/torbrowser/15.0.2/
	checksums := map[string]string{
		// Linux (verified)
		"linux-x86_64": "53a44d12c2500eb7377dd44ff97cde1ee1158ccf166d8ff05d157fa7448625c5",
		// Linux (TODO: verify and populate)
		"linux-i686":    "",
		"linux-aarch64": "",
		// macOS (TODO: verify and populate)
		"macos-x86_64":  "",
		"macos-aarch64": "",
		// Windows (TODO: verify and populate)
		"windows-x86_64": "",
		"windows-i686":   "",
	}

	key := fmt.Sprintf("%s-%s", osName, torArch)
	checksum, ok := checksums[key]
	if !ok {
		// If we don't have a checksum, allow download but warn
		checksum = ""
	}

	// Determine the executable name inside the bundle
	torExe := "tor/tor"
	if goos == "windows" {
		torExe = "tor/tor.exe"
	}

	return &BundleInfo{
		URL:      url,
		SHA256:   checksum,
		FileName: torExe,
	}, nil
}

// BundleManager handles downloading and managing the Tor Expert Bundle.
type BundleManager struct {
	// DataDir is where the bundle is stored (e.g., ~/.config/privatebackup/tor-bundle)
	DataDir string

	// ProgressCallback is called during download with progress updates.
	// progress is 0-100, message describes current operation.
	ProgressCallback func(progress int, message string)
}

// NewBundleManager creates a new bundle manager.
func NewBundleManager(dataDir string) *BundleManager {
	return &BundleManager{
		DataDir: dataDir,
	}
}

// GetTorPath returns the path to the tor executable, downloading if necessary.
func (m *BundleManager) GetTorPath(ctx context.Context) (string, error) {
	info, err := GetBundleInfo()
	if err != nil {
		return "", err
	}

	// Check if tor executable already exists
	torPath := filepath.Join(m.DataDir, info.FileName)
	if _, err := os.Stat(torPath); err == nil {
		// Tor exists, verify it's executable
		if err := verifyExecutable(torPath); err == nil {
			m.progress(100, "Tor binary ready")
			return torPath, nil
		}
		// Executable check failed, re-download
	}

	// Need to download
	m.progress(0, "Downloading Tor...")

	if err := m.download(ctx, info); err != nil {
		return "", fmt.Errorf("failed to download Tor bundle: %w", err)
	}

	// Verify the executable exists now
	if _, err := os.Stat(torPath); err != nil {
		return "", fmt.Errorf("tor executable not found after extraction: %w", err)
	}

	// Make sure it's executable (owner-only for security)
	if err := os.Chmod(torPath, 0700); err != nil {
		return "", fmt.Errorf("failed to make tor executable: %w", err)
	}

	m.progress(100, "Tor binary ready")
	return torPath, nil
}

// download downloads and extracts the Tor Expert Bundle.
func (m *BundleManager) download(ctx context.Context, info *BundleInfo) error {
	// Create data directory
	if err := os.MkdirAll(m.DataDir, 0700); err != nil {
		return fmt.Errorf("failed to create data directory: %w", err)
	}

	// Create temp file for download
	tmpFile, err := os.CreateTemp(m.DataDir, "tor-bundle-*.tar.gz")
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	tmpPath := tmpFile.Name()
	defer os.Remove(tmpPath) // Clean up temp file

	m.progress(5, "Connecting to Tor Project...")

	// Create HTTP request with context
	req, err := http.NewRequestWithContext(ctx, "GET", info.URL, nil)
	if err != nil {
		tmpFile.Close()
		return fmt.Errorf("failed to create request: %w", err)
	}

	// Set a reasonable user agent
	req.Header.Set("User-Agent", "PrivateArchive/1.0")

	// Execute request
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		tmpFile.Close()
		return fmt.Errorf("failed to download: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		tmpFile.Close()
		return fmt.Errorf("download failed with status: %s", resp.Status)
	}

	// Get content length for progress
	contentLength := resp.ContentLength

	m.progress(10, "Downloading Tor bundle...")

	// Download with progress tracking
	hasher := sha256.New()
	writer := io.MultiWriter(tmpFile, hasher)

	var downloaded int64
	buf := make([]byte, 32*1024)
	for {
		select {
		case <-ctx.Done():
			tmpFile.Close()
			return ctx.Err()
		default:
		}

		n, err := resp.Body.Read(buf)
		if n > 0 {
			if _, writeErr := writer.Write(buf[:n]); writeErr != nil {
				tmpFile.Close()
				return fmt.Errorf("failed to write: %w", writeErr)
			}
			downloaded += int64(n)

			// Update progress (10-70% range for download)
			if contentLength > 0 {
				pct := int(10 + (60 * downloaded / contentLength))
				m.progress(pct, fmt.Sprintf("Downloading... %d%%", pct))
			}
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			tmpFile.Close()
			return fmt.Errorf("download error: %w", err)
		}
	}

	tmpFile.Close()

	// Verify checksum — REQUIRED for security
	if info.SHA256 == "" {
		return fmt.Errorf("no checksum available for platform %s-%s: cannot verify bundle integrity", runtime.GOOS, runtime.GOARCH)
	}
	actualHash := hex.EncodeToString(hasher.Sum(nil))
	if !strings.EqualFold(actualHash, info.SHA256) {
		return fmt.Errorf("checksum mismatch: expected %s, got %s", info.SHA256, actualHash)
	}
	m.progress(75, "Checksum verified")

	// Extract the bundle
	m.progress(80, "Extracting Tor bundle...")

	if err := m.extract(tmpPath); err != nil {
		return fmt.Errorf("failed to extract bundle: %w", err)
	}

	m.progress(95, "Extraction complete")

	return nil
}

// extract extracts a tar.gz archive to the data directory.
func (m *BundleManager) extract(archivePath string) error {
	file, err := os.Open(archivePath)
	if err != nil {
		return err
	}
	defer file.Close()

	gzr, err := gzip.NewReader(file)
	if err != nil {
		return err
	}
	defer gzr.Close()

	tr := tar.NewReader(gzr)

	for {
		header, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}

		// Security: prevent path traversal
		target := filepath.Join(m.DataDir, header.Name)
		if !strings.HasPrefix(target, filepath.Clean(m.DataDir)+string(os.PathSeparator)) {
			return fmt.Errorf("invalid file path in archive: %s", header.Name)
		}

		switch header.Typeflag {
		case tar.TypeDir:
			if err := os.MkdirAll(target, 0700); err != nil {
				return err
			}
		case tar.TypeReg:
			// Create parent directory
			if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
				return err
			}

			// Extract file
			outFile, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
			if err != nil {
				return err
			}

			// Limit copy size to prevent decompression bombs
			if _, err := io.Copy(outFile, io.LimitReader(tr, 500*1024*1024)); err != nil {
				outFile.Close()
				return err
			}
			outFile.Close()

		case tar.TypeSymlink:
			// SECURITY: Validate symlink target to prevent path traversal
			// Resolve the symlink target relative to the entry's directory
			linkTarget := header.Linkname
			if !filepath.IsAbs(linkTarget) {
				linkTarget = filepath.Join(filepath.Dir(target), linkTarget)
			}
			linkTarget = filepath.Clean(linkTarget)
			if !strings.HasPrefix(linkTarget, filepath.Clean(m.DataDir)+string(os.PathSeparator)) {
				return fmt.Errorf("symlink target escapes data directory: %s -> %s", header.Name, header.Linkname)
			}
			if err := os.Symlink(header.Linkname, target); err != nil && !os.IsExist(err) {
				return err
			}

		case tar.TypeLink:
			// SECURITY: Reject hardlinks — they can reference files outside the target directory
			return fmt.Errorf("hardlinks not allowed in archive: %s", header.Name)
		}
	}

	return nil
}

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

// verifyExecutable checks if a file is executable.
func verifyExecutable(path string) error {
	info, err := os.Stat(path)
	if err != nil {
		return err
	}

	// Check if it's a regular file with execute permission
	if !info.Mode().IsRegular() {
		return fmt.Errorf("not a regular file")
	}

	// On Unix, check execute bit
	if runtime.GOOS != "windows" {
		if info.Mode()&0111 == 0 {
			return fmt.Errorf("file is not executable")
		}
	}

	return nil
}

// CleanBundle removes the downloaded bundle to force a re-download.
func (m *BundleManager) CleanBundle() error {
	torDir := filepath.Join(m.DataDir, "tor")
	if err := os.RemoveAll(torDir); err != nil && !os.IsNotExist(err) {
		return err
	}
	return nil
}

// GetBundleVersion returns the version of the bundled Tor.
func GetBundleVersion() string {
	return TorVersion
}
