package setup

import (
	crand "crypto/rand"
	"crypto/sha256"
	"embed"
	"encoding/hex"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
	"gitea.dooplex.hu/admin/felhom-controller/internal/report"
	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
	"gitea.dooplex.hu/admin/felhom-controller/internal/web"

	"golang.org/x/crypto/bcrypt"
)

//go:embed templates/*.html
var templateFS embed.FS

// Server handles the setup wizard HTTP routes.
type Server struct {
	cfg     *config.Config
	dataDir string
	logger  *log.Logger
	tmpl    *template.Template
	state   *SetupState
	version string
}

// NewServer creates a new setup wizard server.
func NewServer(cfg *config.Config, dataDir string, logger *log.Logger, version string) *Server {
	s := &Server{
		cfg:     cfg,
		dataDir: dataDir,
		logger:  logger,
		state:   LoadState(dataDir),
		version: version,
	}
	s.loadTemplates()

	// Pre-seed setup state from env vars (set by docker-setup.sh --hub-customer)
	if cid := os.Getenv("FELHOM_SETUP_CUSTOMER_ID"); cid != "" {
		s.state.SetFormField("customer_id", cid)
		logger.Printf("[INFO] Setup: pre-seeded customer_id from env: %s", cid)
	}
	if pw := os.Getenv("FELHOM_SETUP_PASSWORD"); pw != "" {
		s.state.SetFormField("retrieval_password", pw)
		logger.Printf("[INFO] Setup: pre-seeded retrieval_password from env")
	}
	if s.isHubPreseeded() {
		s.state.Save()
	}

	return s
}

// isDebug returns true if logging level is "debug".
func (s *Server) isDebug() bool {
	return s.cfg != nil && s.cfg.Logging.Level == "debug"
}

// isHubPreseeded returns true if both customer_id and retrieval_password are
// pre-seeded in setup state (set by docker-setup.sh --hub-customer).
func (s *Server) isHubPreseeded() bool {
	return s.state.GetFormField("customer_id") != "" && s.state.GetFormField("retrieval_password") != ""
}

func (s *Server) loadTemplates() {
	s.tmpl = template.Must(
		template.New("").Funcs(template.FuncMap{
			"timeNow": func() string { return time.Now().Format("2006-01-02 15:04") },
		}).ParseFS(templateFS, "templates/*.html"),
	)
}

// Handler returns the HTTP handler for the setup wizard.
func (s *Server) Handler() http.Handler {
	mux := http.NewServeMux()

	// Disk-recovery setup paths (drive scan, infra-backup restore) have moved to the
	// host agent (slice 8C). The wizard now offers fresh install (from Hub) + manual.
	mux.HandleFunc("/", s.handleRoot)
	mux.HandleFunc("/setup", s.handleWelcome)
	mux.HandleFunc("/setup/fresh", s.handleFreshHub)
	mux.HandleFunc("/setup/manual", s.handleManual)
	mux.HandleFunc("/setup/failed", s.handleFailed)
	mux.HandleFunc("/static/style.css", s.handleCSS)
	mux.HandleFunc("/static/felhom-logo.svg", s.handleLogo)

	return mux
}

// --- Route Handlers ---

func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.Redirect(w, r, "/setup", http.StatusFound)
		return
	}
	http.Redirect(w, r, "/setup", http.StatusFound)
}

func (s *Server) handleWelcome(w http.ResponseWriter, r *http.Request) {
	csrf := ensureCSRFToken(w, r)
	domain := s.cfg.Customer.Domain
	ips := DetectLocalIPs()

	var accessURLs []string
	if domain != "" {
		accessURLs = append(accessURLs, fmt.Sprintf("https://felhom.%s", domain))
	}
	for _, ip := range ips {
		accessURLs = append(accessURLs, fmt.Sprintf("http://%s%s", ip, s.cfg.Web.SetupListen))
	}

	data := map[string]interface{}{
		"CSRF":          csrf,
		"AccessURLs":    accessURLs,
		"Version":       s.version,
		"HubMode":       s.isHubPreseeded(),
		"HubCustomerID": s.state.GetFormField("customer_id"),
	}
	s.render(w, "setup_welcome", data)
}

