//go:build windows

// Package gui implements the Walk-based graphical interface for xetup.
//
// Three sequential phases, each with its own window:
//  1. Config form  – PC name, product key, profile, step checkboxes,
//                    load/save config buttons for per-client presets
//  2. Live run     – real-time log streamed from PowerShell scripts
//  3. Summary      – per-step OK / ERROR / SKIPPED with elapsed time
//
// Resume mode (state file present): skips the config form and runs only
// the pending steps recorded in the state file. On completion, disables
// autologon and removes the X9-Resume scheduled task.
package gui

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"

	"github.com/lxn/walk"
	. "github.com/lxn/walk/declarative"

	"git.xetup.x9.cz/x9/xetup/internal/config"
	"git.xetup.x9.cz/x9/xetup/internal/preflight"
	"git.xetup.x9.cz/x9/xetup/internal/prereboot"
	"git.xetup.x9.cz/x9/xetup/internal/report"
	"git.xetup.x9.cz/x9/xetup/internal/runner"
	"git.xetup.x9.cz/x9/xetup/internal/runreport"
	"git.xetup.x9.cz/x9/xetup/internal/state"
)

// deployLogPath is the Deploy.log location, captured at Run() so the summary
// phase can attach it to the email and to the Forgejo run report.
var deployLogPath string

// Run opens the xetup window and blocks until the user closes it.
func Run(cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
	deployLogPath = runCfg.LogFile

	// Resume mode: state file present from a previous interrupted run
	if st, err := state.Load(); err == nil {
		resumePhase(st, runCfg)
		return
	}

	// Normal flow: config form → run → summary
	for {
		res := formPhase(cfg, runCfg, cfgPath)
		switch res.action {
		case "quit":
			return
		case "reload":
			cfg = res.cfg
			cfgPath = res.cfgPath
		case "run":
			runCfg.ProfileType = res.cfg.Deployment.ProfileType
			// Update runtime config so scripts see the user's GUI selections
			_ = config.Save(res.cfg, runCfg.ConfigPath)
			results, needsReboot := runPhase(runCfg, res.steps, false)
			if needsReboot {
				prepareRebootAndRestart(res.cfg, res.steps, results, cfgPath, runCfg)
				return
			}
			donePhase(results, nil)
			return
		}
	}
}

// --------------------------------------------------------------------------
// Resume mode
// --------------------------------------------------------------------------

// maxStepRetries is the maximum number of reboot cycles a single step is
// allowed before it is marked as ERROR and skipped.
const maxStepRetries = 5

func resumePhase(st *state.State, runCfg runner.RunConfig) {
	runCfg.LogFile = st.LogFile

	// Initialise retry counts (backward-compat with older state files)
	if st.RetryCounts == nil {
		st.RetryCounts = make(map[string]int)
	}

	steps := runner.StepsByIDs(st.PendingSteps)
	// StepsByIDs returns Enabled=false; resume steps must be enabled.
	for i := range steps {
		steps[i].Enabled = true
	}

	// Check retry limits — skip steps that have been retried too many times
	for i := range steps {
		if st.RetryCounts[steps[i].ID] >= maxStepRetries {
			steps[i].Enabled = false
			st.Results = append(st.Results, state.StepResult{
				StepID: steps[i].ID,
				Num:    steps[i].Num,
				Name:   steps[i].Name,
				Status: "ERROR",
			})
		}
	}

	results, needsReboot := runPhase(runCfg, steps, true)

	// Accumulate completed results (NeedsReboot step excluded – runs again)
	newResults := append(st.Results, toStateResults(results)...) //nolint:gocritic

	if needsReboot {
		// Increment retry counts for steps that requested reboot
		for _, r := range results {
			if r.NeedsReboot {
				st.RetryCounts[r.Step.ID]++
			}
		}

		// Update state and reboot – infrastructure already in place (running as adminx9)
		pending := pendingStepIDs(steps, results)
		newSt := &state.State{
			Config:       st.Config,
			ConfigPath:   st.ConfigPath,
			LogFile:      st.LogFile,
			PendingSteps: pending,
			Results:      newResults,
			RetryCounts:  st.RetryCounts,
		}
		if err := state.Save(newSt); err != nil {
			walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
			return // do NOT reboot if state was not persisted
		}
		reboot()
		return
	}

	// All steps complete
	prereboot.Cleanup()
	_ = state.Delete()
	donePhase(results, st.Results)
}

