using System.Text.Json.Serialization;

namespace NucleicBroker;

/// <summary>
/// The one seam between broker logic and Microsoft.WSL.Containers (docs/WINDOWS_PORT.md
/// §3.3): everything the RPC surface needs from wslc, and nothing WinRT. The real facade
/// (Wslc/WslcFacade.cs, compiled with -p:UseWslc=true) adapts the preview API; unit tests
/// substitute FakeWslc; non-Windows builds get UnavailableWslc. Async events (process stdio,
/// pull progress, session death) flow out through <see cref="IBrokerEvents"/> so no wslc
/// event thread ever blocks on the stdio pipe.
/// </summary>
public interface IWslc
{
    /// <summary>Reported in the `hello` capabilities exchange; null when wslc is absent.</summary>
    string? WslcVersion { get; }

    /// <summary>
    /// What this facade can actually do, merged into the `hello` capabilities so hostd degrades
    /// instead of discovering the gap at the call site (docs/WINDOWS_PORT.md §2.3, D13). The
    /// compat SDK alone cannot serve four of them, so a compat-only broker reports none of:
    /// <list type="bullet">
    /// <item><c>enumerate</c> — service-backed container enumeration. Without it
    ///   <c>container.list</c> answers from the broker's OWN roster, so it goes empty across a
    ///   broker restart and <c>ContainerManager.reconcile</c> sees an empty sandbox.</item>
    /// <item><c>reattach</c> — re-adopting a running session or container. Without it a broker
    ///   restart cannot recover the session and <c>session.ensure</c> fails
    ///   <see cref="WslcError.SessionExists"/>.</item>
    /// <item><c>tty</c> — pty allocation and resize, i.e. §7's Terminal panel.</item>
    /// <item><c>stats</c> — per-container resource sampling. Reported when stats are available
    ///   by ANY means, including the in-guest cgroup read the compat facade falls back to.</item>
    /// </list>
    /// </summary>
    IReadOnlyList<string> Capabilities { get; }

    /// <summary>Install the sink BEFORE any operation that can emit events.</summary>
    void SetEvents(IBrokerEvents events);

    /// <summary>Missing OS components (WSL, the container service, …), empty when ready.</summary>
    Task<IReadOnlyList<string>> MissingComponentsAsync(CancellationToken ct);
    /// <summary>Install missing components; progress via <see cref="IBrokerEvents.InstallProgress"/>.</summary>
    Task InstallComponentsAsync(CancellationToken ct);

    /// <summary>Create-or-attach the per-channel wslc session; returns the WSL-facing host
    /// gateway address guests reach the host on (docs/WINDOWS_PORT.md §5).</summary>
    Task<string> EnsureSessionAsync(SessionSpec spec, CancellationToken ct);
    Task TerminateSessionAsync(CancellationToken ct);

    /// <summary>Pull an OCI image; progress via <see cref="IBrokerEvents.PullProgress"/>.</summary>
    Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct);
    Task<IReadOnlyList<ImageInfo>> ListImagesAsync(CancellationToken ct);
    Task DeleteImageAsync(string reference, CancellationToken ct);
    /// <summary>Digest/size for a local image, or null when not present.</summary>
    Task<ImageInfo?> InspectImageAsync(string reference, CancellationToken ct);

    Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct);
    Task StartContainerAsync(string name, CancellationToken ct);
    Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct);
    Task DeleteContainerAsync(string name, bool force, CancellationToken ct);
    Task<IReadOnlyList<ContainerInfo>> ListContainersAsync(CancellationToken ct);
    /// <summary>One of "running" | "stopped" | "absent" (kept coarse on purpose — the Swift
    /// policy layer only distinguishes these three).</summary>
    Task<string> ContainerStateAsync(string name, CancellationToken ct);
    /// <summary>cgroup counters for the resource monitor; null when not running.</summary>
    Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct);

    /// <summary>
    /// Create a process in a running container and attach its event handlers, but **do not run
    /// it** — the caller runs it with <see cref="IWslcProcess.StartAsync"/> once it has sent the
    /// `procId` downstream. `procId` is minted by the broker and keys every event this process
    /// emits through <see cref="IBrokerEvents"/>.
    ///
    /// The two-step split is not ceremony. See <see cref="IWslcProcess.StartAsync"/>.
    /// </summary>
    Task<IWslcProcess> ExecAsync(long procId, ProcSpec spec, CancellationToken ct);
}

