/**
 * Cloud storage for mailx settings — Google Drive API (drive.file scope).
 *
 * Uses a single app-owned "mailx" folder on Drive, accessed by folder ID.
 * The drive.file scope only sees files/folders created by this OAuth client,
 * which prevents conflicts with existing folders of the same name.
 * All machines using the same OAuth client ID share the same folder.
 *
 * ── Restoring removed providers ──
 * OneDrive (removed 2026-04-06): Required microsoft-credentials.json in ~/.mailx/,
 *   MS_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access",
 *   authenticateOAuth with tokenDir ~/.mailx/tokens/microsoft/.
 *   Read/write via Graph API: GET/PUT https://graph.microsoft.com/v1.0/me/drive/root:/{path}:/content
 *   Azure AD app registration needed: https://portal.azure.com → App registrations → Desktop app
 *
 * Dropbox (removed 2026-04-06): Never implemented — placeholder only.
 *   Would need Dropbox OAuth app, token management, and Dropbox API v2 calls.
 */
import fs from "node:fs";
import path from "node:path";
import { authenticateOAuth } from "@bobfrankston/oauthsupport";
import { loadAccounts, tokenDirName } from "./index.js";
const SETTINGS_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail");
// ── Credentials ──
// Google Drive: reuse iflow's OAuth credentials (same Google Cloud project)
function findGoogleCredentials() {
    // Check mailx local dir first, then iflow package
    const local = path.join(SETTINGS_DIR, "google-credentials.json");
    if (fs.existsSync(local))
        return local;
    try {
        let dir = import.meta.dirname;
        for (let i = 0; i < 5; i++) {
            for (const pkg of ["iflow-direct", "iflow"]) {
                for (const name of ["iflow-credentials.json"]) {
                    const p = path.join(dir, "node_modules", "@bobfrankston", pkg, name);
                    if (fs.existsSync(p))
                        return p;
                }
            }
            const parent = path.dirname(dir);
            if (parent === dir)
                break;
            dir = parent;
        }
    }
    catch { /* iflow not installed */ }
    return null;
}
const GDRIVE_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "gdrive");
// drive: full access so we can find pre-existing folders like "home/.mailx"
// that weren't created by this OAuth client. drive.file was safer but couldn't
// discover folders created manually or by other tools — which broke first-run
// setup when the user's convention is a nested path.
// The Gmail scope is already mail.google.com (full email), so this isn't a
// bigger privacy ask. Token cache at tokens/gdrive/ will re-auth on scope change.
const GDRIVE_SCOPES = "https://www.googleapis.com/auth/drive";
// ── Token helpers ──
/** One-time-per-process archive pass: move every token directory that
 *  doesn't correspond to a currently-configured account into
 *  `tokens/.archive/<original-name>/`. Without this, `cloud.ts` would
 *  walk all token dirs in `~/.rmfmail/tokens/` — including leftovers from
 *  past setups (a Gmail address typed with a dot creates one dir, the
 *  same mailbox without the dot creates another) — and trigger a fresh
 *  OAuth flow on a stale dir that can't refresh, looping the user through
 *  consent forever (Bob 2026-05-25). Archived dirs are recoverable: move
 *  them back if I guessed wrong about which is stale. */
let _staleArchiveRun = false;
function archiveStaleTokenDirs(activeDirs) {
    if (_staleArchiveRun)
        return;
    _staleArchiveRun = true;
    const tokensDir = path.join(SETTINGS_DIR, "tokens");
    if (!fs.existsSync(tokensDir))
        return;
    const archiveRoot = path.join(tokensDir, ".archive");
    let archived = 0;
    try {
        for (const entry of fs.readdirSync(tokensDir)) {
            // Preserve: special sub-dirs, the archive itself, all active entries.
            if (entry === "gdrive" || entry === ".archive")
                continue;
            if (entry.startsWith("."))
                continue;
            if (activeDirs.has(entry))
                continue;
            const src = path.join(tokensDir, entry);
            try {
                if (!fs.statSync(src).isDirectory())
                    continue;
            }
            catch {
                continue;
            }
            const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
            const dst = path.join(archiveRoot, `${stamp}-${entry}`);
            try {
                fs.mkdirSync(archiveRoot, { recursive: true });
                fs.renameSync(src, dst);
                console.log(`  [cloud] Archived stale token dir: ${entry} → .archive/${path.basename(dst)}`);
                archived++;
            }
            catch (e) {
                console.error(`  [cloud] Failed to archive ${entry}: ${e?.message || e}`);
            }
        }
    }
    catch (e) {
        console.error(`  [cloud] tokens dir scan failed: ${e?.message || e}`);
    }
    if (archived > 0) {
        console.log(`  [cloud] Stale token cleanup: ${archived} dir(s) archived to tokens/.archive/`);
    }
}
/** Compute the set of token dir names corresponding to currently-configured
 *  accounts. Used to decide which token dirs are "active" vs orphans. */
