package submit

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"net/mail"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"
)

var messageIDRE = regexp.MustCompile(`^<[^<>\s]+@[^<>\s]+>$`)

func BuildMessage(sub Submission, recipient, messageIDDomain, identiconsCLI string, requireFace bool) (raw string, messageID string, err error) {
	messageID, err = newMessageID(messageIDDomain)
	if err != nil {
		return "", "", err
	}

	headers := []string{
		"From: " + sub.From,
		"To: " + recipient,
		"Subject: " + sub.Subject,
		"Message-ID: " + messageID,
		"Date: " + jitteredDate(),
		"Newsgroups: " + strings.Join(sub.Newsgroups, ","),
		"X-Ed25519-Pub: " + sub.PublicKeyB64,
		"X-Ed25519-Sig: " + sub.SignatureB64,
	}

	face := faceHeader(sub, identiconsCLI)
	if face == "" && requireFace {
		return "", "", fmt.Errorf("face header generation failed")
	}
	if face != "" {
		headers = append(headers, "Face: "+face)
	}

	if refs := normalizeReferences(sub.References); refs != "" {
		headers = append(headers, "References: "+refs, "In-Reply-To: "+refs)
	}

	headers = append(headers,
		"MIME-Version: 1.0",
		"Content-Type: text/plain; charset=utf-8",
		"Content-Transfer-Encoding: 8bit",
		"User-Agent: n2usenet-https-nym/0.1",
		"X-No-Archive: Yes",
	)

	return strings.Join(headers, "\r\n") + "\r\n\r\n" + sub.Message, messageID, nil
}

func newMessageID(domain string) (string, error) {
	domain = cleanHeader(domain)
	if domain == "" {
		domain = "n2usenet.local"
	}
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", err
	}
	return fmt.Sprintf("<%s.%d@%s>", hex.EncodeToString(b[:]), time.Now().Unix(), domain), nil
}

func jitteredDate() string {
	var b [2]byte
	if _, err := rand.Read(b[:]); err != nil {
		return time.Now().UTC().Format(time.RFC1123Z)
	}
	seconds := int(b[0])<<8 | int(b[1])
	seconds = seconds%3601 - 1800
	return time.Now().UTC().Add(time.Duration(seconds) * time.Second).Format(time.RFC1123Z)
}

func normalizeReferences(refs string) string {
	refs = cleanHeader(refs)
	if refs == "" {
		return ""
	}
	if !strings.HasPrefix(refs, "<") {
		refs = "<" + refs
	}
	if !strings.HasSuffix(refs, ">") {
		refs += ">"
	}
	if !messageIDRE.MatchString(refs) {
		return ""
	}
	return refs
}

type FaceIdentity struct {
	Hash    string `json:"hash"`
	Face    string `json:"face"`
	Preview string `json:"preview"`
}

func GenerateFace(username, email, pubkeyB64, identiconsCLI string, size int) (FaceIdentity, error) {
	if identiconsCLI == "" {
		return FaceIdentity{}, fmt.Errorf("identicons cli not configured")
	}
	if st, err := os.Stat(identiconsCLI); err != nil || st.IsDir() || st.Mode()&0111 == 0 {
		return FaceIdentity{}, fmt.Errorf("identicons cli not executable")
	}
	username = cleanHeader(username)
	email = cleanHeader(email)
	pubkeyB64 = strings.TrimSpace(pubkeyB64)
	if username == "" || email == "" || strings.ContainsAny(username, "|") || strings.ContainsAny(email, "|") {
		return FaceIdentity{}, fmt.Errorf("invalid identity fields")
	}
	if _, err := base64.StdEncoding.DecodeString(pubkeyB64); err != nil {
		return FaceIdentity{}, fmt.Errorf("invalid public key")
	}
	if size <= 0 {
		size = 48
	}
	input := username + "|" + email + "|" + pubkeyB64
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	out, err := exec.CommandContext(ctx, identiconsCLI, "-input", input, "-size", fmt.Sprintf("%d", size), "-transparent", "-format", "base64").Output()
	if err != nil {
		return FaceIdentity{}, fmt.Errorf("identicons cli failed: %w", err)
	}
	face := strings.TrimSpace(string(out))
	if _, err := base64.StdEncoding.DecodeString(face); err != nil {
		return FaceIdentity{}, fmt.Errorf("invalid face output")
	}
	hash := sha256.Sum256([]byte(input))
	return FaceIdentity{
		Hash:    hex.EncodeToString(hash[:]),
		Face:    face,
		Preview: "data:image/png;base64," + face,
	}, nil
}

func faceHeader(sub Submission, identiconsCLI string) string {
	addr, err := mail.ParseAddress(sub.From)
	if err != nil {
		return ""
	}
	face, err := GenerateFace(addr.Name, addr.Address, sub.PublicKeyB64, identiconsCLI, 48)
	if err != nil {
		return ""
	}
	return face.Face
}