// --------------------------------------------------------------------------
// Phase 1 – Config form
// --------------------------------------------------------------------------

type formResult struct {
	action  string // "run", "reload", "quit"
	cfg     config.Config
	cfgPath string
	steps   []runner.Step
}

func formPhase(cfg config.Config, runCfg runner.RunConfig, cfgPath string) formResult {
	var mw *walk.MainWindow
	result := formResult{action: "quit", cfgPath: cfgPath}

	var (
		pcNameLE     *walk.LineEdit
		pcDescLE     *walk.LineEdit
		productKeyLE *walk.LineEdit
		profileCB    *walk.ComboBox
	)

	items := runner.AllSelectableItems()
	checkPtrs := make([]*walk.CheckBox, len(items))
	checkWidgets := make([]Widget, len(items))
	for i, item := range items {
		i := i // capture for closure
		checkWidgets[i] = CheckBox{
			AssignTo: &checkPtrs[i],
			Text:     item.Label,
			Checked:  itemEnabled(cfg, item),
		}
	}

	collectCfg := func() config.Config {
		out := cfg
		out.Deployment.PCName = pcNameLE.Text()
		out.Deployment.PCDescription = pcDescLE.Text()
		out.Activation.ProductKey = productKeyLE.Text()
		profiles := []string{"default", "admin", "user"}
		if idx := profileCB.CurrentIndex(); idx >= 0 && idx < len(profiles) {
			out.Deployment.ProfileType = profiles[idx]
		}
		selected := make(map[string]bool, len(items))
		for i, item := range items {
			selected[item.Key] = checkPtrs[i].Checked()
		}
		_, features := buildStepsAndFeatures(selected)
		out.Features = features
		return out
	}

	// Run pre-flight checks and build status widgets
	pfResults := preflight.RunAll()
	var pfWidgets []Widget
	allOK := true
	for _, r := range pfResults {
		color := walk.RGB(0, 140, 0)
		prefix := "\u2713 "
		if !r.OK {
			color = walk.RGB(200, 40, 40)
			prefix = "\u2717 "
			allOK = false
		}
		text := prefix + r.Name + ": " + r.Detail
		lbl := Label{Text: text, Font: Font{PointSize: 9}}
		_ = color // applied after window creation
		pfWidgets = append(pfWidgets, lbl)
	}
	_ = allOK

	// Collect label pointers to set color after creation
	pfLabels := make([]*walk.Label, len(pfResults))
	for i := range pfWidgets {
		pfWidgets[i] = Label{
			AssignTo: &pfLabels[i],
			Text:     pfWidgets[i].(Label).Text,
			Font:     Font{PointSize: 9},
		}
	}

	if err := (MainWindow{
		AssignTo: &mw,
		Title:    "xetup \u2014 Windows deployment",
		Size:     Size{Width: 760, Height: 740},
		Layout:   VBox{},
		Children: []Widget{
			// ── Pre-flight checks ────────────────────────────────────────────
			Composite{
				Layout:   VBox{MarginsZero: true},
				Children: pfWidgets,
			},
			HSeparator{},
			// ── Form fields ──────────────────────────────────────────────────
			Composite{
				Layout: Grid{Columns: 2},
				Children: []Widget{
					Label{Text: "PC jmeno:"},
					LineEdit{
						AssignTo:  &pcNameLE,
						Text:      cfg.Deployment.PCName,
						CueBanner: "napr. NB-KLIENT-01  (prazdne = neprejmenovat)",
					},
					Label{Text: "Popis PC:"},
					LineEdit{
						AssignTo:  &pcDescLE,
						Text:      cfg.Deployment.PCDescription,
						CueBanner: "napr. PC recepce",
					},
					Label{Text: "Product Key:"},
					LineEdit{
						AssignTo:  &productKeyLE,
						Text:      cfg.Activation.ProductKey,
						CueBanner: "prazdne = OA3 / GVLK fallback",
					},
					Label{Text: "Profil:"},
					ComboBox{
						AssignTo:     &profileCB,
						Model:        []string{"default", "admin", "user"},
						CurrentIndex: profileIndex(cfg.Deployment.ProfileType),
					},
				},
			},
			HSeparator{},
			// ── Step checkboxes ──────────────────────────────────────────────
			Label{Text: "Kroky a nastaveni (odskrtnete co nechcete spustit):"},
			ScrollView{
				MinSize:  Size{Height: 400},
				Layout:   VBox{MarginsZero: true},
				Children: checkWidgets,
			},
			HSeparator{},
			// ── Buttons ──────────────────────────────────────────────────────
			Composite{
				Layout: HBox{MarginsZero: true},
				Children: []Widget{
					PushButton{
						Text: "Nacist config...",
						OnClicked: func() {
							dlg := new(walk.FileDialog)
							dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
							dlg.Title = "Nacist konfiguraci"
							ok, err := dlg.ShowOpen(mw)
							if err != nil || !ok {
								return
							}
							data, err := os.ReadFile(dlg.FilePath)
							if err != nil {
								walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
								return
							}
							newCfg := config.DefaultConfig()
							if err := json.Unmarshal(data, &newCfg); err != nil {
								walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
								return
							}
							result = formResult{
								action:  "reload",
								cfg:     newCfg,
								cfgPath: dlg.FilePath,
							}
							mw.Close()
						},
					},
					PushButton{
						Text: "Ulozit config...",
						OnClicked: func() {
							dlg := new(walk.FileDialog)
							dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
							dlg.Title = "Ulozit konfiguraci"
							dlg.FilePath = "config.json"
							ok, err := dlg.ShowSave(mw)
							if err != nil || !ok {
								return
							}
							data, err := json.MarshalIndent(collectCfg(), "", "  ")
							if err != nil {
								walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
								return
							}
							if err := os.WriteFile(dlg.FilePath, data, 0644); err != nil {
								walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
							}
						},
					},
					HSpacer{},
					PushButton{
						Text: "  SPUSTIT  ",
						OnClicked: func() {
							finalCfg := collectCfg()
							selected := make(map[string]bool, len(items))
							for i, item := range items {
								selected[item.Key] = checkPtrs[i].Checked()
							}
							steps, features := buildStepsAndFeatures(selected)
							finalCfg.Features = features
							_ = config.Save(finalCfg, cfgPath)
							result = formResult{
								action:  "run",
								cfg:     finalCfg,
								cfgPath: cfgPath,
								steps:   steps,
							}
							mw.Close()
						},
					},
				},
			},
		},
	}.Create()); err != nil {
		walk.MsgBox(nil, "Chyba", fmt.Sprintf("Nelze vytvorit okno: %v", err), walk.MsgBoxIconError)
		return result
	}

	// Apply colors to pre-flight labels (must be done after window creation)
	for i, r := range pfResults {
		if pfLabels[i] != nil {
			if r.OK {
				pfLabels[i].SetTextColor(walk.RGB(0, 140, 0))
			} else {
				pfLabels[i].SetTextColor(walk.RGB(200, 40, 40))
			}
		}
	}

	mw.Run()
	return result
}

