// rmfshare — Windows Share-target activation handler for rmfmail (C46).
//
// Flow when the user picks "rmfmail" in the Windows Share sheet:
//   1. Windows activates this exe (declared in the sparse AppxManifest that
//      `rmfmail -register-share` registered) with ShareTarget activation.
//   2. We read the DataPackageView — title, text, web link, storage items —
//      while the ShareOperation is still alive (the source app's data is
//      only guaranteed reachable until ReportCompleted).
//   3. Files are copied into ~/.rmfmail/share-staging/<stamp>/ because the
//      source StorageFiles live in the sharing app's sandbox — a raw path
//      may not exist or may vanish as soon as the share pane closes.
//   4. Write ~/.rmfmail/pending-share.json (atomic rename), ReportCompleted,
//      then launch/poke the daemon exactly like the mailto handler: if a
//      daemon runs, its fs.watch fires; if not, we spawn `node mailx.js`
//      and the client's startup poll consumes the file.
//
// Launched without share activation (e.g. double-clicked): just launch
// rmfmail — harmless, and better than an error box.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.DataTransfer;
using Windows.ApplicationModel.DataTransfer.ShareTarget;
using WinRT;   // CastExtensions.As<T> — AOT-safe WinRT QueryInterface
using Windows.Storage;

internal static class Program
{
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern int MessageBoxW(nint hwnd, string text, string caption, uint type);

    private static void ShowError(string msg) =>
        MessageBoxW(0, msg, "rmfmail share target", 0x10 /* MB_ICONERROR */);

