package plan

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"metis/pkg/config"
	"metis/pkg/inject"
	"metis/pkg/inventory"
	"metis/pkg/secrets"
)

// maybeInject writes node-specific config into mounted boot/root paths if the env
// vars METIS_BOOT_PATH or METIS_ROOT_PATH are set. When unset, injection is skipped.
func maybeInject(inv *inventory.Inventory, nodeName string) error {
	boot := os.Getenv("METIS_BOOT_PATH")
	root := os.Getenv("METIS_ROOT_PATH")
	if boot == "" && root == "" {
		return nil
	}
	files, err := Files(inv, nodeName)
	if err != nil {
		return err
	}
	filtered := make([]inject.FileSpec, 0, len(files))
	for _, f := range files {
		if f.RootFS && root == "" {
			continue
		}
		if !f.RootFS && boot == "" {
			continue
		}
		filtered = append(filtered, f)
	}
	if len(filtered) == 0 {
		return nil
	}
	inj := inject.Injector{BootPath: boot, RootPath: root}
	return inj.Write(filtered)
}

// Files resolves the full set of node-specific files, including overlays.
func Files(inv *inventory.Inventory, nodeName string) ([]inject.FileSpec, error) {
	node, class, err := inv.FindNode(nodeName)
	if err != nil {
		return nil, err
	}
	cfg, err := config.Build(inv, nodeName)
	if err != nil {
		return nil, err
	}
	sec := fetchSecrets(node.Hostname)
	if sec != nil {
		if sec.K3sToken != "" {
			cfg.K3s.Token = sec.K3sToken
		}
		if len(sec.Extra) > 0 {
			cfg.Secrets = sec.Extra
		}
	}
	applyNodeMetadataEnv(cfg)
	files, err := buildFiles(cfg, sec)
	if err != nil {
		return nil, err
	}
	overlayFiles, err := collectOverlays(class)
	if err != nil {
		return nil, err
	}
	files = append(files, overlayFiles...)
	_ = node // reserved for future per-node overlays
	return files, nil
}

// Inject writes node-specific config into caller-supplied boot/root mountpoints.
func Inject(inv *inventory.Inventory, nodeName, boot, root string) error {
	oldBoot := os.Getenv("METIS_BOOT_PATH")
	oldRoot := os.Getenv("METIS_ROOT_PATH")
	defer func() {
		if oldBoot == "" {
			_ = os.Unsetenv("METIS_BOOT_PATH")
		} else {
			_ = os.Setenv("METIS_BOOT_PATH", oldBoot)
		}
		if oldRoot == "" {
			_ = os.Unsetenv("METIS_ROOT_PATH")
		} else {
			_ = os.Setenv("METIS_ROOT_PATH", oldRoot)
		}
	}()
	if boot == "" {
		_ = os.Unsetenv("METIS_BOOT_PATH")
	} else {
		_ = os.Setenv("METIS_BOOT_PATH", boot)
	}
	if root == "" {
		_ = os.Unsetenv("METIS_ROOT_PATH")
	} else {
		_ = os.Setenv("METIS_ROOT_PATH", root)
	}
	return maybeInject(inv, nodeName)
}

