package knotserver

import (
	"context"
	"fmt"
	"net/http"

	"github.com/bluesky-social/indigo/xrpc"
	"github.com/urfave/cli/v3"
	"tangled.org/core/api/tangled"
	"tangled.org/core/hook"
	"tangled.org/core/idresolver"
	"tangled.org/core/jetstream"
	"tangled.org/core/knotserver/config"
	knotdb "tangled.org/core/knotserver/db"
	"tangled.org/core/knotserver/sandbox"
	"tangled.org/core/log"
	"tangled.org/core/notifier"
	"tangled.org/core/rbac"
)

func Command() *cli.Command {
	return &cli.Command{
		Name:   "server",
		Usage:  "run a knot server",
		Action: Run,
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:  "secure-mode",
				Usage: "isolate git subprocesses to their own repository directory",
			},
		},
		Description: `
	Environment variables:
		KNOT_SERVER_LISTEN_ADDR          (default: 0.0.0.0:5555)
		KNOT_SERVER_INTERNAL_LISTEN_ADDR (default: 127.0.0.1:5444)
		KNOT_SERVER_DB_PATH              (default: knotserver.db)
		KNOT_SERVER_HOSTNAME             (required)
		KNOT_SERVER_JETSTREAM_ENDPOINT   (default: wss://jetstream1.us-west.bsky.network/subscribe)
		KNOT_SERVER_OWNER                (required)
		KNOT_SERVER_LOG_DIDS             (default: true)
		KNOT_SERVER_DEV                  (default: false)
		KNOT_SERVER_SECURE_MODE          (default: false)
		KNOT_REPO_SCAN_PATH              (default: /home/git)
		KNOT_REPO_README                 (comma-separated list)
		KNOT_REPO_MAIN_BRANCH            (default: main)
		KNOT_GIT_USER_NAME               (default: Tangled)
		KNOT_GIT_USER_EMAIL              (default: noreply@tangled.sh)
		APPVIEW_ENDPOINT                 (default: https://tangled.sh)
	`,
	}
}

func Run(ctx context.Context, cmd *cli.Command) error {
	logger := log.FromContext(ctx)
	logger = log.SubLogger(logger, cmd.Name)
	ctx = log.IntoContext(ctx, logger)

	c, err := config.Load(ctx)
	if err != nil {
		return fmt.Errorf("failed to load config: %w", err)
	}

	// CLI flag overrides env var.
	if cmd.Bool("secure-mode") {
		c.Server.SecureMode = true
	}

	if !c.Server.SecureMode {
		err = hook.Setup(hook.Config(
			hook.WithScanPath(c.Repo.ScanPath),
			hook.WithInternalApi(c.Server.InternalListenAddr),
		))
		if err != nil {
			return fmt.Errorf("failed to setup hooks: %w", err)
		}
		logger.Info("successfully finished setting up hooks")
	}

	if c.Server.Dev {
		logger.Info("running in dev mode, signature verification is disabled")
	}

	db, err := knotdb.Setup(ctx, c.Server.DBPath)
	if err != nil {
		return fmt.Errorf("failed to load db: %w", err)
	}

	e, err := rbac.NewEnforcer(c.Server.DBPath)
	if err != nil {
		return fmt.Errorf("failed to setup rbac enforcer: %w", err)
	}

	e.E.EnableAutoSave(true)

	jc, err := jetstream.NewJetstreamClient(c.Server.JetstreamEndpoint, "knotserver", []string{
		tangled.PublicKeyNSID,
		tangled.RepoNSID,
	}, nil, log.SubLogger(logger, "jetstream"), db, true, c.Server.LogDids)
	if err != nil {
		logger.Error("failed to setup jetstream", "error", err)
	}

	notifier := notifier.New()

	resolver := idresolver.DefaultResolver(c.Server.PlcUrl)

	if err := BackfillKnotMembers(ctx, db, e, c.Server.Owner, logger); err != nil {
		logger.Warn("knot member backfill failed, continuing", "err", err)
	}
	if err := BackfillCollaborators(ctx, db, e, logger, false); err != nil {
		logger.Warn("collaborator backfill failed, continuing", "err", err)
	}

	// probe and initialise the sandbox backend.
	var sb sandbox.Backend
	if c.Server.SecureMode {
		var warn string
		sb, warn = sandbox.New(func(repoPath string) (uint32, uint32, error) {
			return sandbox.LookupUIDForRepoPath(c.Repo.ScanPath, repoPath)
		})
		if warn != "" {
			return fmt.Errorf("secure-mode: %s", warn)
		}
		logger.Info("secure-mode: activated", "backend", sb.Name())

		// refuse to start if any repos have not yet been isolation-migrated.
		unmigrated, countErr := db.CountUnmigratedRepos()
		if countErr != nil {
			return fmt.Errorf("secure-mode: checking unmigrated repos: %w", countErr)
		}
		if unmigrated > 0 {
			return fmt.Errorf(
				"secure-mode: %d repo(s) have not been isolation-migrated; "+
					"run 'knot migrate-isolation' first",
				unmigrated,
			)
		}
	} else {
		sb = &sandbox.NoopBackend{}
	}

	go func() {
		migrated := migrateReposOnStartup(ctx, c, db, e, &notifier, log.SubLogger(logger, "migrate"))
		if err := BackfillCollaborators(ctx, db, e, logger, migrated); err != nil {
			logger.Warn("collaborator backfill failed, continuing", "err", err)
		}
	}()

	mux, err := Setup(ctx, c, db, e, jc, &notifier, resolver, sb)
	if err != nil {
		return fmt.Errorf("failed to setup server: %w", err)
	}

	imux := Internal(ctx, c, db, e, &notifier, resolver)

	logger.Info("starting internal server", "address", c.Server.InternalListenAddr)
	go http.ListenAndServe(c.Server.InternalListenAddr, imux)

	// TODO(boltless): too lazy here. should clear this up
	go func() {
		input := &tangled.SyncRequestCrawl_Input{
			Hostname: c.Server.Hostname,
		}
		for _, knotmirror := range c.KnotMirrors {
			xrpcc := xrpc.Client{Host: knotmirror}
			if err := tangled.SyncRequestCrawl(ctx, &xrpcc, input); err != nil {
				logger.Error("error requesting crawl", "err", err)
			} else {
				logger.Info("crawl requested successfully")
			}
		}
	}()

	logger.Info("starting main server", "address", c.Server.ListenAddr)
	logger.Error("server error", "error", http.ListenAndServe(c.Server.ListenAddr, mux))

	return nil
}
