/**
 * Windows Share-target integration (C46).
 *
 * Share flow when rmfmail is picked in the Windows Share sheet:
 *
 *   1. Windows activates rmfshare.exe (bin/rmfshare-src/, declared in the
 *      AppxManifest registered by `rmfmail -register-share`).
 *   2. rmfshare.exe copies shared files to ~/.rmfmail/share-staging/<stamp>/,
 *      writes ~/.rmfmail/pending-share.json (atomic rename), and launches
 *      `node mailx.js` — which exits at the instance check if a daemon
 *      already runs (its fs.watch on the pending file has fired) or becomes
 *      the daemon (the client's startup poll consumes the file).
 *   3. The daemon loads the staged files as base64 and pushes an `openShare`
 *      event; the client opens compose with the shared content attached.
 *
 * Same pending-file indirection as the P115 mailto handler (bin/mailto.ts) —
 * no IPC socket needed, one share = one file write.
 *
 * Registration is a "loose" AppX deployment: a staged folder holding
 * rmfshare.exe + AppxManifest.xml + Assets, registered per-user via
 * `Add-AppxPackage -Register`. That requires Developer Mode (unsigned
 * manifest) — the registration path checks and says so. No MSIX signing,
 * no Store, no admin.
 */

import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";

export interface PendingShareFile {
    name: string;
    /** Absolute path inside ~/.rmfmail/share-staging/<stamp>/ */
    path: string;
    size: number;
}

export interface PendingShareData {
    title: string;
    text: string;
    url: string;
    files: PendingShareFile[];
    writtenAt: number;
}

export interface ShareAttachment {
    filename: string;
    mimeType: string;
    dataBase64: string;
}

/** Attachments larger than this are skipped (typical SMTP servers reject
 *  messages this size anyway); the skip is reported, never silent. */
export const SHARE_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;

const IDENTITY_NAME = "BobFrankston.rmfmail";

function rmfmailDir(): string {
    const home = process.env.USERPROFILE || process.env.HOME || ".";
    return path.join(home, ".rmfmail");
}

export function getPendingShareFile(): string {
    return path.join(rmfmailDir(), "pending-share.json");
}

/** Read + delete the pending-share file. One-shot, same contract as
 *  consumePendingMailto: caller acts on the result, the file is gone either
 *  way, stale files (crashed handoff) are dropped. */
export function consumePendingShare(): PendingShareData | null {
    const target = getPendingShareFile();
    let raw: string;
    try { raw = fs.readFileSync(target, "utf-8"); }
    catch { return null; }
    try { fs.unlinkSync(target); } catch { /* */ }
    let parsed: PendingShareData;
    try { parsed = JSON.parse(raw) as PendingShareData; }
    catch { return null; }
    const age = Date.now() - (parsed.writtenAt || 0);
    if (age > 10 * 60_000) {
        // Crashed handoff — user shared but nothing consumed it for 10
        // minutes. Drop it (staging cleanup reaps the copied files).
        return null;
    }
    parsed.title = parsed.title || "";
    parsed.text = parsed.text || "";
    parsed.url = parsed.url || "";
    parsed.files = Array.isArray(parsed.files) ? parsed.files : [];
    return parsed;
}

const MIME_BY_EXT: Record<string, string> = {
    ".pdf": "application/pdf",
    ".png": "image/png",
    ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".webp": "image/webp",
    ".bmp": "image/bmp",
    ".svg": "image/svg+xml",
    ".heic": "image/heic",
    ".txt": "text/plain", ".log": "text/plain", ".md": "text/plain",
    ".html": "text/html", ".htm": "text/html",
    ".csv": "text/csv",
    ".json": "application/json",
    ".xml": "application/xml",
    ".zip": "application/zip",
    ".7z": "application/x-7z-compressed",
    ".doc": "application/msword",
    ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    ".xls": "application/vnd.ms-excel",
    ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    ".ppt": "application/vnd.ms-powerpoint",
    ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    ".eml": "message/rfc822",
    ".ics": "text/calendar",
    ".mp3": "audio/mpeg",
    ".mp4": "video/mp4",
    ".mov": "video/quicktime",
};

export function mimeForFilename(name: string): string {
    return MIME_BY_EXT[path.extname(name).toLowerCase()] || "application/octet-stream";
}

/** Load staged share files as compose-init attachments (base64). Oversized
 *  or unreadable files land in `skipped` with a reason — surfaced to the
 *  user in the compose body, never dropped silently. Consumed staging dirs
 *  are deleted afterwards; the content lives on in the compose draft. */
