using System.Reflection;

namespace DodoSSH.Client.Session;

/// <summary>
/// A build newer than the one running.
/// </summary>
/// <remarks>
/// A version string and nothing else, so that whatever the update framework hands back never crosses this
/// seam. The implementation keeps its own richer answer privately and matches on the version when it is
/// handed one of these back; everything above only ever needs the number to print.
/// </remarks>
/// <param name="Version">What the newer build calls itself.</param>
public sealed record AvailableUpdate(string Version);

/// <summary>
/// Where newer builds of this client come from.
/// </summary>
/// <remarks>
/// <para>
/// <b>The release channel is the project's own, and never the deployment the client is signed in to.</b>
/// That is ADR 0011 rule 2 — see <c>docs/adr/0011-android-distribution.md</c> — and it is a security
/// property rather than a preference:
/// an operator who could answer the update check could pin a chosen user to a build with a known hole, by
/// withholding the answer, without holding any key at all. The implementation's feed address is therefore
/// a constant and not a setting, and this interface has no member that would let a caller name one.
/// </para>
/// <para>
/// <b>An interface rather than a delegate</b>, which is a deliberate departure from the shell's habit —
/// <c>SignInHandler</c>, <c>resume</c> and <c>copyToClipboard</c> are all delegates so that the view models
/// stay drivable by a test with no network. That shape fits a single call. This is four operations, an
/// availability question and an order they have to happen in, and four delegates in a constructor is worse
/// than one interface. <see cref="IDeviceKeyStore"/> is the same shape in the same assembly and settled the
/// same trade.
/// </para>
/// <para>
/// Nothing here throws for the ordinary failures. A forge that cannot be reached is a laptop on a train,
/// not a fault, and the caller's answer is to try again later.
/// </para>
/// </remarks>
public interface IUpdateChannel
{
    /// <summary>Whether this copy of the client is one that can replace itself.</summary>
    /// <remarks>
    /// False for a build run from a source checkout, for a copy somebody unzipped by hand, and for every
    /// platform this has not been built for. Asked before offering anything, because an offer that cannot
    /// be carried out is worse than no offer.
    /// </remarks>
    bool IsSupported { get; }

    /// <summary>What this build calls itself.</summary>
    /// <remarks>
    /// Answered even when <see cref="IsSupported"/> is false. Which version is running is a fact worth
    /// printing on a build that cannot update itself — arguably more so, since somebody will have to
    /// replace it by hand.
    /// </remarks>
    string CurrentVersion { get; }

    /// <summary>Asks the release channel whether there is anything newer.</summary>
    /// <returns>
    /// The newer build, or <see langword="null"/> if this one is current. <b>Null means exactly that and
    /// nothing else.</b>
    /// </returns>
    /// <remarks>
    /// <para>
    /// ◆ <b>A channel that cannot be reached throws, and this used to say the opposite.</b> The reasoning
    /// was that the caller does the same thing either way, which is true of a background pass and false of
    /// a pressed button: <c>UpdateViewModel</c> swallows a failure on the timer and reports one on
    /// CHECK NOW, and it can only do the second if there is something to report. Answering null for an
    /// unreachable forge turned every failure into "you are on the latest build".
    /// </para>
    /// <para>
    /// That was not merely a bad answer, it was the wrong one about a real outage: the release repository
    /// was private, so every check on every phone got a 404 and every check said the build was current.
    /// Nobody could see it, which is the whole argument for this direction — the caller decides what
    /// silence is worth, and it cannot decide about a failure it was never told about.
    /// </para>
    /// <para>
    /// <b>The feed must be readable without credentials, and that is a precondition rather than an
    /// implementation detail.</b> A token cannot live anywhere useful here: it would be needed before
    /// unlock, so it cannot be in the encrypted cache, and putting it in plaintext beside the cache is
    /// exactly what <c>ClientSettings</c> refuses. See ADR 0013 and ADR 0014.
    /// </para>
    /// </remarks>
    Task<AvailableUpdate?> CheckAsync(CancellationToken cancellationToken);

    /// <summary>Fetches an update a previous <see cref="CheckAsync"/> found.</summary>
    /// <param name="update">The update to fetch. Must be one this source returned.</param>
    /// <param name="progress">Reports percentage complete.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    Task DownloadAsync(AvailableUpdate update, IProgress<int> progress, CancellationToken cancellationToken);

