// =============================================================================
// TeslaLauncher — Windows Service (Session 0)
// =============================================================================
// Replaces TeslaLauncherService.exe (Elsewhen Studios LLC, .NET Framework 2.0).
// Runs in Session 0, auto-starts at boot before user login.
// Listens on TCP 53290 for OFB-encrypted framed-JSON RPC from TeslaConsole.
// Forwards commands to TeslaLauncherAgent (user session) via Named Pipe.
//
// On first boot (DHCP detected), runs SecureConfig to receive network
// configuration from the Console and establish the session key.
// =============================================================================

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TeslaSecureConfig;
using Tesla.Launcher.Shared;
using Tesla.Net;

namespace Tesla.Launcher.Service
{
    public class TeslaLauncherService : BackgroundService
    {
        // ---------------------------------------------------------------
        // CONFIGURATION
        // ---------------------------------------------------------------

        /// <summary>
        /// TCP port the Console connects to for RPC commands.
        /// Original: ManagePort = 53290 (confirmed from decompiled TeslaLauncherService.exe).
        /// Each connection is wrapped in OFB encryption via NegotiateCryptoStreams
        /// using the session key from SecureConfig (TeslaKeyStore.key).
        /// </summary>
        private const int CONSOLE_PORT = 53290;

        /// <summary>
        /// Named pipe connecting this service to TeslaLauncherAgent.exe.
        /// Must match PIPE_NAME in TeslaLauncherAgent.cs.
        /// </summary>
        private const string PIPE_NAME = "TeslaLauncherIPC";

        /// <summary>
        /// Seconds to wait for the Agent to connect on the Named Pipe.
        /// If the Agent isn't running (user not yet logged in), we return an error.
        /// </summary>
        private const int AGENT_CONNECT_TIMEOUT_MS = 3000;

        // ---------------------------------------------------------------

        /// <summary>Path to session key file (original KeyStore format).</summary>
        private static readonly string KEY_FILE = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
            "TeslaLauncher", "TeslaKeyStore.key");

        private readonly ILogger<TeslaLauncherService> _logger;
        private TcpListener _listener;
        private readonly List<Task> _clientTasks = new();
        private readonly object _taskLock = new();
        private byte[] _sessionKey;

        // Install product — file transfer progress tracking.
        // InitiateInstallProduct creates a Guid; the Console then streams the
        // game archive on the SAME OFB connection.  GetOutOfBandProgress polls
        // this dictionary from a separate connection.
        private readonly Dictionary<Guid, OutOfBandProgress> _installProgress = new();
        private readonly object _installLock = new();