func (s *Server) handleFreshHub(w http.ResponseWriter, r *http.Request) {
	csrf := ensureCSRFToken(w, r)

	if r.Method == http.MethodPost {
		if !validateCSRF(r) {
			http.Error(w, "Invalid CSRF token", http.StatusForbidden)
			return
		}
		s.processFreshHub(w, r)
		return
	}

	// Auto-process if credentials are pre-seeded (hub mode from docker-setup.sh)
	if s.isHubPreseeded() {
		customerID := s.state.GetFormField("customer_id")
		password := s.state.GetFormField("retrieval_password")
		s.autoProcessFreshHub(w, r, customerID, password)
		return
	}

	data := map[string]interface{}{
		"CSRF":       csrf,
		"CustomerID": s.state.GetFormField("customer_id"),
	}
	s.render(w, "setup_fresh_hub", data)
}

func (s *Server) handleManual(w http.ResponseWriter, r *http.Request) {
	csrf := ensureCSRFToken(w, r)

	if r.Method == http.MethodPost {
		if !validateCSRF(r) {
			http.Error(w, "Invalid CSRF token", http.StatusForbidden)
			return
		}
		s.processManual(w, r)
		return
	}

	data := map[string]interface{}{
		"CSRF":       csrf,
		"FormData":   s.state.FormData,
		"DefaultGit": "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git",
	}
	s.render(w, "setup_manual", data)
}

func (s *Server) handleFailed(w http.ResponseWriter, r *http.Request) {
	csrf := ensureCSRFToken(w, r)
	data := map[string]interface{}{
		"CSRF": csrf,
	}
	s.render(w, "setup_failed", data)
}

// --- Static Assets (reuse from web package embed) ---

func (s *Server) handleCSS(w http.ResponseWriter, r *http.Request) {
	// Serve the web package's embedded stylesheet. The previous implementation
	// read a filesystem path derived from dataDir, which does not exist inside
	// the container image — setup mode silently fell back to minimalCSS.
	data, err := web.StyleCSS()
	if err != nil {
		s.logger.Printf("[WARN] Setup: embedded style.css unavailable (%v) — serving minimal fallback CSS", err)
		w.Header().Set("Content-Type", "text/css; charset=utf-8")
		w.Write([]byte(minimalCSS))
		return
	}
	w.Header().Set("Content-Type", "text/css; charset=utf-8")
	w.Header().Set("Cache-Control", "public, max-age=3600")
	w.Write(data)
}

func (s *Server) handleLogo(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/svg+xml")
	w.Header().Set("Cache-Control", "public, max-age=86400")
	fmt.Fprint(w, web.FelhomLogoSVG)
}

// --- Auto-Processing (Hub pre-seeded mode) ---

// autoProcessHubRestore calls PullRecovery with pre-seeded credentials and
// renders the confirmation page directly, skipping the manual form.
// Falls back to the form with an error message on failure.
// autoProcessFreshHub calls PullConfig with pre-seeded credentials and
// proceeds with fresh install, skipping the manual form.
func (s *Server) autoProcessFreshHub(w http.ResponseWriter, r *http.Request, customerID, password string) {
	hubURL := DefaultHubURL

	s.logger.Printf("[INFO] Setup: auto-processing fresh hub install for %s (pre-seeded credentials)", customerID)

	configYAML, err := report.PullConfig(hubURL, customerID, password)
	if err != nil {
		s.logger.Printf("[WARN] Setup: auto fresh hub failed: %v — falling back to form", err)
		// Clear pre-seeded password so form is shown on next attempt
		s.state.SetFormField("retrieval_password", "")
		s.state.Save()
		var msg string
		switch {
		case isError(err, report.ErrHubUnreachable):
			msg = "A Hub (hub.felhom.eu) nem elérhető. Ellenőrizze az internetkapcsolatot."
		case isError(err, report.ErrAuthFailed):
			msg = "Helytelen ügyfél-azonosító vagy jelszó."
		case isError(err, report.ErrNotFound):
			msg = "Ez az ügyfél-azonosító nem található a Hub-on."
		default:
			msg = fmt.Sprintf("Hiba történt: %v", err)
		}
		s.renderError(w, r, "setup_fresh_hub", msg, customerID)
		return
	}

	s.logger.Printf("[INFO] Setup: config downloaded (%d bytes), writing config...", len(configYAML))

	if err := s.writeFreshConfig(configYAML, password); err != nil {
		s.logger.Printf("[ERROR] Setup: writeFreshConfig failed: %v", err)
		s.renderError(w, r, "setup_fresh_hub", fmt.Sprintf("Konfigurációs hiba: %v", err), customerID)
		return
	}

	s.logger.Printf("[INFO] Setup: fresh install from Hub completed for %s", customerID)
	s.finishSetup()
}

