package app

import (
	"database/sql"
	"encoding/hex"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"

	"encoding/base64"

	"github.com/vstockwell/wraith/internal/ai"
	"github.com/vstockwell/wraith/internal/connections"
	"github.com/vstockwell/wraith/internal/credentials"
	"github.com/vstockwell/wraith/internal/db"
	"github.com/vstockwell/wraith/internal/importer"
	"github.com/vstockwell/wraith/internal/plugin"
	"github.com/vstockwell/wraith/internal/rdp"
	"github.com/vstockwell/wraith/internal/session"
	"github.com/vstockwell/wraith/internal/settings"
	"github.com/vstockwell/wraith/internal/sftp"
	"github.com/vstockwell/wraith/internal/ssh"
	"github.com/vstockwell/wraith/internal/theme"
	"github.com/vstockwell/wraith/internal/updater"
	"github.com/vstockwell/wraith/internal/vault"
	sftplib "github.com/pkg/sftp"
	gossh "golang.org/x/crypto/ssh"
	"github.com/wailsapp/wails/v3/pkg/application"
)

// WraithApp is the main application struct that wires together all services
// and exposes vault management methods to the frontend via Wails bindings.
type WraithApp struct {
	db          *sql.DB
	Vault       *vault.VaultService
	Settings    *settings.SettingsService
	Connections *connections.ConnectionService
	Themes      *theme.ThemeService
	Sessions    *session.Manager
	Plugins     *plugin.Registry
	SSH         *ssh.SSHService
	SFTP        *sftp.SFTPService
	RDP         *rdp.RDPService
	Credentials *credentials.CredentialService
	AI          *ai.AIService
	Updater     *updater.UpdateService
	Workspace   *WorkspaceService
	oauthMgr    *ai.OAuthManager
	wailsApp    *application.App
	unlocked    bool
}

// SetWailsApp stores the Wails application reference for event emission.
// Must be called after application.New() and before app.Run().
func (a *WraithApp) SetWailsApp(app *application.App) {
	a.wailsApp = app
}

// New creates and initializes the WraithApp, opening the database, running
// migrations, creating all services, and seeding built-in themes.
// The version string is the build-time semver (e.g. "0.2.0") used by the
// auto-updater to check for newer releases.
func New(version string) (*WraithApp, error) {
	dataDir := dataDirectory()
	dbPath := filepath.Join(dataDir, "wraith.db")

	slog.Info("opening database", "path", dbPath)
	database, err := db.Open(dbPath)
	if err != nil {
		return nil, fmt.Errorf("open database: %w", err)
	}

	if err := db.Migrate(database); err != nil {
		return nil, fmt.Errorf("run migrations: %w", err)
	}

	settingsSvc := settings.NewSettingsService(database)
	connSvc := connections.NewConnectionService(database)
	themeSvc := theme.NewThemeService(database)
	sessionMgr := session.NewManager()
	pluginReg := plugin.NewRegistry()
	pluginReg.RegisterImporter(&importer.MobaConfImporter{})

	// Host key store — persists SSH host key fingerprints for TOFU verification
	hostKeyStore := ssh.NewHostKeyStore(database)

	// SSH output handler — emits Wails events to the frontend.
	// The closure captures `app` (the WraithApp being built). The wailsApp
	// field is set after application.New() in main.go, but SSH sessions only
	// start after app.Run(), so wailsApp is always valid at call time.
	var app *WraithApp
	sshOutputHandler := func(sessionID string, data []byte) {
		if app != nil && app.wailsApp != nil {
			// Base64 encode binary data for safe transport over Wails events
			app.wailsApp.Event.Emit("ssh:data:"+sessionID, base64.StdEncoding.EncodeToString(data))
		}
	}

	// CWD handler — emits Wails events when the remote working directory changes
	sshCWDHandler := func(sessionID string, path string) {
		if app != nil && app.wailsApp != nil {
			app.wailsApp.Event.Emit("ssh:cwd:"+sessionID, path)
		}
	}

	sshSvc := ssh.NewSSHService(database, hostKeyStore, sshOutputHandler, sshCWDHandler)
	sftpSvc := sftp.NewSFTPService()

	// RDP service with platform-aware backend factory.
	// On Windows the factory returns a FreeRDPBackend backed by libfreerdp3.dll;
	// on other platforms it falls back to MockBackend for development.
	rdpSvc := rdp.NewRDPService(func() rdp.RDPBackend {
		return rdp.NewProductionBackend()
	})

	// CredentialService requires the vault to be unlocked, so it starts nil.
	// It is created lazily after the vault is unlocked via initCredentials().

	// AI copilot services — OAuthManager starts without a vault reference;
	// it will be wired once the vault is unlocked.
	oauthMgr := ai.NewOAuthManager(settingsSvc, nil)
	toolRouter := ai.NewToolRouter()
	toolRouter.SetServices(sshSvc, sftpSvc, rdpSvc, sessionMgr, connSvc)
	convMgr := ai.NewConversationManager(database)
	aiSvc := ai.NewAIService(oauthMgr, toolRouter, convMgr)
	toolRouter.SetAIService(aiSvc)

	// Seed built-in themes on every startup (INSERT OR IGNORE keeps it idempotent)
	if err := themeSvc.SeedBuiltins(); err != nil {
		slog.Warn("failed to seed themes", "error", err)
	}

	updaterSvc := updater.NewUpdateService(version)
	workspaceSvc := NewWorkspaceService(settingsSvc)

	// Clear the clean shutdown flag on startup — it will be re-set on clean exit.
	// If it wasn't set, the previous run crashed and the workspace can be restored.
	wasClean := workspaceSvc.WasCleanShutdown()
	if err := workspaceSvc.ClearCleanShutdown(); err != nil {
		slog.Warn("failed to clear clean shutdown flag", "error", err)
	}
	if !wasClean {
		slog.Info("previous shutdown was not clean — workspace restore available")
	}

	app = &WraithApp{
		db:          database,
		Settings:    settingsSvc,
		Connections: connSvc,
		Themes:      themeSvc,
		Sessions:    sessionMgr,
		Plugins:     pluginReg,
		SSH:         sshSvc,
		SFTP:        sftpSvc,
		RDP:         rdpSvc,
		AI:          aiSvc,
		Updater:     updaterSvc,
		Workspace:   workspaceSvc,
		oauthMgr:    oauthMgr,
	}
	return app, nil
}

