using Deploymentcenter.Client;
using Serilog;

namespace Predictalytics.Hosting;

/// <summary>
/// Checks the Deployment Center UpdateService for a newer release
/// (GET /api/updateservice/v1/check).
///
/// The check itself only reads metadata. Installing is done by the standalone
/// update-agent, which replaces the running installation and therefore has to be
/// started explicitly by the user.
///
/// Since Deployment Center 2.4 the release directories sit behind HTTP basic auth:
/// the license key is the credential. Without it every download answers 401, so the
/// key has to travel with both the check and the agent handover.
/// </summary>
public static class DcUpdateService
{
    /// <summary>
    /// Runtime identifier the release directories are split by (win-x64, linux-x64, …).
    /// Taken from the SDK so app and agent agree on the spelling.
    /// </summary>
    public static string Platform => PlatformId.Current;

    public static async Task<UpdateCheckResult> CheckAsync(
        string channel, string? licenseKey = null, CancellationToken ct = default)
    {
        var client = new UpdateClient();
        return await client.CheckForUpdateAsync(
            baseUrl: DcConfig.BaseUrl,
            projectId: DcConfig.ProductSlug,
            currentVersion: DcConfig.AppVersion,
            channel: NormalizeChannel(channel),
            platform: Platform,
            credentials: ResolveCredentials(licenseKey),
            cancellationToken: ct).ConfigureAwait(false);
    }

    /// <summary>Path of the update agent next to the exe, or null if it was not deployed.</summary>
    public static string? FindUpdateAgent()
    {
        return UpdateClient.ResolveAgentPath(AppContext.BaseDirectory);
    }

    /// <summary>
    /// Hands control to the update agent and ends this process. Returns false if the agent
    /// is not present — then the update has to be installed by hand.
    /// </summary>
    public static bool LaunchAgent(string channel, string? licenseKey = null)
    {
        var agent = FindUpdateAgent();
        if (agent is null)
        {
            Log.Warning("Update-Agent ({Agent}) liegt nicht neben der Anwendung — Update bitte manuell einspielen.",
                UpdateClient.AgentFileName);
            return false;
        }

        var key = ResolveLicenseKey(licenseKey);
        if (string.IsNullOrWhiteSpace(key))
        {
            // The agent would download, get 401 and abort — with the app already closed.
            Log.Warning("Kein Lizenzschlüssel verfügbar — der Update-Agent käme nicht an das Paket. " +
                        "Update wird nicht gestartet.");
            return false;
        }

        var restartPath = ResolveRestartPath();
        Log.Information("Starte Update-Agent {Agent} (Kanal {Channel}, Plattform {Platform}) und beende die Anwendung...",
            agent, NormalizeChannel(channel), Platform);

        return UpdateClient.LaunchUpdateAgent(
            agentPath: agent,
            projectId: DcConfig.ProductSlug,
            channel: NormalizeChannel(channel),
            action: "update",
            version: "latest",
            exitCurrentApp: true,
            restartPath: restartPath,
            currentVersion: DcConfig.AppVersion,
            platform: Platform,
            // The agent waits for this process to go away before it copies over
            // the installation — otherwise it writes onto still-locked assemblies.
            waitForCurrentProcess: true,
            licenseKey: key);
    }

    /// <summary>
    /// Basic-auth credentials for the protected release directories. Falls back to the key
    /// of the last successful activation, which also covers the headless path.
    /// </summary>
    private static ReleaseCredentials? ResolveCredentials(string? licenseKey)
        => ReleaseCredentials.FromLicenseKey(ResolveLicenseKey(licenseKey));

    private static string? ResolveLicenseKey(string? licenseKey)
        => string.IsNullOrWhiteSpace(licenseKey)
            ? LicenseClient.TryGetCachedKey(DcConfig.ProductSlug)
            : licenseKey;

    /// <summary>
    /// Executable the agent should start again once it is done.
    ///
    /// Passing null is not an option: the SDK then falls back to Environment.ProcessPath
    /// itself, so the "dotnet" case below cannot be opted out of — it has to be resolved
    /// to the apphost here.
    /// </summary>
    private static string? ResolveRestartPath()
    {
        var path = Environment.ProcessPath;

        // Started as "dotnet Predictalytics.Shell.dll": ProcessPath is the shared runtime
        // host, and restarting that without its argument would launch nothing. The apphost
        // next to the entry assembly is what the user actually started from.
        if (path is null || Path.GetFileNameWithoutExtension(path).Equals("dotnet", StringComparison.OrdinalIgnoreCase))
        {
            var entry = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name;
            if (!string.IsNullOrWhiteSpace(entry))
            {
                var appHost = Path.Combine(
                    AppContext.BaseDirectory,
                    OperatingSystem.IsWindows() ? entry + ".exe" : entry!);
                if (File.Exists(appHost)) return appHost;
            }
        }

        return path;
    }

    private static string NormalizeChannel(string channel)
        => string.IsNullOrWhiteSpace(channel) ? "prod" : channel.Trim();
}
