package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	"github.com/mattn/go-isatty"

	"nadir/internal/auth"
	"nadir/internal/config"
)

const configTemplateBase = `# Nadir configuration - config.yaml
#
# Single source of truth for runtime settings.
#

server:
  secure_tls: %s
  %s
  %s
  %s
  hostname: %s
  port: %d
  release_repo: https://tea.urania.dev/urania/nadir-agent

roles:
  admin:
    "*": ["*"]
  auditor:
    "*": ["read"]

assignments:
  %s: [admin]
  dashboard: [auditor]
`

// resolveConfigPath returns the config file path: CONFIG_PATH env (with ~ expanded)
// when set, otherwise the platform default. Used by run/install/uninstall.
func resolveConfigPath() (string, error) {
	if p := os.Getenv("CONFIG_PATH"); p != "" {
		return config.ExpandPath(p)
	}
	return config.DefaultPath()
}

func getUsername() string {
	if u := os.Getenv("SUDO_USER"); u != "" {
		return u
	}
	if u := os.Getenv("USER"); u != "" {
		return u
	}
	return "admin"
}

func chownToSudoUser(path string) {
	uidStr := os.Getenv("SUDO_UID")
	gidStr := os.Getenv("SUDO_GID")
	if uidStr != "" && gidStr != "" {
		uid, err1 := strconv.Atoi(uidStr)
		gid, err2 := strconv.Atoi(gidStr)
		if err1 == nil && err2 == nil {
			_ = os.Chown(path, uid, gid)
		}
	}
}

const (
	serviceName = "nadir"
	unitPath    = "/etc/systemd/system/" + serviceName + ".service"
	// symlinkPath puts nadir on PATH regardless of where the binary actually
	// lives (built in place, installed via install.sh to /usr/local/bin
	// directly, etc). installService creates it; uninstallService removes it -
	// but only when it's actually a symlink we own, never a regular file that
	// happens to sit there.
	symlinkPath = "/usr/local/bin/nadir"

	// daemonEnv marks a process that was re-exec'd by `run -d`, so it serves in
	// the foreground of its new session instead of detaching again.
	daemonEnv = "NADIR_DAEMON"
	// detachLog is where a backgrounded (`run -d`) server's stdout/stderr go,
	// alongside the SQLite stores. `logs` tails this when nadir isn't a service.
	detachLog = "/var/lib/nadir/server.log"

	// logrotatePath is the file path of the logrotate configuration for nadir.
	logrotatePath = "/etc/logrotate.d/nadir"
	// logrotateConfig defines logrotate rules for background server logs.
	logrotateConfig = `/var/lib/nadir/server.log {
    daily
    rotate 5
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}
`
)