// dataDirectory returns the path where Wraith stores its data.
// On Windows with APPDATA set, it uses %APPDATA%\Wraith.
// On macOS/Linux with XDG_DATA_HOME or HOME, it uses the appropriate path.
// Falls back to the current working directory for development.
func dataDirectory() string {
	// Windows
	if appData := os.Getenv("APPDATA"); appData != "" {
		return filepath.Join(appData, "Wraith")
	}

	// macOS / Linux: use XDG_DATA_HOME or fallback to ~/.local/share
	if home, err := os.UserHomeDir(); err == nil {
		if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
			return filepath.Join(xdg, "wraith")
		}
		return filepath.Join(home, ".local", "share", "wraith")
	}

	// Dev fallback
	return "."
}

// IsFirstRun checks whether the vault has been set up by looking for vault_salt in settings.
func (a *WraithApp) IsFirstRun() bool {
	salt, _ := a.Settings.Get("vault_salt")
	return salt == ""
}

// CreateVault sets up the vault with a master password. It generates a salt,
// derives an encryption key, and stores the salt and a check value in settings.
func (a *WraithApp) CreateVault(password string) error {
	salt, err := vault.GenerateSalt()
	if err != nil {
		return fmt.Errorf("generate salt: %w", err)
	}

	key := vault.DeriveKey(password, salt)
	a.Vault = vault.NewVaultService(key)

	// Store salt as hex in settings
	if err := a.Settings.Set("vault_salt", hex.EncodeToString(salt)); err != nil {
		return fmt.Errorf("store salt: %w", err)
	}

	// Encrypt a known check value — used to verify the password on unlock
	check, err := a.Vault.Encrypt("wraith-vault-check")
	if err != nil {
		return fmt.Errorf("encrypt check value: %w", err)
	}
	if err := a.Settings.Set("vault_check", check); err != nil {
		return fmt.Errorf("store check value: %w", err)
	}

	a.unlocked = true
	a.initCredentials()
	slog.Info("vault created successfully")
	return nil
}

