package main

import (
	"bufio"
	"context"
	"errors"
	"flag"
	"fmt"
	"log"
	"net/url"
	"os"
	"os/signal"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"gitea.lerkolabs.com/lerkolabs/uptop/internal/cluster"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/config"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/importer"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/server"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/store"
	"gitea.lerkolabs.com/lerkolabs/uptop/internal/tui"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/ssh"
	"github.com/charmbracelet/wish"
	bm "github.com/charmbracelet/wish/bubbletea"
	"github.com/mattn/go-isatty"
)

var (
	version = "dev"
	commit  = "none"
	date    = "unknown"
)

func main() {
	log.SetOutput(os.Stderr)

	if len(os.Args) >= 2 {
		switch os.Args[1] {
		case "apply":
			runApply(os.Args[2:])
			return
		case "export":
			runExport(os.Args[2:])
			return
		case "version", "--version", "-v":
			printVersion()
			return
		case "migrate-secrets":
			runMigrateSecrets(os.Args[2:])
			return
		}
	}
	runServe(os.Args[1:])
}

func printVersion() {
	if version == "dev" {
		fmt.Println("uptop dev")
	} else {
		fmt.Printf("uptop %s (%s, %s)\n", version, commit, date)
	}
}

func envOrDefault(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func redactDSN(dsn string) string {
	u, err := url.Parse(dsn)
	if err != nil {
		return "***"
	}
	u.User = nil
	return u.String()
}

func openStore(dbType, dsn string) store.Store {
	var ss *store.SQLStore
	var err error
	if dbType == "postgres" {
		ss, err = store.NewPostgresStore(dsn)
	} else {
		ss, err = store.NewSQLiteStore(dsn)
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "database error: %v\n", err)
		os.Exit(1)
	}
	if encKey := os.Getenv("UPTOP_ENCRYPTION_KEY"); encKey != "" {
		enc, err := store.NewEncryptor(encKey)
		if err != nil {
			fmt.Fprintf(os.Stderr, "encryption key error: %v\n", err)
			os.Exit(1)
		}
		ss.SetEncryptor(enc)
	} else {
		fmt.Println("WARNING: No UPTOP_ENCRYPTION_KEY set. Alert credentials stored unencrypted.")
	}
	if err := ss.Init(); err != nil {
		fmt.Fprintf(os.Stderr, "database init error: %v\n", err)
		os.Exit(1)
	}
	return ss
}