// --------------------------------------------------------------------------
// Phase 2 – Live run view
// --------------------------------------------------------------------------

// stepIndicator tracks a step's label and current status.
type stepIndicator struct {
	label  *walk.Label
	stepID string
	status string // "pending", "running", "ok", "error", "skipped", "cancelled"
}

// runPhase runs steps and streams live log output.
// resuming=true changes the title and initial status message.
// Returns (results, needsReboot).
func runPhase(runCfg runner.RunConfig, steps []runner.Step, resuming bool) ([]runner.Result, bool) {
	var (
		mw        *walk.MainWindow
		statusLbl *walk.Label
		logTE     *walk.TextEdit
		cancelFn  context.CancelFunc
	)

	// Build step indicator labels (one per step, in order)
	indicators := make([]stepIndicator, len(steps))
	stepIndex := make(map[string]int, len(steps))
	indWidgets := make([]Widget, len(steps))
	for i, s := range steps {
		indicators[i] = stepIndicator{stepID: s.ID, status: "pending"}
		stepIndex[s.ID] = i
		indWidgets[i] = Label{
			AssignTo: &indicators[i].label,
			Text:     s.Num + " \u00b7",
			Font:     Font{Family: "Consolas", PointSize: 9},
			MinSize:  Size{Width: 38},
		}
	}

	title := "xetup \u2014 probiha instalace"
	initialStatus := "Spoustim..."
	if resuming {
		title = "xetup \u2014 pokracuji po restartu"
		initialStatus = "Pokracuji po restartu..."
	}

	if err := (MainWindow{
		AssignTo: &mw,
		Title:    title,
		Size:     Size{Width: 760, Height: 740},
		Layout:   VBox{},
		Children: []Widget{
			Label{AssignTo: &statusLbl, Text: initialStatus},
			// Step progress strip
			Composite{
				Layout:   HBox{MarginsZero: true},
				Children: indWidgets,
			},
			HSeparator{},
			TextEdit{
				AssignTo: &logTE,
				ReadOnly: true,
				VScroll:  true,
				Font:     Font{Family: "Consolas", PointSize: 9},
				MinSize:  Size{Height: 530},
			},
			HSeparator{},
			Composite{
				Layout: HBox{MarginsZero: true},
				Children: []Widget{
					HSpacer{},
					PushButton{
						Text: "  ZASTAVIT  ",
						OnClicked: func() {
							if cancelFn != nil {
								cancelFn()
							}
						},
					},
				},
			},
		},
	}.Create()); err != nil {
		return nil, false
	}

	// updateIndicator refreshes a step label's text and color — call inside Synchronize.
	updateIndicator := func(idx int) {
		ind := &indicators[idx]
		s := steps[idx]
		switch ind.status {
		case "running":
			ind.label.SetText(s.Num + " \u25ba") // ►
			ind.label.SetTextColor(walk.RGB(30, 144, 255))
		case "ok":
			ind.label.SetText(s.Num + " \u2713") // ✓
			ind.label.SetTextColor(walk.RGB(0, 180, 0))
		case "error":
			ind.label.SetText(s.Num + " \u2717") // ✗
			ind.label.SetTextColor(walk.RGB(220, 50, 50))
		case "skipped", "cancelled":
			ind.label.SetText(s.Num + " \u2013") // –
			ind.label.SetTextColor(walk.RGB(140, 140, 140))
		default:
			ind.label.SetText(s.Num + " \u00b7") // ·
			ind.label.SetTextColor(walk.RGB(180, 180, 180))
		}
	}

	var (
		mu      sync.Mutex
		results []runner.Result
		done    = make(chan struct{})
	)

	ctx, cancel := context.WithCancel(context.Background())
	cancelFn = cancel

	mw.Closing().Attach(func(_ *bool, _ walk.CloseReason) {
		cancelFn()
	})

	r := runner.New(
		runCfg,
		func(l runner.LogLine) {
			mw.Synchronize(func() {
				logTE.AppendText(l.Text + "\r\n")
			})
		},
		func(step runner.Step) {
			mw.Synchronize(func() {
				statusLbl.SetText(fmt.Sprintf(
					"Krok %s \u2013 %s...", step.Num, step.Name,
				))
				if idx, ok := stepIndex[step.ID]; ok {
					indicators[idx].status = "running"
					updateIndicator(idx)
				}
			})
		},
		func(res runner.Result) {
			mw.Synchronize(func() {
				statusLbl.SetText(fmt.Sprintf(
					"Krok %s \u2013 %s: %s", res.Step.Num, res.Step.Name, res.Status,
				))
				if idx, ok := stepIndex[res.Step.ID]; ok {
					indicators[idx].status = strings.ToLower(res.Status)
					updateIndicator(idx)
				}
			})
		},
	)

	go func() {
		defer close(done)
		res := r.Run(ctx, steps)
		mu.Lock()
		results = res
		mu.Unlock()
		mw.Synchronize(func() {
			mw.Close()
		})
	}()

	mw.Run()
	cancel()
	<-done

	mu.Lock()
	defer mu.Unlock()

	needsReboot := len(results) > 0 && results[len(results)-1].NeedsReboot
	return results, needsReboot
}

