// SPDX-License-Identifier: GPL-3.0-or-later

// Package main provides the entry point for the Private Archive client backend.
package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/privatebackup/client-backend/internal/api"
	"github.com/privatebackup/client-backend/internal/config"
	"github.com/privatebackup/client-backend/internal/jobs"
	"github.com/privatebackup/client-backend/internal/keyfile"
	"github.com/privatebackup/client-backend/internal/restic"
	"github.com/privatebackup/client-backend/internal/server"
	"github.com/privatebackup/client-backend/internal/service"
	"github.com/privatebackup/client-backend/internal/tor"
	"github.com/privatebackup/client-backend/internal/updater"
)

// version is injected at release time:
//
//	go build -ldflags "-X main.version=0.2.0"
//
// The default "dev" sentinel disables the auto-updater so development
// builds never accidentally pull a release binary over their own.
var version = "dev"

const defaultPort = 62774

func main() {
	// Set resource constraints as per spec
	runtime.GOMAXPROCS(1)

	// Check for subcommands before flag parsing.
	if len(os.Args) > 1 && os.Args[1] == "service" {
		if err := runServiceCommand(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}
		os.Exit(0)
	}

	// Parse command line flags
	configPath := flag.String("config", "", "Path to configuration file (not yet implemented)")
	keyfilePath := flag.String("keyfile", "", "Path to backup key file")
	apiPort := flag.Int("port", defaultPort, "API server port")
	apiHost := flag.String("host", "127.0.0.1", "API server host")
	showVersion := flag.Bool("version", false, "Show version and exit")
	showHelp := flag.Bool("help", false, "Show help and exit")

	flag.Parse()

	if *showHelp {
		printUsage()
		os.Exit(0)
	}

	if *showVersion {
		fmt.Printf("Private Archive Client Backend v%s\n", version)
		os.Exit(0)
	}

	// Run the application
	if err := run(*configPath, *keyfilePath, *apiHost, *apiPort); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

// runServiceCommand handles the "service" subcommand for managing the backend
// as a system service (systemd, launchd, OpenRC, or Windows).
func runServiceCommand(args []string) error {
	fs := flag.NewFlagSet("service", flag.ExitOnError)
	systemLevel := fs.Bool("system", false, "Install as system-level service (requires root/admin)")

	fs.Usage = func() {
		fmt.Fprintf(os.Stderr, `Usage: privatebackup-backend service <command> [flags]

Commands:
  install     Install and enable the service (starts on boot)
  uninstall   Stop, disable, and remove the service
  start       Start the service
  stop        Stop the service
  restart     Restart the service
  status      Show service status

Flags:
`)
		fs.PrintDefaults()
		fmt.Fprintf(os.Stderr, `
Examples:
  # Install as user service (no root required)
  privatebackup-backend service install

  # Install as system service (requires root/admin)
  sudo privatebackup-backend service install --system

  # Check status
  privatebackup-backend service status
`)
	}

	if len(args) == 0 {
		fs.Usage()
		return fmt.Errorf("no service command specified")
	}

	command := args[0]
	if err := fs.Parse(args[1:]); err != nil {
		return err
	}

	initSys := service.Detect()
	if initSys == service.InitUnknown {
		return fmt.Errorf("could not detect init system on this host")
	}

	mgr, err := service.New(initSys)
	if err != nil {
		return fmt.Errorf("failed to create service manager: %w", err)
	}

	switch command {
	case "install":
		return serviceInstall(mgr, *systemLevel)
	case "uninstall":
		return serviceUninstall(mgr)
	case "start":
		return serviceStart(mgr)
	case "stop":
		return serviceStop(mgr)
	case "restart":
		return serviceRestart(mgr)
	case "status":
		return serviceStatus(mgr)
	default:
		return fmt.Errorf("unknown service command: %s", command)
	}
}

func serviceInstall(mgr service.Manager, systemLevel bool) error {
	cfg := service.DefaultConfig()
	if systemLevel {
		cfg.Level = service.LevelSystem
	}

	fmt.Printf("Detected init system: %s\n", mgr.InitSystem())
	fmt.Printf("Service level: %s\n", levelStr(cfg.Level))
	fmt.Printf("Binary: %s\n", cfg.BinaryPath)

	if err := mgr.Install(cfg); err != nil {
		return fmt.Errorf("install failed: %w", err)
	}

	fmt.Println("Service installed and enabled successfully.")
	fmt.Println("Starting service...")

	if err := mgr.Start(); err != nil {
		fmt.Printf("Warning: failed to start service: %v\n", err)
		fmt.Println("The service will start automatically on next boot/login.")
		return nil
	}

	fmt.Println("Service started.")
	return nil
}

func serviceUninstall(mgr service.Manager) error {
	if err := mgr.Uninstall(); err != nil {
		return fmt.Errorf("uninstall failed: %w", err)
	}
	fmt.Println("Service uninstalled successfully.")
	return nil
}

func serviceStart(mgr service.Manager) error {
	if err := mgr.Start(); err != nil {
		return fmt.Errorf("start failed: %w", err)
	}
	fmt.Println("Service started.")
	return nil
}

func serviceStop(mgr service.Manager) error {
	if err := mgr.Stop(); err != nil {
		return fmt.Errorf("stop failed: %w", err)
	}
	fmt.Println("Service stopped.")
	return nil
}

func serviceRestart(mgr service.Manager) error {
	if err := mgr.Restart(); err != nil {
		return fmt.Errorf("restart failed: %w", err)
	}
	fmt.Println("Service restarted.")
	return nil
}

func serviceStatus(mgr service.Manager) error {
	st, err := mgr.Status()
	if err != nil {
		return fmt.Errorf("status check failed: %w", err)
	}

	fmt.Printf("Init system:  %s\n", st.InitSystem)
	fmt.Printf("Installed:    %v\n", st.Installed)

	if !st.Installed {
		fmt.Println("\nService is not installed. Run 'privatebackup-backend service install' to set up.")
		return nil
	}

	fmt.Printf("Level:        %s\n", st.Level)
	fmt.Printf("Unit/Task:    %s\n", st.UnitName)
	fmt.Printf("Enabled:      %v\n", st.Enabled)
	fmt.Printf("Running:      %v\n", st.Running)
	if st.PID > 0 {
		fmt.Printf("PID:          %d\n", st.PID)
	}

	return nil
}

func levelStr(l service.ServiceLevel) string {
	if l == service.LevelSystem {
		return "system"
	}
	return "user"
}

func printUsage() {
	fmt.Printf(`Private Archive Client Backend v%s

A privacy-focused backup client that connects to Private Archive servers
over Tor for anonymous, encrypted backups.

Usage:
  privatebackup-backend [flags]           Run the daemon in the foreground
  privatebackup-backend service <cmd>     Manage the background service

Service Commands:
  service install [--system]   Install and enable as a system service
  service uninstall            Remove the system service
  service start                Start the installed service
  service stop                 Stop the installed service
  service restart              Restart the installed service
  service status               Show service installation and runtime status

Flags:
  -config string    Path to configuration file (default: ~/.config/privatebackup/config.yaml)
  -keyfile string   Path to backup key file (default: ~/.config/privatebackup/backup.key)
  -host string      API server host (default: 127.0.0.1)
  -port int         API server port (default: 62774)
  -version          Show version and exit
  -help             Show this help message

Environment Variables:
  PRIVATEBACKUP_CONFIG     Override config file path
  PRIVATEBACKUP_KEYFILE    Override keyfile path
  PRIVATEBACKUP_PORT       Override API port

Examples:
  # Start the daemon in the foreground
  privatebackup-backend

  # Install as a user-level service (starts on boot/login, no root needed)
  privatebackup-backend service install

  # Install as a system-level service (requires root/admin)
  sudo privatebackup-backend service install --system

  # Check service status
  privatebackup-backend service status

Registration:
  On first run, you must register with a Private Archive server.
  Use the API endpoint POST /api/v1/account/register with:
    {"server_address": "your-server.onion"}

`, version)
}

func run(configPath, keyfilePath, apiHost string, apiPort int) error {
	// Set up context with cancellation
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Handle signals for graceful shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	// Load configuration
	loader := config.NewLoader()
	if configPath != "" {
		loader.WithConfigDir(filepath.Dir(configPath))
		loader.WithConfigName(strings.TrimSuffix(filepath.Base(configPath), filepath.Ext(configPath)))
	}
	cfg, err := loader.Load()
	if err != nil {
		return fmt.Errorf("failed to load config: %w", err)
	}

	fmt.Printf("Private Archive Client Backend v%s\n", version)
	fmt.Printf("Server address: %s\n", cfg.Server.Address)

	// Ensure required directories exist
	if err := cfg.EnsureDirectories(); err != nil {
		return fmt.Errorf("failed to create directories: %w", err)
	}

	// Determine keyfile path
	kfPath := keyfilePath
	if kfPath == "" {
		kfPath = os.Getenv("PRIVATEBACKUP_KEYFILE")
	}
	if kfPath == "" {
		kfPath = cfg.Paths.KeyFile
	}

	// Try to load existing keyfile
	var kf *keyfile.BackupKeyFile
	var hasKeyfile bool

	kfMgr := keyfile.NewManager(kfPath)
	if kfMgr.Exists() {
		var err error
		kf, err = kfMgr.Load()
		if err != nil {
			return fmt.Errorf("failed to load keyfile: %w", err)
		}
		hasKeyfile = true
		fmt.Printf("Certificate hash: %s\n", truncateHash(kf.CertHash))
	} else {
		fmt.Println("No backup key file found.")
		fmt.Println("Use POST /api/v1/account/register to register with a server.")
	}

	fmt.Println("Starting services...")

	// Auto-install as a system service on first run (idempotent) so the backend
	// survives reboots — UNLESS the desktop app manages our lifecycle. In that case
	// a boot service would launch a SECOND daemon that fights this one over Tor and
	// the API port, so we skip it (the app starts us on demand instead).
	if os.Getenv("PRIVATEBACKUP_MANAGED") == "1" {
		fmt.Println("  App-managed: skipping boot-service install (the desktop app runs the daemon)")
	} else if installed, err := service.EnsureInstalled(); err != nil {
		fmt.Printf("  Warning: could not auto-install service: %v\n", err)
		fmt.Println("  Run 'privatebackup-backend service install' manually to enable auto-start.")
	} else if installed {
		fmt.Println("  Service installed and enabled (will start automatically on boot)")
	}

	var torClient *tor.Client
	var serverClient *server.Client
	var resticEngine *restic.Engine

	// Always start Tor - it's needed for registration and for operations
	fmt.Print("  Starting Tor... ")
	torConfig := tor.DefaultConfig()
	torConfig.DataDir = cfg.Tor.DataDir
	torConfig.ConnectTimeout = cfg.Tor.ConnectTimeout
	torConfig.CircuitTimeout = cfg.Tor.CircuitTimeout
	torConfig.ExternalSOCKS = cfg.Tor.ExternalSOCKS
	torConfig.MaxRetries = cfg.Tor.MaxRetries
	torConfig.RetryDelay = cfg.Tor.RetryDelay
	torConfig.ClearStateOnRetry = cfg.Tor.ClearStateOnRetry
	torClient = tor.NewClient(torConfig)

	if err := torClient.Connect(ctx); err != nil {
		fmt.Println("FAILED")
		return fmt.Errorf("failed to start Tor: %w", err)
	}
	fmt.Println("OK")

	// Only initialize server client and restic if we have a keyfile with server address
	if hasKeyfile && cfg.Server.Address != "" {
		// Initialize server client
		serverConfig := server.DefaultConfig()
		serverConfig.ServerAddress = cfg.Server.Address
		serverConfig.KeyFile = kf
		serverConfig.Dialer = torClient
		serverConfig.Timeout = cfg.Server.Timeout
		serverConfig.MaxConnections = cfg.Performance.MaxConnections

		var err error
		serverClient, err = server.NewClient(serverConfig)
		if err != nil {
			torClient.Close()
			return fmt.Errorf("failed to create server client: %w", err)
		}

		// Derive repository password
		repoPassword, err := kf.GetRepositoryPassword()
		if err != nil {
			torClient.Close()
			return fmt.Errorf("failed to derive repository password: %w", err)
		}

		// Write CA certificate for restic TLS verification.
		caPEMPath := filepath.Join(cfg.Paths.ConfigDir, "server-ca.pem")
		if err := kf.CreateCACertPEM(caPEMPath); err != nil {
			torClient.Close()
			return fmt.Errorf("failed to create CA PEM file: %w", err)
		}

		// Create combined PEM file for mTLS (cert + key, REST backend only).
		mtlsPEMPath := filepath.Join(cfg.Paths.ConfigDir, "mtls-client.pem")
		if err := kf.CreateCombinedPEM(mtlsPEMPath); err != nil {
			torClient.Close()
			return fmt.Errorf("failed to create mTLS PEM file: %w", err)
		}
		fmt.Printf("  Created mTLS client PEM: %s\n", mtlsPEMPath)

		// Initialize restic engine — prefer S3 backend, fall back to REST.
		resticCfg := restic.DefaultConfig()
		resticCfg.Password = repoPassword
		resticCfg.Compression = restic.CompressionMax
		resticCfg.MaxConnections = cfg.Performance.MaxConnections
		resticCfg.ProxyURL = torClient.SOCKSProxyURL()
		resticCfg.CACertPath = caPEMPath

		// Skip restic TLS verification for .onion servers: Tor provides the
		// server's cryptographic identity via the onion address, and the
		// server's TLS cert is for the cluster-internal name (…svc.cluster.local),
		// not the onion — so hostname verification always fails. Non-onion
		// servers keep full verification. (Mirrors the API client + the
		// post-registration engine build in handlers.reinitializeServices.)
		serverHost := cfg.Server.Address
		if i := strings.LastIndex(serverHost, ":"); i >= 0 {
			serverHost = serverHost[:i]
		}
		if strings.HasSuffix(strings.ToLower(serverHost), ".onion") {
			resticCfg.InsecureSkipVerify = true
		}

		if kf.HasS3Credentials() {
			resticCfg.RepositoryURL = fmt.Sprintf("s3:https://%s/%s", cfg.Server.Address, kf.S3BucketName)
			resticCfg.S3AccessKeyID = kf.S3AccessKeyID
			resticCfg.S3SecretAccessKey = kf.S3SecretAccessKey
		} else {
			resticCfg.RepositoryURL = fmt.Sprintf("rest:https://%s/%s", cfg.Server.Address, kf.CertHash)
			resticCfg.TLSClientCertPath = mtlsPEMPath
		}

		resticEngine, err = restic.NewEngine(resticCfg)
		if err != nil {
			torClient.Close()
			return fmt.Errorf("failed to create restic engine: %w", err)
		}
		fmt.Println("  Server client initialized")
	} else {
		fmt.Println("  Skipping server client (no registration yet)")
	}

	// Initialize job manager
	jobConfig := jobs.DefaultConfig()
	jobConfig.MaxConcurrentJobs = 1      // Per spec
	jobConfig.JobTimeout = 0 // No timeout — large initial backups over Tor can take days

	jobManager, err := jobs.NewManager(jobConfig)
	if err != nil {
		if torClient != nil {
			torClient.Close()
		}
		return fmt.Errorf("failed to create job manager: %w", err)
	}

	jobManager.Start(ctx)

	// Initialize scheduler and load schedules from config.
	scheduler := jobs.NewScheduler(jobManager)
	scheduler.Start(ctx)

	// Wire schedules from config into the scheduler.
	for _, sched := range cfg.Schedules {
		nextRun, err := nextCronTime(sched.Cron)
		if err != nil {
			fmt.Printf("  Warning: invalid cron for schedule %q: %v\n", sched.Name, err)
			continue
		}
		if err := scheduler.AddSchedule(sched.Name, &jobs.Schedule{
			Type:     jobs.JobTypeBackup,
			Interval: 24 * time.Hour,
			Metadata: map[string]interface{}{"paths": sched.Paths},
			Enabled:  true,
			NextRun:  nextRun,
		}); err != nil {
			fmt.Printf("  Warning: failed to add schedule %q: %v\n", sched.Name, err)
			continue
		}
		fmt.Printf("  Schedule %q: next run at %s (%d path(s))\n", sched.Name, nextRun.UTC().Format("15:04 UTC"), len(sched.Paths))
	}

	// Initialize updater (best-effort: missing pubkey or "dev" version
	// disables auto-update without failing startup).
	upd := startUpdater(ctx, cfg, torClient)

	// Initialize API server
	apiCfg := api.DefaultConfig()
	apiCfg.Host = apiHost
	apiCfg.Port = apiPort
	apiCfg.WebDir = cfg.Paths.WebDir

	handlers := api.NewHandlers(api.HandlersConfig{
		TorClient:     torClient,
		ServerAddress: cfg.Server.Address,
		Config:        cfg,
		KeyfileMgr:    kfMgr,
		ServerClient:  serverClient,
		ResticEngine:  resticEngine,
		JobManager:    jobManager,
		Version:       version,
	})
	if upd != nil {
		handlers.SetUpdater(upd)
	}
	apiServer, err := api.NewServer(apiCfg, handlers)
	if err != nil {
		scheduler.Stop()
		jobManager.Stop()
		if torClient != nil {
			torClient.Close()
		}
		return fmt.Errorf("failed to create API server: %w", err)
	}

	// Start API server in background
	apiErrChan := make(chan error, 1)
	go func() {
		fmt.Printf("  API server listening on http://%s:%d\n", apiHost, apiPort)
		if err := apiServer.Start(); err != nil {
			apiErrChan <- err
		}
	}()

	// Allow API server to start
	time.Sleep(100 * time.Millisecond)

	fmt.Println("\nReady. Press Ctrl+C to stop.")
	fmt.Printf("\nAPI endpoint: http://%s:%d/api/v1/\n", apiHost, apiPort)

	// Wait for shutdown signal or error
	select {
	case sig := <-sigChan:
		fmt.Printf("\nReceived signal %v, shutting down...\n", sig)
	case err := <-apiErrChan:
		fmt.Printf("\nAPI server error: %v\n", err)
	}

	// Graceful shutdown
	fmt.Println("Stopping services...")

	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer shutdownCancel()

	if err := apiServer.Stop(shutdownCtx); err != nil {
		fmt.Printf("  Warning: API server shutdown error: %v\n", err)
	}

	scheduler.Stop()
	jobManager.Stop()
	if upd != nil {
		upd.Stop()
	}
	if torClient != nil {
		torClient.Close()
	}

	fmt.Println("Shutdown complete.")
	return nil
}

// startUpdater initialises the update service if a public key is embedded
// and the running build has a non-"dev" version string. It returns nil
// (with a warning printed) if the updater cannot be set up — that is
// expected for development builds and is not fatal.
func startUpdater(ctx context.Context, cfg *config.Config, torClient *tor.Client) *updater.Updater {
	if version == "dev" {
		fmt.Println("  Updater disabled: development build (version=dev)")
		return nil
	}
	exec, err := os.Executable()
	if err != nil {
		fmt.Printf("  Updater disabled: cannot resolve executable path: %v\n", err)
		return nil
	}
	stagingDir := filepath.Join(cfg.Paths.CacheDir, "updates")
	uc := updater.Config{
		CurrentVersion: version,
		ManifestURL:    cfg.Updates.ManifestURL,
		Channel:        cfg.Updates.Channel,
		AutoUpdate:     cfg.Updates.AutoUpdate,
		PollInterval:   cfg.Updates.PollInterval,
		StagingDir:     stagingDir,
		ExecPath:       exec,
		Dialer:         torClient,
		Restarter:      detectRestarter(),
	}
	u, err := updater.New(uc)
	if err != nil {
		fmt.Printf("  Updater disabled: %v\n", err)
		return nil
	}
	go u.Run(ctx)
	mode := "auto"
	if !cfg.Updates.AutoUpdate {
		mode = "manual (auto-update opted out)"
	}
	fmt.Printf("  Updater started: channel=%s mode=%s poll=%s\n",
		cfg.Updates.Channel, mode, cfg.Updates.PollInterval)
	return u
}

// detectRestarter returns a service.Manager-backed restarter when the
// daemon is running under a recognised init system. If no init system is
// detected (e.g. when running interactively from a terminal), the
// returned restarter is nil and the updater leaves the new binary in
// place for the next manual restart.
func detectRestarter() updater.ServiceRestarter {
	initSys := service.Detect()
	if initSys == service.InitUnknown {
		return nil
	}
	mgr, err := service.New(initSys)
	if err != nil {
		return nil
	}
	return mgr
}

func truncateHash(hash string) string {
	if len(hash) > 16 {
		return hash[:16] + "..."
	}
	return hash
}

// nextCronTime parses a "minute hour * * *" cron expression and returns
// the next UTC time the schedule should fire (today if still in the future,
// otherwise tomorrow).
func nextCronTime(expr string) (time.Time, error) {
	parts := strings.Fields(expr)
	if len(parts) != 5 {
		return time.Time{}, fmt.Errorf("expected 5 cron fields, got %d", len(parts))
	}
	minute, err := strconv.Atoi(parts[0])
	if err != nil || minute < 0 || minute > 59 {
		return time.Time{}, fmt.Errorf("invalid minute %q", parts[0])
	}
	hour, err := strconv.Atoi(parts[1])
	if err != nil || hour < 0 || hour > 23 {
		return time.Time{}, fmt.Errorf("invalid hour %q", parts[1])
	}
	now := time.Now().UTC()
	t := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, time.UTC)
	if !t.After(now) {
		t = t.Add(24 * time.Hour)
	}
	return t, nil
}