    private static string RmfmailDir =>
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".rmfmail");

    /// Trace log (~/.rmfmail/logs/rmfshare.log). The first field failure
    /// (Bob 2026-07-30: share restarted the app, no pending file, no error
    /// box) was undiagnosable because this exe wrote nothing anywhere —
    /// every activation now leaves a breadcrumb trail. Append-only, best
    /// effort, never throws.
    private static void Log(string msg)
    {
        try
        {
            var dir = Path.Combine(RmfmailDir, "logs");
            Directory.CreateDirectory(dir);
            File.AppendAllText(Path.Combine(dir, "rmfshare.log"),
                $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {msg}\r\n");
        }
        catch { /* logging must never break the share */ }
    }

    [STAThread]
    private static int Main()
    {
        try
        {
            IActivatedEventArgs? activation = null;
            string activationError = "";
            try { activation = AppInstance.GetActivatedEventArgs(); }
            catch (Exception ex) { activationError = ex.Message; }
            Log($"activated: kind={(activation == null ? $"null ({activationError})" : activation.Kind.ToString())}");

            // AOT trap (field-diagnosed 2026-07-30 via this very log): the
            // idiomatic `activation is ShareTargetActivatedEventArgs` returns
            // FALSE under NativeAOT — .NET type-identity casts on projected
            // WinRT classes fail when trimming drops the projection metadata,
            // even though Kind reads ShareTarget through the base interface.
            // That silently took the "plain launch" branch: no pending file,
            // no --handoff, bare launch → replace-on-launch restarted the
            // whole app on every share. Branch on Kind and cast via WinRT
            // QueryInterface (`.As<T>()`), which is AOT-safe.
            if (activation != null && activation.Kind == ActivationKind.ShareTarget)
            {
                try
                {
                    var share = activation.As<ShareTargetActivatedEventArgs>();
                    HandleShare(share).GetAwaiter().GetResult();
                }
                catch (Exception ex)
                {
                    Log($"share handling FAILED: {ex}");
                    ShowError($"Share to rmfmail failed:\n{ex.Message}");
                }
                // Handoff launch either way — a share was intended; never
                // restart the running daemon out from under the user.
                LaunchDaemon(handoff: true);
            }
            else
            {
                // Plain double-click — a normal launch (replace is fine).
                LaunchDaemon(handoff: false);
            }
            return 0;
        }
        catch (Exception ex)
        {
            Log($"FAILED: {ex}");
            ShowError($"Share to rmfmail failed:\n{ex.Message}");
            return 1;
        }
    }

    private static async Task HandleShare(ShareTargetActivatedEventArgs share)
    {
        var op = share.ShareOperation;
        try { op.ReportStarted(); } catch { /* optional progress hint only */ }

        var data = op.Data;
        string title = "";
        try { title = data.Properties?.Title ?? ""; } catch { }
        string text = "";
        string url = "";
        var files = new List<(string Name, string Path, long Size)>();

        if (data.Contains(StandardDataFormats.Text))
        {
            try { text = await data.GetTextAsync(); } catch { }
        }
        if (data.Contains(StandardDataFormats.WebLink))
        {
            try { url = (await data.GetWebLinkAsync())?.AbsoluteUri ?? ""; } catch { }
        }
        if (data.Contains(StandardDataFormats.StorageItems))
        {
            var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss") + "-" +
                        Random.Shared.Next(10000).ToString("D4");
            var staging = Path.Combine(RmfmailDir, "share-staging", stamp);
            var items = await data.GetStorageItemsAsync();
            foreach (var item in items)
            {
                if (item is not StorageFile f) continue; // folders: not meaningful as attachments
                Directory.CreateDirectory(staging);
                var dest = Path.Combine(staging, SafeName(f.Name, files.Count));
                // Copy via the WinRT stream — f.Path can be empty for
                // virtual/sandboxed items (e.g. shares out of UWP apps).
                using (var src = (await f.OpenReadAsync()).AsStreamForRead())
                using (var dst = File.Create(dest))
                {
                    await src.CopyToAsync(dst);
                }
                files.Add((f.Name, dest, new FileInfo(dest).Length));
            }
        }

        Log($"share: title={title.Length}ch text={text.Length}ch url=\"{url}\" files={files.Count}");
        WritePendingShare(title, text, url, files);
        Log("pending-share.json written");

        try { op.ReportCompleted(); } catch { /* pane may already be gone */ }
    }

    /// Avoid collisions when two shared files carry the same name.
    private static string SafeName(string name, int index)
    {
        foreach (var c in Path.GetInvalidFileNameChars()) name = name.Replace(c, '_');
        return index == 0 ? name
            : Path.GetFileNameWithoutExtension(name) + $" ({index})" + Path.GetExtension(name);
    }

    /// Hand-rolled JSON writer — keeps NativeAOT trivially happy (no
    /// reflection serializer, no source-gen context for a 5-field payload).
    private static void WritePendingShare(
        string title, string text, string url, List<(string Name, string Path, long Size)> files)
    {
        var sb = new StringBuilder();
        sb.Append('{');
        sb.Append("\"title\":").Append(Quote(title)).Append(',');
        sb.Append("\"text\":").Append(Quote(text)).Append(',');
        sb.Append("\"url\":").Append(Quote(url)).Append(',');
        sb.Append("\"files\":[");
        for (int i = 0; i < files.Count; i++)
        {
            if (i > 0) sb.Append(',');
            sb.Append("{\"name\":").Append(Quote(files[i].Name))
              .Append(",\"path\":").Append(Quote(files[i].Path))
              .Append(",\"size\":").Append(files[i].Size).Append('}');
        }
        sb.Append("],");
        sb.Append("\"writtenAt\":").Append(DateTimeOffset.Now.ToUnixTimeMilliseconds());
        sb.Append('}');

        Directory.CreateDirectory(RmfmailDir);
        var target = Path.Combine(RmfmailDir, "pending-share.json");
        var tmp = target + ".tmp";
        File.WriteAllText(tmp, sb.ToString(), new UTF8Encoding(false));
        File.Move(tmp, target, overwrite: true);
    }

    private static string Quote(string s)
    {
        var sb = new StringBuilder(s.Length + 2);
        sb.Append('"');
        foreach (var c in s)
        {
            switch (c)
            {
                case '"': sb.Append("\\\""); break;
                case '\\': sb.Append("\\\\"); break;
                case '\n': sb.Append("\\n"); break;
                case '\r': sb.Append("\\r"); break;
                case '\t': sb.Append("\\t"); break;
                default:
                    if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4"));
                    else sb.Append(c);
                    break;
            }
        }
        sb.Append('"');
        return sb.ToString();
    }

    /// Same node + mailx.js lookup as the Rust rmfmailto launcher: env
    /// overrides first, then the standard npm -g install location, then PATH.
    /// A bare `node mailx.js` is correct both ways — if a daemon is already
    /// running the new process exits at the instance check (fs.watch on the
    /// pending file has already fired); otherwise it becomes the daemon and
    /// the client's startup poll consumes the file.
    private static void LaunchDaemon(bool handoff)
    {
        var mailxJs = Environment.GetEnvironmentVariable("RMFMAIL_JS");
        if (string.IsNullOrEmpty(mailxJs) || !File.Exists(mailxJs))
        {
            var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            mailxJs = Path.Combine(appData, "npm", "node_modules",
                "@bobfrankston", "rmfmail", "bin", "mailx.js");
        }
        if (!File.Exists(mailxJs))
        {
            ShowError($"rmfmail not found at {mailxJs}.\nInstall with: npm install -g @bobfrankston/rmfmail");
            return;
        }

        var node = FindNode();
        if (node == null)
        {
            ShowError("node.exe not found on PATH — rmfmail needs Node.js installed.");
            return;
        }

        var psi = new ProcessStartInfo
        {
            FileName = node,
            UseShellExecute = false,
            CreateNoWindow = true,
            // NOT the mailx.js bin dir: the daemon (and every child it spawns,
            // incl. the session-long msger launcher exe) inherits this cwd,
            // and on Windows a process cwd LOCKS the directory — which made
            // `npm install -g` self-updates fail with EBUSY forever after a
            // share-spawned daemon. The daemon also chdir()s defensively now.
            WorkingDirectory = RmfmailDir,
        };
        psi.ArgumentList.Add(mailxJs);
        // Tells mailx.js this is a share handoff: exit if a daemon is
        // already running (it has the pending file) instead of the default
        // replace-on-launch, which restarted the whole app mid-share.
        if (handoff) psi.ArgumentList.Add("--handoff");
        Log($"launching: {node} {mailxJs}{(handoff ? " --handoff" : "")}");
        Process.Start(psi);
    }

    private static string? FindNode()
    {
        var env = Environment.GetEnvironmentVariable("NODE_EXE");
        if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
        foreach (var dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(';'))
        {
            if (dir.Length == 0) continue;
            try
            {
                var candidate = Path.Combine(dir.Trim(), "node.exe");
                if (File.Exists(candidate)) return candidate;
            }
            catch { /* malformed PATH entry */ }
        }
        return null;
    }
}
