package hostsetup

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

type AppSpec struct {
	Name           string
	Description    string
	DefaultConfig  string
	DefaultDataDir string
	DefaultUnit    string
	ConfigTemplate string
}

type SetupOptions struct {
	BinaryPath      string
	ConfigPath      string
	DataDir         string
	UnitPath        string
	User            string
	Group           string
	Enable          bool
	Start           bool
	Restart         bool
	DryRun          bool
	OverwriteConfig bool

	Runner CommandRunner
	Getuid func() int
}

type CommandRunner interface {
	Run(ctx context.Context, name string, args ...string) ([]byte, error)
}

type execRunner struct{}

func (execRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
	return exec.CommandContext(ctx, name, args...).CombinedOutput()
}

const defaultUser = "iop"

func Run(ctx context.Context, spec AppSpec, opts SetupOptions, out io.Writer) error {
	if out == nil {
		out = io.Discard
	}
	if err := applyDefaults(spec, &opts); err != nil {
		return err
	}
	if err := validateDataDir(opts.DataDir); err != nil {
		return err
	}

	plan := buildPlan(spec, opts)
	fmt.Fprintln(out, plan)

	unit := renderUnit(spec, opts)
	if opts.DryRun {
		fmt.Fprintln(out, "--- unit preview ---")
		fmt.Fprintln(out, unit)
		fmt.Fprintln(out, "--- config preview ---")
		fmt.Fprintln(out, spec.ConfigTemplate)
		return nil
	}

	if needsRoot(opts) && opts.Getuid() != 0 {
		return fmt.Errorf("hostsetup: root required to write %s and %s", opts.UnitPath, opts.ConfigPath)
	}

	if err := ensureUserGroup(ctx, opts, out); err != nil {
		return err
	}
	if err := ensureDirs(opts); err != nil {
		return err
	}
	if err := ensureConfig(spec, opts, out); err != nil {
		return err
	}
	if err := writeUnit(opts, unit); err != nil {
		return err
	}
	if err := chownServicePaths(ctx, opts); err != nil {
		return err
	}

	if _, err := opts.Runner.Run(ctx, "systemctl", "daemon-reload"); err != nil {
		return fmt.Errorf("systemctl daemon-reload: %w", err)
	}
	if opts.Enable {
		if _, err := opts.Runner.Run(ctx, "systemctl", "enable", spec.Name); err != nil {
			return fmt.Errorf("systemctl enable: %w", err)
		}
	}
	if opts.Restart {
		if _, err := opts.Runner.Run(ctx, "systemctl", "restart", spec.Name); err != nil {
			return fmt.Errorf("systemctl restart: %w", err)
		}
	} else if opts.Start {
		if _, err := opts.Runner.Run(ctx, "systemctl", "start", spec.Name); err != nil {
			return fmt.Errorf("systemctl start: %w", err)
		}
	}
	return nil
}

func applyDefaults(spec AppSpec, opts *SetupOptions) error {
	if spec.Name == "" {
		return errors.New("hostsetup: spec.Name required")
	}
	if opts.BinaryPath == "" {
		bin, err := os.Executable()
		if err != nil {
			return fmt.Errorf("resolve binary: %w", err)
		}
		opts.BinaryPath = bin
	}
	if opts.ConfigPath == "" {
		opts.ConfigPath = spec.DefaultConfig
	}
	if opts.DataDir == "" {
		opts.DataDir = spec.DefaultDataDir
	}
	if opts.UnitPath == "" {
		opts.UnitPath = spec.DefaultUnit
	}
	if opts.User == "" {
		opts.User = defaultUser
	}
	if opts.Group == "" {
		opts.Group = opts.User
	}
	if opts.Runner == nil {
		opts.Runner = execRunner{}
	}
	if opts.Getuid == nil {
		opts.Getuid = os.Geteuid
	}
	return nil
}

func needsRoot(opts SetupOptions) bool {
	return isSystemPath(opts.UnitPath) || isSystemPath(opts.ConfigPath) || isSystemPath(opts.DataDir)
}

func isSystemPath(p string) bool {
	return strings.HasPrefix(p, "/etc/") || strings.HasPrefix(p, "/var/lib/") || strings.HasPrefix(p, "/usr/")
}

func buildPlan(spec AppSpec, opts SetupOptions) string {
	var b strings.Builder
	fmt.Fprintf(&b, "hostsetup plan for %s\n", spec.Name)
	fmt.Fprintf(&b, "  binary : %s\n", opts.BinaryPath)
	fmt.Fprintf(&b, "  config : %s\n", opts.ConfigPath)
	fmt.Fprintf(&b, "  data   : %s\n", opts.DataDir)
	fmt.Fprintf(&b, "  unit   : %s\n", opts.UnitPath)
	fmt.Fprintf(&b, "  user   : %s:%s\n", opts.User, opts.Group)
	fmt.Fprintf(&b, "  enable=%t start=%t restart=%t dry-run=%t overwrite-config=%t",
		opts.Enable, opts.Start, opts.Restart, opts.DryRun, opts.OverwriteConfig)
	return b.String()
}