function activeTokenDirs() {
    const active = new Set();
    try {
        const accts = loadAccounts();
        for (const a of accts) {
            const u = a.imap?.user || a.email;
            if (u)
                active.add(tokenDirName(u));
            // Also the email address itself — covers accounts where imap.user
            // differs (e.g. iecc.com uses "bobf2" as user but the email is
            // bob@bob.ma; tokens are keyed by the auth identity).
            if (a.email)
                active.add(tokenDirName(a.email));
        }
    }
    catch { /* accounts.jsonc unreadable — treat all as active (safe default) */ }
    return active;
}
/** Get a GDrive-capable token. Prefers the Gmail token (which now includes
 *  drive scope) — avoids a second OAuth consent prompt. Falls back to the
 *  dedicated GDrive token dir for non-Gmail setups or when Gmail token
 *  doesn't have drive scope yet. */
async function getGoogleDriveToken() {
    // Archive stale token dirs once per process before iteration. Cheap and
    // self-healing — any future stray dir gets stashed out of the loop.
    const active = activeTokenDirs();
    if (active.size > 0)
        archiveStaleTokenDirs(active);
    // Strategy 1: reuse a Gmail token that already has drive scope.
    // Scan the tokens/ dir for any account token with drive in its scope.
    const tokensDir = path.join(SETTINGS_DIR, "tokens");
    if (fs.existsSync(tokensDir)) {
        try {
            for (const entry of fs.readdirSync(tokensDir)) {
                if (entry === "gdrive" || entry === ".archive")
                    continue;
                if (entry.startsWith("."))
                    continue;
                // Only consider dirs that match a currently-configured account.
                // Loads the gating logic before the iteration above (`active`)
                // already excluded orphans by moving them — this is belt-and-
                // braces for the case where loadAccounts() returned empty
                // (e.g. accounts.jsonc unreadable) and we skipped archiving.
                if (active.size > 0 && !active.has(entry)) {
                    console.log(`  [cloud] Skipping non-configured token dir: ${entry}`);
                    continue;
                }
                const tokenPath = path.join(tokensDir, entry, "oauth-token.json");
                if (!fs.existsSync(tokenPath))
                    continue;
                try {
                    const tok = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
                    if (tok.scope?.includes("drive") && tok.refresh_token) {
                        console.log(`  [cloud] Reusing Gmail token from ${entry} (has drive scope)`);
                        const creds = findGoogleCredentials();
                        if (!creds)
                            continue;
                        const refreshed = await authenticateOAuth(creds, {
                            scope: tok.scope,
                            tokenDirectory: path.join(tokensDir, entry),
                            tokenFileName: "oauth-token.json",
                            credentialsKey: "installed",
                            includeOfflineAccess: true,
                            logFn: (type, message) => {
                                const p = `  [oauth gdrive-via-${entry}]`;
                                if (type === "error")
                                    console.error(`${p} ${message}`);
                                else
                                    console.log(`${p} ${message}`);
                            },
                        });
                        if (refreshed?.access_token) {
                            console.log(`  [cloud] GDrive auth: success (via Gmail token)`);
                            return refreshed.access_token;
                        }
                    }
                }
                catch { /* skip this token */ }
            }
        }
        catch { /* tokens dir unreadable */ }
    }
    // Strategy 2: dedicated GDrive token (legacy path or non-Gmail setups)
    const creds = findGoogleCredentials();
    if (!creds) {
        console.error("  [cloud] No Google credentials found (checked ~/.rmfmail/google-credentials.json and iflow package)");
        return null;
    }
    const tokenPath = path.join(GDRIVE_TOKEN_DIR, "token.json");
    if (fs.existsSync(tokenPath)) {
        try {
            const existing = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
            const expired = existing.expires_at ? Date.now() >= existing.expires_at : true;
            const hasRefresh = !!existing.refresh_token;
            console.log(`  [cloud] GDrive token (dedicated): ${expired ? "expired" : "valid"}, refresh_token: ${hasRefresh ? "yes" : "NO"}, scope: ${existing.scope || "?"}`);
            if (existing.scope && existing.scope !== GDRIVE_SCOPES) {
                console.log(`  [cloud] Scope changed (${existing.scope} → ${GDRIVE_SCOPES}) — re-authenticating...`);
                fs.unlinkSync(tokenPath);
            }
        }
        catch { /* ignore */ }
    }
    else {
        console.log(`  [cloud] GDrive token: not found at ${tokenPath} (will auth)`);
    }
    try {
        const token = await authenticateOAuth(creds, {
            scope: GDRIVE_SCOPES,
            tokenDirectory: GDRIVE_TOKEN_DIR,
            tokenFileName: "token.json",
            credentialsKey: "installed",
            includeOfflineAccess: true,
            logFn: (type, message) => {
                const p = "  [oauth gdrive]";
                if (type === "error")
                    console.error(`${p} ${message}`);
                else
                    console.log(`${p} ${message}`);
            },
        });
        if (token?.access_token) {
            console.log(`  [cloud] GDrive auth: success (dedicated token)`);
        }
        else {
            console.error(`  [cloud] GDrive auth: returned null — OAuth likely timed out`);
        }
        return token?.access_token || null;
    }
    catch (e) {
        console.error(`  [cloud] GDrive auth FAILED: ${e.message}`);
        return null;
    }
}
// ── Google Drive API (folder-ID based) ──
/** Walk a nested folder path on GDrive ("home/.mailx" → find "home", find
 *  ".mailx" inside it). Returns the leaf folder ID, or null if any segment is
 *  missing. When `create` is true, creates the LAST segment if missing (won't
 *  create intermediate segments — that's a user error). */