/// <summary>Control half of a running in-container process (output arrives via events).</summary>
public interface IWslcProcess
{
    /// <summary>
    /// Actually run the process. Separate from <see cref="IWslc.ExecAsync"/> because a short
    /// command can finish before the `proc.exec` RESPONSE has been written: output and exit ride
    /// the same ordered outbound queue, so starting first puts `proc.exit` on the wire ahead of
    /// the `procId` that identifies it, and a client that registers interest on receiving that
    /// procId waits forever. Observed on hardware with `echo` (docs/WINDOWS_PORT.md §13.3).
    ///
    /// This is the same reasoning that makes wslc itself split `CreateProcess` from `Start` — so
    /// handlers can attach before output flows — applied one level up, to the RPC boundary.
    /// </summary>
    Task StartAsync(CancellationToken ct);

    Task WriteStdinAsync(ReadOnlyMemory<byte> data, CancellationToken ct);
    Task CloseStdinAsync(CancellationToken ct);
    Task SignalAsync(int signal, CancellationToken ct);
    /// <summary>tty mode only (the Terminal panel); no-op for pipe-mode processes.</summary>
    Task ResizeAsync(int cols, int rows, CancellationToken ct);
}

/// <summary>Event sink the broker hands to the facade; implementations must be
/// non-blocking (they enqueue onto the outbound writer).</summary>
public interface IBrokerEvents
{
    void ProcOutput(long procId, bool stderr, ReadOnlySpan<byte> chunk);
    void ProcExited(long procId, int code);
    void SessionDown(string reason);
    void PullProgress(string reference, string status, long current, long total);
    void InstallProgress(string status, double percent);
}

/// <summary>A structured facade failure, surfaced to hostd as JSON-RPC error -32000 with
/// `data.kind` so the Swift side can branch (e.g. `brokerLost` vs `imagePullFailed`).</summary>
public sealed class WslcError(string kind, string message) : Exception(message)
{
    public string Kind { get; } = kind;

    public const string Unavailable = "wslc_unavailable";
    public const string NotFound = "not_found";
    public const string NotRunning = "not_running";
    public const string PullFailed = "image_pull_failed";
    public const string StartFailed = "start_failed";
    public const string AiUnavailable = "ai_unavailable";

    /// <summary>
    /// This broker build cannot serve the call as asked — no pty on the compat surface, a signal
    /// outside wslc's six, an image whose `setpriv` cannot drop uid.
    ///
    /// "Not implemented yet", NOT "will never work": each of these has a route (the internal COM
    /// pty, a wider signal map, a different image) that is simply **deprioritized until it can no
    /// longer be avoided**. What the kind tells hostd is only that *retrying the same call against
    /// the same broker will not help* — so surface it and move on rather than backing off and
    /// trying again. Closing any of them is a change here, not a change in what wslc can do.
    /// </summary>
    public const string Unsupported = "unsupported";

    /// <summary>
    /// A CONTAINER of that name already exists in the session. Distinct from
    /// <see cref="SessionExists"/> because wslc answers `ERROR_ALREADY_EXISTS` for both and the
    /// remedies differ completely — remove one container, versus restart the whole WSL stack.
    ///
    /// Reachable today because this broker's container roster is process-local (there is no
    /// enumeration on the compat surface), so a container left behind by a crashed broker holds
    /// its name against every later one. See docs/WINDOWS_PORT.md §13.3.
    /// </summary>
    public const string AlreadyExists = "already_exists";

    /// <summary>A session of that name is already running and this facade cannot re-adopt it
    /// (the compat SDK's `Start()` answers ERROR_ALREADY_EXISTS, and its constructor is lazy, so
    /// a second handle is not a second session). Distinct from <see cref="StartFailed"/> because
    /// the remedy differs: the sandbox is UP, this broker just cannot reach it.</summary>
    public const string SessionExists = "session_exists";
}

// DTOs — property names (after camel-casing) match the §3.3 wire keys exactly.

public sealed record SessionSpec(
    string Name, string DataDir, int? Cpu, long? MemoryMB);

public sealed record RegistryAuth(string? Username, string? Password);

public sealed record ImageInfo(
    [property: JsonPropertyName("ref")] string Ref, string? Digest, long? SizeBytes);

public sealed record VolumeSpec(
    string Host, string Guest, [property: JsonPropertyName("ro")] bool ReadOnly);

public sealed record ContainerCreateSpec(
    string Name, string Image, IReadOnlyList<VolumeSpec>? Volumes,
    string? NetworkingMode, string? Hostname,
    IReadOnlyDictionary<string, string>? Env, IReadOnlyList<string>? InitArgv);

public sealed record ContainerInfo(string Name, string Image, string State);

public sealed record ContainerStatsInfo(
    long CpuUsageUsec, long MemoryUsedBytes, long MemoryLimitBytes, long? OomKills);

public sealed record ProcSpec(
    string Container, IReadOnlyList<string> Argv,
    IReadOnlyDictionary<string, string>? Env, string? Cwd,
    int? Uid, int? Gid, bool Tty);
