// Command app is the Bounty Board main service.
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"bountyboard/internal/atomize"
	"bountyboard/internal/auth"
	"bountyboard/internal/config"
	"bountyboard/internal/crypto"
	"bountyboard/internal/domain"
	"bountyboard/internal/files"
	httpx "bountyboard/internal/http"
	"bountyboard/internal/jobs"
	"bountyboard/internal/metrics"
	"bountyboard/internal/store"
	syncpkg "bountyboard/internal/sync"
	"bountyboard/internal/workperform"
	"bountyboard/internal/ws"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "fatal:", err)
		os.Exit(1)
	}
}

func run() error {
	if err := config.LoadDotEnv(".env"); err != nil {
		return fmt.Errorf("load .env: %w", err)
	}
	cfg, err := config.Load()
	if err != nil {
		return err
	}

	log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
	slog.SetDefault(log)

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	st, err := store.Connect(ctx, cfg, log)
	if err != nil {
		return err
	}
	defer func() {
		closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := st.Close(closeCtx); err != nil {
			log.Error("close mongo", "err", err)
		}
	}()

	if err := auth.EnsureBootstrapAdmin(ctx, st, cfg, log); err != nil {
		return err
	}

	reg := metrics.NewRegistry()
	fileStore := files.NewStore(st.DB, cfg.MaxUploadMB)
	srv := httpx.New(cfg, log, reg, st, fileStore)
	srv.AddReadinessCheck(httpx.ReadinessCheck{
		Name:     "mongo",
		Required: true,
		Probe:    st.Ping,
	})

	// WebSocket hub: one multiplexed live channel per session (§2.2).
	hub := ws.NewHub(log, func(r *http.Request) bool {
		origin := r.Header.Get("Origin")
		if origin == "" {
			return true // non-browser client (no CSRF surface on a read feed)
		}
		return strings.HasPrefix(origin, cfg.AppBaseURL) ||
			strings.HasPrefix(origin, "http://"+r.Host) || strings.HasPrefix(origin, "https://"+r.Host)
	})
	srv.SetWSHandler(hub.Handle)
	srv.SetPublishFn(hub.Broadcast)
	srv.SetSendTo(hub.SendTo)
	hub.SetInbound(srv.HandleInboundWS)

	// Atomizer client honoring the admin base-URL override per call.
	atomClient := atomize.New(func() string {
		sctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		if settings, err := st.GetSettings(sctx); err == nil && settings.AtomizerBaseURLOverride != "" {
			return settings.AtomizerBaseURLOverride
		}
		return cfg.AtomizerBaseURL
	}, cfg.AtomizerToken, cfg.AtomizerTimeout)
	srv.SetAtomizerInfo(atomClient)
	srv.AddReadinessCheck(httpx.ReadinessCheck{Name: "atomizer", Probe: atomClient.Healthy})

	// Work Performer client — fully independent service (§5).
	wpClient := workperform.New(func() string { return cfg.WorkPerformerBaseURL },
		cfg.WorkPerformerToken, cfg.WorkPerformerHTTPTimeout)
	srv.SetPerformerClient(wpClient)
	srv.AddReadinessCheck(httpx.ReadinessCheck{Name: "work-performer", Probe: wpClient.Healthy})

	// Background job queue + atomization handlers.
	runner := jobs.NewRunner(st, log, reg, 4)
	atomSvc := atomize.NewService(st, atomClient, log, cfg.AppInternalURL,
		[]byte(cfg.SessionSecret), srv.Publish)
	runner.Register(atomize.JobKindSubdivide, atomSvc.HandleSubdivide)
	runner.Register(atomize.JobKindExtend, atomSvc.HandleExtend)
	srv.SetJobsStatusProvider(runner)
	srv.SetEnqueue(runner.Enqueue)
	go runner.Run(ctx)

	syncMgr := syncpkg.NewManager(st, fileStore, log, reg,
		func(c *domain.Customer) (syncpkg.Credentials, error) {
			if c.Ticketing.CredentialsEnc == "" {
				return syncpkg.Credentials{}, nil
			}
			plain, err := crypto.Open(cfg.CredentialsEncKey, c.Ticketing.CredentialsEnc)
			if err != nil {
				return nil, err
			}
			var creds syncpkg.Credentials
			if err := json.Unmarshal(plain, &creds); err != nil {
				return nil, err
			}
			return creds, nil
		},
		srv.Publish)
	srv.SetSyncStatusProvider(syncMgr)
	srv.SetSyncTrigger(syncMgr.SyncNow)
	go syncMgr.Run(ctx)

	return srv.Run(ctx)
}