async function walkGDrivePath(token, segments, create) {
    const fullPath = segments.join("/");
    let parentId;
    for (let i = 0; i < segments.length; i++) {
        const seg = segments[i];
        const found = await gDriveFindFolder(token, seg, parentId);
        if (found) {
            console.log(`  [cloud] walk '${fullPath}': '${seg}' → ${found}${parentId ? ` (in ${parentId})` : ""}`);
            parentId = found;
        }
        else if (create && i === segments.length - 1) {
            const body = { name: seg, mimeType: "application/vnd.google-apps.folder" };
            if (parentId)
                body.parents = [parentId];
            const res = await fetch("https://www.googleapis.com/drive/v3/files", {
                method: "POST",
                headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
                body: JSON.stringify(body),
            });
            if (!res.ok)
                return null;
            const created = await res.json();
            parentId = created.id;
        }
        else {
            return null;
        }
    }
    return parentId || null;
}
/** Verify a cached folder ID still points to an `.rmfmail`-named folder
 *  the user owns and isn't trashed. Returns true if the ID is good to use,
 *  false if mailx should drop it and re-discover via gDriveFindOrCreateFolder.
 *
 *  Catches the cross-version-corruption case: 1.0.504 had a buggy folder
 *  lookup that created an empty `rmfmail` folder at My Drive root and cached
 *  its ID. Once 1.0.505 fixed the lookup, the cached ID still pointed at the
 *  buggy folder forever — mailx kept reading the 1-account stub instead of
 *  the user's real `home/.rmfmail/accounts.jsonc`. This validator forces a
 *  re-discovery whenever the cached ID is wrong. */