// --------------------------------------------------------------------------
// Pre-reboot helper
// --------------------------------------------------------------------------

// prepareRebootAndRestart saves state, sets up resume infrastructure, and reboots.
func prepareRebootAndRestart(
	cfg config.Config,
	allSteps []runner.Step,
	completedResults []runner.Result,
	cfgPath string,
	runCfg runner.RunConfig,
) {
	pending := pendingStepIDs(allSteps, completedResults)
	st := &state.State{
		Config:       cfg,
		ConfigPath:   cfgPath,
		LogFile:      runCfg.LogFile,
		PendingSteps: pending,
		Results:      toStateResults(completedResults),
	}
	if err := state.Save(st); err != nil {
		walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
		return
	}
	if err := prereboot.Prepare(); err != nil {
		walk.MsgBox(nil, "Chyba", "Prereboot setup selhal: "+err.Error(), walk.MsgBoxIconError)
		return
	}
	reboot()
}

// pendingStepIDs returns IDs of steps that still need to run.
// Steps with NeedsReboot=true (re-run after reboot) and steps not yet
// reached (absent from completedResults) are both included.
func pendingStepIDs(allSteps []runner.Step, completedResults []runner.Result) []string {
	done := make(map[string]bool, len(completedResults))
	for _, r := range completedResults {
		if !r.NeedsReboot {
			done[r.Step.ID] = true
		}
	}
	var pending []string
	for _, s := range allSteps {
		if !done[s.ID] {
			pending = append(pending, s.ID)
		}
	}
	return pending
}

