// /home/m2usenet/m2usenet.go
// Production-ready Mail2Usenet with enhanced security, privacy and anti-analysis features.
// Reads email from STDIN, verifies hashcash token, registers it in JSON DB, sends to NNTP via Tor.

package main

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/sha1"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"math/big"
	"net/mail"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

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

var (
	// Configuration from environment variables
	NNTPServer          = getEnv("NNTP_SERVER", "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion")
	nntpPort, _         = strconv.Atoi(getEnv("NNTP_PORT", "119"))
	torProxyHost        = getEnv("TOR_PROXY_HOST", "127.0.0.1")
	torProxyPort, _     = strconv.Atoi(getEnv("TOR_PROXY_PORT", "9050"))
	maxPostSize, _      = strconv.Atoi(getEnv("MAX_POST_SIZE", "10240"))
	minPostSize, _      = strconv.Atoi(getEnv("MIN_POST_SIZE", "512"))
	delayCrossPost, _   = strconv.Atoi(getEnv("DELAY_CROSSPOST", "2"))
	timeWindowSec, _    = strconv.Atoi(getEnv("TIME_WINDOW_SEC", "1800"))
	hashcashMinBits, _  = strconv.Atoi(getEnv("HASHCASH_MIN_BITS", "24"))
	dbPath              = getEnv("DB_PATH", "/home/m2usenet/hashcash.json")
	tokenTTL, _         = strconv.Atoi(getEnv("TOKEN_TTL_HOURS", "48"))
	nntpTimeout, _      = strconv.Atoi(getEnv("NNTP_TIMEOUT_SEC", "30"))
	maxRetries, _       = strconv.Atoi(getEnv("MAX_RETRIES", "3"))
	paddingEnabled      = getEnv("ENABLE_PADDING", "false") == "true" // Disabled for Usenet text

	tokenMutex sync.Mutex
	
	// Suppress detailed logging in production
	quietMode = getEnv("QUIET_MODE", "false") == "true"
)

func getEnv(key, def string) string {
	if v, ok := os.LookupEnv(key); ok {
		return v
	}
	return def
}

// TokenEntry stores token with expiration
type TokenEntry struct {
	UsedAt  string `json:"used_at"`
	Expires string `json:"expires"`
}

type TokenDB map[string]TokenEntry

// secureLog logs only if not in quiet mode, avoiding detailed error leakage
func secureLog(format string, v ...interface{}) {
	if !quietMode {
		log.Printf(format, v...)
	}
}

// cryptoRandInt generates cryptographically secure random integer in [min, max]
func cryptoRandInt(min, max int64) (int64, error) {
	if max <= min {
		return 0, errors.New("invalid range")
	}
	diff := max - min + 1
	n, err := rand.Int(rand.Reader, big.NewInt(diff))
	if err != nil {
		return 0, err
	}
	return n.Int64() + min, nil
}

// randomDelay adds jitter to prevent timing analysis
func randomDelay(baseSeconds int) {
	if baseSeconds <= 0 {
		return
	}
	jitter, err := cryptoRandInt(0, int64(baseSeconds)*500)
	if err != nil {
		jitter = 0
	}
	delay := time.Duration(baseSeconds)*time.Second + time.Duration(jitter)*time.Millisecond
	time.Sleep(delay)
}

// tokenAlreadySpent checks if token exists (now with atomic operation)
func tokenAlreadySpent(token string) bool {
	tokenMutex.Lock()
	defer tokenMutex.Unlock()

	db, err := loadTokenDB()
	if err != nil {
		return false
	}
	
	entry, exists := db[token]
	if !exists {
		return false
	}
	
	// Check if token is expired
	expires, err := time.Parse(time.RFC3339, entry.Expires)
	if err == nil && time.Now().UTC().After(expires) {
		// Token expired, can be reused
		delete(db, token)
		saveTokenDB(db)
		return false
	}
	
	return true
}

// markTokenSpent registers token atomically with expiration
func markTokenSpent(token string) error {
	tokenMutex.Lock()
	defer tokenMutex.Unlock()

	db, err := loadTokenDB()
	if err != nil {
		db = make(TokenDB)
	}
	
	// Double-check within lock
	if _, exists := db[token]; exists {
		return errors.New("token already spent")
	}
	
	now := time.Now().UTC()
	db[token] = TokenEntry{
		UsedAt:  now.Format(time.RFC3339),
		Expires: now.Add(time.Duration(tokenTTL) * time.Hour).Format(time.RFC3339),
	}
	
	return saveTokenDB(db)
}

