package server

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/redis/go-redis/v9"

	"github.com/ultisuite/ulti-backend/internal/api/admin"
	"github.com/ultisuite/ulti-backend/internal/api/calendar"
	"github.com/ultisuite/ulti-backend/internal/api/contacts"
	"github.com/ultisuite/ulti-backend/internal/api/docs"
	"github.com/ultisuite/ulti-backend/internal/api/drive"
	mailapi "github.com/ultisuite/ulti-backend/internal/api/mail"
	"github.com/ultisuite/ulti-backend/internal/api/mail/drivebridge"
	"github.com/ultisuite/ulti-backend/internal/api/mail/sendguard"
	meetapi "github.com/ultisuite/ulti-backend/internal/api/meet"
	"github.com/ultisuite/ulti-backend/internal/api/middleware"
	"github.com/ultisuite/ulti-backend/internal/api/office"
	photosapi "github.com/ultisuite/ulti-backend/internal/api/photos"
	usersapi "github.com/ultisuite/ulti-backend/internal/api/users"
	"github.com/ultisuite/ulti-backend/internal/automation"
	"github.com/ultisuite/ulti-backend/internal/authentik"
	"github.com/ultisuite/ulti-backend/internal/auth"
	"github.com/ultisuite/ulti-backend/internal/config"
	"github.com/ultisuite/ulti-backend/internal/filescan"
	"github.com/ultisuite/ulti-backend/internal/httpcors"
	mailcredentials "github.com/ultisuite/ulti-backend/internal/mail/credentials"
	imapsync "github.com/ultisuite/ulti-backend/internal/mail/imap"
	mailoauth "github.com/ultisuite/ulti-backend/internal/mail/oauth"
	"github.com/ultisuite/ulti-backend/internal/mail/rules"
	"github.com/ultisuite/ulti-backend/internal/mail/smtp"
	mailstorage "github.com/ultisuite/ulti-backend/internal/mail/storage"
	"github.com/ultisuite/ulti-backend/internal/mail/webhooks"
	"github.com/ultisuite/ulti-backend/internal/meet"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	"github.com/ultisuite/ulti-backend/internal/observability"
	"github.com/ultisuite/ulti-backend/internal/orgpolicy"
	"github.com/ultisuite/ulti-backend/internal/photos"
	"github.com/ultisuite/ulti-backend/internal/realtime"
	"github.com/ultisuite/ulti-backend/internal/search"
	"github.com/ultisuite/ulti-backend/internal/securityaudit"
)

// App holds wired dependencies and the HTTP router.
type App struct {
	Router         chi.Router
	Pool           *pgxpool.Pool
	Redis          *redis.Client
	VerifierHolder *auth.Holder
	MailOAuth      *mailoauth.Service

	ownsPool  bool
	ownsRedis bool
	cancel    context.CancelFunc
}

