package main

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// --------------------------------------------------------------------- //
// Хелперы
// --------------------------------------------------------------------- //

// runCmd — запускает команду, прокидывает stdout/stderr построчно в log.
// Возвращает ошибку с последними строками stderr для удобства отображения.
func runCmd(logFn func(string), name string, args ...string) error {
	logFn(fmt.Sprintf("$ %s %s", name, strings.Join(args, " ")))
	cmd := exec.Command(name, args...)
	out, err := cmd.CombinedOutput()
	for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") {
		if line != "" {
			logFn(line)
		}
	}
	if err != nil {
		return fmt.Errorf("%s: %w", name, err)
	}
	return nil
}

// writeFileIfChanged — пишет файл только если содержимое отличается. Возвращает
// true если файл был создан/изменён (для решения «нужен ли daemon-reload»).
func writeFileIfChanged(path string, content string, mode os.FileMode) (bool, error) {
	existing, err := os.ReadFile(path)
	if err == nil && string(existing) == content {
		return false, nil
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return false, err
	}
	if err := os.WriteFile(path, []byte(content), mode); err != nil {
		return false, err
	}
	return true, nil
}

// --------------------------------------------------------------------- //
// Шаги
// --------------------------------------------------------------------- //

func stepInstallDeps(s *State, log func(string)) (StepStatus, error) {
	log("Обновляю apt-кеш...")
	if err := runCmd(log, "apt-get", "update", "-qq"); err != nil {
		return StepFailed, err
	}

	deps := []string{
		"libgtk-3-0", "libpcsclite1", "libccid", "pcscd",
		"libcurl4", "libkrb5-3", "libgssapi-krb5-2",
		"libsasl2-modules", "libsasl2-modules-gssapi-mit",
		"execstack", "p7zip-full",
	}
	if hasAPTPackage("libldap-2.4-2") {
		deps = append(deps, "libldap-2.4-2")
	} else {
		deps = append(deps, "libldap-2.5-0")
		log("libldap-2.4-2 не найден → ставлю 2.5-0, для zpki будет --force-depends")
	}

	args := append([]string{"install", "-y", "--no-install-recommends"}, deps...)
	if err := runCmd(log, "apt-get", args...); err != nil {
		return StepFailed, err
	}
	return StepDone, nil
}

func stepInstallValidataDebs(s *State, log func(string)) (StepStatus, error) {
	zpki, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zpki-*.amd64.deb"))
	zsdk, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zsdk-*.amd64.deb"))
	if len(zpki) == 0 {
		return StepFailed, fmt.Errorf("zpki-*.amd64.deb не найден в %s/ClientL_Other/", s.artifactsDir)
	}
	useForce := !hasAPTPackage("libldap-2.4-2")
	for _, deb := range append(zpki, zsdk...) {
		args := []string{"-i", deb}
		if useForce {
			args = append([]string{"--force-depends"}, args...)
		}
		if err := runCmd(log, "dpkg", args...); err != nil {
			return StepFailed, err
		}
	}
	if _, err := os.Stat("/opt/Validata/VDCSP/lib/amd64"); err != nil {
		return StepFailed, fmt.Errorf("/opt/Validata не появился после установки")
	}
	return StepDone, nil
}

func stepExecstack(s *State, log func(string)) (StepStatus, error) {
	target := "/opt/Validata/VDCSP/lib/amd64/libvdcsp.so"
	// Проверка состояния
	out, err := exec.Command("execstack", "-q", target).Output()
	if err == nil && strings.HasPrefix(strings.TrimSpace(string(out)), "-") {
		log("executable-stack уже снят")
		return StepSkipped, nil
	}
	return StepDone, runCmd(log, "execstack", "-c", target)
}