// installService writes the systemd unit, enables it on boot, and starts it.
// The unit pins the absolute executable and config paths captured now, so the
// service doesn't depend on the working directory at boot.
func installService(args []string) error {
	// Parse options
	fs := flag.NewFlagSet("install", flag.ContinueOnError)
	tlsOpt := fs.Bool("tls", false, "Generate persistent self-signed TLS cert/key and enable HTTPS")
	unsecureOpt := fs.Bool("unsecure", false, "Serve plaintext HTTP directly")
	trustProxyOpt := fs.Bool("trust-proxy", false, "Serve plaintext HTTP behind a trusted TLS-terminating reverse proxy")
	hostnameOpt := fs.String("hostname", "127.0.0.1", "Hostname to bind to")
	portOpt := fs.Int("port", 9999, "Port to bind to")
	if err := fs.Parse(args); err != nil {
		return err
	}

	optCount := 0
	if *tlsOpt {
		optCount++
	}
	if *unsecureOpt {
		optCount++
	}
	if *trustProxyOpt {
		optCount++
	}

	if optCount > 1 {
		return fmt.Errorf("options --tls, --unsecure, and --trust-proxy are mutually exclusive")
	}

	// Default to unsecure (plain HTTP) if nothing is specified
	isTLS := *tlsOpt
	isUnsecure := *unsecureOpt || optCount == 0
	isTrustProxy := *trustProxyOpt

	cfgPath, err := resolveConfigPath()
	if err != nil {
		return err
	}

	shouldWriteConfig := false
	if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
		shouldWriteConfig = true
	}

	username := getUsername()
	var logFiles map[string][]string

	if fs.NFlag() == 0 && (isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())) {
		reader := bufio.NewReader(os.Stdin)

		if !shouldWriteConfig {
			fmt.Printf("Configuration file already exists at %s. Overwrite? [y/N] (default n): ", cfgPath)
			overwriteInput, _ := reader.ReadString('\n')
			overwriteInput = strings.ToLower(strings.TrimSpace(overwriteInput))
			if overwriteInput != "y" && overwriteInput != "yes" {
				fmt.Println("Keeping existing configuration. Proceeding with installation...")
				if existingCfg, loadErr := config.Load(cfgPath); loadErr == nil {
					*hostnameOpt = existingCfg.Server.Hostname
					if p, err := strconv.Atoi(existingCfg.Server.Port); err == nil {
						*portOpt = p
					}
					isTLS = existingCfg.Server.TLSCert != "" && existingCfg.Server.TLSKey != ""
					isTrustProxy = existingCfg.Server.TrustProxy
					isUnsecure = !isTLS && !isTrustProxy
				}
				goto skipConfigPrompt
			}
			shouldWriteConfig = true
		}

		fmt.Println("Configuring Nadir installation:")
		fmt.Println("  1) Serve plaintext HTTP directly (unsecure) [default]")
		fmt.Println("  2) Generate persistent self-signed TLS cert/key and enable HTTPS (tls)")
		fmt.Println("  3) Serve plaintext HTTP behind a trusted TLS-terminating reverse proxy (trust-proxy)")
		fmt.Print("Enter choice [1-3] (default 1): ")
		choice, _ := reader.ReadString('\n')
		choice = strings.TrimSpace(choice)
		if choice == "" || choice == "1" {
			isUnsecure = true
			isTLS = false
			isTrustProxy = false
		} else if choice == "2" {
			isTLS = true
			isUnsecure = false
			isTrustProxy = false
		} else if choice == "3" {
			isTrustProxy = true
			isTLS = false
			isUnsecure = false
		} else {
			return fmt.Errorf("invalid choice: %q", choice)
		}

		fmt.Printf("Enter hostname to bind to (default %s): ", *hostnameOpt)
		hostChoice, _ := reader.ReadString('\n')
		hostChoice = strings.TrimSpace(hostChoice)
		if hostChoice != "" {
			*hostnameOpt = hostChoice
		}

		fmt.Printf("Enter port to bind to (default %d): ", *portOpt)
		portChoice, _ := reader.ReadString('\n')
		portChoice = strings.TrimSpace(portChoice)
		if portChoice != "" {
			p, err := strconv.Atoi(portChoice)
			if err != nil || p <= 0 || p > 65535 {
				return fmt.Errorf("invalid port: %q", portChoice)
			}
			*portOpt = p
		}

		fmt.Printf("Enter main admin username (default %s): ", username)
		userChoice, _ := reader.ReadString('\n')
		userChoice = strings.TrimSpace(userChoice)
		if userChoice != "" {
			username = userChoice
		}

		fmt.Print("Would you like to expose any log files to the Nadir UI? [y/N] (default n): ")
		logInput, _ := reader.ReadString('\n')
		logInput = strings.ToLower(strings.TrimSpace(logInput))
		if logInput == "y" || logInput == "yes" {
			logFiles = make(map[string][]string)
			for {
				fmt.Print("  Enter service/unit name (e.g. nginx): ")
				unit, _ := reader.ReadString('\n')
				unit = strings.TrimSpace(unit)
				if unit == "" {
					fmt.Println("  Service name cannot be empty. Skipping.")
					continue
				}

				fmt.Printf("  Enter absolute path to log file for %s: ", unit)
				path, _ := reader.ReadString('\n')
				path = strings.TrimSpace(path)
				if path == "" {
					fmt.Println("  Path cannot be empty. Skipping.")
					continue
				}

				logFiles[unit] = append(logFiles[unit], path)

				fmt.Print("  Add another log file? [y/N] (default n): ")
				another, _ := reader.ReadString('\n')
				another = strings.ToLower(strings.TrimSpace(another))
				if another != "y" && another != "yes" {
					break
				}
			}
		}
	}

