package submit

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"html/template"
	"io/fs"
	"log"
	"mime"
	"net"
	"net/http"
	"path"
	"strings"
	"sync"
	"time"

	"n2usenet/internal/config"
	"n2usenet/internal/smtpclient"
	"n2usenet/internal/storage"
)

type Mailer interface {
	Send(ctx context.Context, msg smtpclient.Message) error
}

type TransportState interface {
	Ready() bool
	MarkSuccess()
	MarkFailure(error)
}

type App struct {
	cfg       config.Config
	mailer    Mailer
	replay    *storage.ReplayCache
	staticFS  fs.FS
	indexTmpl *template.Template
	transport TransportState
	csrfKey   []byte
	rateKey   []byte
	rateMu    sync.Mutex
	rates     map[string]rateBucket
	locksMu   sync.Mutex
	locks     map[string]struct{}
}

type rateBucket struct {
	Count int
	Start time.Time
}

type IndexData struct {
	CSRFToken     string
	PublicBaseURL string
}

func NewApp(cfg config.Config, mailer Mailer, transport TransportState, replay *storage.ReplayCache, staticFS fs.FS, indexTemplate string) (*App, error) {
	csrfKey := append([]byte(nil), cfg.Security.CSRFKey...)
	if len(csrfKey) == 0 {
		csrfKey = make([]byte, 32)
	}
	rateKey := make([]byte, 32)
	if len(cfg.Security.CSRFKey) == 0 {
		if _, err := rand.Read(csrfKey); err != nil {
			return nil, fmt.Errorf("csrf key: %w", err)
		}
	}
	if _, err := rand.Read(rateKey); err != nil {
		return nil, fmt.Errorf("rate key: %w", err)
	}
	tmpl, err := template.New("index").Parse(indexTemplate)
	if err != nil {
		return nil, fmt.Errorf("parse index template: %w", err)
	}
	return &App{
		cfg: cfg, mailer: mailer, transport: transport, replay: replay, staticFS: staticFS,
		indexTmpl: tmpl, csrfKey: csrfKey, rateKey: rateKey,
		rates: map[string]rateBucket{}, locks: map[string]struct{}{},
	}, nil
}

func (a *App) Routes() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", a.handleIndex)
	mux.HandleFunc("GET /csrf", a.handleCSRF)
	mux.HandleFunc("GET /healthz", a.handleHealth)
	mux.HandleFunc("GET /readyz", a.handleReadiness)
	mux.HandleFunc("GET /favicon.ico", a.handleFavicon)
	mux.HandleFunc("POST /identity/face", a.handleFace)
	mux.HandleFunc("POST /identicon.php", a.handleIdenticonCompat)
	mux.HandleFunc("POST /submit", a.handleSubmit)
	mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(a.staticFS))))
	return securityHeaders(mux)
}

func (a *App) handleFavicon(w http.ResponseWriter, r *http.Request) {
	icon, err := fs.ReadFile(a.staticFS, "Nym.ico")
	if err != nil {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "image/x-icon")
	w.Header().Set("Cache-Control", "public, max-age=86400")
	_, _ = w.Write(icon)
}

func (a *App) handleFace(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 8192)
	if err := r.ParseForm(); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}
	if !a.verifyCSRF(r) {
		http.Error(w, "invalid request token", http.StatusBadRequest)
		return
	}
	face, err := GenerateFace(r.PostForm.Get("username"), r.PostForm.Get("email"), r.PostForm.Get("pubkey"), a.cfg.Security.IdenticonsCLI, 96)
	if err != nil {
		http.Error(w, "face generation failed", http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	_ = json.NewEncoder(w).Encode(face)
}

func (a *App) handleIdenticonCompat(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 8192)
	if err := r.ParseForm(); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}
	username := r.PostForm.Get("username")
	email := r.PostForm.Get("email")
	pubkey := r.PostForm.Get("pubkey")

	face48, err := GenerateFace(username, email, pubkey, a.cfg.Security.IdenticonsCLI, 48)
	if err != nil {
		http.Error(w, "face generation failed", http.StatusBadRequest)
		return
	}
	preview, err := GenerateFace(username, email, pubkey, a.cfg.Security.IdenticonsCLI, 256)
	if err != nil {
		http.Error(w, "face generation failed", http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	_ = json.NewEncoder(w).Encode(map[string]string{
		"hash":       face48.Hash,
		"face48":     face48.Face,
		"preview":    preview.Preview,
		"faceHeader": "Face: " + face48.Face,
	})
}

func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) {
	token := a.issueCSRFToken(w, r)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	_ = a.indexTmpl.Execute(w, IndexData{CSRFToken: token, PublicBaseURL: a.cfg.PublicBaseURL})
}