func stepCreateBJUser(s *State, log func(string)) (StepStatus, error) {
	if _, err := exec.LookPath("id"); err == nil {
		if exec.Command("id", "bj").Run() == nil {
			log("Пользователь bj уже существует")
		} else {
			if err := runCmd(log, "useradd", "--system", "--create-home",
				"--home-dir", "/var/lib/bj", "--shell", "/bin/bash", "bj"); err != nil {
				return StepFailed, err
			}
		}
	}
	dirs := []struct {
		Path string
		Mode os.FileMode
	}{
		{"/var/lib/bj/usb", 0o755},
		{"/var/lib/bj/.Validata", 0o700},
		{"/var/lib/bj/.Validata/vdkeys", 0o700},
		{"/var/lib/bj/profiles", 0o755},
		{"/var/log/bj", 0o755},
		{"/var/lib/bj/.bj", 0o700},
	}
	for _, d := range dirs {
		if err := os.MkdirAll(d.Path, d.Mode); err != nil {
			return StepFailed, err
		}
	}
	return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj", "/var/log/bj")
}

func stepPcscdDropin(s *State, log func(string)) (StepStatus, error) {
	const dropin = `[Unit]
Requires=
After=
Sockets=

[Service]
ExecStart=
ExecStart=/usr/sbin/pcscd --foreground
`
	changed, err := writeFileIfChanged("/etc/systemd/system/pcscd.service.d/no-autoexit.conf", dropin, 0o644)
	if err != nil {
		return StepFailed, err
	}
	if !changed {
		log("Drop-in уже актуален")
		return StepSkipped, nil
	}
	log("Создан /etc/systemd/system/pcscd.service.d/no-autoexit.conf")
	return StepDone, nil
}

func stepBJCryptoDropins(s *State, log func(string)) (StepStatus, error) {
	files := map[string]string{
		"/etc/systemd/system/bj-crypto.service.d/validata-paths.conf": `[Service]
WorkingDirectory=/opt/Validata/VDCSP/etc
ReadWritePaths=/opt/Validata/VDCSP/etc
ReadWritePaths=/var/lib/bj
`,
		"/etc/systemd/system/bj-crypto.service.d/usb-access.conf": `[Service]
ReadOnlyPaths=/media
ReadOnlyPaths=/var/lib/bj/usb
`,
		"/etc/systemd/system/bj-crypto.service.d/share-crysvc.conf": `[Service]
PrivateTmp=true
BindPaths=/tmp/.crysvc.sock:/tmp/.crysvc.sock
`,
	}
	for path, content := range files {
		if _, err := writeFileIfChanged(path, content, 0o644); err != nil {
			return StepFailed, err
		}
	}
	return StepDone, nil
}

func stepBJServerDropin(s *State, log func(string)) (StepStatus, error) {
	const dropin = `[Service]
ReadWritePaths=/opt/Validata/VDCSP/etc
`
	_, err := writeFileIfChanged("/etc/systemd/system/bj-server.service.d/pki1conf.conf", dropin, 0o644)
	if err != nil {
		return StepFailed, err
	}
	return StepDone, nil
}

func stepSPKIIni(s *State, log func(string)) (StepStatus, error) {
	const path = "/opt/Validata/VDCSP/etc/spki.ini"
	if _, err := os.Stat(path); err == nil {
		log("Файл уже существует")
		return StepSkipped, nil
	}
	const content = `[store]
count = 0

[Parameters]
PkiLdapTimeout = 10
PkiHttpTimeout = 60
`
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		return StepFailed, err
	}
	return StepDone, nil
}

func stepPKI1Prep(s *State, log func(string)) (StepStatus, error) {
	const path = "/opt/Validata/VDCSP/etc/pki1.conf"
	if _, err := os.Stat(path); err != nil {
		log("Файл pki1.conf отсутствует — Валидата создаст при первом запуске")
		return StepSkipped, nil
	}
	if err := runCmd(log, "chgrp", "bj", path); err != nil {
		return StepFailed, err
	}
	if err := runCmd(log, "chmod", "g+w", path); err != nil {
		return StepFailed, err
	}

	existing, _ := os.ReadFile(path)
	if !strings.Contains(string(existing), "# --- bj-server: BEGIN ---") {
		appended := string(existing) + "\n# --- bj-server: BEGIN ---\n# Секции профилей дописываются bj-server при импорте через /admin/setup.\n# --- bj-server: END ---\n"
		if err := os.WriteFile(path, []byte(appended), 0o664); err != nil {
			return StepFailed, err
		}
	}
	return StepDone, nil
}