skipConfigPrompt:
	// Provision the PAM service the server authenticates against, so it exists
	// before the unit starts rather than appearing on first login. Idempotent:
	// EnsurePAMService leaves an existing /etc/pam.d/nadir untouched. runServer
	// still calls it too, so `nadir run` standalone self-heals a fresh box.
	if err := auth.EnsurePAMService(); err != nil {
		return err
	}

	exe, err := os.Executable()
	if err != nil {
		return err
	}

	// Copy executable to /usr/local/bin/nadir so it runs from outside user home
	// directories (e.g. /home/urania) which are restricted by systemd/AppArmor/SELinux and home permissions.
	if exe != symlinkPath {
		if err := copyFile(exe, symlinkPath); err != nil {
			return fmt.Errorf("copy executable to %s: %w", symlinkPath, err)
		}
		exe = symlinkPath
		fmt.Printf("installed binary to %s\n", symlinkPath)
	}

	// Ensure data directory /var/lib/nadir exists so systemd can write service logs there.
	if err := os.MkdirAll("/var/lib/nadir", 0700); err != nil {
		return fmt.Errorf("create data directory: %w", err)
	}

	// Generate and save persistent self-signed TLS certificates if TLS mode
	certPath := filepath.Join("/var/lib/nadir/tls", "cert.pem")
	if isTLS {
		tlsDir := "/var/lib/nadir/tls"
		if err := os.MkdirAll(tlsDir, 0700); err != nil {
			return fmt.Errorf("create tls directory: %w", err)
		}
		keyPath := filepath.Join(tlsDir, "key.pem")
		if _, err := os.Stat(certPath); os.IsNotExist(err) {
			if err := generateAndSaveCert(certPath, keyPath, *hostnameOpt); err != nil {
				return fmt.Errorf("generate certificates: %w", err)
			}
			fmt.Printf("generated persistent self-signed TLS certificate at %s\n", certPath)
		}
	}

	// Construct configuration template content based on installation options
	secureTLSVal := "true"
	trustProxyLine := "# trust_proxy: false"
	certLine := "tls_cert: /var/lib/nadir/tls/cert.pem"
	keyLine := "tls_key:  /var/lib/nadir/tls/key.pem"

	if isUnsecure {
		secureTLSVal = "false"
		trustProxyLine = "# trust_proxy: false"
		certLine = "# tls_cert: /var/lib/nadir/tls/cert.pem"
		keyLine = "# tls_key:  /var/lib/nadir/tls/key.pem"
	} else if isTrustProxy {
		// The proxy terminates TLS, so the browser<->proxy leg is still HTTPS -
		// the cookie stays Secure. Only the proxy<->nadir hop is plaintext, and
		// the browser never sees it.
		secureTLSVal = "true"
		trustProxyLine = "trust_proxy: true"
		certLine = "# tls_cert: /var/lib/nadir/tls/cert.pem"
		keyLine = "# tls_key:  /var/lib/nadir/tls/key.pem"
	}

	configContent := fmt.Sprintf(configTemplateBase, secureTLSVal, trustProxyLine, certLine, keyLine, *hostnameOpt, *portOpt, username)
	if len(logFiles) > 0 {
		var logFilesSection strings.Builder
		logFilesSection.WriteString("\nlog_files:\n")
		for unit, paths := range logFiles {
			logFilesSection.WriteString(fmt.Sprintf("  %s:\n", unit))
			for _, path := range paths {
				logFilesSection.WriteString(fmt.Sprintf("    - %s\n", path))
			}
		}
		configContent += logFilesSection.String()
	}

	// Ensure default config file exists or we explicitly overwrote it
	if shouldWriteConfig {
		if err := saveDefaultConfig(cfgPath, configContent); err != nil {
			return err
		}
	}

	cfgAbs, err := filepath.Abs(cfgPath)
	if err != nil {
		return err
	}

	unit := fmt.Sprintf(`[Unit]
Description=Nadir system administration backend
After=network.target

[Service]
Type=simple
ExecStart=%s run
Environment=CONFIG_PATH=%s
StandardOutput=append:/var/lib/nadir/server.log
StandardError=append:/var/lib/nadir/server.log
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
`, exe, cfgAbs)

	if err := os.WriteFile(unitPath, []byte(unit), 0644); err != nil {
		return fmt.Errorf("write %s: %w (need root)", unitPath, err)
	}
	if err := runForeground("systemctl", "daemon-reload"); err != nil {
		return err
	}
	// enable --now: start now and on every boot.
	if err := runForeground("systemctl", "enable", "--now", serviceName); err != nil {
		return err
	}

	// Write logrotate configuration
	if err := os.WriteFile(logrotatePath, []byte(logrotateConfig), 0644); err != nil {
		fmt.Printf("warning: failed to write logrotate config %s: %v\n", logrotatePath, err)
	} else {
		fmt.Printf("created logrotate configuration %s\n", logrotatePath)
	}

	// Generate a token for the dashboard if it doesn't exist
	var tokenStr string
	store, err := auth.NewTokenStore(tokenDBPath)
	if err == nil {
		defer store.Close()
		infos, err := store.List()
		hasDashboard := false
		if err == nil {
			for _, t := range infos {
				if t.Name == "dashboard" {
					hasDashboard = true
					break
				}
			}
		}
		if !hasDashboard {
			tokenStr, _ = store.Create("dashboard")
		} else {
			tokenStr = "(already created; run 'nadir token add dashboard' to replace/generate a new one if lost)"
		}
	} else {
		fmt.Printf("warning: failed to open token store: %v\n", err)
	}

	// Output credentials to copy to the frontend
	fmt.Println("\n======================================================================")
	fmt.Println("  NADIR CLIENT CREDENTIALS (COPY THESE TO NADIR WEBUI)")
	fmt.Println("======================================================================")
	fmt.Printf("Token for \"dashboard\" (Bearer):\n  %s\n", tokenStr)
	if isTLS {
		certBytes, err := os.ReadFile(certPath)
		if err == nil {
			fmt.Println("\nCA Certificate (Trust Store):")
			fmt.Println(string(certBytes))
		}
	}
	fmt.Println("======================================================================")

	fmt.Printf("installed and started %s; follow logs with: %s logs\n", serviceName, filepath.Base(exe))
	return nil
}

