package submit

import (
	"crypto/ed25519"
	"crypto/sha1"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"net/mail"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"n2usenet/internal/config"
)

var newsgroupRE = regexp.MustCompile(`(?i)^[a-z0-9][a-z0-9.-]*[a-z0-9]$`)

type Submission struct {
	From         string
	FromAddress  string
	Newsgroups   []string
	Subject      string
	References   string
	Hashcash     string
	Message      string
	SignedText   string
	PublicKeyB64 string
	SignatureB64 string
	TokenHash    string
}

func ParseAndValidate(form url.Values, cfg config.SecurityConfig) (Submission, error) {
	sub := Submission{
		From:         strings.TrimSpace(form.Get("from")),
		Subject:      cleanHeader(form.Get("subject")),
		References:   cleanHeader(form.Get("references")),
		Hashcash:     normalizeHashcash(form.Get("xhashcash")),
		Message:      strings.TrimSpace(form.Get("message")),
		PublicKeyB64: strings.TrimSpace(form.Get("x-ed25519-pub")),
		SignatureB64: strings.TrimSpace(form.Get("x-ed25519-sig")),
	}
	if sub.From == "" || sub.Subject == "" || sub.Hashcash == "" || sub.Message == "" {
		return Submission{}, fmt.Errorf("missing required field")
	}
	if len(sub.Subject) > 200 {
		return Submission{}, fmt.Errorf("subject too long")
	}
	if len([]byte(sub.Message)) < cfg.MinMessageBytes || len([]byte(sub.Message)) > cfg.MaxMessageBytes {
		return Submission{}, fmt.Errorf("invalid message size")
	}
	addr, err := mail.ParseAddress(sub.From)
	if err != nil || addr.Address == "" || addr.Name == "" {
		return Submission{}, fmt.Errorf("invalid from")
	}
	sub.FromAddress = addr.Address

	groups := splitNewsgroups(form.Get("newsgroups"))
	if len(groups) == 0 || len(groups) > cfg.MaxNewsgroups {
		return Submission{}, fmt.Errorf("invalid newsgroup count")
	}
	for _, group := range groups {
		if !newsgroupRE.MatchString(group) {
			return Submission{}, fmt.Errorf("invalid newsgroup")
		}
	}
	sub.Newsgroups = groups

	if err := VerifyHashcash(sub.Hashcash, sub.FromAddress, cfg.MinHashcashBits, 48*time.Hour, 2*time.Hour); err != nil {
		return Submission{}, err
	}
	h := sha256.Sum256([]byte(sub.Hashcash))
	sub.TokenHash = hex.EncodeToString(h[:])

	if sub.PublicKeyB64 == "" || sub.SignatureB64 == "" {
		return Submission{}, fmt.Errorf("missing signature")
	}
	signedText := SignedPayload(sub.Message, sub.SignatureB64)
	if err := VerifySignature(signedText, sub.PublicKeyB64, sub.SignatureB64); err != nil {
		return Submission{}, err
	}
	sub.SignedText = signedText
	return sub, nil
}

func splitNewsgroups(raw string) []string {
	var out []string
	for _, part := range strings.Split(raw, ",") {
		part = strings.ToLower(strings.TrimSpace(part))
		if part != "" {
			out = append(out, part)
		}
	}
	return out
}

func cleanHeader(v string) string {
	v = strings.TrimSpace(v)
	v = strings.ReplaceAll(v, "\r", "")
	v = strings.ReplaceAll(v, "\n", "")
	return v
}

func normalizeHashcash(token string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(token)), "")
}

func VerifyHashcash(token, resource string, minBits int, maxAge, maxFuture time.Duration) error {
	parts := strings.Split(token, ":")
	if len(parts) != 7 {
		return fmt.Errorf("invalid hashcash format")
	}
	if parts[0] != "1" {
		return fmt.Errorf("unsupported hashcash version")
	}
	bits, err := strconv.Atoi(parts[1])
	if err != nil || bits < minBits {
		return fmt.Errorf("insufficient hashcash bits")
	}
	if !strings.EqualFold(strings.TrimSpace(parts[3]), strings.TrimSpace(resource)) {
		return fmt.Errorf("hashcash resource mismatch")
	}
	ts, err := parseHashcashTime(parts[2])
	if err != nil {
		return fmt.Errorf("invalid hashcash date")
	}
	now := time.Now().UTC()
	if now.Sub(ts) > maxAge || ts.Sub(now) > maxFuture {
		return fmt.Errorf("hashcash date outside allowed window")
	}
	sum := sha1.Sum([]byte(token))
	if leadingZeroBits(sum[:]) < bits {
		return fmt.Errorf("hashcash proof invalid")
	}
	return nil
}

func parseHashcashTime(v string) (time.Time, error) {
	layouts := []string{"060102150405", "0601021504", "06010215", "060102"}
	for _, layout := range layouts {
		if len(v) != len(layout) {
			continue
		}
		if t, err := time.ParseInLocation(layout, v, time.UTC); err == nil {
			return t, nil
		}
	}
	return time.Time{}, fmt.Errorf("unsupported date")
}

func leadingZeroBits(b []byte) int {
	total := 0
	for _, x := range b {
		if x == 0 {
			total += 8
			continue
		}
		for i := 7; i >= 0; i-- {
			if x&(1<<uint(i)) == 0 {
				total++
				continue
			}
			return total
		}
	}
	return total
}

func SignedPayload(message, sigB64 string) string {
	message = strings.TrimSpace(message)
	marker := "\n\n--- Digital Signature ---\n" + strings.TrimSpace(sigB64)
	if strings.HasSuffix(message, marker) {
		return strings.TrimSpace(strings.TrimSuffix(message, marker))
	}
	return message
}

func VerifySignature(message, pubB64, sigB64 string) error {
	pub, err := base64.StdEncoding.DecodeString(pubB64)
	if err != nil || len(pub) != ed25519.PublicKeySize {
		return fmt.Errorf("invalid public key")
	}
	sig, err := base64.StdEncoding.DecodeString(sigB64)
	if err != nil || len(sig) != ed25519.SignatureSize {
		return fmt.Errorf("invalid signature")
	}
	if !ed25519.Verify(ed25519.PublicKey(pub), []byte(message), sig) {
		return fmt.Errorf("signature verification failed")
	}
	return nil
}