func runApply(args []string) {
	fs := flag.NewFlagSet("apply", flag.ExitOnError)
	filePath := fs.String("f", "", "Path to YAML config file (required)")
	dryRun := fs.Bool("dry-run", false, "Show planned changes without applying")
	prune := fs.Bool("prune", false, "Delete monitors/alerts not in YAML")
	dbType := fs.String("db-type", envOrDefault("UPTOP_DB_TYPE", "sqlite"), "Database type")
	dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN")
	_ = fs.Parse(args) // ExitOnError: parse errors exit before returning

	if *filePath == "" {
		fmt.Fprintln(os.Stderr, "error: -f flag is required")
		fs.Usage()
		os.Exit(1)
	}

	s := openStore(*dbType, *dsn)

	f, err := config.LoadFile(*filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	changes, err := config.Apply(s, f, config.ApplyOpts{
		DryRun: *dryRun,
		Prune:  *prune,
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	fmt.Print(config.FormatChanges(changes, *dryRun))
}

func runExport(args []string) {
	fs := flag.NewFlagSet("export", flag.ExitOnError)
	outPath := fs.String("o", "-", "Output file path (- for stdout)")
	dbType := fs.String("db-type", envOrDefault("UPTOP_DB_TYPE", "sqlite"), "Database type")
	dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN")
	_ = fs.Parse(args) // ExitOnError: parse errors exit before returning

	s := openStore(*dbType, *dsn)

	f, err := config.Export(s)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	if err := config.WriteFile(f, *outPath); err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}

func runMigrateSecrets(args []string) {
	fs := flag.NewFlagSet("migrate-secrets", flag.ExitOnError)
	dbType := fs.String("db-type", envOrDefault("UPTOP_DB_TYPE", "sqlite"), "Database type")
	dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN")
	_ = fs.Parse(args)

	encKey := os.Getenv("UPTOP_ENCRYPTION_KEY")
	if encKey == "" {
		fmt.Fprintln(os.Stderr, "error: UPTOP_ENCRYPTION_KEY must be set")
		os.Exit(1)
	}
	enc, err := store.NewEncryptor(encKey)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	var ss *store.SQLStore
	if *dbType == "postgres" {
		ss, err = store.NewPostgresStore(*dsn)
	} else {
		ss, err = store.NewSQLiteStore(*dsn)
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "database error: %v\n", err)
		os.Exit(1)
	}
	if err := ss.Init(); err != nil {
		fmt.Fprintf(os.Stderr, "database init error: %v\n", err)
		os.Exit(1)
	}

	alerts, err := ss.GetAllAlerts()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error loading alerts: %v\n", err)
		os.Exit(1)
	}

	ss.SetEncryptor(enc)
	migrated := 0
	for _, a := range alerts {
		if err := ss.UpdateAlert(a.ID, a.Name, a.Type, a.Settings); err != nil {
			fmt.Fprintf(os.Stderr, "error migrating alert %q: %v\n", a.Name, err)
			os.Exit(1)
		}
		migrated++
	}
	fmt.Printf("Migrated %d alert(s) to encrypted storage.\n", migrated)
}

func runServe(args []string) {
	portVal := 23234
	dbType := "sqlite"
	dbDSN := "uptop.db"
	httpPort := 8080
	enableStatus := false
	statusTitle := "System Status"
	clusterMode := "leader"
	clusterPeer := ""
	clusterKey := ""

	if v := os.Getenv("UPTOP_PORT"); v != "" {
		if p, err := strconv.Atoi(v); err == nil {
			portVal = p
		}
	}
	if v := os.Getenv("UPTOP_DB_TYPE"); v != "" {
		dbType = v
	}
	if v := os.Getenv("UPTOP_DB_DSN"); v != "" {
		dbDSN = v
	}
	if v := os.Getenv("UPTOP_HTTP_PORT"); v != "" {
		if p, err := strconv.Atoi(v); err == nil {
			httpPort = p
		}
	}
	if v := os.Getenv("UPTOP_STATUS_ENABLED"); v == "true" {
		enableStatus = true
	}
	if v := os.Getenv("UPTOP_STATUS_TITLE"); v != "" {
		statusTitle = v
	}
	if v := os.Getenv("UPTOP_CLUSTER_MODE"); v != "" {
		clusterMode = v
	}
	if v := os.Getenv("UPTOP_PEER_URL"); v != "" {
		clusterPeer = v
	}
	if v := os.Getenv("UPTOP_CLUSTER_SECRET"); v != "" {
		clusterKey = v
	}

	nodeID := os.Getenv("UPTOP_NODE_ID")
	nodeName := os.Getenv("UPTOP_NODE_NAME")
	nodeRegion := os.Getenv("UPTOP_NODE_REGION")
	aggStrategy := os.Getenv("UPTOP_AGG_STRATEGY")

	if clusterMode == "probe" {
		if nodeID == "" {
			fmt.Fprintln(os.Stderr, "UPTOP_NODE_ID is required for probe mode")
			os.Exit(1)
		}
		if clusterPeer == "" {
			fmt.Fprintln(os.Stderr, "UPTOP_PEER_URL is required for probe mode")
			os.Exit(1)
		}

		fmt.Printf("Cluster: Running as PROBE (node=%s, region=%s)\n", nodeID, nodeRegion)

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()
		done := make(chan os.Signal, 1)
		signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
		go func() {
			<-done
			cancel()
		}()

		probeAllowPrivate := os.Getenv("UPTOP_ALLOW_PRIVATE_TARGETS") == "true"
		if probeAllowPrivate {
			fmt.Println("WARNING: Private target blocking disabled. Monitor URLs can reach internal networks.")
		}

		if err := cluster.RunProbe(ctx, cluster.ProbeConfig{
			NodeID:              nodeID,
			NodeName:            nodeName,
			Region:              nodeRegion,
			LeaderURL:           clusterPeer,
			SharedKey:           clusterKey,
			Interval:            30,
			AllowPrivateTargets: probeAllowPrivate,
		}); err != nil {
			fmt.Fprintf(os.Stderr, "Probe error: %v\n", err)
		}
		return
	}

	fs := flag.NewFlagSet("serve", flag.ExitOnError)
	port := fs.Int("port", portVal, "SSH Port")
	flagDBType := fs.String("db-type", dbType, "Database type")
	flagDSN := fs.String("dsn", dbDSN, "Database DSN")
	demo := fs.Bool("demo", false, "Seed demo data")
	importKuma := fs.String("import-kuma", "", "Import Uptime Kuma backup JSON file")
	_ = fs.Parse(args) // ExitOnError: parse errors exit before returning

	var ss *store.SQLStore
	var dbErr error
	if *flagDBType == "postgres" {
		ss, dbErr = store.NewPostgresStore(*flagDSN)
		fmt.Printf("Using PostgreSQL: %s\n", redactDSN(*flagDSN))
	} else {
		ss, dbErr = store.NewSQLiteStore(*flagDSN)
		fmt.Printf("Using SQLite: %s\n", *flagDSN)
	}
	if dbErr != nil {
		fmt.Fprintf(os.Stderr, "database connection error: %v\n", dbErr)
		os.Exit(1)
	}
	defer ss.Close()

	if encKey := os.Getenv("UPTOP_ENCRYPTION_KEY"); encKey != "" {
		enc, err := store.NewEncryptor(encKey)
		if err != nil {
			fmt.Fprintf(os.Stderr, "encryption key error: %v\n", err)
			os.Exit(1)
		}
		ss.SetEncryptor(enc)
	} else {
		fmt.Println("WARNING: No UPTOP_ENCRYPTION_KEY set. Alert credentials stored unencrypted.")
	}

	var s store.Store = ss
	if err := s.Init(); err != nil {
		fmt.Fprintf(os.Stderr, "database init error: %v\n", err)
		os.Exit(1)
	}
	if *demo {
		seedDemoData(s)
	}

	seedKeysFromEnv(s)

	if *importKuma != "" {
		kb, err := importer.LoadKumaFile(*importKuma)
		if err != nil {
			fmt.Fprintf(os.Stderr, "kuma import error: %v\n", err)
			os.Exit(1)
		}
		backup := importer.ConvertKuma(kb)
		if err := s.ImportData(backup); err != nil {
			fmt.Fprintf(os.Stderr, "import failed: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Imported %d monitors and %d alerts from Uptime Kuma v%s\n", len(backup.Sites), len(backup.Alerts), kb.Version)
	}

	allowPrivate := os.Getenv("UPTOP_ALLOW_PRIVATE_TARGETS") == "true"
	if allowPrivate {
		fmt.Println("WARNING: Private target blocking disabled. Monitor URLs can reach internal networks.")
	}

	eng := monitor.NewEngineWithOpts(s, allowPrivate)
	if os.Getenv("UPTOP_INSECURE_SKIP_VERIFY") == "true" {
		eng.SetInsecureSkipVerify(true)
	}
	if aggStrategy != "" {
		eng.SetAggStrategy(monitor.AggregationStrategy(aggStrategy))
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	eng.InitHistory()
	eng.InitLogs()
	eng.InitAlertHealth()
	eng.Start(ctx)

	tlsCert := os.Getenv("UPTOP_TLS_CERT")
	tlsKey := os.Getenv("UPTOP_TLS_KEY")

	httpSrv := server.Start(server.ServerConfig{
		Port:          httpPort,
		EnableStatus:  enableStatus,
		Title:         statusTitle,
		ClusterKey:    clusterKey,
		TLSCert:       tlsCert,
		TLSKey:        tlsKey,
		ClusterMode:   clusterMode,
		MetricsPublic: os.Getenv("UPTOP_METRICS_PUBLIC") == "true",
		CORSOrigin:    os.Getenv("UPTOP_CORS_ORIGIN"),
	}, s, eng)

	cluster.Start(ctx, cluster.Config{
		Mode:      clusterMode,
		PeerURL:   clusterPeer,
		SharedKey: clusterKey,
	}, eng)

	kc := newKeyCache(s)
	sshSrv := startSSHServer(*port, s, eng, kc)

	if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
		p := tea.NewProgram(tui.InitialModel(true, s, eng, version), tea.WithAltScreen(), tea.WithMouseCellMotion())
		if _, err := p.Run(); err != nil {
			fmt.Fprintf(os.Stderr, "error: %v\n", err)
		}
	} else {
		fmt.Println("uptop running in HEADLESS mode")
		done := make(chan os.Signal, 1)
		signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
		<-done
		fmt.Println("Shutting down...")
	}
	cancel()

	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer shutdownCancel()
	if httpSrv != nil {
		if err := httpSrv.Shutdown(shutdownCtx); err != nil {
			log.Printf("HTTP shutdown error: %v", err)
		}
	}
	if sshSrv != nil {
		if err := sshSrv.Shutdown(shutdownCtx); err != nil {
			log.Printf("SSH shutdown error: %v", err)
		}
	}
}

func startSSHServer(port int, db store.Store, eng *monitor.Engine, kc *keyCache) *ssh.Server {
	s, err := wish.NewServer(
		wish.WithAddress(fmt.Sprintf(":%d", port)),
		wish.WithHostKeyPath(envOrDefault("UPTOP_SSH_HOST_KEY", ".ssh/id_ed25519")),
		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
			return kc.IsAllowed(key)
		}),
		wish.WithMiddleware(
			bm.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
				return tui.InitialModel(false, db, eng, version), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()}
			}),
		),
	)
	if err != nil {
		fmt.Fprintf(os.Stderr, "SSH server error: %v\n", err)
		return nil
	}
	go func() {
		if err := s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
			log.Printf("SSH server error: %v", err)
		}
	}()
	return s
}

