package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/mixeme/selfpost/internal/app"
	"github.com/mixeme/selfpost/internal/buildinfo"
	"github.com/mixeme/selfpost/internal/dmarc"
	"github.com/mixeme/selfpost/internal/domain"
	"github.com/mixeme/selfpost/internal/inbound"
	"github.com/mixeme/selfpost/internal/postfix"
	"github.com/mixeme/selfpost/internal/store"
	"github.com/mixeme/selfpost/internal/web"
)

// mailStack is the panel's domain and application services plus the on-disk
// mail-path adapters they write through.
type mailStack struct {
	Domains        *domain.Service
	Apps           *app.Service
	Inbound        *inbound.Service
	DMARC          *dmarc.Service
	pf             *postfix.Postfix
	odk            *domain.OpenDKIM
	inboundEnabled bool
	dmarcEnabled   bool
}

func newMailStack(cfg config, st *store.Store) *mailStack {
	pf := postfix.New(cfg.postfixDir)
	odk := domain.NewOpenDKIM(cfg.opendkimDir)
	apps := app.NewService(st, app.NewSASLDB(cfg.saslDBPath, cfg.saslRealm), pf)
	domains := domain.NewService(st, odk, apps, cfg.dkimSelectorDef)
	inb := inbound.NewService(st, pf)
	dmarcSvc := dmarc.NewService(st, pf, cfg.hostname, cfg.dmarcEnabled)
	return &mailStack{
		Domains: domains, Apps: apps, Inbound: inb, DMARC: dmarcSvc,
		pf: pf, odk: odk, inboundEnabled: cfg.inboundEnabled, dmarcEnabled: cfg.dmarcEnabled,
	}
}

// Resync rebuilds OpenDKIM's tables and Postfix's sender map from SQLite and
// reloads both daemons — the same work as the Status page's Reload button.
func (m *mailStack) Resync() error {
	if err := m.Domains.Resync(); err != nil {
		return fmt.Errorf("opendkim resync: %w", err)
	}
	if err := m.Apps.Resync(); err != nil {
		return fmt.Errorf("postfix resync: %w", err)
	}
	if m.inboundEnabled {
		if err := m.Inbound.Resync(); err != nil {
			return fmt.Errorf("inbound maps resync: %w", err)
		}
	}
	if m.dmarcEnabled {
		if err := m.DMARC.Resync(); err != nil {
			return fmt.Errorf("dmarc maps resync: %w", err)
		}
	}
	return nil
}

func (m *mailStack) skipReloadForTest() {
	m.pf.SetReloadHook(func() error { return nil })
	m.odk.SetReloadHook(func() error { return nil })
}

// resyncAfterRestore runs one mail-path Resync on the first boot after a
// backup restore. testNoReload skips the supervisord reload step so restore
// tests can verify file regeneration without a running mail stack.
func resyncAfterRestore(cfg config, st *store.Store, testNoReload bool) error {
	ms := newMailStack(cfg, st)
	if testNoReload {
		ms.skipReloadForTest()
	}
	return ms.Resync()
}

// newPanel wires the panel's services over the shared database handle and
// builds the HTTP application from cfg. It is the composition of the panel as
// the environment describes it, with nothing bound to a port yet.
func newPanel(cfg config, st *store.Store) (*web.Server, error) {
	ms := newMailStack(cfg, st)
	// postfix-config.sh has already run (entrypoint). postconf -h is the
	// effective config, including a manual override; the panel keeps this
	// snapshot for the process lifetime (architecture.md).
	retryPolicy := postfix.LoadRetryPolicy()
	return web.New(st, ms.Domains, ms.Apps, ms.Inbound, ms.DMARC, web.Config{
		Hostname:               cfg.hostname,
		CookieSecure:           cfg.cookieSecure,
		SubmissionEnabled:      cfg.submissionEnabled,
		MailLogPath:            cfg.mailLog,
		DataDir:                cfg.dataDir,
		DBPath:                 cfg.dbPath,
		DeployRoot:             cfg.deployRoot,
		Version:                buildinfo.Version,
		TrustedProxyCIDRs:      cfg.trustedProxies,
		TLSCertFile:            cfg.tlsCertFile,
		OpenDKIMSocket:         cfg.opendkimSocket,
		JournalSocket:          cfg.journalSocket,
		SessionIdleDays:        cfg.sessionIdleDays,
		DNSResolvers:           cfg.dnsResolvers,
		RateLimitMessagesPerIP: cfg.rateLimitMessagesPerIP,
		RateLimitWindowSeconds: cfg.rateLimitWindowSeconds,
		RetryPolicy:                 retryPolicy,
		InboundEnabled:              cfg.inboundEnabled,
		DMARCEnabled:                cfg.dmarcEnabled,
		SendLogRetentionEnvDefault: cfg.retentionDays,
	}, cfg.setupTokenPath)
}

// serveHTTP runs the control-panel HTTP server until ctx is cancelled, using
// the database handle shared by all roles: setup, login and the authenticated
// panel surface (security.md).
func serveHTTP(ctx context.Context, cfg config, st *store.Store) error {
	srvApp, err := newPanel(cfg, st)
	if err != nil {
		return err
	}
	if err := srvApp.Start(); err != nil {
		return err
	}

	srv := &http.Server{
		Addr:              cfg.httpAddr,
		Handler:           srvApp.Handler(),
		ReadHeaderTimeout: 10 * time.Second,
	}

	// Shut the server down cleanly when the process is asked to stop.
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = srv.Shutdown(shutdownCtx)
	}()

	log.Printf("http panel listening on %s", cfg.httpAddr)
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		return err
	}
	return nil
}