// --- Processing Logic ---

func (s *Server) processFreshHub(w http.ResponseWriter, r *http.Request) {
	customerID := strings.TrimSpace(r.FormValue("customer_id"))
	password := r.FormValue("password")
	hubURL := DefaultHubURL

	s.state.SetFormField("customer_id", customerID)

	if customerID == "" || password == "" {
		s.renderError(w, r, "setup_fresh_hub", "Kérem töltse ki mindkét mezőt.", customerID)
		return
	}

	s.logger.Printf("[INFO] Setup: downloading config from Hub (%s) for customer %s", hubURL, customerID)
	configYAML, err := report.PullConfig(hubURL, customerID, password)
	if err != nil {
		s.logger.Printf("[ERROR] Setup: Hub config download failed: %v", err)
		var msg string
		switch {
		case isError(err, report.ErrHubUnreachable):
			msg = "A Hub (hub.felhom.eu) nem elérhető. Ellenőrizze az internetkapcsolatot."
		case isError(err, report.ErrAuthFailed):
			msg = "Helytelen ügyfél-azonosító vagy jelszó."
		case isError(err, report.ErrNotFound):
			msg = "Ez az ügyfél-azonosító nem található a Hub-on."
		default:
			msg = fmt.Sprintf("Hiba történt: %v", err)
		}
		s.renderError(w, r, "setup_fresh_hub", msg, customerID)
		return
	}
	s.logger.Printf("[INFO] Setup: config downloaded (%d bytes), writing config...", len(configYAML))

	// Write config and finish setup
	if s.isDebug() {
		s.logger.Printf("[DEBUG] Setup: writing fresh config (%d bytes)", len(configYAML))
	}
	s.state.SetFormField("retrieval_password", password)
	if err := s.writeFreshConfig(configYAML, password); err != nil {
		s.logger.Printf("[ERROR] Setup: writeFreshConfig failed: %v", err)
		s.renderError(w, r, "setup_fresh_hub", fmt.Sprintf("Konfigurációs hiba: %v", err), customerID)
		return
	}

	s.logger.Printf("[INFO] Setup: fresh install from Hub completed for %s", customerID)
	s.finishSetup()
}