// copyFile copies src file to dst, overwriting if it exists and keeping executable permissions.
func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	// If destination exists, remove it first to avoid ETXTBSY if running,
	// or overwrite issues.
	_ = os.Remove(dst)

	out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, in)
	return err
}

// removeSymlink removes symlinkPath, either if it's a symlink or a regular file (e.g. from install.sh).
func removeSymlink() error {
	info, err := os.Lstat(symlinkPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // nothing there
		}
		return fmt.Errorf("stat %s: %w", symlinkPath, err)
	}
	if err := os.Remove(symlinkPath); err != nil {
		return fmt.Errorf("remove %s: %w", symlinkPath, err)
	}
	if info.Mode()&os.ModeSymlink != 0 {
		fmt.Printf("removed symlink %s\n", symlinkPath)
	} else {
		fmt.Printf("removed binary %s\n", symlinkPath)
	}
	return nil
}

// uninstallService stops and disables the service and removes the install
// artifacts (systemd unit, PAM config, logrotate config, symlink/binary).
//
// User data - the data directory (/var/lib/nadir: audit log, token store,
// sessions, logs) and the config file - is KEPT by default so an uninstall
// doesn't silently destroy the audit trail or force re-issuing tokens. Pass
// complete=true to delete everything, leaving no trace.
func uninstallService(complete bool) error {
	// Ignore errors: the service may already be stopped or never enabled.
	_ = runForeground("systemctl", "disable", "--now", serviceName)

	// Remove systemd unit file
	if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
		fmt.Printf("warning: failed to remove systemd unit file %s: %v\n", unitPath, err)
	} else if err == nil {
		fmt.Printf("removed %s\n", unitPath)
	}

	// Remove PAM configuration
	pamPath := "/etc/pam.d/nadir"
	if err := os.Remove(pamPath); err != nil && !os.IsNotExist(err) {
		fmt.Printf("warning: failed to remove PAM config %s: %v\n", pamPath, err)
	} else if err == nil {
		fmt.Printf("removed %s\n", pamPath)
	}

	// Remove logrotate configuration
	if err := os.Remove(logrotatePath); err != nil && !os.IsNotExist(err) {
		fmt.Printf("warning: failed to remove logrotate config %s: %v\n", logrotatePath, err)
	} else if err == nil {
		fmt.Printf("removed %s\n", logrotatePath)
	}

	// Remove binary or symlink
	if err := removeSymlink(); err != nil {
		fmt.Printf("warning: %v\n", err)
	}

	// /var/lib/nadir holds user data (audit log, token store, sessions, logs).
	// Keep it unless --complete: a routine uninstall must not destroy the audit
	// trail or force operators to re-mint every machine token.
	varLibPath := "/var/lib/nadir"
	if complete {
		if err := os.RemoveAll(varLibPath); err != nil && !os.IsNotExist(err) {
			fmt.Printf("warning: failed to remove data directory %s: %v\n", varLibPath, err)
		} else if err == nil {
			fmt.Printf("removed data directory %s\n", varLibPath)
		}
	} else if _, err := os.Stat(varLibPath); err == nil {
		fmt.Printf("kept data directory %s (audit log, tokens, sessions); remove with: nadir uninstall --complete\n", varLibPath)
	}

	// Determine configuration path to inform the user. Under sudo, resolveConfigPath
	// would hit root's HOME — check the invoking user's ~/.config first.
	cfgPath, err := resolveConfigPath()
	if err != nil {
		cfgPath = "/root/.config/nadir/config.yaml"
	}
	if os.Getenv("CONFIG_PATH") == "" {
		if u := os.Getenv("SUDO_USER"); u != "" && u != "root" {
			candidate := filepath.Join("/home", u, ".config", "nadir", "config.yaml")
			if _, statErr := os.Stat(candidate); statErr == nil {
				cfgPath = candidate
			}
		}
	}

	if complete {
		if err := os.Remove(cfgPath); err != nil && !os.IsNotExist(err) {
			fmt.Printf("warning: failed to remove config file %s: %v\n", cfgPath, err)
		} else if err == nil {
			fmt.Printf("removed config file %s\n", cfgPath)
		}
		fmt.Println("uninstall completed (--complete): all nadir data removed.")
	} else {
		fmt.Printf("uninstall completed. Config kept at: %s (remove everything with: nadir uninstall --complete)\n", cfgPath)
	}

	return runForeground("systemctl", "daemon-reload")
}