// Unlock verifies the master password against the stored check value and
// initializes the vault service for decryption.
func (a *WraithApp) Unlock(password string) error {
	saltHex, err := a.Settings.Get("vault_salt")
	if err != nil || saltHex == "" {
		return fmt.Errorf("vault not set up — call CreateVault first")
	}

	salt, err := hex.DecodeString(saltHex)
	if err != nil {
		return fmt.Errorf("decode salt: %w", err)
	}

	key := vault.DeriveKey(password, salt)
	vs := vault.NewVaultService(key)

	// Verify by decrypting the stored check value
	checkEncrypted, err := a.Settings.Get("vault_check")
	if err != nil || checkEncrypted == "" {
		return fmt.Errorf("vault check value missing")
	}

	checkPlain, err := vs.Decrypt(checkEncrypted)
	if err != nil {
		return fmt.Errorf("incorrect master password")
	}
	if checkPlain != "wraith-vault-check" {
		return fmt.Errorf("incorrect master password")
	}

	a.Vault = vs
	a.unlocked = true
	a.initCredentials()
	slog.Info("vault unlocked successfully")
	return nil
}

// IsUnlocked returns whether the vault is currently unlocked.
func (a *WraithApp) IsUnlocked() bool {
	return a.unlocked
}

// initCredentials creates the CredentialService after the vault is unlocked
// and wires the vault to services that need it (OAuth, etc.).
func (a *WraithApp) initCredentials() {
	if a.Vault != nil {
		a.Credentials = credentials.NewCredentialService(a.db, a.Vault)
		if a.oauthMgr != nil {
			a.oauthMgr.SetVault(a.Vault)
		}
	}
}

// ConnectSSH opens an SSH session to the given connection ID.
// It resolves credentials from the vault, builds auth methods, and returns a session ID.
// The frontend calls this instead of SSHService.Connect directly (which takes Go-only types).
func (a *WraithApp) ConnectSSH(connectionID int64, cols, rows int) (string, error) {
	conn, err := a.Connections.GetConnection(connectionID)
	if err != nil {
		return "", fmt.Errorf("connection not found: %w", err)
	}

	// Build SSH auth methods from the connection's credential
	var authMethods []gossh.AuthMethod
	username := "root" // default

	slog.Info("ConnectSSH resolving auth",
		"connectionID", connectionID,
		"host", conn.Hostname,
		"credentialID", conn.CredentialID,
		"vaultUnlocked", a.Credentials != nil,
	)

	if conn.CredentialID != nil && a.Credentials != nil {
		cred, err := a.Credentials.GetCredential(*conn.CredentialID)
		if err != nil {
			slog.Warn("failed to load credential", "id", *conn.CredentialID, "error", err)
		} else {
			slog.Info("credential loaded", "name", cred.Name, "type", cred.Type, "username", cred.Username, "sshKeyID", cred.SSHKeyID)
			if cred.Username != "" {
				username = cred.Username
			}

			switch cred.Type {
			case "password":
				pw, err := a.Credentials.DecryptPassword(cred.ID)
				if err != nil {
					slog.Warn("failed to decrypt password", "error", err)
				} else {
					authMethods = append(authMethods, gossh.Password(pw))
				}
			case "ssh_key":
				if cred.SSHKeyID != nil {
					keyPEM, passphrase, err := a.Credentials.DecryptSSHKey(*cred.SSHKeyID)
					if err != nil {
						slog.Warn("failed to decrypt SSH key", "error", err)
					} else {
						var signer gossh.Signer
						if passphrase != "" {
							signer, err = gossh.ParsePrivateKeyWithPassphrase(keyPEM, []byte(passphrase))
						} else {
							signer, err = gossh.ParsePrivateKey(keyPEM)
						}
						if err != nil {
							slog.Warn("failed to parse SSH key", "error", err)
						} else {
							authMethods = append(authMethods, gossh.PublicKeys(signer))
						}
					}
				}
			}
		}
	}

	// If no stored credentials, return an error telling the frontend to prompt
	if len(authMethods) == 0 {
		return "", fmt.Errorf("NO_CREDENTIALS: no credentials configured for this connection — assign a credential or use ConnectSSHWithPassword")
	}

	slog.Info("SSH auth configured", "username", username, "authMethods", len(authMethods))

	sessionID, err := a.SSH.Connect(conn.Hostname, conn.Port, username, authMethods, cols, rows)
	if err != nil {
		return "", fmt.Errorf("SSH connect failed: %w", err)
	}

	// Register SFTP client on the same SSH connection
	if sess, ok := a.SSH.GetSession(sessionID); ok && sess != nil {
		sftpClient, err := sftplib.NewClient(sess.Client)
		if err != nil {
			slog.Warn("failed to create SFTP client", "error", err)
			// Non-fatal — SSH still works without SFTP
		} else {
			a.SFTP.RegisterClient(sessionID, sftpClient)
		}
	}

	// Update last_connected timestamp
	if _, err := a.db.Exec("UPDATE connections SET last_connected = CURRENT_TIMESTAMP WHERE id = ?", connectionID); err != nil {
		slog.Warn("failed to update last_connected", "error", err)
	}

	// Register with session manager using the SSH session's own UUID
	if _, err := a.Sessions.CreateWithID(sessionID, connectionID, "ssh"); err != nil {
		slog.Warn("failed to register SSH session in manager", "error", err)
	} else {
		_ = a.Sessions.SetState(sessionID, session.StateConnected)
	}

	// Save workspace state after session change
	a.saveWorkspaceState()

	slog.Info("SSH session started", "sessionID", sessionID, "host", conn.Hostname, "user", username)
	return sessionID, nil
}