func (a *App) handleCSRF(w http.ResponseWriter, r *http.Request) {
	token := a.issueCSRFToken(w, r)
	setNoStoreHeaders(w)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{"token": token})
}

func (a *App) issueCSRFToken(w http.ResponseWriter, r *http.Request) string {
	if token, ok := a.csrfTokenFromCookie(r); ok {
		return token
	}
	token := randomHex(32)
	http.SetCookie(w, &http.Cookie{
		Name:     "m2u_csrf",
		Value:    a.signCSRF(token),
		Path:     "/",
		Secure:   a.cfg.Security.SecureCookies,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	})
	return token
}

func (a *App) csrfTokenFromCookie(r *http.Request) (string, bool) {
	cookie, err := r.Cookie("m2u_csrf")
	if err != nil {
		return "", false
	}
	dot := strings.LastIndexByte(cookie.Value, '.')
	if dot <= 0 {
		return "", false
	}
	token := cookie.Value[:dot]
	if !hmac.Equal([]byte(cookie.Value), []byte(a.signCSRF(token))) {
		return "", false
	}
	return token, true
}

func (a *App) handleHealth(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ok\n"))
}

func (a *App) handleReadiness(w http.ResponseWriter, _ *http.Request) {
	setNoStoreHeaders(w)
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	if a.transport == nil || !a.transport.Ready() {
		w.WriteHeader(http.StatusServiceUnavailable)
		_, _ = w.Write([]byte("transport unavailable\n"))
		return
	}
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ready\n"))
}

func (a *App) handleSubmit(w http.ResponseWriter, r *http.Request) {
	if !a.allowRate(r) {
		a.errorResponse(w, r, http.StatusTooManyRequests, "Too many requests. Try again later.")
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, int64(a.cfg.Security.MaxMessageBytes+16384))
	if err := r.ParseForm(); err != nil {
		a.errorResponse(w, r, http.StatusBadRequest, "Invalid request.")
		return
	}
	if !a.verifyCSRF(r) {
		a.errorResponse(w, r, http.StatusBadRequest, "Invalid request token.")
		return
	}

	sub, err := ParseAndValidate(r.PostForm, a.cfg.Security)
	if err != nil {
		a.errorResponse(w, r, http.StatusBadRequest, "Validation failed.")
		return
	}

	locked := a.acquire(sub.TokenHash)
	if !locked {
		a.errorResponse(w, r, http.StatusConflict, "Duplicate submission detected.")
		return
	}
	defer a.release(sub.TokenHash)

	replayed, err := a.replay.CheckAndMark(sub.TokenHash)
	if err != nil {
		a.errorResponse(w, r, http.StatusInternalServerError, "Internal error.")
		return
	}
	if replayed {
		a.errorResponse(w, r, http.StatusConflict, "Token already used.")
		return
	}

	raw, messageID, err := BuildMessage(sub, a.cfg.SMTP.Recipient, a.cfg.SMTP.Sender, a.cfg.Security.MessageIDDomain, a.cfg.Security.IdenticonsCLI, a.cfg.Security.RequireFace)
	if err != nil {
		a.errorResponse(w, r, http.StatusInternalServerError, "Message build failed.")
		return
	}
	if err := a.mailer.Send(r.Context(), smtpclient.Message{EnvelopeFrom: sub.FromAddress, Raw: raw}); err != nil {
		if a.transport != nil {
			a.transport.MarkFailure(err)
		}
		log.Printf("submission delivery failed message_id=%s: %v", messageID, err)
		a.errorResponse(w, r, http.StatusBadGateway, "Delivery failed.")
		return
	}
	if a.transport != nil {
		a.transport.MarkSuccess()
	}
	a.successResponse(w, r, messageID)
}

