/**
 * 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";
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";
/** Paths to try when no config.path is set (fresh install). Order matters:
 *  "rmfmail" is the new auto-created default; "home/.mailx" + "mailx" are
 *  legacy from the pre-rename era and stay in the list so existing setups
 *  keep working without manual reconfig. First one that exists on GDrive
 *  wins. (REMOVE legacy entries once nobody is on the old layout.) */
const GDRIVE_PATH_SEARCH_ORDER = ["rmfmail", "home/.mailx", "mailx"];
// ── Token helpers ──
/** 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() {
    // 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")
                    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,
                        });
                        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,
        });
        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;
}
/** Resolve a folder ID back to its name (for diagnostic logging). */
async function gDriveGetFolderName(token, folderId) {
    try {
        const res = await fetch(`https://www.googleapis.com/drive/v3/files/${folderId}?fields=name,parents`, {
            headers: { Authorization: `Bearer ${token}` },
        });
        if (!res.ok)
            return null;
        const data = await res.json();
        return data.name || null;
    }
    catch {
        return null;
    }
}
/** 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 the settings folder on GDrive, or create a flat "mailx" folder.
 *  Supports nested paths from config (e.g. "home/.mailx") by walking each
 *  segment. Falls back to the folderId in config if set, so a stale ID that
 *  points to the wrong folder gets corrected on next lookup. */
export async function gDriveFindOrCreateFolder() {
    const token = await getGoogleDriveToken();
    if (!token)
        return null;
    // Read path from config — supports nested like "home/.mailx"
    let cfgEntry = null;
    try {
        const cfgPath = path.join(SETTINGS_DIR, "config.jsonc");
        if (fs.existsSync(cfgPath)) {
            const raw = fs.readFileSync(cfgPath, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
            cfgEntry = JSON.parse(raw).sharedDir;
        }
    }
    catch { /* ignore */ }
    // Build search list: configured path first, then common conventions
    const configuredPath = (typeof cfgEntry === "object" && cfgEntry?.path) ? cfgEntry.path : null;
    const pathsToTry = configuredPath
        ? [configuredPath]
        : GDRIVE_PATH_SEARCH_ORDER;
    try {
        for (const tryPath of pathsToTry) {
            console.log(`  [cloud] Trying GDrive path: '${tryPath}'`);
            const segments = tryPath.split(/[/\\]/).filter(Boolean);
            const folderId = await walkGDrivePath(token, segments, false);
            if (folderId) {
                // Resolve folder ID back to name for verification
                const name = await gDriveGetFolderName(token, folderId);
                console.log(`  [cloud] Found existing '${tryPath}' → folder '${name || "?"}' (${folderId})`);
                return folderId;
            }
            console.log(`  [cloud] Path '${tryPath}' not found on GDrive`);
        }
        // None found — create the new-install default (first entry).
        // Was previously LAST entry, but legacy paths now sit at the tail
        // for migration discovery; fresh installs should land in the new
        // "rmfmail" folder, not the legacy "mailx".
        const createPath = configuredPath || GDRIVE_PATH_SEARCH_ORDER[0];
        const segments = createPath.split(/[/\\]/).filter(Boolean);
        const created = await walkGDrivePath(token, segments, true);
        if (created)
            console.log(`  [cloud] Created '${createPath}' folder: ${created}`);
        return created;
    }
    catch (e) {
        console.error(`  [cloud] gdrive folder setup: ${e.message}`);
        return null;
    }
}
/** 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)`, {
            headers: { Authorization: `Bearer ${token}` },
        });
        if (!res.ok) {
            console.error(`  [cloud] gdrive lookup '${fileName}': ${res.status}`);
            return null;
        }
        const data = await res.json();
        const fileId = data.files?.[0]?.id;
        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)`, {
        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();
    const existingId = findData.files?.[0]?.id;
    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