// Command mailgoserver is the Go port of PyMTA-server: an async SMTP MTA (direct-to-MX
// delivery, DKIM signing, IP/user auth) plus its admin web UI, mirroring app.py.
package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"sync/atomic"
	"time"

	"mailgoserver/internal/abuseguard"
	"mailgoserver/internal/acmecert"
	"mailgoserver/internal/config"
	"mailgoserver/internal/db"
	"mailgoserver/internal/dkim"
	"mailgoserver/internal/imapserver"
	"mailgoserver/internal/mailstore"
	"mailgoserver/internal/relay"
	"mailgoserver/internal/smtpserver"
	"mailgoserver/internal/tlsutil"
	"mailgoserver/internal/toolbox"
	"mailgoserver/internal/webui"
)

func main() {
	smtpOnly := flag.Bool("smtp-only", false, "Run SMTP server only")
	webOnly := flag.Bool("web-only", false, "Run web frontend only")
	host := flag.String("host", "127.0.0.1", "Web server host")
	port := flag.Int("port", 5000, "Web server port")
	debug := flag.Bool("debug", false, "Enable debug mode")
	initData := flag.Bool("init-data", false, "Initialize sample data and exit")
	configFlag := flag.String("config", "settings.ini", "Configuration file path")
	flag.Parse()
	portFlagSet := false
	flag.Visit(func(f *flag.Flag) {
		if f.Name == "port" {
			portFlagSet = true
		}
	})

	root, err := os.Getwd()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	configPath := *configFlag
	if !filepath.IsAbs(configPath) {
		configPath = filepath.Join(root, configPath)
	}

	cfg, err := config.Load(configPath)
	if err != nil {
		fmt.Fprintln(os.Stderr, "load settings:", err)
		os.Exit(1)
	}
	toolbox.Configure(cfg)
	logger := toolbox.GetLogger("main")

	if *initData {
		// Mirrors app.py's --init-data: the actual seeding call is commented out
		// there too, so this flag currently only logs and exits without touching
		// the DB — preserved as-is rather than silently making it do more.
		logger.Info("Initializing sample data...")
		logger.Info("Sample data initialization complete")
		return
	}

	dbPath := config.AbsoluteSQLitePath(cfg.Section("Database").Key("DATABASE_URL").String(), root)
	if err := toolbox.EnsureFolderExists(dbPath); err != nil {
		logger.Error("create database directory: %v", err)
		os.Exit(1)
	}
	database, err := db.Open(dbPath)
	if err != nil {
		logger.Error("open database: %v", err)
		os.Exit(1)
	}
	defer database.Close()

	if err := database.SeedDefaultAdminIfEmpty(); err != nil {
		logger.Error("seed default admin: %v", err)
		os.Exit(1)
	}

	dkimKeySize := cfg.Section("DKIM").Key("DKIM_KEY_SIZE").MustInt(2048)
	dkimMgr := dkim.New(database, dkimKeySize)
	relayer := relay.New(database, cfg, toolbox.GetLogger("relay"))

	heloHostname := cfg.Section("Server").Key("helo_hostname").String()
	if heloHostname == "" {
		heloHostname = cfg.Section("Server").Key("HOSTNAME").MustString("localhost")
	}
	attachmentsBase := cfg.Section("Attachments").Key("attachments_path").String()
	if !filepath.IsAbs(attachmentsBase) {
		attachmentsBase = filepath.Join(root, attachmentsBase)
	}

	mailstoreBase := absPath(root, cfg.Section("Mailstore").Key("mailstore_path").MustString("server_data/mailstore"))
	masterKeyPath := absPath(root, cfg.Section("Mailstore").Key("master_key_path").MustString("server_data/mailstore_master.key"))
	masterKey, err := mailstore.LoadOrCreateMasterKey(masterKeyPath)
	if err != nil {
		logger.Error("load mailstore master key: %v", err)
		os.Exit(1)
	}
	mstore := mailstore.New(database, masterKey, mailstoreBase)
	relayer.Mailstore = mstore

	// Three independent certificate "slots" — custom (self-signed by default, or your
	// own uploaded cert/key), letsencrypt_dns, letsencrypt_http — each with its own
	// file pair and CertReloader. [TLS]'s smtp_tls_cert/imap_tls_cert/web_https_cert
	// independently pick which slot each listener uses, e.g. an HTTP-01 cert for mail
	// while a DNS-01 cert serves the dashboard. Every slot is seeded with a self-signed
	// cert if missing, so a reloader can always be constructed even before any ACME
	// manager has obtained anything yet.
	customCertFile := absPath(root, cfg.Section("TLS").Key("TLS_CERT_FILE").String())
	customKeyFile := absPath(root, cfg.Section("TLS").Key("TLS_KEY_FILE").String())
	dnsCertFile := absPath(root, "ssl_certs/letsencrypt_dns.crt")
	dnsKeyFile := absPath(root, "ssl_certs/letsencrypt_dns.key")
	httpCertFile := absPath(root, "ssl_certs/letsencrypt_http.crt")
	httpKeyFile := absPath(root, "ssl_certs/letsencrypt_http.key")
	for _, pair := range [][2]string{
		{customCertFile, customKeyFile}, {dnsCertFile, dnsKeyFile}, {httpCertFile, httpKeyFile},
	} {
		if err := tlsutil.GenerateSelfSignedCert(pair[0], pair[1]); err != nil {
			logger.Error("generate TLS certificate: %v", err)
			os.Exit(1)
		}
	}
	customReloader, err := tlsutil.NewCertReloader(customCertFile, customKeyFile)
	if err != nil {
		logger.Error("load TLS certificate: %v", err)
		os.Exit(1)
	}
	dnsReloader, err := tlsutil.NewCertReloader(dnsCertFile, dnsKeyFile)
	if err != nil {
		logger.Error("load TLS certificate: %v", err)
		os.Exit(1)
	}
	httpReloader, err := tlsutil.NewCertReloader(httpCertFile, httpKeyFile)
	if err != nil {
		logger.Error("load TLS certificate: %v", err)
		os.Exit(1)
	}
	certSlots := map[string]*tlsutil.CertReloader{
		"custom": customReloader, "letsencrypt_dns": dnsReloader, "letsencrypt_http": httpReloader,
	}
	resolveCertSlot := func(key string) *tlsutil.CertReloader {
		if r, ok := certSlots[cfg.Section("TLS").Key(key).MustString("custom")]; ok {
			return r
		}
		return customReloader
	}
	smtpTLSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("smtp_tls_cert"))
	imapTLSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("imap_tls_cert"))
	webHTTPSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("web_https_cert"))

	acmeDataDir := absPath(root, "server_data/acme")
	acmeMgr := acmecert.New(cfg, "LetsEncrypt", "dns-01", dnsCertFile, dnsKeyFile, acmeDataDir, dnsReloader, toolbox.GetLogger("acme"))
	acmeHTTPMgr := acmecert.New(cfg, "LetsEncryptHTTP", "http-01", httpCertFile, httpKeyFile, acmeDataDir, httpReloader, toolbox.GetLogger("acme-http"))

	// The HTTP-01 challenge responder is long-lived (unlike lego's own ephemeral
	// per-obtain listener) so an operator behind NAT/a reverse proxy can verify their
	// port-forwarding actually reaches this host before/without triggering a real,
	// rate-limited ACME attempt — curling it should return 200 once this is up.
	// Started once at boot if enabled; toggling [LetsEncryptHTTP] enabled needs a
	// restart to start or stop this listener, same as the *_cert routing settings.
	if cfg.Section("LetsEncryptHTTP").Key("enabled").MustBool(false) {
		httpChallengeServer := acmecert.NewHTTP01Server()
		httpPort := cfg.Section("Server").Key("HTTP_LETSENCRYPT_PORT").MustString("80")
		httpChallengeServer.Start(fmt.Sprintf(":%s", httpPort), toolbox.GetLogger("acme-http"))
		acmeHTTPMgr.HTTP01Server = httpChallengeServer
		logger.Info("HTTP-01 challenge responder listening on :%s", httpPort)
	}

	backend := &smtpserver.Backend{
		DB: database, DKIM: dkimMgr, Relay: relayer, Cfg: cfg, Mailstore: mstore,
		Logger: toolbox.GetLogger("smtp"), HeloHostname: heloHostname, AttachmentsBasePath: attachmentsBase,
	}
	imapBackend := &imapserver.Backend{DB: database, Mailstore: mstore, Logger: toolbox.GetLogger("imap"), Cfg: cfg}

	var smtpRunning atomic.Bool

	runSMTP := func() {
		smtpPort := cfg.Section("Server").Key("SMTP_PORT").MustInt(25)
		smtpTLSPort := cfg.Section("Server").Key("SMTP_TLS_PORT").MustInt(465)
		banner := smtpserver.ResolveBanner(cfg, heloHostname)

		// Bind IPv4-only ("0.0.0.0"), matching both listeners in server_runner.py
		// exactly (they hardcode hostname='0.0.0.0' regardless of the BIND_IP
		// setting) — a bare ":port" address binds dual-stack on most systems,
		// which would accept IPv6 connections the Python version never did.
		plainServer := smtpserver.NewPlainServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpPort), banner)
		tlsServer := smtpserver.NewTLSServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpTLSPort), banner, smtpTLSConfig)

		smtpRunning.Store(true)
		logger.Info("Plain SMTP listening on :%d, direct-TLS SMTP listening on :%d", smtpPort, smtpTLSPort)

		// Listeners are built explicitly (rather than via ListenAndServe/ListenAndServeTLS)
		// so abuseguard can reject blacklisted IPs before the SMTP banner is ever sent.
		go func() {
			l, err := net.Listen("tcp", plainServer.Addr)
			if err != nil {
				logger.Error("plain SMTP listen: %v", err)
				return
			}
			if err := plainServer.Serve(abuseguard.GuardListener(l, database, logger)); err != nil {
				logger.Error("plain SMTP server: %v", err)
			}
		}()
		tlsListener, err := tls.Listen("tcp", tlsServer.Addr, smtpTLSConfig)
		if err != nil {
			logger.Error("TLS SMTP listen: %v", err)
			return
		}
		if err := tlsServer.Serve(abuseguard.GuardListener(tlsListener, database, logger)); err != nil {
			logger.Error("TLS SMTP server: %v", err)
		}
	}

	runIMAP := func() {
		imapPort := cfg.Section("IMAP").Key("IMAP_PORT").MustInt(143)
		imapTLSPort := cfg.Section("IMAP").Key("IMAP_TLS_PORT").MustInt(993)

		plainServer := imapserver.NewPlainServer(imapBackend)
		tlsServer := imapserver.NewTLSServer(imapBackend, imapTLSConfig)

		logger.Info("Plain IMAP listening on :%d, direct-TLS IMAP listening on :%d", imapPort, imapTLSPort)

		// Listeners are built explicitly so abuseguard can reject blacklisted IPs before
		// the IMAP greeting is ever sent — same reasoning as runSMTP above.
		go func() {
			l, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", imapPort))
			if err != nil {
				logger.Error("plain IMAP listen: %v", err)
				return
			}
			if err := plainServer.Serve(abuseguard.GuardListener(l, database, logger)); err != nil {
				logger.Error("plain IMAP server: %v", err)
			}
		}()
		imapTLSListener, err := tls.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", imapTLSPort), imapTLSConfig)
		if err != nil {
			logger.Error("TLS IMAP listen: %v", err)
			return
		}
		if err := tlsServer.Serve(abuseguard.GuardListener(imapTLSListener, database, logger)); err != nil {
			logger.Error("TLS IMAP server: %v", err)
		}
	}

	// runCertRenewal is the first periodic/background job in this codebase — everything
	// else is purely request-driven. Checks soon after boot (so enabling Let's Encrypt
	// and restarting converges quickly) and every 12h thereafter. NeedsRenewal is a pure
	// disk-state check (it reads the cert file itself, including recognizing the
	// self-signed placeholder by its issuer) — deliberately not gated on any in-memory
	// "has this process attempted yet" flag, so restarting an already-working setup
	// never triggers a redundant re-obtain of an already-valid real certificate.
	runCertRenewal := func() {
		time.Sleep(1 * time.Minute)
		checkAndRenewOne := func(mgr *acmecert.Manager) {
			if !mgr.Enabled() {
				return
			}
			if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
				return
			}
			if err := mgr.ObtainOrRenew(context.Background()); err != nil {
				logger.Error("ACME obtain/renew (%s): %v", mgr.Section, err)
			}
		}
		checkAndRenew := func() {
			checkAndRenewOne(acmeMgr)
			checkAndRenewOne(acmeHTTPMgr)
		}
		checkAndRenew()
		ticker := time.NewTicker(12 * time.Hour)
		defer ticker.Stop()
		for range ticker.C {
			checkAndRenew()
		}
	}

	if *smtpOnly {
		go runIMAP()
		go runCertRenewal()
		runSMTP()
		return
	}

	appSecretPath := absPath(root, cfg.Section("Security").Key("app_secret_path").MustString("server_data/app_secret.key"))
	appSecret, err := webui.LoadOrCreateAppSecret(appSecretPath)
	if err != nil {
		logger.Error("load app secret: %v", err)
		os.Exit(1)
	}

	app, err := webui.New(database, dkimMgr, mstore, acmeMgr, acmeHTTPMgr, relayer, cfg, configPath, toolbox.GetLogger("web"), smtpRunning.Load, appSecret)
	if err != nil {
		logger.Error("init web UI: %v", err)
		os.Exit(1)
	}

	mux := app.Mux()
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		writeHealthJSON(w, database, smtpRunning.Load())
	})
	// Most visitors are mailbox owners, not admins — default the bare root to the
	// self-service webmail login, with a "Login as Admin" button there for the admin
	// dashboard's login instead of requiring the admin URL to be typed by hand.
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, webui.MailboxPrefix+"/login", http.StatusFound)
	})

	if !*webOnly {
		go runSMTP()
		go runIMAP()
		go runCertRenewal()
		time.Sleep(500 * time.Millisecond)
	}

	// Every request (admin, webmail, login, static, /health) funnels through mux, so
	// wrapping it once here — rather than at each of webui.Mux()'s internal
	// registration points — is the single correct place for cross-cutting middleware
	// that has to apply uniformly across the whole app.
	handler := webui.SecurityHeaders(app.CSRFProtect(mux))

	httpsPort := cfg.Section("Server").Key("WEB_HTTPS_PORT").MustInt(5001)
	httpsAddr := fmt.Sprintf("%s:%d", *host, httpsPort)
	httpsServer := &http.Server{Addr: httpsAddr, Handler: handler, TLSConfig: webHTTPSConfig}
	go func() {
		logger.Info("Web interface (HTTPS) starting at https://%s", httpsAddr)
		// Empty cert/key paths: TLSConfig.GetCertificate (backed by certReloader) supplies
		// the certificate, so ListenAndServeTLS never reads from disk itself — the same
		// self-signed-by-default, Let's-Encrypt-when-enabled cert the SMTP/IMAP TLS
		// listeners already use, hot-reloaded without restarting this server either.
		if err := httpsServer.ListenAndServeTLS("", ""); err != nil {
			logger.Error("web https server: %v", err)
		}
	}()

	httpPort := *port
	if !portFlagSet {
		httpPort = cfg.Section("Server").Key("WEB_HTTP_PORT").MustInt(5000)
	}
	addr := fmt.Sprintf("%s:%d", *host, httpPort)
	logger.Info("Web interface starting at http://%s (debug=%v)", addr, *debug)
	if err := http.ListenAndServe(addr, handler); err != nil {
		logger.Error("web server: %v", err)
		os.Exit(1)
	}
}

func absPath(root, p string) string {
	if filepath.IsAbs(p) {
		return p
	}
	return filepath.Join(root, p)
}

func writeHealthJSON(w http.ResponseWriter, database *db.DB, smtpUp bool) {
	dbStatus := "ok"
	if err := database.Ping(); err != nil {
		dbStatus = "error"
	}
	smtpStatus := "stopped"
	if smtpUp {
		smtpStatus = "running"
	}
	overall := "healthy"
	if smtpStatus == "stopped" || dbStatus == "error" {
		overall = "degraded"
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"status":%q,"timestamp":%q,"services":{"smtp_server":%q,"web_frontend":"running","database":%q},"version":"1.0.0"}`,
		overall, time.Now().Format(time.RFC3339), smtpStatus, dbStatus)
}