func stepUSBMount(s *State, log func(string)) (StepStatus, error) {
	files := map[string]string{
		"/etc/udev/rules.d/99-bj-usb.rules": `# Авто-mount USB-флешек в /var/lib/bj/usb/<UUID> с владельцем bj.
ACTION=="add", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \
    ENV{ID_FS_TYPE}!="", \
    ENV{SYSTEMD_WANTS}="bj-usb-mount@$env{ID_FS_UUID}.service"
ACTION=="remove", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \
    ENV{ID_FS_TYPE}!="", \
    ENV{SYSTEMD_WANTS}="bj-usb-umount@$env{ID_FS_UUID}.service"
`,
		"/etc/systemd/system/bj-usb-mount@.service": `[Unit]
Description=Mount USB %i to /var/lib/bj/usb/%i for bj
DefaultDependencies=no
After=local-fs.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/bash -c 'mkdir -p /var/lib/bj/usb/%i && /usr/bin/mount -o uid=$(id -u bj),gid=$(id -g bj),fmask=0133,dmask=0022 UUID=%i /var/lib/bj/usb/%i'
ExecStop=/usr/bin/umount /var/lib/bj/usb/%i || true
`,
		"/etc/systemd/system/bj-usb-umount@.service": `[Unit]
Description=Umount USB %i from /var/lib/bj/usb/%i
DefaultDependencies=no

[Service]
Type=oneshot
ExecStart=/usr/bin/bash -c '/usr/bin/umount /var/lib/bj/usb/%i 2>/dev/null; /usr/bin/rmdir /var/lib/bj/usb/%i 2>/dev/null; true'
`,
	}
	anyChanged := false
	for path, content := range files {
		ch, err := writeFileIfChanged(path, content, 0o644)
		if err != nil {
			return StepFailed, err
		}
		anyChanged = anyChanged || ch
	}
	if anyChanged {
		_ = runCmd(log, "udevadm", "control", "--reload-rules")
		_ = runCmd(log, "udevadm", "trigger")
	}
	return StepDone, nil
}

func stepInstallBJServer(s *State, log func(string)) (StepStatus, error) {
	src := filepath.Join(s.artifactsDir, "bj-server")
	if _, err := os.Stat(src); err != nil {
		return StepSkipped, nil // нет бинаря — может ставится через rpm/deb
	}
	if err := os.MkdirAll("/opt/bj", 0o755); err != nil {
		return StepFailed, err
	}
	if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0755", src, "/opt/bj/bj-server"); err != nil {
		return StepFailed, err
	}
	return StepDone, nil
}

func stepInstallCryptoJar(s *State, log func(string)) (StepStatus, error) {
	src := filepath.Join(s.artifactsDir, "crypto-service.jar")
	if _, err := os.Stat(src); err != nil {
		return StepSkipped, nil
	}
	if err := os.MkdirAll("/opt/bj", 0o755); err != nil {
		return StepFailed, err
	}
	if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0644", src, "/opt/bj/crypto-service.jar"); err != nil {
		return StepFailed, err
	}
	return StepDone, nil
}