// ConnectSSHWithPassword opens an SSH session using an ad-hoc username/password.
// Used when no stored credential exists — the frontend prompts and passes creds directly.
func (a *WraithApp) ConnectSSHWithPassword(connectionID int64, username, password string, cols, rows int) (string, error) {
	conn, err := a.Connections.GetConnection(connectionID)
	if err != nil {
		return "", fmt.Errorf("connection not found: %w", err)
	}

	authMethods := []gossh.AuthMethod{
		gossh.Password(password),
		gossh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
			answers := make([]string, len(questions))
			for i := range answers {
				answers[i] = password
			}
			return answers, nil
		}),
	}

	sessionID, err := a.SSH.Connect(conn.Hostname, conn.Port, username, authMethods, cols, rows)
	if err != nil {
		return "", fmt.Errorf("SSH connect failed: %w", err)
	}

	// Register SFTP client
	if sess, ok := a.SSH.GetSession(sessionID); ok && sess != nil {
		sftpClient, err := sftplib.NewClient(sess.Client)
		if err != nil {
			slog.Warn("failed to create SFTP client", "error", err)
		} else {
			a.SFTP.RegisterClient(sessionID, sftpClient)
		}
	}

	if _, err := a.db.Exec("UPDATE connections SET last_connected = CURRENT_TIMESTAMP WHERE id = ?", connectionID); err != nil {
		slog.Warn("failed to update last_connected", "error", err)
	}

	// Register with session manager
	if _, err := a.Sessions.CreateWithID(sessionID, connectionID, "ssh"); err != nil {
		slog.Warn("failed to register SSH session in manager", "error", err)
	} else {
		_ = a.Sessions.SetState(sessionID, session.StateConnected)
	}

	// Save workspace state after session change
	a.saveWorkspaceState()

	slog.Info("SSH session started (ad-hoc password)", "sessionID", sessionID, "host", conn.Hostname, "user", username)
	return sessionID, nil
}

// GetVersion returns the build-time version string.
func (a *WraithApp) GetVersion() string {
	return a.Updater.CurrentVersion()
}

// DisconnectSession closes an active SSH session and its SFTP client,
// and removes it from the session manager.
func (a *WraithApp) DisconnectSession(sessionID string) error {
	a.SFTP.RemoveClient(sessionID)
	a.Sessions.Remove(sessionID)
	a.saveWorkspaceState()
	return a.SSH.Disconnect(sessionID)
}

// ConnectRDP opens an RDP session to the given connection ID.
// It resolves credentials from the vault, builds an RDPConfig, and returns a session ID.
// width and height are the initial desktop dimensions in pixels.
func (a *WraithApp) ConnectRDP(connectionID int64, width, height int) (string, error) {
	conn, err := a.Connections.GetConnection(connectionID)
	if err != nil {
		return "", fmt.Errorf("connection not found: %w", err)
	}

	config := rdp.RDPConfig{
		Hostname: conn.Hostname,
		Port:     conn.Port,
		Width:    width,
		Height:   height,
	}

	if conn.CredentialID != nil && a.Credentials != nil {
		cred, err := a.Credentials.GetCredential(*conn.CredentialID)
		if err != nil {
			slog.Warn("failed to load credential", "id", *conn.CredentialID, "error", err)
		} else {
			if cred.Username != "" {
				config.Username = cred.Username
			}
			if cred.Domain != "" {
				config.Domain = cred.Domain
			}

			if cred.Type == "password" {
				pw, err := a.Credentials.DecryptPassword(cred.ID)
				if err != nil {
					slog.Warn("failed to decrypt password", "error", err)
				} else {
					config.Password = pw
				}
			}
		}
	}

	sessionID, err := a.RDP.Connect(config, connectionID)
	if err != nil {
		return "", fmt.Errorf("RDP connect failed: %w", err)
	}

	// Update last_connected timestamp
	if _, err := a.db.Exec("UPDATE connections SET last_connected = CURRENT_TIMESTAMP WHERE id = ?", connectionID); err != nil {
		slog.Warn("failed to update last_connected", "error", err)
	}

	// Register with session manager
	if _, err := a.Sessions.CreateWithID(sessionID, connectionID, "rdp"); err != nil {
		slog.Warn("failed to register RDP session in manager", "error", err)
	} else {
		_ = a.Sessions.SetState(sessionID, session.StateConnected)
	}

	// Save workspace state after session change
	a.saveWorkspaceState()

	slog.Info("RDP session started", "sessionID", sessionID, "host", conn.Hostname)
	return sessionID, nil
}

