package lkgateway

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli"
)

// urlQ — экранирование строки для query-параметра flash.
func urlQ(s string) string { return url.QueryEscape(s) }

// Пошаговый мастер установки ключа Валидаты на съёмный носитель (USB keymedia
// ИШ). Реализует то, что просил пользователь: загрузил архив + пароль →
// распаковка → запись на флешку → формирование справочника сертификатов →
// проверка Валидаты → «Готово» → можно слать тестовый документ.
//
// Привилегированные операции с флешкой (бэкап, remount rw, запись, перезапуск
// VDCrySvc) делает помощник /usr/local/sbin/bj-keymedia-install через узкий
// sudoers (bj-server работает под непривилегированным bj).


// keyWizardStep — один шаг мастера.
type keyWizardStep struct {
	Title  string
	Status string // pending | active | ok | error
	Detail string
}

// keyWizardState — состояние одного прогона мастера (в памяти, один активный).
type keyWizardState struct {
	mu        sync.Mutex
	StagingID string          // id распаковки в /var/lib/bj/media/iso/<id>
	VDK       string          // имя файла ключа
	Profile   string          // имя установленного профиля на носителе
	Backup    string          // путь бэкапа
	Steps     []keyWizardStep // 1..5
	Done      bool            // все шаги пройдены
	Flash     string
}

func newKeyWizardState() *keyWizardState {
	return &keyWizardState{Steps: defaultKeySteps()}
}

// reset обнуляет поля прогона, НЕ трогая мьютекс (вызывать под Lock).
func (s *keyWizardState) reset() {
	s.StagingID = ""
	s.VDK = ""
	s.Profile = ""
	s.Backup = ""
	s.Steps = defaultKeySteps()
	s.Done = false
	s.Flash = ""
}

func defaultKeySteps() []keyWizardStep {
	return []keyWizardStep{
		{Title: "Загрузка архива и распаковка", Status: "pending"},
		{Title: "Запись ключа на выбранную флешку (с бэкапом)", Status: "pending"},
		{Title: "Формирование справочника сертификатов (CRL)", Status: "pending"},
		{Title: "Перезапуск и проверка ИШ", Status: "pending"},
		{Title: "Готово — можно отправлять тестовый документ", Status: "pending"},
	}
}

func (s *keyWizardState) set(i int, status, detail string) {
	if i >= 0 && i < len(s.Steps) {
		s.Steps[i].Status = status
		if detail != "" {
			s.Steps[i].Detail = detail
		}
	}
}

// flashDrive — съёмный носитель (USB), обнаруженный в системе.
type flashDrive struct {
	Device     string // /dev/sdb1
	Size       string // 1,9G
	Label      string
	FSType     string
	Mountpoint string // пусто если не смонтирован
	Model      string // USB2FlashStorage
	IsKeymedia bool   // смонтирован как текущий ключевой носитель ИШ
}

// keyWizardData — данные шаблона admin_keywizard.html.
type keyWizardData struct {
	page
	State  *keyWizardState
	Drives []flashDrive
}

const keymediaMount = "/var/lib/igate/keymedia"

// listFlashDrives перечисляет съёмные (removable/hotplug) USB-носители с ФС —
// чтобы пользователь выбрал, на какую флешку писать ключ.
func listFlashDrives() []flashDrive {
	out, err := exec.Command("lsblk", "-J", "-b", "-o",
		"NAME,SIZE,LABEL,MOUNTPOINT,RM,HOTPLUG,TYPE,MODEL,FSTYPE,PATH").Output()
	if err != nil {
		return nil
	}
	var parsed struct {
		Blockdevices []json.RawMessage `json:"blockdevices"`
	}
	if json.Unmarshal(out, &parsed) != nil {
		return nil
	}
	var drives []flashDrive
	var walk func(raw []byte, parentRemovable bool, parentModel string)
	walk = func(raw []byte, parentRemovable bool, parentModel string) {
		var d struct {
			Name       string            `json:"name"`
			Size       int64             `json:"size"`
			Label      string            `json:"label"`
			Mountpoint string            `json:"mountpoint"`
			RM         bool              `json:"rm"`
			Hotplug    bool              `json:"hotplug"`
			Type       string            `json:"type"`
			Model      string            `json:"model"`
			FSType     string            `json:"fstype"`
			Path       string            `json:"path"`
			Children   []json.RawMessage `json:"children"`
		}
		if json.Unmarshal(raw, &d) != nil {
			return
		}
		removable := d.RM || d.Hotplug || parentRemovable
		model := strings.TrimSpace(d.Model)
		if model == "" {
			model = parentModel
		}
		// Носитель с ФС — кандидат на запись.
		if removable && d.Type == "part" && d.FSType != "" {
			drives = append(drives, flashDrive{
				Device:     d.Path,
				Size:       humanSize(d.Size),
				Label:      d.Label,
				FSType:     d.FSType,
				Mountpoint: d.Mountpoint,
				Model:      model,
				IsKeymedia: d.Mountpoint == keymediaMount,
			})
		}
		for _, c := range d.Children {
			walk(c, removable, model)
		}
	}
	for _, b := range parsed.Blockdevices {
		walk(b, false, "")
	}
	return drives
}