        public TeslaLauncherService(ILogger<TeslaLauncherService> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation(
                "Tesla Launcher Service starting on port {Port}", CONSOLE_PORT);

            // ── Boot diagnostic — always written so every reboot is traceable ──
            // Logs adapter state to podconf.log before IsMachineConfigured() decides,
            // visible even if SecureConfig does not run.
            var logPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
            void StartupLog(string msg) => File.AppendAllText(logPath,
                $"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");
            StartupLog("=== Service starting ===");
            foreach (var nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
            {
                if (nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Ethernet) continue;
                if (nic.OperationalStatus    != System.Net.NetworkInformation.OperationalStatus.Up)          continue;
                try
                {
                    var ipv4 = nic.GetIPProperties().GetIPv4Properties();
                    StartupLog($"  NIC [{nic.Name}] DHCP={ipv4.IsDhcpEnabled} Virtual={IsVirtualAdapter(nic)}");
                }
                catch { StartupLog($"  NIC [{nic.Name}] (no IPv4 props)"); }
            }

            // ── First-boot secure configuration ──────────────────────────────
            // If the Ethernet adapter is still on DHCP the cockpit has not yet
            // been configured.  Run PodSecureConfigurator here in Session 0:
            //   • broadcasts the RQST beacon (MAC + RequestId)
            //   • displays passphrase on COM2 / PlasmaIO
            //   • receives UDP RPLY with AES-encrypted network configuration
            //   • applies netsh IP settings and hostname
            //   • completes TCP handshake with Console + RSA key exchange
            //   • saves 32-byte session key to TeslaKeyStore.key
            // After SecureConfig, fall through to the normal command listener —
            // the original Service does the same (ExecutionMode after Configure).
            if (!IsMachineConfigured())
            {
                StartupLog("IsMachineConfigured=false → running SecureConfig");
                _logger.LogInformation("Machine is unconfigured (DHCP). Running SecureConfiguration.");
                await Task.Run(() => RunSecureConfiguration(stoppingToken), stoppingToken);
                // Load key that was saved during SecureConfig
                _sessionKey = LoadSessionKey();
                if (_sessionKey == null)
                {
                    _logger.LogWarning("SecureConfig completed but no session key was saved. Cannot start command listener.");
                    return;
                }
                StartupLog("SecureConfig complete → starting command listener on port 53290");
            }
            else
            {
                StartupLog("IsMachineConfigured=true → loading session key");
                _sessionKey = LoadSessionKey();
                if (_sessionKey == null)
                {
                    _logger.LogWarning("Machine is configured but TeslaKeyStore.key not found. Reboot with DHCP to re-run SecureConfig.");
                    StartupLog("ERROR: TeslaKeyStore.key not found — cannot start command listener");
                    return;
                }
                StartupLog($"Session key loaded ({_sessionKey.Length} bytes) → starting normal service");
            }
            _logger.LogInformation("Starting command listener on port {Port}.", CONSOLE_PORT);

            try
            {
                _listener = new TcpListener(IPAddress.Any, CONSOLE_PORT);
                _listener.Start();
                _logger.LogInformation("Listening. Waiting for Console connections...");

                // net48's TcpListener has no CancellationToken overload for accept;
                // stop the listener on cancellation so the pending accept unblocks.
                using var stopReg = stoppingToken.Register(() => { try { _listener.Stop(); } catch { } });

                while (!stoppingToken.IsCancellationRequested)
                {
                    TcpClient client;
                    try
                    {
                        client = await _listener.AcceptTcpClientAsync();
                    }
                    catch (OperationCanceledException) { break; }
                    catch (Exception) when (stoppingToken.IsCancellationRequested) { break; }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error accepting Console connection");
                        continue;
                    }

                    _logger.LogInformation(
                        "Console connected from {EP}", client.Client.RemoteEndPoint);

                    // Handle each Console connection on its own thread.
                    // Frame reads are synchronous, so we offload to a
                    // thread-pool thread via Task.Run.
                    var task = Task.Run(
                        () => HandleConsoleClient(client, stoppingToken),
                        stoppingToken);

                    lock (_taskLock)
                    {
                        _clientTasks.Add(task);
                        _clientTasks.RemoveAll(t => t.IsCompleted);
                    }
                }
            }
            finally
            {
                _listener?.Stop();
                _logger.LogInformation("Tesla Launcher Service stopped.");
            }
        }

        // ── Console Connection Handler ───────────────────────────────────

