/**
 * 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.
 *
 * OneDrive (restored 2026-07-12): Microsoft Graph, PATH-addressed (no folder
 *   IDs — `root:/{path}:/content`), so the whole GDrive folder-ID
 *   discovery/validation/ownership machinery has no OneDrive counterpart.
 *   Needs ~/.rmfmail/microsoft-credentials.json (Azure public-client app;
 *   same registration the Outlook Graph mail provider uses — different
 *   scopes on the same token infrastructure).
 *
 * 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(): string | null {
    // 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: Set<string>): void {
    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;
    let migrated = 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; }
            // MIGRATE before archive: older builds wrote the token dir with
            // different casing (Pascal vs. lowercase) or different dot handling.
            // If this dir's lowercased name matches an active dir, it's the
            // same account's tokens under an obsolete naming convention —
            // moving the token file to the canonical dir lets the user keep
            // signing in instead of being thrown back to Google's consent
            // screen for the third time (Bob 2026-05-26 "why authenticating
            // again"). The migration preserves the oauth-token.json file; the
            // now-empty source dir falls through to archive below.
            const entryLc = entry.toLowerCase();
            if (activeDirs.has(entryLc) && entryLc !== entry) {
                const tokenFile = path.join(src, "oauth-token.json");
                if (fs.existsSync(tokenFile)) {
                    const dstDir = path.join(tokensDir, entryLc);
                    const dstTokenFile = path.join(dstDir, "oauth-token.json");
                    try {
                        if (fs.existsSync(dstTokenFile)) {
                            // Target dir already has a token — pick the newer one.
                            const srcMtime = fs.statSync(tokenFile).mtimeMs;
                            const dstMtime = fs.statSync(dstTokenFile).mtimeMs;
                            if (srcMtime > dstMtime) {
                                fs.copyFileSync(tokenFile, dstTokenFile);
                                console.log(`  [cloud] Migrated newer token from ${entry} → ${entryLc} (replacing older)`);
                                migrated++;
                            } else {
                                console.log(`  [cloud] Skipped migrating ${entry} → ${entryLc} (target token is newer)`);
                            }
                        } else {
                            fs.mkdirSync(dstDir, { recursive: true });
                            fs.copyFileSync(tokenFile, dstTokenFile);
                            console.log(`  [cloud] Migrated token from ${entry} → ${entryLc}`);
                            migrated++;
                        }
                    } catch (e: any) {
                        console.error(`  [cloud] Token migration ${entry} → ${entryLc} failed: ${e?.message || e}`);
                    }
                }
                // Fall through to archive — the source dir is now an artifact.
            }
            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: any) {
                console.error(`  [cloud] Failed to archive ${entry}: ${e?.message || e}`);
            }
        }
    } catch (e: any) {
        console.error(`  [cloud] tokens dir scan failed: ${e?.message || e}`);
    }
    if (migrated > 0) {
        console.log(`  [cloud] Stale token cleanup: ${migrated} token(s) migrated to canonical dirs`);
    }
    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(): Set<string> {
    const active = new Set<string>();
    try {
        const accts = loadAccounts();
        for (const a of accts) {
            const u = (a as any).imap?.user || (a as any).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 as any).email) active.add(tokenDirName((a as any).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(): Promise<string | null> {
    // 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: "info" | "error", message: string) => {
                                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: "info" | "error", message: string) => {
                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: any) {
        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: string, segments: string[], create: boolean): Promise<string | null> {
    const fullPath = segments.join("/");
    let parentId: string | undefined;
    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: any = { 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() as any;
            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: string): Promise<boolean> {
    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() as any;
        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: any) {
        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: string, name: string, parentId?: string): Promise<string | null> {
    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() as any;
    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(): Promise<string | null> {
    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: any) {
        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: any[] | undefined, fileName: string): string | undefined {
    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: string, fileName: string): Promise<string | null> {
    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() as any;
        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: any) {
        console.error(`  [cloud] gdrive read ${fileName}: ${e.message}`);
        return null;
    }
}

/** Read a file by name from a folder as RAW BYTES, returned base64-encoded.
 *  Same lookup as gDriveReadFromFolder but downloads via arrayBuffer so binary
 *  assets (e.g. a custom reminder sound) survive — `.text()` would corrupt
 *  them. Returns null if not found / on error. */
