package commands

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	"github.com/dustin/go-humanize/english"
	"github.com/sevlyar/go-daemon"
	"github.com/urfave/cli/v2"

	"github.com/photoprism/photoprism/internal/auth/session"
	"github.com/photoprism/photoprism/internal/config"
	"github.com/photoprism/photoprism/internal/mutex"
	"github.com/photoprism/photoprism/internal/photoprism/backup"
	"github.com/photoprism/photoprism/internal/server"
	"github.com/photoprism/photoprism/internal/server/process"
	"github.com/photoprism/photoprism/internal/workers"
	"github.com/photoprism/photoprism/internal/workers/auto"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/fs"
	"github.com/photoprism/photoprism/pkg/txt/report"
)

// StartCommand configures the command name, flags, and action.
var StartCommand = &cli.Command{
	Name:    "start",
	Aliases: []string{"up"},
	Usage:   "Starts the Web server",
	Flags:   startFlags,
	Action:  startAction,
}

// startFlags specifies the start command parameters.
var startFlags = []cli.Flag{
	&cli.BoolFlag{
		Name:    "detach-server",
		Aliases: []string{"d"},
		Usage:   "detach from the console (daemon mode)",
		EnvVars: config.EnvVars("DETACH_SERVER"),
	},
	&cli.BoolFlag{
		Name:    "config",
		Aliases: []string{"c"},
		Usage:   "show config",
	},
}

// startAction starts the Web server and initializes the daemon.
func startAction(ctx *cli.Context) error {
	conf, err := InitConfig(ctx)

	if err != nil {
		return err
	}

	if ctx.IsSet("config") {
		// Create config report.
		cols := []string{"Name", "Value"}
		rows := [][]string{
			{"detach-server", fmt.Sprintf("%t", conf.DetachServer())},
			{"http-mode", conf.HttpMode()},
			{"http-compression", conf.HttpCompression()},
			{"http-cache-public", fmt.Sprintf("%t", conf.HttpCachePublic())},
			{"http-cache-maxage", fmt.Sprintf("%d", conf.HttpCacheMaxAge())},
			{"http-video-maxage", fmt.Sprintf("%d", conf.HttpVideoMaxAge())},
			{"http-host", conf.HttpHost()},
			{"http-port", fmt.Sprintf("%d", conf.HttpPort())},
		}

		// Render config report.
		opt := report.Options{Format: report.CliFormat(ctx), NoWrap: true}
		result, _ := report.Render(rows, cols, opt)
		fmt.Printf("\n%s\n", result)
		return nil
	}

	if conf.HttpPort() < 1 || conf.HttpPort() > 65535 {
		log.Fatal("server port must be a number between 1 and 65535")
	}

	// Pass this context down the chain.
	cctx, cancel := context.WithCancel(context.Background())

	// Initialize the index database.
	conf.InitDb()

	// Check if the daemon is running, if not, initialize the daemon.
	dctx := new(daemon.Context)
	dctx.LogFileName = conf.LogFilename()
	dctx.PidFileName = conf.PIDFilename()
	dctx.Args = ctx.Args().Slice()

	if !daemon.WasReborn() && conf.DetachServer() {
		conf.Shutdown()
		cancel()

		if pid, ok := childAlreadyRunning(conf.PIDFilename()); ok {
			log.Infof("daemon already running with process id %v\n", pid)
			return nil
		}

		child, contextErr := dctx.Reborn()

		if contextErr != nil {
			return fmt.Errorf("daemon context error: %w", contextErr)
		}

		if child != nil {
			if writeErr := fs.WriteString(conf.PIDFilename(), strconv.Itoa(child.Pid)); writeErr != nil {
				log.Error(writeErr)
				return fmt.Errorf("failed writing process id to %s", clean.Log(conf.PIDFilename()))
			}

			log.Infof("daemon started with process id %v\n", child.Pid)
			return nil
		}
	}

	// Show info if read-only mode is enabled.
	if conf.ReadOnly() {
		log.Infof("config: enabled read-only mode")
	}

	// Start built-in web server.
	go server.Start(cctx, conf)

	// Restore albums from YAML files.
	if count, restoreErr := backup.RestoreAlbums(conf.BackupAlbumsPath(), false); restoreErr != nil {
		log.Errorf("restore: %s (albums)", restoreErr)
	} else if count > 0 {
		log.Infof("restore: %s restored", english.Plural(count, "album backup", "album backups"))
	}

	// Start worker that periodically deletes expired sessions.
	session.Cleanup(conf.SessionCacheDuration() * 4)

	// Start sync and metadata maintenance background workers.
	workers.Start(conf)

	// Start auto-indexing background worker.
	auto.Start(conf)

	// Wait for signal to trigger server shutdown or restart.
	signal.Notify(process.Signal, os.Interrupt, syscall.SIGTERM, syscall.SIGUSR1)
	sig := <-process.Signal

	// Stop all background activity.
	auto.Shutdown()
	workers.Shutdown()
	session.Shutdown()
	mutex.CancelAll()

	log.Info("shutting down...")
	cancel()

	if contextErr := dctx.Release(); contextErr != nil {
		log.Error(contextErr)
	}

	// Finally, close the DB connection after a short grace period.
	time.Sleep(2 * time.Second)
	conf.Shutdown()

	// Exit with status code 1 if the shutdown was initiated with SIGUSR1 to request a restart.
	//
	// Note that this requires an entrypoint script or other process to
	// spawns a new instance when the server exists with status code 1.
	if sig == syscall.SIGUSR1 {
		os.Exit(1)
		return nil
	}

	return nil
}