func (a *App) verifyCSRF(r *http.Request) bool {
	formToken := strings.TrimSpace(r.PostForm.Get("csrf_token"))
	cookie, err := r.Cookie("m2u_csrf")
	if err != nil || formToken == "" {
		return false
	}
	return hmac.Equal([]byte(cookie.Value), []byte(a.signCSRF(formToken)))
}

func (a *App) signCSRF(token string) string {
	mac := hmac.New(sha256.New, a.csrfKey)
	mac.Write([]byte(token))
	return token + "." + hex.EncodeToString(mac.Sum(nil))
}

func (a *App) allowRate(r *http.Request) bool {
	key := a.clientKey(r)
	now := time.Now()
	a.rateMu.Lock()
	defer a.rateMu.Unlock()
	for k, b := range a.rates {
		if now.Sub(b.Start) > a.cfg.Security.RateLimitWindow {
			delete(a.rates, k)
		}
	}
	b := a.rates[key]
	if b.Start.IsZero() || now.Sub(b.Start) > a.cfg.Security.RateLimitWindow {
		a.rates[key] = rateBucket{Count: 1, Start: now}
		return true
	}
	b.Count++
	a.rates[key] = b
	return b.Count <= a.cfg.Security.RateLimitCount
}

func (a *App) clientKey(r *http.Request) string {
	ip := ""
	if a.cfg.Security.TrustProxy {
		ip = strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0])
		if ip == "" {
			ip = strings.TrimSpace(r.Header.Get("CF-Connecting-IP"))
		}
	}
	if ip == "" {
		host, _, err := net.SplitHostPort(r.RemoteAddr)
		if err == nil {
			ip = host
		} else {
			ip = r.RemoteAddr
		}
	}
	mac := hmac.New(sha256.New, a.rateKey)
	mac.Write([]byte(ip))
	return hex.EncodeToString(mac.Sum(nil))
}

func (a *App) acquire(key string) bool {
	a.locksMu.Lock()
	defer a.locksMu.Unlock()
	if _, exists := a.locks[key]; exists {
		return false
	}
	a.locks[key] = struct{}{}
	return true
}

func (a *App) release(key string) {
	a.locksMu.Lock()
	delete(a.locks, key)
	a.locksMu.Unlock()
}

func wantsJSON(r *http.Request) bool {
	return strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json")
}

func setNoStoreHeaders(w http.ResponseWriter) {
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "0")
}

func (a *App) errorResponse(w http.ResponseWriter, r *http.Request, status int, msg string) {
	setNoStoreHeaders(w)
	if wantsJSON(r) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
		return
	}
	a.errorHTML(w, status, msg)
}

func (a *App) errorHTML(w http.ResponseWriter, status int, msg string) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(status)
	_, _ = fmt.Fprintf(w, "<!doctype html><meta charset=utf-8><title>Request failed</title><main><h1>Request failed</h1><p>%s</p><p><a href=\"/\">Back</a></p></main>", template.HTMLEscapeString(msg))
}

func (a *App) successResponse(w http.ResponseWriter, r *http.Request, messageID string) {
	setNoStoreHeaders(w)
	w.Header().Set("Clear-Site-Data", "\"storage\"")
	if wantsJSON(r) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_ = json.NewEncoder(w).Encode(map[string]string{
			"messageId": messageID,
			"status":    "accepted",
		})
		return
	}
	a.successHTML(w, messageID)
}

func (a *App) successHTML(w http.ResponseWriter, messageID string) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	_, _ = fmt.Fprintf(w, "<!doctype html><meta charset=utf-8><title>Message accepted</title><main><h1>Message accepted by the mail relay</h1><p>Final Usenet delivery is asynchronous.</p><p><strong>Message-ID:</strong></p><pre>%s</pre><p><a href=\"/\">Send another</a></p></main>", template.HTMLEscapeString(messageID))
}

func randomHex(n int) string {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	return hex.EncodeToString(b)
}

func securityHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Content-Type-Options", "nosniff")
		w.Header().Set("Referrer-Policy", "no-referrer")
		w.Header().Set("X-Frame-Options", "DENY")
		w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'")
		ext := path.Ext(r.URL.Path)
		if ext != "" {
			if ctype := mime.TypeByExtension(ext); ctype != "" {
				w.Header().Set("Content-Type", ctype)
			}
		}
		next.ServeHTTP(w, r)
	})
}