async function gDriveReadBinaryFromFolder(folderId: string, fileName: string): Promise<string | null> {
    const token = await getGoogleDriveToken();
    if (!token) { console.error(`  [cloud] gdrive readBinary ${fileName}: no token`); return null; }
    try {
        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() as any;
        const fileId = pickDriveFile(data.files, fileName);
        if (!fileId) return null;
        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; }
        const buf = Buffer.from(await contentRes.arrayBuffer());
        return buf.toString("base64");
    } catch (e: any) {
        console.error(`  [cloud] gdrive readBinary ${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: string, fileName: string, content: string): Promise<void> {
    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() as any;
    // 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)}`);
        }
    }
}

// ── OneDrive (Microsoft Graph, path-addressed) ──

const MS_TOKEN_DIR = path.join(SETTINGS_DIR, "tokens", "microsoft");
// offline_access is a SCOPE on the Microsoft identity platform (unlike
// Google's access_type=offline query param) — required for a refresh token.
const ONEDRIVE_SCOPES = "https://graph.microsoft.com/Files.ReadWrite offline_access";
const GRAPH_ROOT = "https://graph.microsoft.com/v1.0/me/drive";

function findMicrosoftCredentials(): string | null {
    const local = path.join(SETTINGS_DIR, "microsoft-credentials.json");
    return fs.existsSync(local) ? local : null;
}

async function getOneDriveToken(): Promise<string | null> {
    const creds = findMicrosoftCredentials();
    if (!creds) {
        console.error(`  [cloud] No Microsoft credentials — expected ${path.join(SETTINGS_DIR, "microsoft-credentials.json")} (Azure portal → App registrations → public/native client)`);
        return null;
    }
    try {
        const token = await authenticateOAuth(creds, {
            scope: ONEDRIVE_SCOPES,
            tokenDirectory: MS_TOKEN_DIR,
            tokenFileName: "onedrive-token.json",
            credentialsKey: "installed",
            // Google-ism; Microsoft grants refresh tokens via the
            // offline_access scope above instead.
            includeOfflineAccess: false,
            logFn: (type: "info" | "error", message: string) => {
                const p = "  [oauth onedrive]";
                if (type === "error") console.error(`${p} ${message}`);
                else console.log(`${p} ${message}`);
            },
        });
        if (!token?.access_token) console.error("  [cloud] OneDrive auth: returned null — OAuth likely timed out");
        return token?.access_token || null;
    } catch (e: any) {
        console.error(`  [cloud] OneDrive auth FAILED: ${e.message}`);
        return null;
    }
}

/** Graph item URL for `<basePath>/<fileName>` under the drive root. */
function graphContentUrl(basePath: string, fileName: string): string {
    const rel = [basePath.replace(/^\/+|\/+$/g, ""), fileName]
        .filter(Boolean).join("/")
        .split("/").map(encodeURIComponent).join("/");
    return `${GRAPH_ROOT}/root:/${rel}:/content`;
}

/** Create every segment of basePath that doesn't exist yet. Graph's
 *  PUT :/content does NOT auto-create parent folders — a fresh account's
 *  first write would 404. conflictBehavior:fail + tolerating the
 *  nameAlreadyExists error makes this an idempotent walk. Once per
 *  process per path — after that the folders exist. */
const _ensuredPaths = new Set<string>();
async function oneDriveEnsurePath(token: string, basePath: string): Promise<void> {
    const clean = basePath.replace(/^\/+|\/+$/g, "");
    if (!clean || _ensuredPaths.has(clean)) return;
    let parent = "";
    for (const seg of clean.split("/")) {
        const url = parent
            ? `${GRAPH_ROOT}/root:/${parent.split("/").map(encodeURIComponent).join("/")}:/children`
            : `${GRAPH_ROOT}/root/children`;
        const r = await fetch(url, {
            method: "POST",
            headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
            body: JSON.stringify({ name: seg, folder: {}, "@microsoft.graph.conflictBehavior": "fail" }),
        });
        // 201 created, 409 nameAlreadyExists — both mean the segment exists now.
        if (!r.ok && r.status !== 409) {
            throw new Error(`OneDrive folder create "${seg}" failed: ${r.status} ${await r.text().catch(() => "")}`);
        }
        parent = parent ? `${parent}/${seg}` : seg;
    }
    _ensuredPaths.add(clean);
}

async function oneDriveRead(basePath: string, fileName: string): Promise<string | null> {
    const token = await getOneDriveToken();
    if (!token) return null;
    const r = await fetch(graphContentUrl(basePath, fileName), { headers: { "Authorization": `Bearer ${token}` } });
    if (r.status === 404) return null;
    if (!r.ok) {
        console.error(`  [cloud] OneDrive read ${fileName}: ${r.status}`);
        return null;
    }
    return await r.text();
}