// cleanupExpiredTokens removes expired entries (no persistent metadata retention)
func cleanupExpiredTokens() error {
	tokenMutex.Lock()
	defer tokenMutex.Unlock()

	db, err := loadTokenDB()
	if err != nil {
		return err
	}
	
	now := time.Now().UTC()
	cleaned := 0
	
	for token, entry := range db {
		expires, err := time.Parse(time.RFC3339, entry.Expires)
		if err != nil || now.After(expires) {
			delete(db, token)
			cleaned++
		}
	}
	
	if cleaned > 0 {
		secureLog("Cleaned %d expired tokens", cleaned)
		return saveTokenDB(db)
	}
	
	return nil
}

func loadTokenDB() (TokenDB, error) {
	db := make(TokenDB)
	if _, err := os.Stat(dbPath); os.IsNotExist(err) {
		return db, nil
	}
	data, err := os.ReadFile(dbPath)
	if err != nil {
		return nil, err
	}
	if len(data) == 0 {
		return db, nil
	}
	if err := json.Unmarshal(data, &db); err != nil {
		return nil, err
	}
	return db, nil
}

func saveTokenDB(db TokenDB) error {
	dir := filepath.Dir(dbPath)
	if err := os.MkdirAll(dir, 0700); err != nil {
		return err
	}
	data, err := json.MarshalIndent(db, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(dbPath, data, 0600)
}

func parseHashcashDate(dateStr string) (time.Time, error) {
	layouts := []string{"0601021504", "060102150405"}
	var t time.Time
	var err error
	for _, layout := range layouts {
		t, err = time.Parse(layout, dateStr)
		if err == nil {
			return t, nil
		}
	}
	return t, errors.New("invalid date format")
}

// verifyHashcashToken with constant-time comparisons where possible
func verifyHashcashToken(token, fromAddr string) bool {
	parts := strings.Split(token, ":")
	if len(parts) != 7 {
		secureLog("Invalid token format")
		return false
	}
	
	version, bitsStr, dateStr, resource, ext, randPart, counter := 
		parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]
	
	// Use constant-time comparison for version
	if subtle.ConstantTimeCompare([]byte(version), []byte("1")) != 1 {
		secureLog("Invalid token version")
		return false
	}
	
	bits, err := strconv.Atoi(bitsStr)
	if err != nil || bits < hashcashMinBits || bits%4 != 0 {
		secureLog("Invalid bits")
		return false
	}
	
	// Normalize addresses for comparison
	normalizedResource := strings.TrimSpace(strings.ToLower(resource))
	normalizedFrom := strings.TrimSpace(strings.ToLower(fromAddr))
	
	if subtle.ConstantTimeCompare([]byte(normalizedResource), []byte(normalizedFrom)) != 1 {
		secureLog("Resource mismatch")
		return false
	}
	
	tokenTime, err := parseHashcashDate(dateStr)
	if err != nil {
		secureLog("Invalid token date")
		return false
	}
	
	now := time.Now().UTC()
	timeDiff := now.Sub(tokenTime)
	if timeDiff < 0 {
		timeDiff = -timeDiff
	}
	
	if timeDiff > time.Duration(timeWindowSec)*time.Second {
		secureLog("Token outside time window")
		return false
	}
	
	// Verify hash
	assembled := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", 
		version, bitsStr, dateStr, resource, ext, randPart, counter)
	hash := sha1.Sum([]byte(assembled))
	shaHex := fmt.Sprintf("%x", hash)
	target := strings.Repeat("0", bits/4)
	
	if !strings.HasPrefix(shaHex, target) {
		secureLog("Hash verification failed")
		return false
	}
	
	return true
}

// validateOnionAddress ensures NNTP server is a valid .onion address
func validateOnionAddress(addr string) bool {
	if !strings.HasSuffix(addr, ".onion") {
		return false
	}
	parts := strings.Split(addr, ".")
	if len(parts) != 2 {
		return false
	}
	// v3 onion addresses are 56 chars
	return len(parts[0]) == 56
}

// addAdaptivePadding adds random padding to prevent size correlation
func addAdaptivePadding(message string) string {
	if !paddingEnabled {
		return message
	}
	
	currentSize := len([]byte(message))
	
	// Pad to next 1KB boundary
	targetSize := ((currentSize / 1024) + 1) * 1024
	if targetSize > maxPostSize {
		targetSize = maxPostSize
	}
	
	paddingNeeded := targetSize - currentSize
	if paddingNeeded <= 0 {
		return message
	}
	
	// Add random padding as comments
	padding := make([]byte, paddingNeeded)
	rand.Read(padding)
	paddingHex := hex.EncodeToString(padding)
	
	// Truncate to needed length
	if len(paddingHex) > paddingNeeded-20 {
		paddingHex = paddingHex[:paddingNeeded-20]
	}
	
	return message + "\r\n\r\n[padding:" + paddingHex + "]"
}