// RDPGetFrame returns the current frame for an RDP session as a base64-encoded
// string. Go []byte is serialised by Wails as a base64 string over the JSON
// bridge, so the frontend decodes it with atob() to recover the raw RGBA bytes.
func (a *WraithApp) RDPGetFrame(sessionID string) (string, error) {
	raw, err := a.RDP.GetFrame(sessionID)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(raw), nil
}

// RDPSendMouse forwards a mouse event to an RDP session.
// flags uses the RDP mouse-event flag constants defined in internal/rdp/input.go.
func (a *WraithApp) RDPSendMouse(sessionID string, x, y int, flags uint32) error {
	return a.RDP.SendMouse(sessionID, x, y, flags)
}

// RDPSendKey forwards a key event (scancode + press/release) to an RDP session.
func (a *WraithApp) RDPSendKey(sessionID string, scancode uint32, pressed bool) error {
	return a.RDP.SendKey(sessionID, scancode, pressed)
}

// RDPSendClipboard forwards clipboard text to an RDP session.
func (a *WraithApp) RDPSendClipboard(sessionID string, text string) error {
	return a.RDP.SendClipboard(sessionID, text)
}

// RDPDisconnect tears down an RDP session and removes it from the session manager.
func (a *WraithApp) RDPDisconnect(sessionID string) error {
	a.Sessions.Remove(sessionID)
	a.saveWorkspaceState()
	return a.RDP.Disconnect(sessionID)
}

// ---------- Credential proxy methods ----------
// CredentialService is nil until the vault is unlocked. These proxies expose
// it via WraithApp (which IS registered as a Wails service at startup).

// ListCredentials returns all stored credentials (no encrypted values).
func (a *WraithApp) ListCredentials() ([]credentials.Credential, error) {
	if a.Credentials == nil {
		return nil, fmt.Errorf("vault is locked")
	}
	return a.Credentials.ListCredentials()
}

// CreatePassword creates a password credential encrypted via the vault.
func (a *WraithApp) CreatePassword(name, username, password, domain string) (*credentials.Credential, error) {
	if a.Credentials == nil {
		return nil, fmt.Errorf("vault is locked")
	}
	return a.Credentials.CreatePassword(name, username, password, domain)
}

// CreateSSHKeyCredential imports an SSH private key and creates a Credential
// record referencing it. privateKeyPEM is the raw PEM string (NOT base64 encoded).
func (a *WraithApp) CreateSSHKeyCredential(name, username string, privateKeyPEM string, passphrase string) (*credentials.Credential, error) {
	if a.Credentials == nil {
		return nil, fmt.Errorf("vault is locked")
	}
	return a.Credentials.CreateSSHKeyCredential(name, username, []byte(privateKeyPEM), passphrase)
}

// DeleteCredential removes a credential by ID.
func (a *WraithApp) DeleteCredential(id int64) error {
	if a.Credentials == nil {
		return fmt.Errorf("vault is locked")
	}
	return a.Credentials.DeleteCredential(id)
}

// ImportClaudeCodeToken reads the local Claude Code credentials.json and imports
// the OAuth token into Wraith's vault. Fallback for when Wraith's own OAuth fails.
func (a *WraithApp) ImportClaudeCodeToken() error {
	if a.oauthMgr == nil {
		return fmt.Errorf("OAuth manager not initialized")
	}
	return a.oauthMgr.ImportClaudeCodeToken()
}