func buildFiles(cfg *config.NodeConfig, sec *secrets.NodeSecrets) ([]inject.FileSpec, error) {
	files := []inject.FileSpec{
		{Path: "etc/hostname", Content: []byte(cfg.Hostname + "\n"), Mode: 0o644, RootFS: true},
		{Path: "etc/hosts", Content: []byte(hostsContent(cfg.Hostname)), Mode: 0o644, RootFS: true},
		{Path: "etc/rancher/k3s/config.yaml", Content: []byte(k3sConfigContent(cfg)), Mode: 0o644, RootFS: true},
		{Path: "etc/metis/firstboot.env", Content: []byte(firstbootEnvContent(cfg, sec)), Mode: 0o600, RootFS: true},
		{Path: "usr/local/sbin/metis-apply-node-identity.sh", Content: []byte(nodeIdentityScriptContent()), Mode: 0o755, RootFS: true},
		{Path: "etc/cloud/cloud.cfg.d/90-metis-recovery.cfg", Content: []byte(cloudInitRootFSContent(sec)), Mode: 0o644, RootFS: true},
	}
	if cfg.IP != "" {
		files = append(files, inject.FileSpec{
			Path:    "etc/NetworkManager/system-connections/end0-static.nmconnection",
			Content: []byte(networkManagerConnectionContent("end0-static", "end0", cfg.IP)),
			Mode:    0o600,
			RootFS:  true,
		})
		files = append(files, inject.FileSpec{
			Path:    "etc/NetworkManager/system-connections/eth0-static.nmconnection",
			Content: []byte(networkManagerConnectionContent("eth0-static", "eth0", cfg.IP)),
			Mode:    0o600,
			RootFS:  true,
		})
		files = append(files, inject.FileSpec{
			Path:    "etc/systemd/network/10-end0-static.network",
			Content: []byte(systemdNetworkContent(cfg.IP)),
			Mode:    0o644,
			RootFS:  true,
		})
	}
	if len(cfg.SSHKeys) > 0 && cfg.SSHUser != "" {
		auth := strings.Join(cfg.SSHKeys, "\n") + "\n"
		files = append(files, inject.FileSpec{
			Path:    fmt.Sprintf("home/%s/.ssh/authorized_keys", cfg.SSHUser),
			Content: []byte(auth),
			Mode:    0o600,
			RootFS:  true,
		})
		files = append(files, inject.FileSpec{
			Path:    "etc/metis/authorized_keys",
			Content: []byte(auth),
			Mode:    0o600,
			RootFS:  true,
		})
	}
	if passwordAuth := sshPasswordConfigContent(sec); passwordAuth != "" {
		files = append(files, inject.FileSpec{
			Path:    "etc/ssh/sshd_config.d/90-metis-password-auth.conf",
			Content: []byte(passwordAuth),
			Mode:    0o644,
			RootFS:  true,
		})
	}
	if cfg.SSHUser == "atlas" {
		sudoers := hecateSudoersContent(cfg.SSHUser)
		files = append(files, inject.FileSpec{
			Path:    "etc/sudoers.d/90-hecate-atlas",
			Content: []byte(sudoers),
			Mode:    0o440,
			RootFS:  true,
		})
		files = append(files, inject.FileSpec{
			Path:    "etc/metis/sudoers-hecate",
			Content: []byte(sudoers),
			Mode:    0o440,
			RootFS:  true,
		})
	}
	if len(cfg.Fstab) > 0 {
		files = append(files, inject.FileSpec{
			Path:    "etc/metis/fstab.append",
			Content: []byte(fstabAppendContent(cfg)),
			Mode:    0o644,
			RootFS:  true,
		})
	}

	raw, err := jsonMarshalIndent(cfg)
	if err != nil {
		return nil, err
	}
	files = append(files, inject.FileSpec{
		Path:    "etc/metis/node.json",
		Content: raw,
		Mode:    0o644,
		RootFS:  true,
	})
	if sec != nil {
		secRaw, err := jsonMarshalIndent(redactedSecretsForImage(sec))
		if err != nil {
			return nil, err
		}
		files = append(files, inject.FileSpec{
			Path:    "etc/metis/secrets.json",
			Content: secRaw,
			Mode:    0o600,
			RootFS:  true,
		})
	}

	userData := cloudInitUserData(cfg, sec)
	if userData != "" {
		files = append(files, inject.FileSpec{
			Path:    "user-data",
			Content: []byte(userData),
			Mode:    0o644,
			RootFS:  false,
		})
		files = append(files, inject.FileSpec{
			Path:    "meta-data",
			Content: []byte(fmt.Sprintf("instance-id: %s\nlocal-hostname: %s\n", cfg.Hostname, cfg.Hostname)),
			Mode:    0o644,
			RootFS:  false,
		})
	}
	return files, nil
}

func hostsContent(hostname string) string {
	return fmt.Sprintf("127.0.0.1\tlocalhost\n127.0.1.1\t%s\n\n# Injected by metis\n", hostname)
}