// systemctl proxies a lifecycle action (start/stop/restart/status/enable/
// disable) to the nadir unit, inheriting stdio so output reaches the terminal.
func systemctl(action string) error {
	return runForeground("systemctl", action, serviceName)
}

// logsCmd handles the logs subcommand: clear, or tail by default. Rotation is
// left to the installed logrotate config (see logrotateConfig), which runs daily
// for both service and `run -d` modes - no hand-rolled rotation here.
func logsCmd(args []string) error {
	if len(args) == 0 {
		return tailLogs()
	}
	if args[0] != "clear" {
		return fmt.Errorf("unknown logs subcommand %q. Usage: nadir logs [clear]", args[0])
	}
	return clearLogs()
}

// clearLogs truncates the background log file to 0 bytes if it exists.
func clearLogs() error {
	if _, err := os.Stat(detachLog); err == nil {
		if err := os.Truncate(detachLog, 0); err != nil {
			return fmt.Errorf("clear %s: %w", detachLog, err)
		}
		fmt.Printf("cleared log file %s\n", detachLog)
	} else if os.IsNotExist(err) {
		fmt.Printf("no log file found at %s\n", detachLog)
	} else {
		return err
	}
	return nil
}

// tailLogs follows nadir's logs from the unified log file, falling back to journald.
func tailLogs() error {
	if _, err := os.Stat(detachLog); err == nil {
		return runForeground("tail", "-n", "200", "-f", detachLog)
	}
	return runForeground("journalctl", "-u", serviceName, "-n", "200", "-f")
}