export function loadShareAttachments(files: PendingShareFile[]): {
    attachments: ShareAttachment[];
    skipped: { name: string; reason: string }[];
} {
    const attachments: ShareAttachment[] = [];
    const skipped: { name: string; reason: string }[] = [];
    const dirs = new Set<string>();
    for (const f of files) {
        try {
            const st = fs.statSync(f.path);
            if (st.size > SHARE_ATTACHMENT_MAX_BYTES) {
                skipped.push({ name: f.name, reason: `larger than ${Math.round(SHARE_ATTACHMENT_MAX_BYTES / 1024 / 1024)} MB` });
            } else {
                attachments.push({
                    filename: f.name,
                    mimeType: mimeForFilename(f.name),
                    dataBase64: fs.readFileSync(f.path).toString("base64"),
                });
            }
            dirs.add(path.dirname(f.path));
        } catch (e: any) {
            skipped.push({ name: f.name, reason: e?.message || "unreadable" });
        }
    }
    for (const d of dirs) {
        // Only reap inside our own staging root — a hand-written pending
        // file must never turn this into an arbitrary directory delete.
        if (d.startsWith(path.join(rmfmailDir(), "share-staging") + path.sep)) {
            try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* next startup sweep */ }
        }
    }
    return { attachments, skipped };
}

/** Reap staging dirs older than 2 days — leftovers from shares that never
 *  got consumed (daemon crash between copy and compose). Called once at
 *  daemon startup. */
export function cleanupShareStaging(): void {
    const root = path.join(rmfmailDir(), "share-staging");
    let entries: fs.Dirent[];
    try { entries = fs.readdirSync(root, { withFileTypes: true }); }
    catch { return; }
    const cutoff = Date.now() - 2 * 24 * 3600_000;
    for (const e of entries) {
        if (!e.isDirectory()) continue;
        const full = path.join(root, e.name);
        try {
            if (fs.statSync(full).mtimeMs < cutoff) {
                fs.rmSync(full, { recursive: true, force: true });
            }
        } catch { /* in use or already gone */ }
    }
}

// ── Registration (Windows only) ─────────────────────────────────────────

function buildAppxManifest(version: string): string {
    // Unsigned loose deployment: Publisher only has to be well-formed, it
    // isn't validated against a certificate. AppListEntry="none" keeps the
    // entry out of the Start menu — rmfshare.exe is a share sink, not an
    // app the user launches.
    return `<?xml version="1.0" encoding="utf-8"?>
<Package
    xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
    xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
    xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
    IgnorableNamespaces="uap rescap">
  <Identity Name="${IDENTITY_NAME}" Publisher="CN=Bob Frankston" Version="${version}" ProcessorArchitecture="x64" />
  <Properties>
    <DisplayName>rmfmail</DisplayName>
    <PublisherDisplayName>Bob Frankston</PublisherDisplayName>
    <Logo>Assets\\logo150.png</Logo>
  </Properties>
  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
  </Dependencies>
  <Resources>
    <Resource Language="en-us" />
  </Resources>
  <Applications>
    <Application Id="rmfmail" Executable="rmfshare.exe" EntryPoint="Windows.FullTrustApplication">
      <uap:VisualElements DisplayName="rmfmail" Description="Share to rmfmail"
          BackgroundColor="transparent"
          Square150x150Logo="Assets\\logo150.png" Square44x44Logo="Assets\\logo44.png"
          AppListEntry="none" />
      <Extensions>
        <uap:Extension Category="windows.shareTarget">
          <uap:ShareTarget Description="Share to rmfmail">
            <uap:SupportedFileTypes>
              <uap:SupportsAnyFileType />
            </uap:SupportedFileTypes>
            <uap:DataFormat>Text</uap:DataFormat>
            <uap:DataFormat>WebLink</uap:DataFormat>
            <uap:DataFormat>StorageItems</uap:DataFormat>
          </uap:ShareTarget>
        </uap:Extension>
      </Extensions>
    </Application>
  </Applications>
  <Capabilities>
    <rescap:Capability Name="runFullTrust" />
  </Capabilities>
</Package>
`;
}

function powershell(script: string): string {
    return execFileSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script],
        { stdio: ["pipe", "pipe", "pipe"] }).toString();
}

function developerModeEnabled(): boolean {
    try {
        const out = powershell(
            "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock' -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense");
        return out.trim() === "1";
    } catch { return false; }
}