func humanSize(b int64) string {
	switch {
	case b >= 1<<30:
		return fmt.Sprintf("%.1f ГБ", float64(b)/(1<<30))
	case b >= 1<<20:
		return fmt.Sprintf("%.0f МБ", float64(b)/(1<<20))
	default:
		return fmt.Sprintf("%d Б", b)
	}
}

// registerKeyWizard вешает маршруты мастера установки ключа.
func (h *setupHandlers) registerKeyWizard(mux *http.ServeMux) {
	if h.keyWiz == nil {
		h.keyWiz = newKeyWizardState()
	}
	mux.HandleFunc("/admin/setup/keywizard", h.renderKeyWizard)
	mux.HandleFunc("/admin/setup/keywizard/upload", h.keyWizardUpload)
	mux.HandleFunc("/admin/setup/keywizard/install", h.keyWizardInstall)
	mux.HandleFunc("/admin/setup/keywizard/reset", h.keyWizardReset)
}

func (h *setupHandlers) renderKeyWizard(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	h.keyWiz.mu.Lock()
	h.keyWiz.Flash = r.URL.Query().Get("flash")
	st := h.keyWiz
	h.keyWiz.mu.Unlock()
	// Список флешек нужен на шаге выбора носителя (когда архив уже загружен).
	var drives []flashDrive
	if st.StagingID != "" && !st.Done {
		drives = listFlashDrives()
	}
	render(w, h.tpl.a.keyWizard, keyWizardData{page: nowPage("Установка ключа", "setup"), State: st, Drives: drives})
}

func (h *setupHandlers) keyWizardReset(w http.ResponseWriter, r *http.Request) {
	h.keyWiz.mu.Lock()
	h.keyWiz.reset()
	h.keyWiz.mu.Unlock()
	http.Redirect(w, r, "/admin/setup/keywizard", http.StatusSeeOther)
}

// keyWizardUpload — шаг 1: приём .7z + пароль, распаковка, инспекция.
func (h *setupHandlers) keyWizardUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	if err := r.ParseMultipartForm(500 << 20); err != nil {
		h.keyWizFlash(w, r, "Ошибка чтения формы: "+err.Error())
		return
	}
	file, header, err := r.FormFile("archive")
	if err != nil {
		h.keyWizFlash(w, r, "Выберите файл архива (.7z/.zip)")
		return
	}
	defer file.Close()
	lower := strings.ToLower(header.Filename)
	if !(strings.HasSuffix(lower, ".7z") || strings.HasSuffix(lower, ".zip")) {
		h.keyWizFlash(w, r, "Архив должен быть .7z или .zip")
		return
	}
	password := r.FormValue("password")

	isoDir := "/var/lib/bj/iso"
	if err := os.MkdirAll(isoDir, 0o755); err != nil {
		h.keyWizFlash(w, r, "Не удалось создать "+isoDir+": "+err.Error())
		return
	}
	dst := filepath.Join(isoDir, time.Now().UTC().Format("20060102-150405-")+filepath.Base(header.Filename))
	out, err := os.Create(dst)
	if err != nil {
		h.keyWizFlash(w, r, "Запись архива: "+err.Error())
		return
	}
	if _, err := io.Copy(out, file); err != nil {
		out.Close()
		_ = os.Remove(dst)
		h.keyWizFlash(w, r, "Запись архива: "+err.Error())
		return
	}
	out.Close()

	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
	defer cancel()
	m, err := ExtractISO(ctx, dst, password)
	if err != nil {
		h.keyWizFlash(w, r, "Распаковка не удалась (проверьте пароль): "+err.Error())
		return
	}

	h.keyWiz.mu.Lock()
	h.keyWiz.reset()
	h.keyWiz.StagingID = m.ID
	// Инспекция через помощник (читает staging, находит .vdk/gdbm/pse).
	staging := filepath.Join("/var/lib/bj/media/iso", m.ID)
	insp, ierr := runKeymediaHelper(ctx, "inspect", staging, "", "", "")
	if ierr == nil && insp["ok"] == true {
		if v, ok := insp["vdk"].(string); ok {
			h.keyWiz.VDK = v
		}
	}
	detail := fmt.Sprintf("Ключ: %s · справочник сертификатов: %s",
		fallback(h.keyWiz.VDK, "—"), yesNo(insp["has_gdbm"]))
	h.keyWiz.set(0, "ok", detail)
	h.keyWiz.set(1, "active", "")
	h.keyWiz.mu.Unlock()

	http.Redirect(w, r, "/admin/setup/keywizard", http.StatusSeeOther)
}