func stepSystemdUnits(s *State, log func(string)) (StepStatus, error) {
	units := map[string]string{
		"/etc/systemd/system/bj-crypto.service": `[Unit]
Description=Bridge-and-Join-s — Crypto sidecar (Java + Валидата Клиент L)
Before=bj-server.service
After=network-online.target pcscd.service
Wants=network-online.target

[Service]
Type=simple
User=bj
Group=bj
RuntimeDirectory=bj
RuntimeDirectoryMode=0750
Environment=BJ_CRYPTO_SOCKET=/run/bj/crypto.sock
Environment=BJ_CRYPTO_PROVIDER=validata
Environment=LD_LIBRARY_PATH=/opt/Validata/VDCSP/lib/amd64
ExecStart=/usr/bin/java -Djava.library.path=/opt/Validata/VDCSP/lib/amd64 -jar /opt/bj/crypto-service.jar
Restart=on-failure
RestartSec=5
StandardOutput=append:/var/log/bj/crypto-service.log
StandardError=append:/var/log/bj/crypto-service.err
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/run/bj /var/log/bj
PrivateTmp=true

[Install]
WantedBy=multi-user.target
`,
		"/etc/systemd/system/bj-server.service": `[Unit]
Description=Bridge-and-Join-s — единый сервис M2M-переводов
After=network-online.target bj-crypto.service
Wants=network-online.target

[Service]
Type=simple
User=bj
Group=bj
WorkingDirectory=/var/lib/bj
ExecStart=/opt/bj/bj-server
Restart=on-failure
RestartSec=5
Environment=BJ_HTTP_ADDR=:8080
Environment=BJ_SETUP_PATH=/var/lib/bj/.bj/setup.json
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/bj /var/log/bj

[Install]
WantedBy=multi-user.target
`,
	}
	for path, content := range units {
		if _, err := writeFileIfChanged(path, content, 0o644); err != nil {
			return StepFailed, err
		}
	}
	return StepDone, runCmd(log, "systemctl", "daemon-reload")
}

func stepInstallISH(s *State, log func(string)) (StepStatus, error) {
	matches, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ish", "igate_*.deb"))
	if len(matches) == 0 {
		log("Дистрибутив ИШ не найден — пропускаю (можно установить позже)")
		return StepSkipped, nil
	}
	if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil {
		// допустим, что зависимости подтянутся
		_ = runCmd(log, "apt-get", "-f", "install", "-y")
		if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil {
			return StepFailed, err
		}
	}
	return StepDone, nil
}

func stepSaveConfig(s *State, log func(string)) (StepStatus, error) {
	cfg := s.Snapshot().Config
	if cfg.OrgINN == "" && cfg.AdminEmail == "" && cfg.LicenseKey == "" {
		return StepSkipped, nil
	}
	b, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return StepFailed, err
	}
	if err := os.MkdirAll("/var/lib/bj/.bj", 0o700); err != nil {
		return StepFailed, err
	}
	if err := os.WriteFile("/var/lib/bj/.bj/setup.json", b, 0o600); err != nil {
		return StepFailed, err
	}
	return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj/.bj")
}

func stepStartServices(s *State, log func(string)) (StepStatus, error) {
	// disable+stop socket activation для pcscd
	_ = runCmd(log, "systemctl", "stop", "pcscd.socket")
	_ = runCmd(log, "systemctl", "disable", "pcscd.socket")

	for _, svc := range []string{"pcscd", "bj-crypto", "bj-server"} {
		if err := runCmd(log, "systemctl", "enable", svc); err != nil {
			return StepFailed, err
		}
		if err := runCmd(log, "systemctl", "restart", svc); err != nil {
			return StepFailed, err
		}
	}
	return StepDone, nil
}

func stepHealthCheck(s *State, log func(string)) (StepStatus, error) {
	var bad []string
	for _, svc := range []string{"pcscd", "vdcrysvc", "bj-crypto", "bj-server"} {
		if err := exec.Command("systemctl", "is-active", "--quiet", svc).Run(); err != nil {
			bad = append(bad, svc)
		} else {
			log(svc + ": active")
		}
	}
	if len(bad) > 0 {
		return StepFailed, fmt.Errorf("сервисы не запустились: %s", strings.Join(bad, ", "))
	}
	return StepDone, nil
}

// hasAPTPackage — проверяет наличие пакета в apt-cache (доступен ли для установки).
func hasAPTPackage(name string) bool {
	out, err := exec.Command("apt-cache", "show", name).CombinedOutput()
	if err != nil {
		return false
	}
	return strings.Contains(string(out), "Package: "+name)
}