// toStateResults converts runner results to state.StepResult for persistence.
// Steps with NeedsReboot=true are excluded (they will run again next round).
func toStateResults(results []runner.Result) []state.StepResult {
	var sr []state.StepResult
	for _, r := range results {
		if r.NeedsReboot {
			continue
		}
		sr = append(sr, state.StepResult{
			StepID:  r.Step.ID,
			Num:     r.Step.Num,
			Name:    r.Step.Name,
			Status:  r.Status,
			Elapsed: r.Elapsed,
		})
	}
	return sr
}

// --------------------------------------------------------------------------
// Phase 3 – Summary with auto-reboot countdown
// --------------------------------------------------------------------------

const rebootCountdown = 60 // seconds before automatic reboot

// donePhase shows the final summary screen.
// currentResults are from the last run; prevResults accumulate earlier rounds.
func donePhase(currentResults []runner.Result, prevResults []state.StepResult) {
	var (
		mw           *walk.MainWindow
		countdownLbl *walk.Label
	)

	// Combined ordered list: previous rounds first, then current round
	type displayRow struct {
		Num     string
		Name    string
		Status  string
		Elapsed time.Duration
	}

	var rows []displayRow
	var emailRows []report.StepResult
	for _, r := range prevResults {
		rows = append(rows, displayRow{r.Num, r.Name, r.Status, r.Elapsed})
		emailRows = append(emailRows, report.StepResult{Num: r.Num, Name: r.Name, Status: r.Status, Elapsed: r.Elapsed})
	}
	for _, r := range currentResults {
		if r.NeedsReboot {
			continue
		}
		rows = append(rows, displayRow{r.Step.Num, r.Step.Name, r.Status, r.Elapsed})
		emailRows = append(emailRows, report.StepResult{Num: r.Step.Num, Name: r.Step.Name, Status: r.Status, Elapsed: r.Elapsed})
	}

	ok, errs, skipped := 0, 0, 0
	widgets := make([]Widget, 0, len(rows))
	for _, row := range rows {
		var icon string
		switch row.Status {
		case "OK":
			ok++
			icon = "OK    "
		case "ERROR":
			errs++
			icon = "ERR   "
		default:
			skipped++
			icon = "\u2013     "
		}
		text := icon + row.Num + " \u2013 " + row.Name
		if row.Elapsed > 0 {
			text += fmt.Sprintf("  (%s)", row.Elapsed.Round(time.Second))
		}
		widgets = append(widgets, Label{
			Text: text,
			Font: Font{Family: "Consolas", PointSize: 9},
		})
	}

	summaryText := fmt.Sprintf("OK: %d   CHYBY: %d   PRESKOCENO: %d", ok, errs, skipped)

	// Report the run (non-blocking). On failure, runreport files a Forgejo
	// issue and returns its URL; the email then links to it and attaches the
	// zipped log. Both steps retry internally and never block completion.
	go func() {
		issueURL := runreport.Report(emailRows, deployLogPath)
		_ = report.Send(emailRows, issueURL, deployLogPath)
	}()

	cancelReboot := make(chan struct{})

	if err := (MainWindow{
		AssignTo: &mw,
		Title:    "xetup \u2014 hotovo",
		Size:     Size{Width: 760, Height: 740},
		Layout:   VBox{},
		Children: []Widget{
			Label{
				Text:      "Hotovo",
				Alignment: AlignHCenterVNear,
				Font:      Font{Bold: true, PointSize: 12},
			},
			HSeparator{},
			ScrollView{
				MinSize:  Size{Height: 510},
				Layout:   VBox{MarginsZero: true},
				Children: widgets,
			},
			HSeparator{},
			Label{
				Text:      summaryText,
				Alignment: AlignHCenterVNear,
				Font:      Font{Bold: true},
			},
			Label{
				AssignTo:  &countdownLbl,
				Text:      fmt.Sprintf("Restart za %ds...", rebootCountdown),
				Alignment: AlignHCenterVNear,
			},
			Composite{
				Layout: HBox{MarginsZero: true},
				Children: []Widget{
					HSpacer{},
					PushButton{
						Text: "  Restartovat ted  ",
						OnClicked: func() {
							close(cancelReboot)
							reboot()
							mw.Close()
						},
					},
					PushButton{
						Text: "  Zrusit restart  ",
						OnClicked: func() {
							select {
							case <-cancelReboot:
							default:
								close(cancelReboot)
							}
							countdownLbl.SetText("Restart zrusen.")
						},
					},
					HSpacer{},
				},
			},
		},
	}.Create()); err != nil {
		return
	}

	// Countdown goroutine
	go func() {
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
		remaining := rebootCountdown
		for {
			select {
			case <-cancelReboot:
				return
			case <-ticker.C:
				remaining--
				r := remaining
				mw.Synchronize(func() {
					if r > 0 {
						countdownLbl.SetText(fmt.Sprintf("Restart za %ds...", r))
					} else {
						countdownLbl.SetText("Restartuji...")
					}
				})
				if remaining <= 0 {
					reboot()
					mw.Synchronize(func() { mw.Close() })
					return
				}
			}
		}
	}()

	mw.Run()
}