// daemonize re-execs this binary as a detached background `run`, redirecting its
// output to detachLog and starting a new session so it survives the terminal.
func daemonize() {
	exe, err := os.Executable()
	if err != nil {
		fatalIf(err)
	}
	if err := os.MkdirAll(filepath.Dir(detachLog), 0700); err != nil {
		fatalIf(err)
	}
	logf, err := os.OpenFile(detachLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0640)
	if err != nil {
		fatalIf(err)
	}
	defer logf.Close()

	cmd := exec.Command(exe, "run")
	cmd.Env = append(os.Environ(), daemonEnv+"=1")
	cmd.Stdout, cmd.Stderr = logf, logf
	cmd.Stdin = nil
	cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // detach from controlling terminal
	if err := cmd.Start(); err != nil {
		fatalIf(fmt.Errorf("detach: %w", err))
	}
	fmt.Printf("nadir running in background (pid %d); logs: %s\n", cmd.Process.Pid, detachLog)
	fmt.Printf("follow with: %s logs\n", filepath.Base(exe))
}

// runForeground runs a command with the terminal's stdio attached.
func runForeground(name string, args ...string) error {
	c := exec.Command(name, args...)
	c.Stdout, c.Stderr, c.Stdin = os.Stdout, os.Stderr, os.Stdin
	return c.Run()
}

func fatalIf(err error) {
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

// DefaultConfigContent returns the default configuration template filled with the username.
func DefaultConfigContent(username string) string {
	return fmt.Sprintf(configTemplateBase, "false", "# trust_proxy: false", "# tls_cert: /var/lib/nadir/tls/cert.pem", "# tls_key:  /var/lib/nadir/tls/key.pem", "127.0.0.1", 9999, username)
}

// saveDefaultConfig writes the default configuration template to cfgPath.
func saveDefaultConfig(cfgPath, content string) error {
	dir := filepath.Dir(cfgPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("create config directory: %w", err)
	}
	chownToSudoUser(dir)

	if err := os.WriteFile(cfgPath, []byte(content), 0600); err != nil {
		return fmt.Errorf("write default config: %w", err)
	}
	chownToSudoUser(cfgPath)
	fmt.Printf("created default configuration file at %s\n", cfgPath)
	return nil
}

func usage(w io.Writer) {
	fmt.Fprint(w, `nadir - Linux system administration backend

Usage:
  nadir [run] [-d] [-f <path>]                      Start the server (-d / --detach: run in background)
  nadir --save-config [-f <path>]                   Save default configuration to path and exit
  nadir install [--tls|--unsecure|--trust-proxy]    Install + enable the systemd service (starts on boot)
                                                    (--tls: enable HTTPS with self-signed certificate)
                                                    (--unsecure: serve HTTP directly, default)
                                                    (--trust-proxy: serve HTTP behind a reverse proxy)
  nadir uninstall [--complete]                      Remove the service (keeps data/config; --complete wipes all)
  nadir start|stop|restart|status                   Control the running service
  nadir enable|disable                              Toggle start-on-boot without removing the unit
  nadir logs [clear]                                Follow (default) or clear server logs
  nadir token add <name>                            Mint a machine credential (Bearer token), shown once
  nadir token rm <name>                             Revoke a token (effective immediately, no restart)
  nadir token ls                                    List token names and when they were created
  nadir update [--check|--force]                    Fetch the latest release from server.release_repo and restart
                                                    (--check: report only; --force: re-download when already current)
  nadir help                                        Show this help

Most commands need root. Config path is specified via -f/--config or CONFIG_PATH (default ~/.config/config.yaml).
`)
}