func (s *Server) processManual(w http.ResponseWriter, r *http.Request) {
	// Save all form fields
	fields := []string{"customer_id", "display_name", "domain", "email",
		"cf_tunnel_token", "cf_api_token", "system_data_path",
		"password", "password_confirm",
		"git_repo_url", "git_username", "git_token"}
	for _, f := range fields {
		s.state.SetFormField(f, r.FormValue(f))
	}

	// Validate
	customerID := strings.TrimSpace(r.FormValue("customer_id"))
	domain := strings.TrimSpace(r.FormValue("domain"))
	password := r.FormValue("password")
	passwordConfirm := r.FormValue("password_confirm")

	var errs []string
	if customerID == "" {
		errs = append(errs, "Ügyfél-azonosító kötelező")
	}
	if domain == "" || domain == "homeserver.local" {
		errs = append(errs, "Érvényes domain szükséges")
	}
	if password != "" && len(password) < 8 {
		errs = append(errs, "A jelszó legalább 8 karakter legyen")
	}
	if password != passwordConfirm {
		errs = append(errs, "A jelszavak nem egyeznek")
	}

	if len(errs) > 0 {
		csrf := ensureCSRFToken(w, r)
		data := map[string]interface{}{
			"CSRF":       csrf,
			"FormData":   s.state.FormData,
			"DefaultGit": "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git",
			"Errors":     errs,
		}
		s.render(w, "setup_manual", data)
		return
	}

	// Generate controller.yaml
	configYAML := s.generateManualConfig()
	if s.isDebug() {
		s.logger.Printf("[DEBUG] Setup: generated manual config (%d bytes) for customer %s", len(configYAML), customerID)
	}
	if err := s.writeFreshConfig(configYAML, ""); err != nil {
		csrf := ensureCSRFToken(w, r)
		data := map[string]interface{}{
			"CSRF":       csrf,
			"FormData":   s.state.FormData,
			"DefaultGit": "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git",
			"Errors":     []string{fmt.Sprintf("Konfigurációs hiba: %v", err)},
		}
		s.render(w, "setup_manual", data)
		return
	}

	s.logger.Printf("[INFO] Setup: manual configuration completed for %s", customerID)
	s.finishSetup()
}

// --- Restore Execution ---

func (s *Server) writeFreshConfig(configYAML, retrievalPassword string) error {
	configPath := "/opt/docker/felhom-controller/controller.yaml"
	if err := atomicWriteFile(configPath, []byte(configYAML), 0600); err != nil {
		return fmt.Errorf("writing controller.yaml: %w", err)
	}

	// Create initial settings with password hash and retrieval password
	sett, err := settings.Load(filepath.Join(s.dataDir, "settings.json"), s.logger)
	if err != nil {
		sett = &settings.Settings{}
	}

	// Hash the dashboard password if provided in form
	if pw := s.state.GetFormField("password"); pw != "" {
		hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
		if err == nil {
			sett.SetPasswordHash(string(hash))
		}
	}

	if retrievalPassword != "" {
		sett.SetRetrievalPassword(retrievalPassword)
	}

	return nil
}