    /// <summary>
    /// Whether <see cref="ApplyAndRestart"/> ends this process itself.
    /// </summary>
    /// <remarks>
    /// <para>
    /// True on a desktop, where applying an update is this application replacing its own files and
    /// restarting — the caller tears the vault down first, because that is what zeroes the identity keys,
    /// the vault keys and the cache key while there is still a process to zero them in.
    /// </para>
    /// <para>
    /// <b>False on the phone, and it is not a detail.</b> There, applying hands the package to Android's
    /// own installer and returns; what happens next is a system dialogue the user may perfectly well say
    /// no to. A caller that had already disposed the vault would answer "not now" with a locked keychain
    /// and every shell gone — punishing somebody for declining an update. Where this is false the caller
    /// leaves the session alone, and the keys go when the platform kills the process, which it does the
    /// moment the install is agreed to.
    /// </para>
    /// </remarks>
    bool ApplyingEndsTheProcess { get; }

    /// <summary>
    /// Swaps this build for the downloaded one and starts it.
    /// </summary>
    /// <remarks>
    /// <b>This does not return where <see cref="ApplyingEndsTheProcess"/> is true.</b> It is the reason the
    /// whole capability is behind an interface rather
    /// than being three lines inline in the view model: a test cannot drive a method that ends the process,
    /// and the single most important thing to be able to assert about this feature is that a downloaded
    /// update is <em>never</em> applied unless somebody asked for it. A fake that records the call instead
    /// of making it is what makes that assertion writable.
    /// </remarks>
    /// <param name="update">The update to apply. Must be one that has been downloaded.</param>
    void ApplyAndRestart(AvailableUpdate update);
}

/// <summary>
/// A copy of the client that cannot replace itself.
/// </summary>
/// <remarks>
/// <para>
/// Four different situations resolve to this one object, and they are deliberately not distinguished: a
/// build started from a source checkout, a copy extracted by hand from an archive, a desktop platform this
/// has not been packaged for, and the phone — whose distribution is settled separately by ADR 0011 and has
/// no updater at all. All four mean the same thing. <em>This copy was not installed by anything that knows
/// how to replace it</em>, and saying so is different from pretending otherwise.
/// </para>
/// <para>
/// A null object rather than a nullable field on the view model, following
/// <see cref="UnavailableDeviceKeyStore"/>: the caller then has one shape to write against, and "no
/// updater here" is a thing the interface can express rather than a case every call site has to remember.
/// </para>
/// </remarks>
public sealed class UnavailableUpdateChannel : IUpdateChannel
{
    /// <inheritdoc />
    public bool IsSupported => false;

    /// <inheritdoc />
    public string CurrentVersion => ClientVersion.Current;

    /// <summary>Always true, and never read: nothing here applies anything.</summary>
    /// <remarks>
    /// True rather than false because it is the answer that makes a caller who ignores
    /// <see cref="IsSupported"/> do the safer thing — tear the session down — before reaching a method
    /// that is about to throw at it anyway.
    /// </remarks>
    public bool ApplyingEndsTheProcess => true;

    /// <inheritdoc />
    public Task<AvailableUpdate?> CheckAsync(CancellationToken cancellationToken) =>
        Task.FromResult<AvailableUpdate?>(null);

    /// <inheritdoc />
    public Task DownloadAsync(
        AvailableUpdate update,
        IProgress<int> progress,
        CancellationToken cancellationToken) =>
        throw new NotSupportedException(
            "This copy of DodoSSH cannot replace itself. Check IsSupported before offering an update.");

    /// <inheritdoc />
    public void ApplyAndRestart(AvailableUpdate update) =>
        throw new NotSupportedException(
            "This copy of DodoSSH cannot replace itself. Check IsSupported before offering an update.");
}

/// <summary>
/// What this build calls itself.
/// </summary>
/// <remarks>
/// <para>
/// Read from the assembly rather than from the update framework, so that it answers on a build that has no
/// updater — a source checkout, or the phone. The version a user is shown should not depend on whether the
/// thing that could replace it happens to be present.
/// </para>
/// <para>
/// The informational version is the one that carries the whole number, including any prerelease part;
/// <c>AssemblyVersion</c> is <c>major.0.0.0</c> by MinVer's design and would print <c>0.0.0</c> for every
/// 0.x build. The <c>+sha</c> build metadata is trimmed because it is for a machine, and this string is
/// read aloud in support conversations.
/// </para>
/// </remarks>
public static class ClientVersion
{
    /// <summary>The running build's version, without build metadata.</summary>
    public static string Current { get; } = Read();

    private static string Read()
    {
        var informational = (Assembly.GetEntryAssembly() ?? typeof(ClientVersion).Assembly)
            .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
            ?.InformationalVersion;

        if (string.IsNullOrWhiteSpace(informational))
        {
            return "0.0.0";
        }

        var plus = informational.IndexOf('+', StringComparison.Ordinal);

        return plus < 0 ? informational : informational[..plus];
    }
}