func seedDemoData(s store.Store) {
	existing, _ := s.GetSites()
	if len(existing) > 0 {
		return
	}
	fmt.Println("Seeding demo data...")

	if err := s.AddAlert("Discord Ops", "discord", map[string]string{"url": "https://discord.com/api/webhooks/demo/token"}); err != nil {
		log.Printf("demo seed: add alert: %v", err)
		return
	}
	if err := s.AddAlert("Slack Infra", "slack", map[string]string{"url": "https://hooks.slack.com/services/DEMO/WEBHOOK"}); err != nil {
		log.Printf("demo seed: add alert: %v", err)
		return
	}
	if err := s.AddAlert("Email Oncall", "email", map[string]string{
		"host": "smtp.example.com", "port": "587",
		"user": "oncall@example.com", "pass": "replace-me",
		"from": "oncall@example.com", "to": "team@example.com",
	}); err != nil {
		log.Printf("demo seed: add alert: %v", err)
		return
	}

	alerts, _ := s.GetAllAlerts()
	alertID := 0
	if len(alerts) > 0 {
		alertID = alerts[0].ID
	}

	demoSites := []models.Site{
		{Name: "Google", URL: "https://www.google.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 14, MaxRetries: 2},
		{Name: "GitHub", URL: "https://github.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 7, MaxRetries: 3},
		{Name: "Cloudflare DNS", URL: "https://1.1.1.1", Type: "http", Interval: 60, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 1},
		{Name: "JSON Placeholder", URL: "https://jsonplaceholder.typicode.com/posts/1", Type: "http", Interval: 45, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 2},
		{Name: "Nonexistent Site", URL: "https://this-domain-does-not-exist-12345.com", Type: "http", Interval: 30, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 3},
		{Name: "Bad Port", URL: "https://localhost:19999", Type: "http", Interval: 30, ExpiryThreshold: 7, MaxRetries: 1},
		{Name: "Backup Cron", Type: "push", Interval: 300, AlertID: alertID, ExpiryThreshold: 7},
		{Name: "DB Healthcheck", Type: "push", Interval: 120, AlertID: alertID, ExpiryThreshold: 7},
		{Name: "Gateway", Type: "ping", Interval: 30, AlertID: alertID, Hostname: "10.0.0.1", Timeout: 5, ExpiryThreshold: 7},
		{Name: "SSH Server", Type: "port", Interval: 60, AlertID: alertID, Hostname: "10.0.0.1", Port: 22, Timeout: 5, ExpiryThreshold: 7},
	}
	for _, site := range demoSites {
		if err := s.AddSite(site); err != nil {
			log.Printf("demo seed: add site %q: %v", site.Name, err)
		}
	}
}

type keyCache struct {
	mu      sync.RWMutex
	keys    []ssh.PublicKey
	updated time.Time
	ttl     time.Duration
	db      store.Store
}

func newKeyCache(db store.Store) *keyCache {
	return &keyCache{db: db, ttl: 30 * time.Second}
}

func (c *keyCache) refresh() {
	users, err := c.db.GetAllUsers()
	if err != nil {
		return
	}
	keys := make([]ssh.PublicKey, 0, len(users))
	for _, u := range users {
		k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(u.PublicKey))
		if err != nil {
			continue
		}
		keys = append(keys, k)
	}
	c.mu.Lock()
	c.keys = keys
	c.updated = time.Now()
	c.mu.Unlock()
}

func (c *keyCache) Invalidate() {
	c.mu.Lock()
	c.updated = time.Time{}
	c.mu.Unlock()
}

func (c *keyCache) IsAllowed(incomingKey ssh.PublicKey) bool {
	c.mu.RLock()
	stale := time.Since(c.updated) > c.ttl
	c.mu.RUnlock()

	if stale {
		c.refresh()
	}

	c.mu.RLock()
	defer c.mu.RUnlock()
	for _, k := range c.keys {
		if ssh.KeysEqual(k, incomingKey) {
			return true
		}
	}
	return false
}

func seedKeysFromEnv(s store.Store) {
	var keys []string

	if v := os.Getenv("UPTOP_ADMIN_KEY"); v != "" {
		keys = append(keys, strings.TrimSpace(v))
	}

	if path := os.Getenv("UPTOP_KEYS"); path != "" {
		f, err := os.Open(filepath.Clean(path))
		if err == nil {
			scanner := bufio.NewScanner(f)
			for scanner.Scan() {
				line := strings.TrimSpace(scanner.Text())
				if line == "" || strings.HasPrefix(line, "#") {
					continue
				}
				keys = append(keys, line)
			}
			_ = f.Close()
		}
	}

	if len(keys) == 0 {
		return
	}

	existing, err := s.GetAllUsers()
	if err != nil {
		fmt.Fprintf(os.Stderr, "warning: could not check existing users: %v\n", err)
		return
	}

	existingKeys := make(map[string]bool)
	for _, u := range existing {
		existingKeys[u.PublicKey] = true
	}

	added := 0
	for i, key := range keys {
		if existingKeys[key] {
			continue
		}

		username := usernameFromKey(key, i, len(existing)+added)
		if err := s.AddUser(username, key, "admin"); err != nil {
			fmt.Fprintf(os.Stderr, "warning: failed to seed user %q: %v\n", username, err)
			continue
		}
		fmt.Printf("Seeded admin user %q from %s\n", username, seedSource(i, len(keys), os.Getenv("UPTOP_ADMIN_KEY") != ""))
		added++
	}
}

func usernameFromKey(key string, index, totalExisting int) string {
	parts := strings.Fields(key)
	if len(parts) >= 3 {
		comment := parts[2]
		if at := strings.Index(comment, "@"); at > 0 {
			return comment[:at]
		}
		return comment
	}
	if index == 0 && totalExisting == 0 {
		return "admin"
	}
	return fmt.Sprintf("user-%d", totalExisting+1)
}

func seedSource(index, total int, hasEnvKey bool) string {
	if hasEnvKey && index == 0 {
		return "UPTOP_ADMIN_KEY"
	}
	return "UPTOP_KEYS"
}