// keyWizardInstall — шаги 2-5: запись на флешку, справочник, проверка, готово.
func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	h.keyWiz.mu.Lock()
	id := h.keyWiz.StagingID
	h.keyWiz.mu.Unlock()
	if id == "" {
		h.keyWizFlash(w, r, "Сначала загрузите архив (шаг 1)")
		return
	}
	staging := filepath.Join("/var/lib/bj/media/iso", id)

	// Выбор флешки и имя профиля из формы.
	profileName := strings.TrimSpace(r.FormValue("profile_name"))
	targetDev := strings.TrimSpace(r.FormValue("target_device"))
	targetMnt := ""
	if targetDev != "" {
		for _, d := range listFlashDrives() {
			if d.Device == targetDev {
				targetMnt = d.Mountpoint
				break
			}
		}
	}

	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
	defer cancel()

	// Шаг 2-3: запись ключа на флешку + справочник + CRL (привилегированный воркер).
	res, err := runKeymediaHelper(ctx, "install", staging, profileName, targetDev, targetMnt)
	h.keyWiz.mu.Lock()
	if err != nil || res["ok"] != true {
		msg := errStr(err)
		if e, ok := res["error"].(string); ok {
			msg = e
		}
		h.keyWiz.set(1, "error", "Запись на флешку не удалась: "+msg)
		h.keyWiz.mu.Unlock()
		h.keyWizFlash(w, r, "Установка прервана: "+msg)
		return
	}
	if p, ok := res["profile"].(string); ok {
		h.keyWiz.Profile = p
	}
	if b, ok := res["backup"].(string); ok {
		h.keyWiz.Backup = b
	}
	tgt, _ := res["target"].(string)
	spr, _ := res["spr"].(string)
	h.keyWiz.set(1, "ok", fmt.Sprintf("Профиль «%s» записан на %s. Бэкап: %s", h.keyWiz.Profile, fallback(tgt, "носитель"), h.keyWiz.Backup))
	crl, _ := res["crl"].(string)
	h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник «%s» сформирован. CRL: %s", fallback(spr, "—"), crlRu(crl)))
	h.keyWiz.set(3, "active", "")
	h.keyWiz.mu.Unlock()

	// Шаг 4: перезапуск и проверка ИШ. Перезапуск VDCrySvc мог сбросить
	// активацию серверного профиля bj-crypto — восстанавливаем. Затем
	// перезапускаем ИШ и проверяем engine/state: ИШ поднялся с новым ключом.
	h.reactivateCryptoProfile(ctx)
	ishOK, ishMsg := h.restartAndVerifyISH(ctx)
	h.keyWiz.mu.Lock()
	if !ishOK {
		h.keyWiz.set(3, "error", "ИШ не подтвердил готовность: "+ishMsg)
		h.keyWiz.mu.Unlock()
		h.keyWizFlash(w, r, "Ключ записан, но ИШ не готов: "+ishMsg)
		return
	}
	h.keyWiz.set(3, "ok", "ИШ перезапущен и работает: "+ishMsg)
	h.keyWiz.set(4, "ok", "Теперь подпишите новым ключом — отправьте тестовый документ роботу НРД ниже")
	h.keyWiz.Done = true
	h.keyWiz.mu.Unlock()

	http.Redirect(w, r, "/admin/setup/keywizard?flash="+urlQ("Готово! Ключ на флешке, справочник сформирован, ИШ перезапущен и работает. Финальная проверка — тестовым документом роботу."), http.StatusSeeOther)
}

// restartAndVerifyISH перезапускает Интеграционный шлюз и проверяет, что он
// поднялся (engine/state). Возвращает (ok, сообщение).
func (h *setupHandlers) restartAndVerifyISH(ctx context.Context) (bool, string) {
	// Перезапуск igate через привилегированный воркер (bj не sudoer).
	res, err := runKeymediaHelper(ctx, "restart-ish", "/var/lib/bj/media/iso", "", "", "")
	if err != nil || res["ok"] != true {
		// restart-ish может быть не поддержан — не критично, проверим состояние.
		_ = err
	}
	// Проверяем состояние ИШ через nsd-адаптер (engine/state).
	deadline := time.Now().Add(40 * time.Second)
	for time.Now().Before(deadline) {
		if st := h.ishEngineState(ctx); st != "" {
			return true, "engine "+st
		}
		select {
		case <-ctx.Done():
			return false, "таймаут"
		case <-time.After(3 * time.Second):
		}
	}
	return false, "ИШ не ответил на /api/admin/engine/state за 40 сек"
}