async function oneDriveReadBinary(basePath: string, fileName: string): Promise<string | null> {
    const token = await getOneDriveToken();
    if (!token) return null;
    const r = await fetch(graphContentUrl(basePath, fileName), { headers: { "Authorization": `Bearer ${token}` } });
    if (r.status === 404) return null;
    if (!r.ok) {
        console.error(`  [cloud] OneDrive readBinary ${fileName}: ${r.status}`);
        return null;
    }
    return Buffer.from(await r.arrayBuffer()).toString("base64");
}

async function oneDriveWrite(basePath: string, fileName: string, content: string): Promise<void> {
    const token = await getOneDriveToken();
    if (!token) throw new Error("OneDrive auth failed — cannot write");
    const put = () => fetch(graphContentUrl(basePath, fileName), {
        method: "PUT",
        headers: { "Authorization": `Bearer ${token}`, "Content-Type": "text/plain" },
        body: content,
    });
    let r = await put();
    if (r.status === 404 || r.status === 409) {
        // Parent path missing (fresh account) — create it and retry once.
        await oneDriveEnsurePath(token, basePath);
        r = await put();
    }
    if (!r.ok) throw new Error(`OneDrive write ${fileName} failed: ${r.status} ${await r.text().catch(() => "")}`);
}

/** Fetch the authenticated Microsoft user's profile (setup-flow display). */
export async function getMicrosoftProfile(): Promise<{ name?: string; email?: string } | null> {
    const token = await getOneDriveToken();
    if (!token) return null;
    try {
        const r = await fetch("https://graph.microsoft.com/v1.0/me", { headers: { "Authorization": `Bearer ${token}` } });
        if (!r.ok) return null;
        const me: any = await r.json();
        return { name: me.displayName, email: me.mail || me.userPrincipalName };
    } catch { return null; }
}

// ── Public API ──

export type CloudProvider = "gdrive" | "google" | "onedrive" | "local";

export interface CloudFile {
    /** Read a file. For gdrive, path is just the filename (folder ID is implicit). */
    read(filePath: string): Promise<string | null>;
    /** Read a file as raw bytes, base64-encoded (binary-safe). */
    readBinary(filePath: string): Promise<string | null>;
    /** Write a file. For gdrive, path is just the filename. Throws on failure with a descriptive error. */
    write(filePath: string, content: string): Promise<void>;
    /** Check if a file exists. */
    exists(filePath: string): Promise<boolean>;
}

/**
 * Get a cloud file provider. The locator is provider-specific: gdrive takes
 * the folder ID; onedrive takes the base PATH under the drive root (e.g.
 * "home/.rmfmail"). Files are stored flat in that folder either way — the
 * CloudFile contract is identical, only the FS API underneath differs.
 */
export function getCloudProvider(provider: string, locator?: string): CloudFile | null {
    switch (provider) {
        case "google":
        case "gdrive": {
            const folderId = locator;
            if (!folderId) {
                console.error("  [cloud] gdrive requires a folder ID — run initCloudConfig first");
                return null;
            }
            return {
                read: (fileName) => gDriveReadFromFolder(folderId, fileName),
                readBinary: (fileName) => gDriveReadBinaryFromFolder(folderId, fileName),
                write: (fileName, content) => gDriveWriteToFolder(folderId, fileName, content),
                exists: async (fileName) => (await gDriveReadFromFolder(folderId, fileName)) !== null,
            };
        }
        case "onedrive": {
            const basePath = locator || "home/.rmfmail";
            return {
                read: (fileName) => oneDriveRead(basePath, fileName),
                readBinary: (fileName) => oneDriveReadBinary(basePath, fileName),
                write: (fileName, content) => oneDriveWrite(basePath, fileName, content),
                exists: async (fileName) => (await oneDriveRead(basePath, fileName)) !== null,
            };
        }
        case "local":
            return {
                read: async (p) => { try { return fs.readFileSync(p, "utf-8"); } catch { return null; } },
                readBinary: async (p) => { try { return fs.readFileSync(p).toString("base64"); } 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 — gdrive and onedrive are 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: string): Promise<{ name?: string; email?: string } | null> {
    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() as any;
        const name = data.names?.find((n: any) => n.displayName)?.displayName
                  || data.names?.[0]?.displayName;
        const email = data.emailAddresses?.find((e: any) => e.metadata?.primary)?.value
                   || data.emailAddresses?.[0]?.value;
        return { name, email };
    } catch (e: any) {
        console.error(`  [cloud] getGoogleProfile: ${e.message}`);
        return null;
    }
}