// sendViaTor with timeout, retry logic and exponential backoff
func sendViaTor(server string, port int, message, messageID string) (bool, error) {
	if !validateOnionAddress(server) {
		return false, errors.New("invalid onion address")
	}
	
	var lastErr error
	
	for attempt := 0; attempt < maxRetries; attempt++ {
		if attempt > 0 {
			// Exponential backoff with jitter
			backoff := (1 << attempt) // 2^attempt seconds
			randomDelay(backoff)
			secureLog("Retry attempt %d/%d", attempt+1, maxRetries)
		}
		
		success, err := attemptSend(server, port, message, messageID)
		if err == nil && success {
			return true, nil
		}
		lastErr = err
		
		// Don't retry on certain errors
		if err != nil && strings.Contains(err.Error(), "IHAVE not accepted") {
			return false, err
		}
	}
	
	return false, fmt.Errorf("failed after %d attempts: %v", maxRetries, lastErr)
}

func attemptSend(server string, port int, message, messageID string) (bool, error) {
	torAddr := fmt.Sprintf("%s:%d", torProxyHost, torProxyPort)
	dialer, err := proxy.SOCKS5("tcp", torAddr, nil, proxy.Direct)
	if err != nil {
		return false, fmt.Errorf("SOCKS5 dialer error: %v", err)
	}
	
	// Add timeout
	conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%d", server, port))
	if err != nil {
		return false, fmt.Errorf("NNTP connection failed: %v", err)
	}
	defer conn.Close()
	
	// Set deadline
	deadline := time.Now().Add(time.Duration(nntpTimeout) * time.Second)
	conn.SetDeadline(deadline)
	
	reader := bufio.NewReader(conn)
	
	// Read welcome
	welcome, err := reader.ReadString('\n')
	if err != nil {
		return false, fmt.Errorf("welcome read error: %v", err)
	}
	secureLog("Connected to NNTP")
	
	if !strings.HasPrefix(welcome, "200") && !strings.HasPrefix(welcome, "201") {
		return false, errors.New("unexpected welcome response")
	}
	
	// Send IHAVE
	ihaveCmd := fmt.Sprintf("IHAVE %s\r\n", messageID)
	if _, err := conn.Write([]byte(ihaveCmd)); err != nil {
		return false, err
	}
	
	ihaveResp, err := reader.ReadString('\n')
	if err != nil {
		return false, err
	}
	
	if !strings.HasPrefix(ihaveResp, "335") {
		return false, errors.New("IHAVE not accepted")
	}
	
	// Send message
	fullMessage := message + "\r\n.\r\n"
	if _, err := conn.Write([]byte(fullMessage)); err != nil {
		return false, err
	}
	
	postResp, err := reader.ReadString('\n')
	if err != nil {
		return false, err
	}
	
	secureLog("Post response received")
	
	conn.Write([]byte("QUIT\r\n"))
	
	return strings.HasPrefix(postResp, "235"), nil
}

// safeParseFrom safely extracts email address from From header
func safeParseFrom(fromHeader string) (string, error) {
	if fromHeader == "" {
		return "", errors.New("empty From header")
	}
	
	// Try parsing as RFC 5322 address
	addr, err := mail.ParseAddress(fromHeader)
	if err == nil {
		return addr.Address, nil
	}
	
	// Fallback to manual parsing
	if strings.Contains(fromHeader, "<") && strings.Contains(fromHeader, ">") {
		start := strings.Index(fromHeader, "<")
		end := strings.Index(fromHeader, ">")
		if end > start {
			return strings.TrimSpace(fromHeader[start+1 : end]), nil
		}
	}
	
	return strings.TrimSpace(fromHeader), nil
}

// generateSecureMessageID creates unpredictable Message-ID
func generateSecureMessageID() (string, error) {
	timestamp := time.Now().Unix()
	
	// 16 bytes of crypto random data
	randomBytes := make([]byte, 16)
	if _, err := rand.Read(randomBytes); err != nil {
		return "", err
	}
	randomHex := hex.EncodeToString(randomBytes)
	
	return fmt.Sprintf("<%d.%s@mail2usenet.local>", timestamp, randomHex), nil
}