func (s *Server) generateManualConfig() string {
	fd := s.state.FormData

	customerID := fd["customer_id"]
	displayName := fd["display_name"]
	if displayName == "" {
		displayName = customerID
	}
	domain := fd["domain"]
	email := fd["email"]
	cfTunnelToken := fd["cf_tunnel_token"]
	cfAPIToken := fd["cf_api_token"]
	systemDataPath := fd["system_data_path"]
	if systemDataPath == "" {
		systemDataPath = "/mnt/sys_drive"
	}

	// Generate session secret
	secretBytes := make([]byte, 32)
	crand.Read(secretBytes)
	sessionSecret := hex.EncodeToString(secretBytes)

	// Generate password hash
	passwordHash := ""
	if pw := fd["password"]; pw != "" {
		if hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost); err == nil {
			passwordHash = string(hash)
		}
	}

	gitRepoURL := fd["git_repo_url"]
	if gitRepoURL == "" {
		gitRepoURL = "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git"
	}
	gitUsername := fd["git_username"]
	gitToken := fd["git_token"]

	// Build YAML manually (simple key-value, no templates needed)
	var b strings.Builder
	b.WriteString("# Generated by felhom-controller setup wizard\n")
	b.WriteString("customer:\n")
	fmt.Fprintf(&b, "  id: %q\n", customerID)
	fmt.Fprintf(&b, "  name: %q\n", displayName)
	fmt.Fprintf(&b, "  domain: %q\n", domain)
	if email != "" {
		fmt.Fprintf(&b, "  email: %q\n", email)
	}
	b.WriteString("\ninfrastructure:\n")
	if cfTunnelToken != "" {
		fmt.Fprintf(&b, "  cf_tunnel_token: %q\n", cfTunnelToken)
	}
	if cfAPIToken != "" {
		fmt.Fprintf(&b, "  cf_api_token: %q\n", cfAPIToken)
	}
	b.WriteString("\npaths:\n")
	b.WriteString("  stacks_dir: \"/opt/docker/stacks\"\n")
	b.WriteString("  data_dir: \"/opt/docker/felhom-controller/data\"\n")
	fmt.Fprintf(&b, "  system_data_path: %q\n", systemDataPath)
	b.WriteString("\nsystem:\n")
	b.WriteString("  reserved_memory_mb: 384\n")
	b.WriteString("\nweb:\n")
	b.WriteString("  listen: \":8080\"\n")
	b.WriteString("  setup_listen: \":8081\"\n")
	if passwordHash != "" {
		fmt.Fprintf(&b, "  password_hash: %q\n", passwordHash)
	}
	fmt.Fprintf(&b, "  session_secret: %q\n", sessionSecret)
	b.WriteString("\ngit:\n")
	fmt.Fprintf(&b, "  repo_url: %q\n", gitRepoURL)
	b.WriteString("  branch: \"main\"\n")
	b.WriteString("  sync_interval: \"15m\"\n")
	if gitUsername != "" {
		fmt.Fprintf(&b, "  username: %q\n", gitUsername)
	}
	if gitToken != "" {
		fmt.Fprintf(&b, "  token: %q\n", gitToken)
	}
	b.WriteString("\nstacks:\n")
	b.WriteString("  protected:\n")
	b.WriteString("    - \"traefik\"\n")
	b.WriteString("    - \"cloudflared\"\n")
	b.WriteString("    - \"felhom-controller\"\n")
	b.WriteString("    - \"filebrowser\"\n")
	b.WriteString("  update_window: \"03:00-05:00\"\n")
	b.WriteString("\nbackup:\n")
	b.WriteString("  enabled: true\n")
	b.WriteString("  restic_password_file: \"/opt/docker/felhom-controller/data/restic-password\"\n")
	b.WriteString("  db_dump_schedule: \"02:30\"\n")
	b.WriteString("  restic_schedule: \"03:00\"\n")
	b.WriteString("  retention:\n")
	b.WriteString("    keep_daily: 7\n")
	b.WriteString("    keep_weekly: 4\n")
	b.WriteString("    keep_monthly: 6\n")
	b.WriteString("  prune_schedule: \"weekly\"\n")
	b.WriteString("\nmonitoring:\n")
	b.WriteString("  enabled: true\n")
	b.WriteString("  healthchecks_base: \"https://status.felhom.eu\"\n")
	b.WriteString("  system_health_interval: \"5m\"\n")
	b.WriteString("  health_check_schedule: \"06:00\"\n")
	b.WriteString("\nhub:\n")
	b.WriteString("  enabled: true\n")
	b.WriteString("  url: \"https://hub.felhom.eu\"\n")
	// Generate a Hub API key from customer ID
	apiKeyHash := sha256.Sum256([]byte(customerID + "-" + sessionSecret))
	fmt.Fprintf(&b, "  api_key: %q\n", hex.EncodeToString(apiKeyHash[:]))
	b.WriteString("  push_interval: \"15m\"\n")
	b.WriteString("\nself_update:\n")
	b.WriteString("  enabled: true\n")
	b.WriteString("  check_interval: \"6h\"\n")
	b.WriteString("  image: \"gitea.dooplex.hu/admin/felhom-controller\"\n")
	b.WriteString("  auto_update: false\n")
	b.WriteString("  health_timeout_seconds: 60\n")
	b.WriteString("\nlogging:\n")
	b.WriteString("  level: \"info\"\n")

	return b.String()
}

// --- Helpers ---

func (s *Server) finishSetup() {
	s.state.Remove()
	s.logger.Printf("[INFO] Setup complete — restarting controller")
	os.Exit(0) // Docker restart policy will restart us
}