// ImportMobaConf parses a MobaXTerm .mobaconf file and imports its contents
// (groups, connections, host keys) into the database.
func (a *WraithApp) ImportMobaConf(fileContent string) (*plugin.ImportResult, error) {
	imp := &importer.MobaConfImporter{}
	result, err := imp.Parse([]byte(fileContent))
	if err != nil {
		return nil, fmt.Errorf("parse mobaconf: %w", err)
	}

	// Create groups and track name -> ID mapping
	groupMap := make(map[string]int64)
	for _, g := range result.Groups {
		created, err := a.Connections.CreateGroup(g.Name, nil)
		if err != nil {
			slog.Warn("failed to create group during import", "name", g.Name, "error", err)
			continue
		}
		groupMap[g.Name] = created.ID
	}

	// Create connections
	for _, c := range result.Connections {
		var groupID *int64
		if id, ok := groupMap[c.GroupName]; ok {
			groupID = &id
		}
		_, err := a.Connections.CreateConnection(connections.CreateConnectionInput{
			Name:     c.Name,
			Hostname: c.Hostname,
			Port:     c.Port,
			Protocol: c.Protocol,
			GroupID:  groupID,
			Notes:   c.Notes,
		})
		if err != nil {
			slog.Warn("failed to create connection during import", "name", c.Name, "error", err)
		}
	}

	// Store host keys
	for _, hk := range result.HostKeys {
		_, err := a.db.Exec(
			`INSERT OR IGNORE INTO host_keys (hostname, port, key_type, fingerprint) VALUES (?, ?, ?, ?)`,
			hk.Hostname, hk.Port, hk.KeyType, hk.Fingerprint,
		)
		if err != nil {
			slog.Warn("failed to store host key during import", "hostname", hk.Hostname, "error", err)
		}
	}

	slog.Info("mobaconf import complete",
		"groups", len(result.Groups),
		"connections", len(result.Connections),
		"hostKeys", len(result.HostKeys),
	)
	return result, nil
}

// ---------- Workspace proxy methods ----------

// saveWorkspaceState builds a workspace snapshot from the current session manager
// state and persists it. Called automatically on session open/close.
func (a *WraithApp) saveWorkspaceState() {
	if a.Workspace == nil {
		return
	}

	sessions := a.Sessions.List()
	tabs := make([]WorkspaceTab, 0, len(sessions))
	for _, s := range sessions {
		tabs = append(tabs, WorkspaceTab{
			ConnectionID: s.ConnectionID,
			Protocol:     s.Protocol,
			Position:     s.TabPosition,
		})
	}

	snapshot := &WorkspaceSnapshot{
		Tabs: tabs,
	}
	if err := a.Workspace.Save(snapshot); err != nil {
		slog.Warn("failed to save workspace state", "error", err)
	}
}

// SaveWorkspace explicitly saves the current workspace snapshot.
// Exposed to the frontend for manual save triggers.
func (a *WraithApp) SaveWorkspace(snapshot *WorkspaceSnapshot) error {
	if a.Workspace == nil {
		return fmt.Errorf("workspace service not initialized")
	}
	return a.Workspace.Save(snapshot)
}

// LoadWorkspace returns the last saved workspace snapshot, or nil if none exists.
func (a *WraithApp) LoadWorkspace() (*WorkspaceSnapshot, error) {
	if a.Workspace == nil {
		return nil, fmt.Errorf("workspace service not initialized")
	}
	return a.Workspace.Load()
}

// MarkCleanShutdown records that the app is shutting down cleanly.
// Called before app exit so the next startup knows whether to restore workspace.
func (a *WraithApp) MarkCleanShutdown() error {
	if a.Workspace == nil {
		return nil
	}
	return a.Workspace.MarkCleanShutdown()
}

// WasCleanShutdown returns whether the previous app exit was clean.
func (a *WraithApp) WasCleanShutdown() bool {
	if a.Workspace == nil {
		return true
	}
	return a.Workspace.WasCleanShutdown()
}

// DeleteHostKey removes stored host keys for a hostname:port, allowing
// reconnection after a legitimate server re-key.
func (a *WraithApp) DeleteHostKey(hostname string, port int) error {
	store := ssh.NewHostKeyStore(a.db)
	return store.Delete(hostname, port)
}

// GetSessionCWD returns the current tracked working directory for an SSH session.
func (a *WraithApp) GetSessionCWD(sessionID string) string {
	sess, ok := a.SSH.GetSession(sessionID)
	if !ok || sess.CWDTracker == nil {
		return ""
	}
	return sess.CWDTracker.GetCWD()
}