func main() {
	// Cleanup expired tokens on startup
	if err := cleanupExpiredTokens(); err != nil {
		secureLog("Token cleanup warning: %v", err)
	}
	
	// Read email from STDIN
	input, err := io.ReadAll(os.Stdin)
	if err != nil {
		log.Fatalf("Failed to read input")
	}
	
	msg, err := mail.ReadMessage(bytes.NewReader(input))
	if err != nil {
		log.Fatalf("Failed to parse email")
	}
	
	header := msg.Header
	
	fromHeader := header.Get("From")
	fromAddr, err := safeParseFrom(fromHeader)
	if err != nil {
		log.Fatalf("Invalid From header")
	}
	
	newsgroups := header.Get("Newsgroups")
	if newsgroups == "" {
		log.Fatalf("Missing Newsgroups header")
	}
	
	subject := header.Get("Subject")
	if subject == "" {
		subject = "(No subject)"
	}
	
	xhashcash := header.Get("X-Hashcash")
	if xhashcash == "" {
		log.Fatalf("Missing X-Hashcash header")
	}
	
	// Read body
	bodyBuf := new(bytes.Buffer)
	if _, err = io.Copy(bodyBuf, msg.Body); err != nil {
		log.Fatalf("Failed to read body")
	}
	body := strings.TrimSpace(bodyBuf.String())
	if body == "" {
		log.Fatalf("Empty message body")
	}
	
	// ATOMIC token check and mark
	tokenMutex.Lock()
	if tokenAlreadySpent(xhashcash) {
		tokenMutex.Unlock()
		log.Fatalf("Token already used")
	}
	if !verifyHashcashToken(xhashcash, fromAddr) {
		tokenMutex.Unlock()
		log.Fatalf("Invalid hashcash token")
	}
	if err := markTokenSpent(xhashcash); err != nil {
		tokenMutex.Unlock()
		log.Fatalf("Failed to register token")
	}
	tokenMutex.Unlock()
	
	// Limit newsgroups
	groups := strings.Split(newsgroups, ",")
	for i := range groups {
		groups[i] = strings.TrimSpace(groups[i])
	}
	if len(groups) > 3 {
		groups = groups[:3]
		secureLog("Limited to 3 newsgroups")
	}
	limitedGroups := strings.Join(groups, ", ")
	
	// Randomized delay for cross-posting
	if len(groups) > 1 {
		randomDelay(delayCrossPost)
	}
	
	// Generate secure Message-ID
	messageID, err := generateSecureMessageID()
	if err != nil {
		log.Fatalf("Failed to generate Message-ID")
	}
	
	dateHeader := time.Now().UTC().Format(time.RFC1123Z)
	
	// Build message
	headers := []string{
		fmt.Sprintf("Message-ID: %s", messageID),
		fmt.Sprintf("Date: %s", dateHeader),
		fmt.Sprintf("From: %s", fromHeader),
		fmt.Sprintf("Newsgroups: %s", limitedGroups),
		fmt.Sprintf("Subject: %s", subject),
		"Path: mail2usenet",
		"Organization: Victor Hostile Communication Center 1312",
		fmt.Sprintf("X-Hashcash: %s", xhashcash),
		"X-No-Archive: Yes",
		"Mime-Version: 1.0",
		"Content-Type: text/plain; charset=UTF-8",
		"Content-Transfer-Encoding: 7bit",
		"User-Agent: m2usenet-go v1.0.0",
	}
	
	// Add Ed25519 headers if present
	if ed25519pub := header.Get("X-Ed25519-Pub"); ed25519pub != "" {
		headers = append(headers, fmt.Sprintf("X-Ed25519-Pub: %s", ed25519pub))
	}
	if ed25519sig := header.Get("X-Ed25519-Sig"); ed25519sig != "" {
		headers = append(headers, fmt.Sprintf("X-Ed25519-Sig: %s", ed25519sig))
	}
	if ref := header.Get("References"); ref != "" {
		headers = append(headers, fmt.Sprintf("References: %s", ref))
	}
	
	usenetPost := strings.Join(headers, "\r\n") + "\r\n\r\n" + body
	
	// Add adaptive padding to prevent size correlation
	usenetPost = addAdaptivePadding(usenetPost)
	
	postSize := len([]byte(usenetPost))
	if postSize > maxPostSize {
		log.Fatalf("Message exceeds size limit")
	}
	if postSize < minPostSize && paddingEnabled {
		log.Fatalf("Message too small even with padding")
	}
	
	success, err := sendViaTor(NNTPServer, nntpPort, usenetPost, messageID)
	if err != nil {
		log.Fatalf("Send failed: %v", err)
	}
	
	if success {
		secureLog("Message sent successfully")
		os.Exit(0)
	} else {
		log.Fatalln("Message delivery failed")
	}
}