func ensureUserGroup(ctx context.Context, opts SetupOptions, out io.Writer) error {
	if _, err := opts.Runner.Run(ctx, "getent", "group", opts.Group); err != nil {
		if _, err := opts.Runner.Run(ctx, "groupadd", "--system", opts.Group); err != nil {
			return fmt.Errorf("groupadd %s: %w", opts.Group, err)
		}
		fmt.Fprintf(out, "created group %s\n", opts.Group)
	}
	if _, err := opts.Runner.Run(ctx, "id", "-u", opts.User); err != nil {
		if _, err := opts.Runner.Run(ctx, "useradd",
			"--system", "--gid", opts.Group,
			"--home-dir", opts.DataDir, "--shell", "/usr/sbin/nologin",
			opts.User); err != nil {
			return fmt.Errorf("useradd %s: %w", opts.User, err)
		}
		fmt.Fprintf(out, "created user %s\n", opts.User)
	}
	return nil
}

var unsafeDataDirs = map[string]bool{
	"/":          true,
	"/etc":       true,
	"/home":      true,
	"/opt":       true,
	"/root":      true,
	"/srv":       true,
	"/tmp":       true,
	"/usr":       true,
	"/usr/lib":   true,
	"/usr/local": true,
	"/var":       true,
	"/var/lib":   true,
	"/var/log":   true,
	"/var/run":   true,
}

func ensureTraversable(path string) error {
	info, err := os.Stat(path)
	if err != nil {
		return fmt.Errorf("stat %s: %w", path, err)
	}
	cur := info.Mode().Perm()
	wanted := cur | 0o011 // g+x, o+x — minimum to traverse for service user
	if wanted == cur {
		return nil
	}
	if err := os.Chmod(path, wanted); err != nil {
		return fmt.Errorf("chmod %s: %w", path, err)
	}
	return nil
}

func validateDataDir(p string) error {
	if p == "" {
		return nil
	}
	cleaned := filepath.Clean(p)
	if unsafeDataDirs[cleaned] {
		return fmt.Errorf("hostsetup: data dir %q is not an app-specific path; refuse to recursively chown", cleaned)
	}
	return nil
}

func ensureDirs(opts SetupOptions) error {
	if dir := filepath.Dir(opts.ConfigPath); dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("mkdir %s: %w", dir, err)
		}
	}
	if opts.DataDir != "" {
		if parent := filepath.Dir(opts.DataDir); parent != "" && parent != "." && parent != "/" {
			if err := os.MkdirAll(parent, 0o755); err != nil {
				return fmt.Errorf("mkdir %s: %w", parent, err)
			}
			if err := ensureTraversable(parent); err != nil {
				return err
			}
		}
		if err := os.MkdirAll(opts.DataDir, 0o750); err != nil {
			return fmt.Errorf("mkdir %s: %w", opts.DataDir, err)
		}
	}
	if dir := filepath.Dir(opts.UnitPath); dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("mkdir %s: %w", dir, err)
		}
	}
	return nil
}

func ensureConfig(spec AppSpec, opts SetupOptions, out io.Writer) error {
	_, err := os.Stat(opts.ConfigPath)
	switch {
	case err == nil && !opts.OverwriteConfig:
		fmt.Fprintf(out, "config %s already exists, skipping\n", opts.ConfigPath)
		return nil
	case err == nil && opts.OverwriteConfig:
		fmt.Fprintf(out, "overwriting config %s\n", opts.ConfigPath)
	case errors.Is(err, os.ErrNotExist):
		fmt.Fprintf(out, "writing config %s\n", opts.ConfigPath)
	default:
		return fmt.Errorf("stat config: %w", err)
	}
	return os.WriteFile(opts.ConfigPath, []byte(spec.ConfigTemplate), 0o600)
}

func writeUnit(opts SetupOptions, unit string) error {
	return os.WriteFile(opts.UnitPath, []byte(unit), 0o644)
}

func chownServicePaths(ctx context.Context, opts SetupOptions) error {
	owner := opts.User + ":" + opts.Group
	if opts.DataDir != "" {
		if _, err := opts.Runner.Run(ctx, "chown", "-R", owner, opts.DataDir); err != nil {
			return fmt.Errorf("chown %s %s: %w", owner, opts.DataDir, err)
		}
	}
	if opts.ConfigPath != "" {
		if _, err := opts.Runner.Run(ctx, "chown", owner, opts.ConfigPath); err != nil {
			return fmt.Errorf("chown %s %s: %w", owner, opts.ConfigPath, err)
		}
	}
	return nil
}