// ishEngineState запрашивает состояние движка ИШ; пусто если недоступен.
func (h *setupHandlers) ishEngineState(ctx context.Context) string {
	s := h.rc.Snapshot()
	base := s.NSD.IGWBaseURL
	if base == "" {
		return ""
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/api/admin/engine/state", nil)
	if err != nil {
		return ""
	}
	cl := &http.Client{Timeout: 5 * time.Second}
	resp, err := cl.Do(req)
	if err != nil {
		return ""
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return ""
	}
	b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	return strings.TrimSpace(string(b))
}

func crlRu(s string) string {
	switch s {
	case "updated":
		return "обновлены из точек распространения"
	case "failed":
		return "не удалось обновить (проверьте сеть/CDP)"
	case "skip":
		return "пропущено"
	default:
		return s
	}
}

// reactivateCryptoProfile повторно активирует текущий серверный профиль
// bj-crypto (после перезапуска VDCrySvc активация в сайдкаре сбрасывается).
// Best-effort: возвращает true при успехе.
func (h *setupHandlers) reactivateCryptoProfile(ctx context.Context) bool {
	s := h.rc.Snapshot()
	if s.Crypto.Profile == "" {
		return false
	}
	cli := cryptocli.New(cryptocli.Config{
		Provider:   cryptocli.Provider(s.Crypto.Provider),
		SocketPath: s.Crypto.SocketPath,
	})
	defer cli.Close()
	res, err := cli.Activate(ctx, s.Crypto.Profile)
	return err == nil && res.OK
}

// vdcrysvcActive проверяет, что демон Валидаты (vdmkdev) запущен.
func vdcrysvcActive() bool {
	out, _ := exec.Command("systemctl", "is-active", "vdmkdev.service").Output()
	return strings.TrimSpace(string(out)) == "active"
}

func boolRu(b bool, yes, no string) string {
	if b {
		return yes
	}
	return no
}

const keymediaReqDir = "/var/lib/bj/keymedia-requests"

// runKeymediaHelper передаёт запрос привилегированному воркеру через файловый
// обмен: bj-server (в песочнице) пишет <id>.req, root-сервис bj-keymedia
// (host namespace, триггерится bj-keymedia.path) выполняет операцию с флешкой
// и пишет <id>.res. bj-server опрашивает результат. Так привилегированная
// работа идёт вне mount-namespace песочницы, где доступно перемонтирование USB.
func runKeymediaHelper(ctx context.Context, action, staging, profile, targetDev, targetMnt string) (map[string]any, error) {
	id := fmt.Sprintf("%s-%d", action, time.Now().UnixNano())
	reqPath := filepath.Join(keymediaReqDir, id+".req")
	resPath := filepath.Join(keymediaReqDir, id+".res")
	defer os.Remove(resPath)

	reqBody, _ := json.Marshal(map[string]string{
		"action": action, "staging": staging, "profile": profile,
		"target_dev": targetDev, "target_mnt": targetMnt,
	})
	// Пишем атомарно (tmp → rename), чтобы .path не подхватил полупустой файл.
	tmp := reqPath + ".tmp"
	if err := os.WriteFile(tmp, reqBody, 0o660); err != nil {
		return nil, fmt.Errorf("запись запроса: %w", err)
	}
	if err := os.Rename(tmp, reqPath); err != nil {
		return nil, fmt.Errorf("публикация запроса: %w", err)
	}

	// Опрос результата.
	ticker := time.NewTicker(200 * time.Millisecond)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return nil, fmt.Errorf("таймаут ожидания воркера установки ключа")
		case <-ticker.C:
			b, err := os.ReadFile(resPath)
			if err != nil {
				continue // ещё не готово
			}
			res := map[string]any{}
			if jerr := json.Unmarshal(b, &res); jerr != nil {
				return nil, fmt.Errorf("разбор ответа воркера: %v (%s)", jerr, strings.TrimSpace(string(b)))
			}
			if res["ok"] != true {
				msg, _ := res["error"].(string)
				return res, fmt.Errorf("воркер: %s", fallback(msg, "ошибка"))
			}
			return res, nil
		}
	}
}

func (h *setupHandlers) keyWizFlash(w http.ResponseWriter, r *http.Request, msg string) {
	http.Redirect(w, r, "/admin/setup/keywizard?flash="+urlQ(msg), http.StatusSeeOther)
}

func fallback(s, def string) string {
	if s == "" {
		return def
	}
	return s
}

func yesNo(v any) string {
	if b, ok := v.(bool); ok && b {
		return "да"
	}
	return "нет"
}

func errStr(err error) string {
	if err == nil {
		return "неизвестная ошибка"
	}
	return err.Error()
}