// reboot issues an immediate Windows restart.
func reboot() {
	exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck
}

// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------

func itemEnabled(cfg config.Config, item runner.SelectableItem) bool {
	if item.FeatureID == "" {
		if v, ok := cfg.Steps[item.StepID]; ok {
			return v
		}
		return true
	}
	if feats, ok := cfg.Features[item.StepID]; ok {
		if v, ok2 := feats[item.FeatureID]; ok2 {
			return v
		}
	}
	return true
}

func buildStepsAndFeatures(selected map[string]bool) ([]runner.Step, config.Features) {
	items := runner.AllSelectableItems()
	features := make(config.Features)
	stepOn := make(map[string]bool)

	for _, item := range items {
		if item.FeatureID == "" {
			stepOn[item.StepID] = selected[item.Key]
		} else {
			if features[item.StepID] == nil {
				features[item.StepID] = make(map[string]bool)
			}
			features[item.StepID][item.FeatureID] = selected[item.Key]
			if selected[item.Key] {
				stepOn[item.StepID] = true
			}
		}
	}

	allSteps := runner.AllSteps()
	steps := make([]runner.Step, len(allSteps))
	for i, s := range allSteps {
		s.Enabled = stepOn[s.ID]
		steps[i] = s
	}
	return steps, features
}

func profileIndex(p string) int {
	switch p {
	case "admin":
		return 1
	case "user":
		return 2
	default:
		return 0
	}
}