// New builds the application router and optional background workers.
func New(ctx context.Context, cfg *config.Config, opts Options) (*App, error) {
	if cfg == nil {
		return nil, fmt.Errorf("config is nil")
	}

	workerCtx, cancel := context.WithCancel(ctx)

	pool := opts.Pool
	ownsPool := false
	if pool == nil {
		var err error
		pool, err = pgxpool.New(workerCtx, cfg.DatabaseURL)
		if err != nil {
			cancel()
			return nil, fmt.Errorf("connect database: %w", err)
		}
		ownsPool = true
	}

	if !opts.SkipAuthentikProvisioner {
		authentik.StartProvisioner(workerCtx, pool, cfg)
	}

	rdb := opts.Redis
	ownsRedis := false
	if rdb == nil {
		rdb = redis.NewClient(&redis.Options{
			Addr:     cfg.KeyDBAddr,
			Password: cfg.KeyDBPassword,
			DB:       cfg.KeyDBDB,
		})
		ownsRedis = true
	}

	minioClient, err := minio.New(cfg.RustFSEndpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(cfg.RustFSAccessKey, cfg.RustFSSecretKey, ""),
		Secure: cfg.RustFSUseSSL,
	})
	if err != nil {
		cancel()
		if ownsPool {
			pool.Close()
		}
		if ownsRedis {
			_ = rdb.Close()
		}
		return nil, fmt.Errorf("rustfs client: %w", err)
	}
	attachmentStorage := mailstorage.NewClient(minioClient, cfg.MailAttachmentsBucket)
	if err := attachmentStorage.EnsureBucket(workerCtx); err != nil {
		slog.Warn("mail attachments bucket check failed", "error", err)
	}

	verifierHolder := opts.VerifierHolder
	if verifierHolder == nil {
		verifier, verr := auth.NewVerifierWithRetry(workerCtx, cfg.OIDCIssuer, cfg.OIDCClientID, cfg.Domain, 45, 2*time.Second)
		if verr != nil {
			slog.Warn("OIDC verifier not available at startup (Authentik may still be starting)", "error", verr)
		}
		verifierHolder = auth.NewHolder(verifier)
		if !verifierHolder.Ready() && cfg.OIDCIssuer != "" && cfg.OIDCClientID != "" {
			pending := auth.NewHolderPending(cfg.OIDCIssuer, cfg.OIDCClientID, cfg.Domain)
			verifierHolder = pending
			verifierHolder.StartBackgroundRetry(workerCtx, 5*time.Second)
		}
	}

	if cfg.IsProduction() {
		if cfg.OIDCIssuer == "" || cfg.OIDCClientID == "" {
			cancel()
			closeOwned(ownsPool, pool, ownsRedis, rdb)
			return nil, fmt.Errorf("missing required OIDC configuration in production")
		}
		if !verifierHolder.Ready() {
			cancel()
			closeOwned(ownsPool, pool, ownsRedis, rdb)
			return nil, fmt.Errorf("OIDC verifier initialization failed in production")
		}
		if err := cfg.ValidateSecretRotation(); err != nil {
			cancel()
			closeOwned(ownsPool, pool, ownsRedis, rdb)
			return nil, fmt.Errorf("secret rotation policy: %w", err)
		}
	} else if err := cfg.ValidateSecretRotation(); err != nil {
		slog.Warn("secret rotation policy warning", "error", err)
	}

	credentialManager, err := mailcredentials.NewManager(cfg.MailCredentialKeys, cfg.MailActiveCredentialKeyID)
	if err != nil {
		cancel()
		closeOwned(ownsPool, pool, ownsRedis, rdb)
		return nil, fmt.Errorf("mail credential encryption: %w", err)
	}

	auditLogger := securityaudit.NewLogger(pool)

	var ncClient *nextcloud.Client
	if cfg.NextcloudEnabled {
		ncClient = nextcloud.NewClient(cfg.NextcloudURL, cfg.NCAdminUser, cfg.NCAdminPass).
			WithPublicURL(cfg.NextcloudPublicURL).
			WithDrivePublicURL(cfg.DrivePublicURL).
			WithDAVCredentials(nextcloud.NewDAVCredentialStore(pool, credentialManager))
		slog.Info("nextcloud enabled", "url", cfg.NextcloudURL, "public_url", cfg.NextcloudPublicURL, "drive_public_url", cfg.DrivePublicURL)
	}

	var meetCfg *meet.Config
	if cfg.JitsiEnabled {
		meetCfg = meet.NewConfig(cfg.JitsiAppID, cfg.JitsiAppSecret, cfg.Domain)
		slog.Info("jitsi meet enabled", "domain", cfg.JitsiDomain)
	}

	var photosClient *photos.Client
	if cfg.ImmichEnabled {
		photosClient = photos.NewClient(cfg.ImmichAPIURL)
		slog.Info("immich photos enabled", "url", cfg.ImmichAPIURL)
	}

	hub := realtime.NewHub(verifierHolder, pool)
	healthChecker := observability.NewHealthChecker(cfg, pool, rdb)

	hookExec := webhooks.NewExecutor(pool)
	rulesEngine := rules.NewEngineWithWebhooks(pool, hookExec)
	autoDispatcher := automation.NewDispatcher(pool, rulesEngine, hookExec)

	oauthRedirect := cfg.MailOAuthRedirectURL
	if oauthRedirect == "" {
		oauthRedirect = fmt.Sprintf("http://localhost:%d/api/v1/mail/accounts/oauth/callback", cfg.Port)
		if cfg.Domain != "" && cfg.Domain != "localhost" {
			oauthRedirect = fmt.Sprintf("https://%s/api/v1/mail/accounts/oauth/callback", cfg.Domain)
		}
	}
	mailOAuthSvc := mailoauth.NewService(mailoauth.Config{
		GoogleClientID:     cfg.MailGoogleOAuthClientID,
		GoogleClientSecret: cfg.MailGoogleOAuthClientSecret,
		MicrosoftClientID:  cfg.MailMicrosoftOAuthClientID,
		MicrosoftSecret:    cfg.MailMicrosoftOAuthSecret,
		MicrosoftTenant:    cfg.MailMicrosoftOAuthTenant,
		RedirectURL:        oauthRedirect,
	}, rdb)

	orgPolicyLoader := orgpolicy.NewLoader(pool, cfg)
	fileScanner := filescan.NewScanner(orgPolicyLoader, slog.Default())

	var syncWorker *imapsync.SyncWorker
	if !opts.WithoutWorkers {
		syncWorker = imapsync.NewSyncWorker(pool, cfg.MailSyncInterval, credentialManager, mailOAuthSvc, imapsync.SyncDeps{
			Storage:      attachmentStorage,
			AttachBucket: cfg.MailAttachmentsBucket,
			Automation:   autoDispatcher,
			Hub:          hub,
			FileScanner:  fileScanner,
		})
		go syncWorker.Start(workerCtx)
	}

	sender := smtp.NewSender(pool, credentialManager, mailOAuthSvc)
	smtpCircuit := smtp.NewCircuitBreaker(cfg.MailSMTPCircuitFailures, cfg.MailSMTPCircuitCooldown)
	guardedSender := smtp.NewGuardedSender(sender, smtpCircuit)
	if !opts.WithoutWorkers {
		go smtp.NewOutboxProcessor(
			pool,
			guardedSender,
			cfg.MailOutboxInterval,
			cfg.MailOutboxMaxRetries,
			smtp.WithAttachmentLoader(&smtp.StorageAttachmentLoader{Client: attachmentStorage}),
		).Start(workerCtx)
	}

	sendRateLimiter := sendguard.NewRateLimiter(cfg.MailSendRatePerMinute, cfg.MailSendBurst)
	mailHandler := mailapi.NewHandler(pool, auditLogger, credentialManager, attachmentStorage, cfg.MailAttachmentsBucket, sendRateLimiter, mailOAuthSvc, cfg.MailAppURL, sender)
	mailHandler.SetFileScanner(fileScanner)
	if syncWorker != nil {
		mailHandler.SetAccountSync(syncWorker)
	}

	r := chi.NewRouter()
	r.Use(httpcors.Middleware(cfg))
	r.Use(middleware.TraceID)
	r.Use(observability.HTTPMetrics)
	r.Use(middleware.Logging)

	r.Mount("/api/docs", docs.NewHandler().Routes())

	r.Get("/healthz", func(w http.ResponseWriter, req *http.Request) {
		report := healthChecker.Check(req.Context())
		statusCode := http.StatusOK
		if report.Status != "ok" {
			statusCode = http.StatusServiceUnavailable
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(statusCode)
		_ = json.NewEncoder(w).Encode(report)
	})
	r.Handle("/metrics", promhttp.Handler())

	r.Get("/ws", hub.HandleWS)
	r.Get("/api/v1/mail/accounts/oauth/callback", mailHandler.OAuthCallback)

	var driveHandler *drive.Handler
	var driveSvc *drive.Service
	var contactsHandler *contacts.Handler
	if ncClient != nil {
		driveSvc = drive.NewService(ncClient, hub, pool)
		driveSvc.SetAutomation(autoDispatcher)
		driveSvc.SetFileScanner(fileScanner)
		driveHandler = drive.NewHandlerWithService(driveSvc)
		mailHandler.SetDriveUploader(&drivebridge.Bridge{Svc: driveSvc})
		contactsHandler = contacts.NewHandler(ncClient, pool)
		contactsHandler.SetAutomation(autoDispatcher)
	}
	if ncClient != nil && cfg.OnlyOfficeEnabled && driveSvc != nil {
		officeSvc := office.NewService(ncClient, office.Config{
			Enabled:        true,
			DocumentURL:    cfg.OnlyOfficeURL,
			PublicURL:      cfg.OnlyOfficePublicURL,
			APIInternalURL: cfg.OnlyOfficeAPIInternalURL,
			JWTSecret:      cfg.OnlyOfficeJWTSecret,
		})
		officeHandler := office.NewHandler(officeSvc, driveSvc)
		r.Mount("/api/v1/office", officeHandler.Routes(middleware.Auth(verifierHolder, pool, auditLogger, orgPolicyLoader)))
		driveHandler.SetPublicOffice(officeHandler)
	}
	if driveHandler != nil {
		r.Mount("/api/v1/drive/public", driveHandler.PublicRoutes())
	}

	r.Group(func(r chi.Router) {
		r.Use(middleware.Auth(verifierHolder, pool, auditLogger, orgPolicyLoader))
		r.Use(middleware.EnforceApiTokenPolicy())

		r.Mount("/api/v1/users", usersapi.NewHandler(pool).Routes())
		r.Mount("/api/v1/admin", admin.NewHandler(pool, auditLogger, cfg, ncClient).Routes())
		if driveHandler != nil {
			r.Mount("/api/v1/drive", driveHandler.Routes())
		}

		r.Group(func(r chi.Router) {
			r.Use(middleware.RequireFullAccount)
			r.Mount("/api/v1/mail", mailHandler.Routes())
			r.Get("/api/v1/search", search.NewHandler(pool, search.Options{
				Nextcloud:           ncClient,
				Engine:              cfg.SearchEngine,
				MeilisearchURL:      cfg.MeilisearchURL,
				MeilisearchKey:      cfg.MeilisearchKey,
				MeilisearchIndex:    cfg.MeilisearchIndex,
				TypesenseURL:        cfg.TypesenseURL,
				TypesenseKey:        cfg.TypesenseKey,
				TypesenseCollection: cfg.TypesenseCollection,
			}).Search)
			if driveHandler != nil {
				r.Mount("/api/v1/calendar", calendar.NewHandler(ncClient, meetCfg).Routes())
				r.Mount("/api/v1/contacts", contactsHandler.Routes())
			}
			if meetCfg != nil {
				r.Mount("/api/v1/meet", meetapi.NewHandler(meetCfg).Routes())
			}
			if photosClient != nil {
				r.Mount("/api/v1/photos", photosapi.NewHandler(photosClient, ncClient).Routes())
			}
		})
	})

	slog.Info("mail oauth providers", "enabled", mailOAuthSvc.EnabledProviders(), "redirect", oauthRedirect)

	return &App{
		Router:         r,
		Pool:           pool,
		Redis:          rdb,
		VerifierHolder: verifierHolder,
		MailOAuth:      mailOAuthSvc,
		ownsPool:       ownsPool,
		ownsRedis:      ownsRedis,
		cancel:         cancel,
	}, nil
}

// Close stops workers and releases owned connections.
func (a *App) Close() {
	if a == nil {
		return
	}
	if a.cancel != nil {
		a.cancel()
	}
	if a.ownsPool && a.Pool != nil {
		a.Pool.Close()
	}
	if a.ownsRedis && a.Redis != nil {
		_ = a.Redis.Close()
	}
}

func closeOwned(ownsPool bool, pool *pgxpool.Pool, ownsRedis bool, rdb *redis.Client) {
	if ownsPool && pool != nil {
		pool.Close()
	}
	if ownsRedis && rdb != nil {
		_ = rdb.Close()
	}
}