export async function gDriveValidateCachedFolder(folderId) {
    const token = await getGoogleDriveToken();
    if (!token)
        return true; // can't verify → trust the cache (no token = nothing else to do)
    try {
        const res = await fetch(`https://www.googleapis.com/drive/v3/files/${folderId}?fields=id,name,trashed,mimeType`, { headers: { Authorization: `Bearer ${token}` } });
        if (res.status === 404) {
            console.log(`  [cloud] cached folderId ${folderId} → 404 (folder deleted), re-discovering`);
            return false;
        }
        if (!res.ok)
            return true;
        const data = await res.json();
        if (data.trashed) {
            console.log(`  [cloud] cached folderId ${folderId} is trashed, re-discovering`);
            return false;
        }
        if (data.mimeType !== "application/vnd.google-apps.folder") {
            console.log(`  [cloud] cached folderId ${folderId} is not a folder (${data.mimeType}), re-discovering`);
            return false;
        }
        if (data.name !== ".rmfmail") {
            console.log(`  [cloud] cached folderId ${folderId} is named '${data.name}', not '.rmfmail' — re-discovering`);
            return false;
        }
        return true;
    }
    catch (e) {
        console.error(`  [cloud] gDriveValidateCachedFolder: ${e.message}`);
        return true; // network failure → trust the cache
    }
}
/** Find a single folder by name, optionally inside a parent. */
async function gDriveFindFolder(token, name, parentId) {
    let query = `name='${name}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
    if (parentId)
        query += ` and '${parentId}' in parents`;
    const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`, {
        headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) {
        console.error(`  [cloud] gdrive folder search '${name}': ${res.status} ${res.statusText}`);
        return null;
    }
    const data = await res.json();
    return data.files?.[0]?.id || null;
}
/** Find or create the rmfmail settings folder on Google Drive.
 *
 *  Lookup order — match the Android side (`mailx-store-web/android-bootstrap.ts`):
 *    1. `home/.rmfmail/` — the established convention. Bob's family-of-two
 *       layout has been here since the rebrand. Path scope (`'root' in
 *       parents` for `home`, then `<homeId> in parents` for `.rmfmail`) is
 *       what the Android side does, so desktop matches.
 *    2. `.rmfmail/` at My Drive root — fallback for users who don't have a
 *       `home/` folder. Mostly hypothetical; included so a brand-new Drive
 *       still works.
 *    3. Create `.rmfmail` at My Drive root if neither exists. Don't auto-
 *       create `home/` — that's a user-organization choice we shouldn't
 *       make for them.
 *
 *  Previously this looked for a literal `rmfmail` (no dot) at root, which
 *  on clean install created a NEW empty folder alongside the real data,
 *  orphaning the user's accounts/contacts/etc. Switched to the dotted name
 *  matching the actual folder convention. */
export async function gDriveFindOrCreateFolder() {
    const token = await getGoogleDriveToken();
    if (!token)
        return null;
    try {
        // 1. Try home/.rmfmail (the canonical location)
        const inHome = await walkGDrivePath(token, ["home", ".rmfmail"], false);
        if (inHome) {
            console.log(`  [cloud] Found 'home/.rmfmail' folder (${inHome})`);
            return inHome;
        }
        // 2. Try .rmfmail at root
        const atRoot = await walkGDrivePath(token, [".rmfmail"], false);
        if (atRoot) {
            console.log(`  [cloud] Found '.rmfmail' folder at My Drive root (${atRoot})`);
            return atRoot;
        }
        // 3. Create .rmfmail at root (don't try to create `home/`)
        const created = await walkGDrivePath(token, [".rmfmail"], true);
        if (created)
            console.log(`  [cloud] Created '.rmfmail' folder at My Drive root (${created})`);
        return created;
    }
    catch (e) {
        console.error(`  [cloud] gdrive folder setup: ${e.message}`);
        return null;
    }
}
/** Google Drive allows two files with the SAME name in one folder (a sync
 *  conflict or a double-create produces them; the desktop mount can only
 *  show one, so the user never sees the stray). A blind `files[0]` then
 *  picks arbitrarily — and reading an empty duplicate over a real 1.5 MB
 *  config file silently wipes contacts/accounts. Pick the LARGEST match (an
 *  empty stub must never beat the real file) and warn so the dup gets
 *  cleaned up. */