/** Register (or unregister) rmfmail as a Windows Share target. Mirrors
 *  registerMailtoWindows in mailx.ts: returns outcome text instead of
 *  printing so the CLI and any future Settings toggle share one path. */
export function registerShareWindows(unregister: boolean): { ok: boolean; message: string } {
    const lines: string[] = [];

    if (unregister) {
        try {
            powershell(`Get-AppxPackage '${IDENTITY_NAME}' | Remove-AppxPackage`);
            lines.push("rmfmail removed from the Windows Share menu.");
        } catch (e: any) {
            return { ok: false, message: `Remove-AppxPackage failed: ${e?.stderr?.toString?.() || e.message}` };
        }
        return { ok: true, message: lines.join("\n") };
    }

    const pkgExe = path.join(import.meta.dirname, "rmfshare.exe");
    if (!fs.existsSync(pkgExe)) {
        return { ok: false, message: `rmfshare.exe not found at ${pkgExe} — package was built without it. Re-run 'npm run build' on the build machine.` };
    }
    if (!developerModeEnabled()) {
        return {
            ok: false,
            message: "Registering an unsigned Share target needs Windows Developer Mode:\n" +
                "  Settings → System → For developers → Developer Mode → On\n" +
                "then re-run `rmfmail -register-share`.",
        };
    }

    // Stage under %LOCALAPPDATA%\rmfmail\share-target — NOT inside
    // node_modules: the registered package points at absolute paths, and an
    // npm upgrade replacing node_modules would strand the registration.
    const stageDir = path.join(process.env.LOCALAPPDATA || rmfmailDir(), "rmfmail", "share-target");
    const assetsDir = path.join(stageDir, "Assets");
    fs.mkdirSync(assetsDir, { recursive: true });
    fs.copyFileSync(pkgExe, path.join(stageDir, "rmfshare.exe"));

    // Manifest logos scaled from the app icon at register time (System.
    // Drawing via PowerShell — registration is already a Windows-only,
    // PowerShell-driven path).
    const iconPng = path.join(import.meta.dirname, "..", "client", "icon.png");
    try {
        powershell(`
Add-Type -AssemblyName System.Drawing
$src = [System.Drawing.Image]::FromFile('${iconPng.replace(/'/g, "''")}')
foreach ($s in 44,150) {
  $bmp = New-Object System.Drawing.Bitmap $s, $s
  $g = [System.Drawing.Graphics]::FromImage($bmp)
  $g.InterpolationMode = 'HighQualityBicubic'
  $g.DrawImage($src, 0, 0, $s, $s)
  $bmp.Save('${assetsDir.replace(/'/g, "''")}\\logo' + $s + '.png', [System.Drawing.Imaging.ImageFormat]::Png)
  $g.Dispose(); $bmp.Dispose()
}
$src.Dispose()`);
    } catch (e: any) {
        return { ok: false, message: `Icon scaling failed: ${e?.stderr?.toString?.() || e.message}` };
    }

    // Manifest version tracks the package version so re-registering after an
    // upgrade redeploys cleanly (4th part must be 0 for AppX).
    let pkgVersion = "1.0.0";
    try {
        pkgVersion = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version || pkgVersion;
    } catch { /* fall back */ }
    const appxVersion = pkgVersion.split(".").slice(0, 3).map(p => String(parseInt(p, 10) || 0)).join(".") + ".0";
    fs.writeFileSync(path.join(stageDir, "AppxManifest.xml"), buildAppxManifest(appxVersion));

    try {
        // Remove any prior registration first — Add-AppxPackage -Register
        // rejects same-version re-registration, and downgrades after a
        // rollback would fail outright.
        powershell(`Get-AppxPackage '${IDENTITY_NAME}' | Remove-AppxPackage`);
    } catch { /* not registered — fine */ }
    try {
        powershell(`Add-AppxPackage -Register '${path.join(stageDir, "AppxManifest.xml").replace(/'/g, "''")}'`);
    } catch (e: any) {
        return { ok: false, message: `Add-AppxPackage failed: ${e?.stderr?.toString?.() || e.message}` };
    }

    lines.push("rmfmail registered as a Windows Share target.");
    lines.push("It now appears in the Share sheet (Explorer right-click → Share, browser Share, etc.).");
    lines.push(`Staged at: ${stageDir}`);
    lines.push("Note: re-run -register-share after major upgrades so the staged rmfshare.exe stays current.");
    return { ok: true, message: lines.join("\n") };
}