func (s *Server) render(w http.ResponseWriter, name string, data interface{}) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := s.tmpl.ExecuteTemplate(w, name, data); err != nil {
		s.logger.Printf("[ERROR] Template %s render error: %v", name, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
	}
}

func (s *Server) renderError(w http.ResponseWriter, r *http.Request, tmpl, msg, customerID string) {
	csrf := ensureCSRFToken(w, r)
	data := map[string]interface{}{
		"CSRF":       csrf,
		"Error":      msg,
		"CustomerID": customerID,
	}
	s.render(w, tmpl, data)
}

func atomicWriteFile(path string, data []byte, perm os.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		return err
	}
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, data, perm); err != nil {
		os.Remove(tmp)
		return err
	}
	if err := os.Rename(tmp, path); err != nil {
		os.Remove(tmp)
		// Rename fails on bind-mounted files (EBUSY). Fall back to direct write.
		return os.WriteFile(path, data, perm)
	}
	return nil
}

func isError(err, target error) bool {
	return err != nil && strings.Contains(err.Error(), target.Error())
}

// Minimal CSS for when the main stylesheet can't be loaded (v2 tokens)
const minimalCSS = `
:root { --bg-primary: #0A1220; --bg-card: #0F1B2E; --text-primary: #EDF2F9; --text-secondary: #94A6BF; --accent-blue: #0083D8; --border: #22344F; --green: #0083D8; --red: #E5534B; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg-primary); color: var(--text-primary); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
.setup-container { max-width: 700px; margin: 0 auto; padding: 2rem 1.5rem; }
.setup-header { text-align: center; margin-bottom: 2rem; }
.setup-header img { width: 120px; margin-bottom: 1rem; }
.setup-header h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.setup-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 2px; padding: 1.5rem; margin-bottom: 1rem; cursor: pointer; transition: border-color 0.2s; }
.setup-card:hover { border-color: var(--accent-blue); }
.setup-card h3 { margin-bottom: 0.5rem; }
.setup-card p { color: var(--text-secondary); font-size: 0.9rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.25rem; font-size: 0.9rem; color: var(--text-secondary); }
.form-control { width: 100%; padding: 0.5rem 0.75rem; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 2px; color: var(--text-primary); font-size: 0.9rem; }
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.6rem 1.5rem; border-radius: 2px; border: none; font-size: 0.9rem; font-weight: 500; cursor: pointer; text-decoration: none; }
.btn-primary { background: var(--green); color: #fff; }
.btn-primary:hover { background: #2EA8F5; }
.btn-outline { background: transparent; color: var(--text-secondary); border: 1px solid var(--border); }
.alert { padding: 0.75rem 1rem; border-radius: 2px; margin-bottom: 1rem; font-size: 0.9rem; }
.alert-error { background: rgba(229,83,75,.12); color: #E5534B; border: 1px solid rgba(229,83,75,.3); }
.alert-info { background: rgba(0,131,216,.13); color: #2EA8F5; border: 1px solid rgba(0,131,216,.3); }
.info-box { background: rgba(0,131,216,.13); border: 1px solid rgba(0,131,216,.2); border-radius: 2px; padding: 0.75rem 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; color: var(--text-secondary); }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; padding: 0.5rem 0.75rem; color: var(--text-secondary); font-size: 0.85rem; border-bottom: 1px solid var(--border); }
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; }
.badge-ok { background: rgba(63,185,80,0.15); color: var(--green); }
.badge-error { background: rgba(229,83,75,.12); color: var(--red); }
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.section { margin-bottom: 1.5rem; }
.section-header { cursor: pointer; padding: 0.75rem 1rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: 2px; display: flex; justify-content: space-between; align-items: center; }
.section-body { padding: 1rem; border: 1px solid var(--border); border-top: none; border-radius: 0 0 6px 6px; }
.step-list { list-style: none; }
.step-list li { padding: 0.5rem 0; display: flex; align-items: center; gap: 0.75rem; }
.step-done { color: var(--green); }
.step-running { color: var(--accent-blue); }
.step-failed { color: var(--red); }
`