func k3sConfigContent(cfg *config.NodeConfig) string {
	var labelList []string
	for k, v := range cfg.Labels {
		if !allowK3sNodeLabel(cfg.K3s.Role, k) {
			continue
		}
		labelList = append(labelList, fmt.Sprintf("%s=%s", k, v))
	}
	sort.Strings(labelList)
	taints := append([]string{}, cfg.Taints...)
	sort.Strings(taints)

	var b bytes.Buffer
	if cfg.K3s.Role != "agent" {
		b.WriteString("write-kubeconfig-mode: \"0644\"\n")
	}
	if cfg.K3s.URL != "" {
		b.WriteString(fmt.Sprintf("server: %s\n", cfg.K3s.URL))
	}
	if cfg.K3s.Token != "" {
		b.WriteString(fmt.Sprintf("token: %s\n", cfg.K3s.Token))
	}
	b.WriteString(fmt.Sprintf("node-name: %s\n", cfg.Hostname))
	if cfg.IP != "" {
		b.WriteString(fmt.Sprintf("node-ip: %s\n", cfg.IP))
	}
	if len(labelList) > 0 {
		b.WriteString("node-label:\n")
		for _, l := range labelList {
			b.WriteString(fmt.Sprintf("  - %s\n", l))
		}
	}
	if len(taints) > 0 {
		b.WriteString("node-taint:\n")
		for _, t := range taints {
			b.WriteString(fmt.Sprintf("  - %s\n", t))
		}
	}
	return b.String()
}

func allowK3sNodeLabel(role, key string) bool {
	if role != "agent" {
		return true
	}
	return !strings.HasPrefix(key, "node-role.kubernetes.io/")
}

func networkManagerConnectionContent(id, iface, ip string) string {
	gateway := ip
	if lastDot := strings.LastIndex(gateway, "."); lastDot >= 0 {
		gateway = gateway[:lastDot+1] + "1"
	}
	return fmt.Sprintf(`[connection]
id=%s
type=ethernet
interface-name=%s
autoconnect=true
autoconnect-priority=100

[ethernet]

[ipv4]
method=manual
address1=%s/24,%s
dns=%s;
dns-search=titan;
may-fail=false

[ipv6]
method=ignore

[proxy]
`, id, iface, ip, gateway, gateway)
}

func systemdNetworkContent(ip string) string {
	gateway := ip
	if lastDot := strings.LastIndex(gateway, "."); lastDot >= 0 {
		gateway = gateway[:lastDot+1] + "1"
	}
	return fmt.Sprintf(`[Match]
Name=end0 eth0

[Network]
Address=%s/24
Gateway=%s
DNS=%s
Domains=titan
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
`, ip, gateway, gateway)
}

func fstabAppendContent(cfg *config.NodeConfig) string {
	var lines []string
	for _, entry := range cfg.Fstab {
		source := entry.Source
		switch {
		case source != "":
		case entry.UUID != "":
			source = "UUID=" + entry.UUID
		case entry.Label != "":
			source = "LABEL=" + entry.Label
		default:
			source = "none"
		}
		lines = append(lines, fmt.Sprintf(
			"%s %s %s %s 0 0",
			source,
			entry.Mountpoint,
			entry.FS,
			entry.Options,
		))
	}
	sort.Strings(lines)
	return strings.Join(lines, "\n") + "\n"
}

func hecateSudoersContent(user string) string {
	return fmt.Sprintf(
		"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl, /usr/sbin/poweroff, /sbin/poweroff, /usr/local/bin/hecate, /usr/local/bin/k3s, /usr/bin/k3s\n",
		user,
	)
}

func collectOverlays(class *inventory.NodeClass) ([]inject.FileSpec, error) {
	var files []inject.FileSpec
	if class == nil {
		return files, nil
	}
	if class.BootOverlay != "" {
		more, err := overlayFiles(class.BootOverlay, false)
		if err != nil {
			return nil, err
		}
		files = append(files, more...)
	}
	if class.RootOverlay != "" {
		more, err := overlayFiles(class.RootOverlay, true)
		if err != nil {
			return nil, err
		}
		files = append(files, more...)
	}
	return files, nil
}

func overlayFiles(dir string, rootfs bool) ([]inject.FileSpec, error) {
	var specs []inject.FileSpec
	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		rel, err := filepath.Rel(dir, path)
		if err != nil {
			return err
		}
		content, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		specs = append(specs, inject.FileSpec{
			Path:    rel,
			Content: content,
			Mode:    info.Mode(),
			RootFS:  rootfs,
		})
		return nil
	})
	return specs, err
}