        private async Task HandleConsoleClient(TcpClient tcpClient, CancellationToken ct)
        {
            using var client = tcpClient;
            using var netStream = client.GetStream();
            netStream.WriteTimeout = 10_000;
            netStream.ReadTimeout  = 30_000;

            // ── OFB crypto negotiation ────────────────────────────────────
            // Both sides exchange 16-byte IVs, then verify "CONF" handshake.

            // Read Console's IV
            var consoleIv = new byte[16];
            try { ReadExactSync(netStream, consoleIv); }
            catch (Exception ex)
            {
                _logger.LogWarning("Failed reading Console IV: {Msg}", ex.Message);
                return;
            }

            // Generate and send our IV
            var podIv = new byte[16];
            using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(podIv);
            netStream.Write(podIv, 0, 16);
            netStream.Flush();

            // Create OFB duplex stream — lives for the entire session
            using var ofb = new OFBDuplexStream(netStream, _sessionKey,
                writeIv: consoleIv,   // pod→console (other's IV)
                readIv:  podIv);      // console→pod (own IV)

            // CONF handshake — verify both sides have the same session key
            var confMsg = Encoding.UTF8.GetBytes("CONF");
            ofb.Write(confMsg, 0, confMsg.Length);
            ofb.Flush();

            var confReply = new byte[4];
            try { ReadExactSync(ofb, confReply); }
            catch (Exception ex)
            {
                _logger.LogWarning("CONF read failed: {Msg}", ex.Message);
                return;
            }
            if (!confReply.SequenceEqual(confMsg))
            {
                _logger.LogWarning("CONF mismatch — session key mismatch, dropping connection.");
                return;
            }

            Stream rpcStream = ofb;
            var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log");
            _logger.LogInformation("Console session started.");

            try
            {
                while (!ct.IsCancellationRequested && client.Connected)
                {
                    RpcRequest request;
                    try
                    {
                        request = PodRpc.ReadRequest(rpcStream);
                    }
                    catch (EndOfStreamException)
                    {
                        _logger.LogInformation("Console disconnected (EOF).");
                        break;
                    }
                    catch (IOException ex)
                    {
                        _logger.LogInformation("Console disconnected (I/O: {Msg}).", ex.Message);
                        break;
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning("Request read error ({Type}): {Msg}",
                            ex.GetType().Name, ex.Message);
                        File.AppendAllText(diagLog,
                            $"[{DateTime.Now:HH:mm:ss}] REQ ERROR: {ex}{Environment.NewLine}");
                        break;
                    }

                    if (request == null) break;

                    var funcName = request.Method ?? "???";
                    IReadOnlyList<JsonElement> args = request.Args ?? new List<JsonElement>();
                    _logger.LogInformation("Command: {Func}({ArgCount} args)", funcName, args.Count);

                    var start  = DateTime.UtcNow;
                    object resultObj = null;
                    string error = null;

                    try
                    {
                        resultObj = await DispatchCommandAsync(funcName, args, ct);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error executing {Func}", funcName);
                        error = ex.Message;
                        File.AppendAllText(diagLog,
                            $"[{DateTime.Now:HH:mm:ss}] CMD {funcName} ERROR: {ex.Message}{Environment.NewLine}");
                    }

                    var dur = DateTime.UtcNow - start;
                    if (error == null)
                    {
                        File.AppendAllText(diagLog,
                            $"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({dur.TotalMilliseconds:F0}ms)" +
                            $"{(resultObj != null ? $" → {resultObj}" : "")}{Environment.NewLine}");
                    }

                    try
                    {
                        PodRpc.WriteResponse(rpcStream, resultObj, error);
                    }
                    catch (IOException)
                    {
                        _logger.LogInformation("Console disconnected while writing response.");
                        break;
                    }
                    catch (Exception serEx)
                    {
                        // Result type not JSON-serializable — send error instead.
                        _logger.LogWarning("Serialize failed for {Func}: {Msg}",
                            funcName, serEx.Message);
                        try { PodRpc.WriteResponse(rpcStream, null, $"Pod error: {serEx.Message}"); }
                        catch { break; }
                    }

                    // ── File receive after InitiateInstallProduct ────────────
                    // The Console streams the game archive on this same OFB
                    // connection immediately after receiving the Guid response:
                    //   8-byte Int64 file size + raw file bytes
                    // Then closes the connection (FIN).
                    if (funcName == "InitiateInstallProduct"
                        && error == null
                        && resultObj is Guid installGuid)
                    {
                        ReceiveInstallFile(rpcStream, installGuid, diagLog);
                        break; // connection done after file transfer
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Unhandled error in Console handler");
                File.AppendAllText(diagLog,
                    $"[{DateTime.Now:HH:mm:ss}] HANDLER ERROR: {ex.GetType().Name}: {ex.Message}{Environment.NewLine}" +
                    $"[{DateTime.Now:HH:mm:ss}]   Inner: {ex.InnerException?.Message}{Environment.NewLine}");
            }

            _logger.LogInformation("Console session ended.");
        }

        // ── Command Dispatch ─────────────────────────────────────────────
        // Arguments arrive as JSON elements (RpcRequest.Args); each case extracts
        // the typed values it needs. The returned object is serialized straight
        // into the RpcResponse.

        private async Task<object> DispatchCommandAsync(
            string functionName, IReadOnlyList<JsonElement> args, CancellationToken ct)
        {
            switch (functionName)
            {
                case "Ping":
                    // Echo back the DateTime parameter (Console measures round-trip)
                    return args.Count > 0 && args[0].ValueKind != JsonValueKind.Null
                        ? args[0].GetDateTime()
                        : DateTime.Now;

                case "GetLaunchableApps":
                {
                    var json = await ForwardToAgentJsonAsync(functionName, args, ct);
                    var wire = FlatArrayToWire(json);
                    var pairs = new LaunchPair[wire.Length];
                    for (int i = 0; i < wire.Length; i++)
                        pairs[i] = wire[i].LaunchPair;
                    return pairs;
                }

                case "GetInstalledApps":
                {
                    var json = await ForwardToAgentJsonAsync(functionName, args, ct);
                    return FlatArrayToWire(json);
                }

                case "GetLaunchedApps":
                {
                    var json = await ForwardToAgentJsonAsync(functionName, args, ct);
                    return FlatLaunchedArrayToWire(json);
                }

                case "FullUpdate":
                {
                    var json = await ForwardToAgentJsonAsync(functionName, args, ct);
                    if (json == null) return new FullUpdateData();
                    var flat = JsonSerializer.Deserialize<FlatFullUpdateData>(json);
                    if (flat == null) return new FullUpdateData();
                    var result2 = new FullUpdateData
                    {
                        InstalledApps = flat.InstalledApps != null
                            ? Array.ConvertAll(flat.InstalledApps, FlatToWire)
                            : Array.Empty<LaunchData>(),
                        LaunchedApps = flat.LaunchedApps != null
                            ? Array.ConvertAll(flat.LaunchedApps, FlatLaunchedToWire)
                            : Array.Empty<LaunchedAppData>(),
                        VolumeLevel = flat.VolumeLevel
                    };
                    return result2;
                }

                case "GetOutOfBandProgress":
                {
                    // Handled directly in the Service — progress is tracked
                    // during file receive on the InitiateInstallProduct connection.
                    var diagPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
                    Guid progressId = Guid.Empty;
                    bool guidMatch = false;
                    if (args.Count > 0 && args[0].ValueKind == JsonValueKind.String
                        && Guid.TryParse(args[0].GetString(), out var pg))
                    {
                        progressId = pg;
                        guidMatch = true;
                    }

                    if (guidMatch)
                    {
                        lock (_installLock)
                        {
                            if (_installProgress.TryGetValue(progressId, out var prog))
                            {
                                _logger.LogDebug(
                                    "GetOutOfBandProgress({Id}): {Pct}% {Status} done={Done}",
                                    progressId, prog.PercentComplete, prog.Status, prog.IsCompleted);
                                return prog;
                            }
                        }
                        File.AppendAllText(diagPath,
                            $"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}");
                    }
                    else
                    {
                        File.AppendAllText(diagPath,
                            $"[{DateTime.Now:HH:mm:ss}] OOBP: arg[0] not a Guid (kind={(args.Count > 0 ? args[0].ValueKind.ToString() : "none")}){Environment.NewLine}");
                    }
                    return new OutOfBandProgress
                    {
                        PercentComplete = 0,
                        Status          = "Unknown call ID",
                        IsCompleted     = true
                    };
                }

                case "LaunchApp":
                {
                    var json = await ForwardToAgentJsonAsync(functionName, args, ct);
                    if (json == null) return 0;
                    var doc = JsonDocument.Parse(json);
                    if (doc.RootElement.TryGetProperty("ProcessId", out var pid))
                        return pid.GetInt32();
                    return 0;
                }

                case "get_VolumeLevel":
                {
                    var json = await ForwardToAgentJsonAsync(functionName, args, ct);
                    return json != null ? JsonSerializer.Deserialize<float>(json) : 1.0f;
                }

                case "InitiateInstallProduct":
                {
                    var callId = Guid.NewGuid();
                    lock (_installLock)
                    {
                        _installProgress[callId] = new OutOfBandProgress
                        {
                            PercentComplete = 0,
                            Status          = "Waiting for file...",
                            IsCompleted     = false
                        };
                    }
                    return callId;
                }

                case "InstallApp":
                {
                    // Flatten the wire LaunchData into the Agent's flat JSON format.
                    var ld = args.Count > 0
                        ? args[0].Deserialize<LaunchData>(PodRpc.JsonOptions)
                        : default;
                    var flat = new
                    {
                        LaunchKey        = ld.LaunchPair.LaunchKey.ToString(),
                        DisplayName      = ld.LaunchPair.DisplayName,
                        WorkingDirectory = ld.WorkingDirectory,
                        ExeFile          = ld.ExeFile,
                        Arguments        = ld.Arguments,
                        AutoRestart      = ld.AutoRestart
                    };
                    var payloadJson = JsonSerializer.Serialize(new object[] { flat });
                    await ForwardToAgentCoreAsync(functionName, launchKey: null, payloadJson, ct);
                    return null;
                }

                case "KillApp":
                case "KillAllOfType":
                case "KillAllApps":
                case "Shutdown":
                case "ClearStore":
                case "RemoveApp":
                case "UninstallApp":
                {
                    await ForwardToAgentJsonAsync(functionName, args, ct);
                    return null;
                }

                case "set_VolumeLevel":
                    await ForwardToAgentJsonAsync(functionName, args, ct);
                    return null;

                default:
                    _logger.LogWarning("Unknown command: {Func}", functionName);
                    await ForwardToAgentJsonAsync(functionName, args, ct);
                    return null;
            }
        }

        // ── Install Product File Receive ─────────────────────────────────

        private const string GAMES_DIR = @"C:\Games";

        private void ReceiveInstallFile(Stream stream, Guid callId, string logPath)
        {
            void Log(string msg) => File.AppendAllText(logPath,
                $"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");

            string tempZip = null;

            try
            {
                // ── Phase 1: Receive file ────────────────────────────────
                var sizeBuf = new byte[8];
                ReadExactSync(stream, sizeBuf);
                long fileSize = BitConverter.ToInt64(sizeBuf, 0);

                Log($"INSTALL {callId:N}: receiving {fileSize:N0} bytes...");
                UpdateProgress(callId, 0, "Receiving file...");

                var dataDir = Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                    "TeslaLauncher");
                Directory.CreateDirectory(dataDir);

                tempZip = Path.Combine(dataDir, $"install_{callId:N}.zip");

                using (var fs = File.Create(tempZip))
                {
                    var buffer = new byte[65536];
                    long received = 0;

                    while (received < fileSize)
                    {
                        var toRead = (int)Math.Min(buffer.Length, fileSize - received);
                        var n = stream.Read(buffer, 0, toRead);
                        if (n == 0) throw new IOException("Connection closed during file transfer.");
                        fs.Write(buffer, 0, n);
                        received += n;

                        // Report receive progress as 0–50%
                        int pct = (int)(received * 50 / fileSize);
                        UpdateProgress(callId, pct, "Receiving file...");
                    }
                }

                Log($"INSTALL {callId:N}: file received ({fileSize:N0} bytes)");

                // ── Phase 2: Extract to C:\Games\ ────────────────────────
                UpdateProgress(callId, 50, "Extracting...");
                Directory.CreateDirectory(GAMES_DIR);

                using (var zip = ZipFile.OpenRead(tempZip))
                {
                    int total = zip.Entries.Count;
                    int done  = 0;

                    foreach (var entry in zip.Entries)
                    {
                        var destPath = Path.GetFullPath(
                            Path.Combine(GAMES_DIR, entry.FullName));

                        // Zip-slip protection
                        if (!destPath.StartsWith(GAMES_DIR, StringComparison.OrdinalIgnoreCase))
                            continue;

                        if (string.IsNullOrEmpty(entry.Name))
                        {
                            // Directory entry
                            Directory.CreateDirectory(destPath);
                        }
                        else
                        {
                            Directory.CreateDirectory(Path.GetDirectoryName(destPath));
                            entry.ExtractToFile(destPath, overwrite: true);
                        }

                        done++;
                        // Report extract progress as 50–95%
                        int pct = 50 + (done * 45 / Math.Max(total, 1));
                        UpdateProgress(callId, pct, "Extracting...");
                    }
                }

                Log($"INSTALL {callId:N}: extracted to {GAMES_DIR}");

                // ── Phase 3: Run postinstall.bat if present ──────────────
                var postInstall = Path.Combine(GAMES_DIR, "postinstall.bat");
                if (File.Exists(postInstall))
                {
                    UpdateProgress(callId, 96, "Running postinstall...");
                    Log($"INSTALL {callId:N}: running postinstall.bat");

                    try
                    {
                        using var proc = System.Diagnostics.Process.Start(
                            new System.Diagnostics.ProcessStartInfo
                            {
                                FileName         = postInstall,
                                WorkingDirectory = GAMES_DIR,
                                UseShellExecute  = false,
                                CreateNoWindow   = true
                            });
                        proc?.WaitForExit(60_000); // 60 s timeout
                    }
                    catch (Exception ex)
                    {
                        Log($"INSTALL {callId:N}: postinstall.bat error: {ex.Message}");
                    }

                    try { File.Delete(postInstall); } catch { }
                    Log($"INSTALL {callId:N}: postinstall.bat completed and removed");
                }

                // ── Done ─────────────────────────────────────────────────
                // Console checks PercentComplete == 99 to break out of its
                // 3-iteration install loop (InstallProductWorker in PodInfo).
                // Using 100 causes the Console to retry all 3 times.
                UpdateProgress(callId, 99, "Complete", isCompleted: true);
                Log($"INSTALL {callId:N}: install complete");
            }
            catch (Exception ex)
            {
                UpdateProgress(callId, 0, $"Failed: {ex.Message}", isCompleted: true);
                Log($"INSTALL {callId:N}: FAILED: {ex.Message}");
                _logger.LogError(ex, "Install {Id} failed", callId);
            }
            finally
            {
                // Clean up temp zip
                try { if (tempZip != null) File.Delete(tempZip); } catch { }
            }
        }

        private string[] GetProgressKeys()
        {
            lock (_installLock)
            {
                return Array.ConvertAll(
                    new List<Guid>(_installProgress.Keys).ToArray(),
                    g => g.ToString());
            }
        }

        // ── Flat ↔ Wire Conversion ────────────────────────────────────────

        private class FlatLaunchData
        {
            public string LaunchKey        { get; set; }
            public string DisplayName      { get; set; }
            public string WorkingDirectory { get; set; }
            public string ExeFile          { get; set; }
            public string Arguments        { get; set; }
            public bool   AutoRestart      { get; set; }
        }

        private class FlatLaunchedAppData
        {
            public string LaunchKey { get; set; }
            public int    ProcessId { get; set; }
        }

        private class FlatFullUpdateData
        {
            public FlatLaunchData[]        InstalledApps { get; set; }
            public FlatLaunchedAppData[]   LaunchedApps  { get; set; }
            public float                   VolumeLevel   { get; set; }
        }

        private static LaunchData FlatToWire(FlatLaunchData f)
        {
            return new LaunchData
            {
                LaunchPair = new LaunchPair
                {
                    LaunchKey   = Guid.TryParse(f.LaunchKey, out var g) ? g : Guid.Empty,
                    DisplayName = f.DisplayName
                },
                WorkingDirectory = f.WorkingDirectory,
                ExeFile          = f.ExeFile,
                Arguments        = f.Arguments,
                AutoRestart      = f.AutoRestart
            };
        }

        private static LaunchedAppData FlatLaunchedToWire(FlatLaunchedAppData f)
        {
            return new LaunchedAppData
            {
                LaunchKey = Guid.TryParse(f.LaunchKey, out var g) ? g : Guid.Empty,
                ProcessId = f.ProcessId
            };
        }

        private static LaunchedAppData[] FlatLaunchedArrayToWire(string json)
        {
            if (json == null) return Array.Empty<LaunchedAppData>();
            var flat = JsonSerializer.Deserialize<FlatLaunchedAppData[]>(json);
            if (flat == null) return Array.Empty<LaunchedAppData>();
            return Array.ConvertAll(flat, FlatLaunchedToWire);
        }

        private static LaunchData[] FlatArrayToWire(string json)
        {
            if (json == null) return Array.Empty<LaunchData>();
            var flat = JsonSerializer.Deserialize<FlatLaunchData[]>(json);
            if (flat == null) return Array.Empty<LaunchData>();
            var result = new LaunchData[flat.Length];
            for (int i = 0; i < flat.Length; i++)
                result[i] = FlatToWire(flat[i]);
            return result;
        }

        private void UpdateProgress(Guid callId, int pct, string status, bool isCompleted = false)
        {
            lock (_installLock)
            {
                _installProgress[callId] = new OutOfBandProgress
                {
                    PercentComplete = pct,
                    Status          = status,
                    IsCompleted     = isCompleted
                };
            }
        }

        // ── Secure Configuration ─────────────────────────────────────────

        private static bool IsVirtualAdapter(System.Net.NetworkInformation.NetworkInterface nic)
        {
            var desc = nic.Description ?? ""; var name = nic.Name ?? "";
            string[] markers = { "Virtual", "Hyper-V", "VMware", "VirtualBox",
                "Loopback", "Tunnel", "Miniport", "Wi-Fi Direct", "Bluetooth" };
            foreach (var m in markers)
                if (desc.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0 ||
                    name.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0) return true;
            var mac = nic.GetPhysicalAddress().GetAddressBytes();
            if (mac.Length == 6 && (mac[0] & 0x02) != 0) return true;
            return false;
        }

        private static bool IsMachineConfigured()
        {
            foreach (var nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
            {
                if (nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Ethernet) continue;
                if (nic.OperationalStatus    != System.Net.NetworkInformation.OperationalStatus.Up)          continue;
                if (IsVirtualAdapter(nic))                                                                    continue;
                try
                {
                    var ipv4 = nic.GetIPProperties().GetIPv4Properties();
                    if (!ipv4.IsDhcpEnabled) return true;   // static = configured
                }
                catch (System.Net.NetworkInformation.NetworkInformationException) { }
            }
            return false;
        }

        private void RunSecureConfiguration(CancellationToken ct)
        {
            var logPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
            void Log(string msg) => File.AppendAllText(logPath,
                $"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");

            try
            {
                using var configurator = new PodSecureConfigurator(
                    adapterName: null,
                    logger: msg => { Log(msg); _logger.LogInformation(msg); });

                // 10-minute timeout — rebroadcasts automatically every 2 s
                var config = configurator.Configure(timeoutMs: 600_000);

                if (config == null)
                {
                    _logger.LogWarning(
                        "SecureConfiguration timed out. Machine will need a reboot to retry.");
                    return;
                }

                _logger.LogInformation("SecureConfiguration complete.");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "SecureConfiguration failed");
            }
        }

        // ── Session Key ──────────────────────────────────────────────────

        private static byte[] LoadSessionKey()
        {
            if (!File.Exists(KEY_FILE)) return null;
            using var fs = File.Open(KEY_FILE, FileMode.Open, FileAccess.Read, FileShare.Read);
            var len = fs.ReadByte();
            if (len <= 0) return null;
            var key = new byte[len];
            int off = 0;
            while (off < key.Length)
            {
                int n = fs.Read(key, off, key.Length - off);
                if (n == 0) break;
                off += n;
            }
            return off == key.Length ? key : null;
        }

        /// <summary>Reads exactly buf.Length bytes from stream; throws IOException on EOF.</summary>
        private static void ReadExactSync(Stream stream, byte[] buf)
        {
            int off = 0;
            while (off < buf.Length)
            {
                int n = stream.Read(buf, off, buf.Length - off);
                if (n == 0) throw new IOException("Connection closed before all bytes received.");
                off += n;
            }
        }

        // ── Named Pipe IPC ───────────────────────────────────────────────

        private Task<string> ForwardToAgentJsonAsync(
            string functionName, IReadOnlyList<JsonElement> args, CancellationToken ct)
        {
            string launchKey = null;
            if (args != null && args.Count > 0 && args[0].ValueKind == JsonValueKind.String
                && Guid.TryParse(args[0].GetString(), out var guid))
                launchKey = guid.ToString();

            string payloadJson = (args != null && args.Count > 0)
                ? JsonSerializer.Serialize(args)
                : null;

            return ForwardToAgentCoreAsync(functionName, launchKey, payloadJson, ct);
        }

        private async Task<string> ForwardToAgentCoreAsync(
            string functionName, string launchKey, string payloadJson, CancellationToken ct)
        {
            var msg = new IpcMessage
            {
                Command     = functionName.ToUpperInvariant(),
                LaunchKey   = launchKey,
                PayloadJson = payloadJson
            };

            using var pipe = new NamedPipeClientStream(
                ".", PIPE_NAME, PipeDirection.InOut, PipeOptions.Asynchronous);

            try
            {
                await pipe.ConnectAsync(AGENT_CONNECT_TIMEOUT_MS, ct);
            }
            catch (TimeoutException)
            {
                _logger.LogWarning(
                    "Agent pipe timeout — is TeslaLauncherAgent.exe running?");
                throw new Exception(
                    "Cockpit agent is not available. " +
                    "Ensure TeslaLauncherAgent.exe is running in the user session.");
            }

            // Send IpcMessage
            var reqBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg));
            await pipe.WriteAsync(BitConverter.GetBytes(reqBytes.Length), 0, 4, ct);
            await pipe.WriteAsync(reqBytes, 0, reqBytes.Length, ct);
            await pipe.FlushAsync(ct);