function pickDriveFile(files, fileName) {
    if (!files || files.length === 0)
        return undefined;
    if (files.length === 1)
        return files[0].id;
    const sorted = [...files].sort((a, b) => parseInt(b.size || "0", 10) - parseInt(a.size || "0", 10));
    console.warn(`  [cloud] ${files.length} files named '${fileName}' in the folder — using the largest (${sorted[0].size || "?"} bytes). DELETE the duplicate(s) in Google Drive — ids: ${sorted.slice(1).map(f => f.id).join(", ")}`);
    return sorted[0].id;
}
/** Read a file by name from a folder (by ID) */
async function gDriveReadFromFolder(folderId, fileName) {
    const token = await getGoogleDriveToken();
    if (!token) {
        console.error(`  [cloud] gdrive read ${fileName}: no token`);
        return null;
    }
    try {
        // Find file in folder
        const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
        const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name,size)`, {
            headers: { Authorization: `Bearer ${token}` },
        });
        if (!res.ok) {
            console.error(`  [cloud] gdrive lookup '${fileName}': ${res.status}`);
            return null;
        }
        const data = await res.json();
        const fileId = pickDriveFile(data.files, fileName);
        if (!fileId)
            return null;
        // Download content
        const contentRes = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
            headers: { Authorization: `Bearer ${token}` },
        });
        if (!contentRes.ok) {
            console.error(`  [cloud] gdrive download ${fileName}: ${contentRes.status}`);
            return null;
        }
        return await contentRes.text();
    }
    catch (e) {
        console.error(`  [cloud] gdrive read ${fileName}: ${e.message}`);
        return null;
    }
}
/** Write a file by name to a folder (by ID) — creates or updates.
 *  Throws on failure with a descriptive error so callers can surface it to the UI. */
async function gDriveWriteToFolder(folderId, fileName, content) {
    const token = await getGoogleDriveToken();
    if (!token)
        throw new Error("Google Drive: no auth token (OAuth not granted or expired)");
    // Check if file exists in folder
    const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
    const findRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name,size)`, {
        headers: { Authorization: `Bearer ${token}` },
    });
    if (!findRes.ok) {
        const body = await findRes.text().catch(() => "");
        throw new Error(`Google Drive: lookup '${fileName}' failed (${findRes.status} ${findRes.statusText}) ${body.slice(0, 200)}`);
    }
    const findData = await findRes.json();
    // Write to the SAME file a read would pick (largest), so read/write stay
    // consistent when a duplicate exists.
    const existingId = pickDriveFile(findData.files, fileName);
    if (existingId) {
        // Update existing file
        const res = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${existingId}?uploadType=media`, {
            method: "PATCH",
            headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
            body: content,
        });
        if (!res.ok) {
            const body = await res.text().catch(() => "");
            throw new Error(`Google Drive: update '${fileName}' failed (${res.status} ${res.statusText}) ${body.slice(0, 200)}`);
        }
    }
    else {
        // Create new file in folder
        const boundary = "mailx_boundary_" + Date.now();
        const metadata = JSON.stringify({ name: fileName, parents: [folderId] });
        const body = `--${boundary}\r\nContent-Type: application/json\r\n\r\n${metadata}\r\n--${boundary}\r\nContent-Type: application/json\r\n\r\n${content}\r\n--${boundary}--`;
        const res = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", {
            method: "POST",
            headers: { Authorization: `Bearer ${token}`, "Content-Type": `multipart/related; boundary=${boundary}` },
            body,
        });
        if (!res.ok) {
            const errBody = await res.text().catch(() => "");
            throw new Error(`Google Drive: create '${fileName}' failed (${res.status} ${res.statusText}) ${errBody.slice(0, 200)}`);
        }
    }
}
/**
 * Get a cloud file provider. For gdrive, pass the folder ID.
 * Files are stored flat in the folder (no subdirectory navigation).
 */
export function getCloudProvider(provider, folderId) {
    switch (provider) {
        case "google":
        case "gdrive":
            if (!folderId) {
                console.error("  [cloud] gdrive requires a folder ID — run initCloudConfig first");
                return null;
            }
            return {
                read: (fileName) => gDriveReadFromFolder(folderId, fileName),
                write: (fileName, content) => gDriveWriteToFolder(folderId, fileName, content),
                exists: async (fileName) => (await gDriveReadFromFolder(folderId, fileName)) !== null,
            };
        case "local":
            return {
                read: async (p) => { try {
                    return fs.readFileSync(p, "utf-8");
                }
                catch {
                    return null;
                } },
                write: async (p, c) => { fs.writeFileSync(p, c); },
                exists: async (p) => fs.existsSync(p),
            };
        default:
            console.error(`  [cloud] Provider "${provider}" not supported — only gdrive is available`);
            return null;
    }
}
/** Fetch the authenticated Google user's profile (name + email) via the People API.
 *  Caller must supply an OAuth access token whose scopes include `contacts.readonly`
 *  or `userinfo.profile`. The Drive token (drive.file) does NOT have the right scope
 *  on its own — use the Gmail/IMAP OAuth token instead (mailx-imap's
 *  ImapManager.getOAuthToken). Returns null on failure. */
export async function getGoogleProfile(token) {
    if (!token)
        return null;
    try {
        const res = await fetch("https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses", {
            headers: { Authorization: `Bearer ${token}` },
        });
        if (!res.ok) {
            console.error(`  [cloud] People API: ${res.status} ${res.statusText}`);
            return null;
        }
        const data = await res.json();
        const name = data.names?.find((n) => n.displayName)?.displayName
            || data.names?.[0]?.displayName;
        const email = data.emailAddresses?.find((e) => e.metadata?.primary)?.value
            || data.emailAddresses?.[0]?.value;
        return { name, email };
    }
    catch (e) {
        console.error(`  [cloud] getGoogleProfile: ${e.message}`);
        return null;
    }
}
//# sourceMappingURL=cloud.js.map