            // Read IpcResponse
            var lenBuf = new byte[4];
            await ReadExactAsync(pipe, lenBuf, ct);
            var resBuf = new byte[BitConverter.ToInt32(lenBuf, 0)];
            await ReadExactAsync(pipe, resBuf, ct);

            var response = JsonSerializer.Deserialize<IpcResponse>(
                Encoding.UTF8.GetString(resBuf));

            if (response == null)
                throw new Exception("Null response from Agent.");

            if (!response.Success)
                throw new Exception(response.Message ?? "Agent returned failure.");

            if (response.Data is System.Text.Json.JsonElement je)
                return je.GetRawText();
            return response.Data?.ToString();
        }

        private static async Task ReadExactAsync(Stream stream, byte[] buf, CancellationToken ct)
        {
            var off = 0;
            while (off < buf.Length)
            {
                var n = await stream.ReadAsync(buf, off, buf.Length - off, ct);
                if (n == 0) throw new IOException("Pipe closed before all bytes were read.");
                off += n;
            }
        }
    }

    // ── Entry Point ──────────────────────────────────────────────────────

    public class Program
    {
        public static void Main(string[] args)
        {
            Host.CreateDefaultBuilder(args)
                .UseWindowsService(options =>
                {
                    options.ServiceName = "Tesla Application Launcher";
                })
                .ConfigureServices(services =>
                {
                    services.AddHostedService<TeslaLauncherService>();
                })
                .ConfigureLogging(logging =>
                {
                    logging.AddEventLog(settings =>
                    {
                        settings.SourceName = "Tesla Application Launcher";
                    });
                })
                .Build()
                .Run();
        }
    }
}
