/**
 * @bobfrankston/mailx-settings
 * Settings management with shared/local split.
 *
 * Files (shared — on OneDrive or similar):
 *   accounts.jsonc    — IMAP/SMTP account configs
 *   preferences.jsonc — UI, sync, font settings
 *   allowlist.jsonc   — remote content allow-list
 *
 * Local overrides (~/.mailx/):
 *   config.jsonc      — pointer to shared dir + local-only settings (storePath, historyDays)
 *   accounts.jsonc    — cached copy, fallback when shared unavailable
 *   preferences.jsonc — local overrides merged on top of shared
 *   allowlist.jsonc   — cached copy
 *
 * The old settings.jsonc is still supported for backward compatibility.
 */
import * as fs from "node:fs";
import * as path from "node:path";
import { parse as parseJsonc } from "jsonc-parser";
import { getCloudProvider, gDriveFindOrCreateFolder } from "./cloud.js";
// ── Paths ──
// REMOVE 2026-05-15 — one-shot migration of legacy ~/.mailx → ~/.rmfmail.
// Runs at module load before any consumer touches LOCAL_DIR. Idempotent
// (no-op if newDir already exists or oldDir is missing). The cloud-side
// rename (the "mailx" folder on Google Drive) is handled separately by
// the user via the Drive web UI — folderId in config.jsonc still resolves
// regardless of the display name, so this is cosmetic rather than load-
// bearing for cloud auth.
{
    const home = process.env.USERPROFILE || process.env.HOME || ".";
    const oldDir = path.join(home, ".mailx");
    const newDir = path.join(home, ".rmfmail");
    if (!fs.existsSync(newDir) && fs.existsSync(oldDir)) {
        try {
            fs.renameSync(oldDir, newDir);
            console.log("[migrate] ~/.mailx → ~/.rmfmail");
        }
        catch (e) {
            console.error(`[migrate] rename failed: ${e?.message || e}`);
        }
    }
}
const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail");
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
const LEGACY_CONFIG_PATH = path.join(LOCAL_DIR, "config.json");
const DEFAULT_STORE_PATH = path.join(LOCAL_DIR, "mailxstore");
/** Resolve a path from config — relative to ~/.mailx/, ~ expands to home */
function resolvePath(p) {
    if (!p)
        return p;
    const home = process.env.USERPROFILE || process.env.HOME || ".";
    // Expand ~ to home directory
    if (p.startsWith("~/") || p.startsWith("~\\"))
        return path.join(home, p.slice(2));
    if (p === "~")
        return home;
    // Absolute path — use as-is
    if (path.isAbsolute(p))
        return p;
    // Relative — resolve from config directory (~/.mailx/)
    return path.resolve(LOCAL_DIR, p);
}
function readLocalConfig() {
    // Migrate config.json → config.jsonc
    if (!fs.existsSync(LOCAL_CONFIG_PATH) && fs.existsSync(LEGACY_CONFIG_PATH)) {
        fs.renameSync(LEGACY_CONFIG_PATH, LOCAL_CONFIG_PATH);
    }
    if (!fs.existsSync(LOCAL_CONFIG_PATH))
        return {};
    return readJsonc(LOCAL_CONFIG_PATH) || {};
}
/** Resolve provider config to a filesystem path (checks for local Google Drive mount) */
function resolveProvider(cfg) {
    // Cloud providers use API only — no filesystem mount scanning
    if (cfg.provider === "gdrive" || cfg.provider === "google")
        return undefined;
    if (cfg.provider === "local")
        return resolvePath(cfg.path);
    return undefined;
}
/** Pending cloud config for API fallback (set when mount not found) */
let pendingCloudConfig = null;
/** Last cloud API error (for UI display) */
let lastCloudError = null;
const cloudErrorListeners = [];
export function onCloudError(cb) {
    cloudErrorListeners.push(cb);
    return () => {
        const i = cloudErrorListeners.indexOf(cb);
        if (i >= 0)
            cloudErrorListeners.splice(i, 1);
    };
}
function setCloudError(error, context) {
    lastCloudError = error;
    for (const cb of cloudErrorListeners) {
        try {
            cb(error, context);
        }
        catch { /* listener faults shouldn't break writes */ }
    }
}
export function getLastCloudError() { return lastCloudError; }
function resolveSharedEntry(entry) {
    if (typeof entry === "string") {
        const p = resolvePath(entry);
        return fs.existsSync(p) ? p : undefined;
    }
    return resolveProvider(entry);
}
function getSharedDir() {
    const config = readLocalConfig();
    if (config.sharedDir) {
        const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
        for (const entry of entries) {
            const resolved = resolveSharedEntry(entry);
            if (resolved)
                return resolved;
        }
        // Set pending cloud config for API access
        if (!pendingCloudConfig) {
            const lastProvider = [...entries].reverse().find(e => typeof e !== "string");
            if (lastProvider)
                pendingCloudConfig = lastProvider;
        }
    }
    // Legacy settingsPath no longer used for shared dir — use loadLegacySettings() for reading only.
    return LOCAL_DIR;
}
/** Read a file via cloud API (when filesystem mount not available) */
export async function cloudRead(filename) {
    if (!pendingCloudConfig)
        return null;
    // Ensure we have a folder ID
    if (!pendingCloudConfig.folderId) {
        pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
        if (pendingCloudConfig.folderId)
            saveFolderIdToConfig(pendingCloudConfig.folderId);
        if (!pendingCloudConfig.folderId) {
            setCloudError(`Cannot read ${filename}: Google Drive folder unavailable (OAuth not granted?)`, { op: "read", filename });
            return null;
        }
    }
    const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
    if (!provider) {
        setCloudError(`No cloud provider for ${pendingCloudConfig.provider}`, { op: "read", filename });
        return null;
    }
    console.log(`  [cloud] Reading ${filename} via ${pendingCloudConfig.provider} API...`);
    const content = await provider.read(filename);
    if (content) {
        setCloudError(null);
        // Cache locally
        try {
            fs.mkdirSync(LOCAL_DIR, { recursive: true });
            fs.writeFileSync(path.join(LOCAL_DIR, filename), content);
        }
        catch { /* ignore cache write failure */ }
    }
    // Don't set error for missing files — they may not exist yet (e.g., clients.jsonc on first run)
    return content;
}
/** Write a file via cloud API. Throws on failure with a descriptive error,
 *  and updates lastCloudError so UI banners pick it up via getStorageInfo()
 *  and the onCloudError listener. */
export async function cloudWrite(filename, content) {
    if (!pendingCloudConfig) {
        const err = `Cloud not initialized yet — cannot save ${filename} (pendingCloudConfig is null; loadSettings may not have run, or config.jsonc has no sharedDir)`;
        console.error(`  [cloud] cloudWrite: ${err}`);
        setCloudError(err, { op: "write", filename });
        throw new Error(err);
    }
    // Ensure we have a folder ID
    if (!pendingCloudConfig.folderId) {
        pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
        if (pendingCloudConfig.folderId)
            saveFolderIdToConfig(pendingCloudConfig.folderId);
        if (!pendingCloudConfig.folderId) {
            const err = `Cannot save ${filename}: Google Drive folder unavailable (OAuth not granted or token expired)`;
            setCloudError(err, { op: "write", filename });
            throw new Error(err);
        }
    }
    const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
    if (!provider) {
        const err = `No cloud provider for ${pendingCloudConfig.provider}`;
        setCloudError(err, { op: "write", filename });
        throw new Error(err);
    }
    try {
        await provider.write(filename, content);
        setCloudError(null);
    }
    catch (e) {
        setCloudError(e.message, { op: "write", filename });
        throw e;
    }
}
/** Persist the discovered folder ID back to config.jsonc so we don't search again */
function saveFolderIdToConfig(folderId) {
    try {
        const config = readLocalConfig();
        if (config.sharedDir && typeof config.sharedDir === "object" && !Array.isArray(config.sharedDir)) {
            config.sharedDir.folderId = folderId;
            atomicWrite(LOCAL_CONFIG_PATH, config);
        }
    }
    catch { /* non-critical */ }
}
/** Whether cloud API fallback is active */
export function isCloudMode() {
    return pendingCloudConfig !== null;
}
/** Get storage provider info for display */
export function getStorageInfo() {
    const config = readLocalConfig();
    if (config.sharedDir) {
        const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
        for (const entry of entries) {
            if (typeof entry === "string") {
                const resolved = resolveSharedEntry(entry);
                if (resolved && resolved !== LOCAL_DIR) {
                    return { provider: "local", mode: "local", cloudPath: resolved };
                }
                continue;
            }
            // Provider-based entry — check for API mode (folderId) first, then mount
            const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
            if (entry.folderId) {
                // Has folder ID → API mode (don't scan filesystem for mounts)
                return { provider: name, mode: "api", cloudPath: entry.path, cloudError: lastCloudError || undefined };
            }
            const resolved = resolveSharedEntry(entry);
            if (resolved && resolved !== LOCAL_DIR) {
                return { provider: name, mode: "mount", cloudPath: entry.path };
            }
        }
        // Not mounted and no folderId — check pendingCloudConfig from initCloudConfig()
        if (pendingCloudConfig) {
            const name = (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "gdrive" : pendingCloudConfig.provider;
            return { provider: name, mode: "api", cloudPath: pendingCloudConfig.path, cloudError: lastCloudError || undefined };
        }
    }
    return { provider: "local", mode: "local" };
}
// ── File helpers ──
/** Read JSON or JSONC file. If exact path not found, tries .json/.jsonc variant. */
function readJsonc(filePath) {
    let actual = filePath;
    if (!fs.existsSync(actual)) {
        // Try alternate extension
        if (actual.endsWith(".jsonc"))
            actual = actual.replace(/\.jsonc$/, ".json");
        else if (actual.endsWith(".json"))
            actual = actual.replace(/\.json$/, ".jsonc");
        if (!fs.existsSync(actual))
            return null;
    }
    try {
        return parseJsonc(fs.readFileSync(actual, "utf-8").replace(/\r/g, ""));
    }
    catch (e) {
        console.error(`Failed to read ${actual}: ${e.message}`);
        return null;
    }
}
function atomicWrite(filePath, data) {
    const dir = path.dirname(filePath);
    fs.mkdirSync(dir, { recursive: true });
    const tmp = filePath + ".tmp";
    fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
    fs.renameSync(tmp, filePath);
}
/** Read a config file: shared first, local fallback, merge local overrides */
function loadFile(filename, defaults) {
    const sharedDir = getSharedDir();
    const sharedPath = path.join(sharedDir, filename);
    const localPath = path.join(LOCAL_DIR, filename);
    // Read shared version
    let data = readJsonc(sharedPath);
    // If no shared, try local
    if (!data)
        data = readJsonc(localPath);
    // If neither exists, use defaults
    if (!data)
        return defaults;
    // Cache shared → local (for offline fallback)
    if (sharedDir !== LOCAL_DIR && fs.existsSync(sharedPath)) {
        try {
            const shared = fs.readFileSync(sharedPath, "utf-8");
            fs.mkdirSync(LOCAL_DIR, { recursive: true });
            fs.writeFileSync(localPath, shared);
        }
        catch { /* ignore cache failures */ }
    }
    return { ...defaults, ...data };
}
/** Save a config file to the shared directory (and cloud API if active).
 *  Always writes the local copy first so the data is never lost.
 *  If a cloud provider is configured, also writes there — failures set
 *  lastCloudError and notify onCloudError listeners so the UI can show
 *  a banner. The cloud write is fire-and-forget at this layer; callers
 *  who need the result should use the typed save* helpers below. */
function saveFile(filename, data) {
    const sharedDir = getSharedDir();
    atomicWrite(path.join(sharedDir, filename), data);
    // Also update local cache
    if (sharedDir !== LOCAL_DIR) {
        try {
            atomicWrite(path.join(LOCAL_DIR, filename), data);
        }
        catch { /* ignore */ }
    }
    // Write to cloud API if filesystem mount unavailable (creates app-owned file for drive.file scope)
    if (pendingCloudConfig) {
        cloudWrite(filename, JSON.stringify(data, null, 2))
            .then(() => console.log(`  [cloud] Saved ${filename} via ${pendingCloudConfig.provider} API`))
            .catch(e => console.error(`  [cloud] Failed to save ${filename}: ${e.message}`));
        // Note: we don't await — saveFile is sync. cloudWrite() already calls
        // setCloudError(), which fires onCloudError listeners synchronously
        // when the promise rejects, so the UI gets the failure even though
        // we don't propagate it back through the call chain here.
    }
}
const PROVIDERS = {
    "gmail.com": {
        label: "Gmail",
        imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
        smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
    },
    "googlemail.com": {
        label: "Gmail",
        imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
        smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
    },
    "outlook.com": {
        label: "Outlook",
        imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
        smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
    },
    "hotmail.com": {
        label: "Hotmail",
        imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
        smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
    },
    "yahoo.com": {
        label: "Yahoo",
        imap: { host: "imap.mail.yahoo.com", port: 993, tls: true, auth: "password" },
        smtp: { host: "smtp.mail.yahoo.com", port: 587, tls: true, auth: "password" },
    },
    "aol.com": {
        label: "AOL",
        imap: { host: "imap.aol.com", port: 993, tls: true, auth: "password" },
        smtp: { host: "smtp.aol.com", port: 587, tls: true, auth: "password" },
    },
    "icloud.com": {
        label: "iCloud",
        imap: { host: "imap.mail.me.com", port: 993, tls: true, auth: "password" },
        smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "password" },
    },
};
/** Fill in provider defaults for an account based on email domain.
 *  Exported so mailx-service's leanAccountsJsonc helper can reuse the same
 *  canonicalization rules that loadAccounts uses. */
export function normalizeAccount(acct, globalName) {
    const email = acct.email || "";
    const localPart = email.split("@")[0]?.toLowerCase() || "";
    const domain = email.split("@")[1]?.toLowerCase() || "";
    const provider = PROVIDERS[domain];
    const user = acct.imap?.user || acct.user || email;
    // P14: auto-derive id and label so a known-provider account works with just
    // { email, password? } in accounts.jsonc. id defaults to local-part (most
    // accounts have a unique local-part); label defaults to provider name or id.
    // Generic local-parts (info, admin, support, no-reply, ...) fall back to
    // the domain stem to avoid collisions across accounts.
    const GENERIC_LOCALS = new Set(["info", "admin", "support", "no-reply", "noreply", "contact", "hello", "mail", "office"]);
    const domainStem = domain.split(".")[0] || "";
    const autoId = (localPart && !GENERIC_LOCALS.has(localPart)) ? localPart : (domainStem || "account");
    return {
        id: acct.id || autoId,
        name: acct.name || globalName || localPart,
        label: acct.label || provider?.label || acct.id || autoId,
        email,
        imap: {
            host: acct.imap?.host || provider?.imap.host || `imap.${domain}`,
            port: acct.imap?.port || provider?.imap.port || 993,
            tls: acct.imap?.tls ?? provider?.imap.tls ?? true,
            auth: acct.imap?.auth || provider?.imap.auth || "password",
            user: acct.imap?.user || user,
            password: acct.imap?.password || acct.password,
        },
        smtp: {
            host: acct.smtp?.host || provider?.smtp.host || `smtp.${domain}`,
            port: acct.smtp?.port || provider?.smtp.port || 587,
            tls: acct.smtp?.tls ?? provider?.smtp.tls ?? true,
            auth: acct.smtp?.auth || provider?.smtp.auth || "password",
            user: acct.smtp?.user || user,
            password: acct.smtp?.password || acct.password,
        },
        enabled: acct.enabled ?? true,
        primary: acct.primary,
        primaryCalendar: acct.primaryCalendar,
        primaryTasks: acct.primaryTasks,
        primaryContacts: acct.primaryContacts,
        defaultSend: acct.defaultSend,
        syncContacts: acct.syncContacts ?? (provider?.imap.auth === "oauth2"),
        relayDomains: acct.relayDomains,
        deliveredToPrefix: acct.deliveredToPrefix,
        identityDomains: acct.identityDomains,
        // `spam` passthrough retired 2026-04-22 — markAsSpamMessages now finds
        // the junk folder via `specialUse === "junk"` on the DB folder record
        // (populated by mailx-imap from iflow's getSpecialFolders()). Authoritative
        // per-server info beats per-domain guesses.
        // `signature` is on AccountConfig in mailx-types but the workspace
        // build order sometimes leaves a stale .d.ts for type-check; using
        // `as any` is the minimum-blast-radius way to add the field without
        // blocking the build. Once mailx-types has rebuilt once (post-field)
        // this cast can be removed.
        ...(acct.signature ? { signature: acct.signature } : {}),
        ...(acct.sig && typeof acct.sig === "object" && typeof acct.sig.text === "string"
            ? { sig: { text: acct.sig.text, html: !!acct.sig.html } } : {}),
    };
}
// ── Defaults ──
const DEFAULT_ACCOUNTS = [];
const DEFAULT_PREFERENCES = {
    ui: {
        theme: "system",
        editor: "quill",
        folderWidth: 220,
        listViewerSplit: 40,
        fontSize: 15,
    },
    sync: {
        intervalMinutes: 5,
        historyDays: 30,
        prefetch: true,
    },
    autocomplete: {
        enabled: false,
        provider: "ollama",
        ollamaUrl: "http://localhost:11434",
        ollamaModel: "qwen2.5-coder:1.5b",
        cloudApiKey: "",
        cloudModel: "claude-sonnet-4-20250514",
        debounceMs: 600,
        maxTokens: 60,
    },
};
const DEFAULT_AUTOCOMPLETE = {
    enabled: false,
    provider: "ollama",
    ollamaUrl: "http://localhost:11434",
    ollamaModel: "qwen2.5-coder:1.5b",
    cloudApiKey: "",
    cloudModel: "claude-sonnet-4-20250514",
    debounceMs: 600,
    maxTokens: 60,
};
const DEFAULT_ALLOWLIST = {
    senders: [],
    domains: [],
    recipients: [],
    // Flagged senders/domains — surfaced as a red warning on the
    // remote-content banner. Distinct from the positive lists above:
    // these don't auto-allow anything, they make the banner louder when
    // a known-suspicious correspondent's mail shows up. Future: a shared
    // GitHub list users can opt in to (community-flagged phishing /
    // tracker-heavy senders) — see TODO.md.
    flaggedSenders: [],
    flaggedDomains: [],
};
// ── Public API ──
/** Load account configs */
export function loadAccounts() {
    const sharedDir = getSharedDir();
    const sharedPath = path.join(sharedDir, "accounts.jsonc");
    const localPath = path.join(LOCAL_DIR, "accounts.jsonc");
    // Try shared first, then local cache
    let accounts = readJsonc(sharedPath);
    if (!accounts)
        accounts = readJsonc(localPath);
    if (accounts?.accounts || Array.isArray(accounts)) {
        // Cache shared to local for offline fallback — but ONLY if the
        // content actually differs. Unconditionally writing on every load
        // retriggers fs.watch on the local copy, which fires the config-
        // changed banner and cloud-poll cycle even when nothing changed.
        // Result: "accounts.jsonc changed" notification firing constantly.
        if (sharedDir !== LOCAL_DIR && fs.existsSync(sharedPath)) {
            try {
                const sharedContent = fs.readFileSync(sharedPath, "utf-8");
                let localContent = "";
                try {
                    localContent = fs.readFileSync(localPath, "utf-8");
                }
                catch { /* missing */ }
                // Normalize before comparing — GDrive-mounted copies often
                // differ in BOM / line endings / trailing newline without any
                // semantic change, and that triggered the spurious banner.
                const norm = (s) => s.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/[ \t\r\n]+$/, "");
                if (norm(sharedContent) !== norm(localContent)) {
                    fs.mkdirSync(LOCAL_DIR, { recursive: true });
                    fs.writeFileSync(localPath, sharedContent);
                }
            }
            catch { /* ignore */ }
        }
        const raw = accounts.accounts || accounts;
        const globalName = accounts.name || "";
        const result = deduplicateAccounts(raw.map((a) => normalizeAccount(a, globalName)));
        return applyAccountOverrides(result);
    }
    // Legacy: read from settings.jsonc
    const legacy = loadLegacySettings();
    if (legacy?.accounts)
        return applyAccountOverrides(legacy.accounts.map((a) => normalizeAccount(a, legacy.name)));
    return DEFAULT_ACCOUNTS;
}
/** Normalize email for dedup — Gmail ignores dots before @ */
function normalizeEmail(email) {
    const [local, domain] = email.toLowerCase().split("@");
    if (!domain)
        return email.toLowerCase();
    if (domain === "gmail.com" || domain === "googlemail.com") {
        return local.replace(/\./g, "") + "@gmail.com";
    }
    return `${local}@${domain}`;
}
/** Remove duplicate accounts (same email after normalization) */
function deduplicateAccounts(accounts) {
    const seen = new Set();
    return accounts.filter(a => {
        const key = normalizeEmail(a.email);
        if (seen.has(key))
            return false;
        seen.add(key);
        return true;
    });
}
/** Apply local per-account overrides (enabled, etc.) */
function applyAccountOverrides(accounts) {
    const localConfig = readLocalConfig();
    const overrides = localConfig.accountOverrides;
    if (!overrides)
        return accounts;
    for (const acct of accounts) {
        const ov = overrides[acct.id];
        if (!ov)
            continue;
        if (ov.enabled !== undefined)
            acct.enabled = ov.enabled;
    }
    return accounts;
}
/** Load accounts with cloud API fallback (async — use when cloud settings may not be mounted) */
export async function loadAccountsAsync() {
    // Try sync first (filesystem)
    const accounts = loadAccounts();
    if (accounts.length > 0)
        return accounts;
    // Try cloud API fallback
    if (pendingCloudConfig) {
        console.log("  [cloud] Trying cloud API for accounts...");
        const content = await cloudRead("accounts.jsonc");
        if (content) {
            const data = parseJsonc(content);
            if (data?.accounts || Array.isArray(data)) {
                const raw = data.accounts || data;
                const globalName = data.name || "";
                return applyAccountOverrides(raw.map((a) => normalizeAccount(a, globalName)));
            }
        }
        // Legacy settings.jsonc is no longer read — use accounts.jsonc only
    }
    return [];
}
/** Strip default-valued fields from a normalized AccountConfig so the
 *  serialized JSONC stays compact and human-editable. The previous version
 *  was round-tripping every defaulted field (port: 993, tls: true, auth:
 *  "password", enabled: true, etc.), which bloated a 10-line accounts.jsonc
 *  to 60+ lines and embedded unnecessary "knowledge" about defaults.
 *
 *  Rules:
 *  - Drop fields that match the provider default (host/port/tls/auth derived
 *    from email domain via PROVIDERS).
 *  - Drop `enabled: true` (default).
 *  - Drop `name` if equal to the file-level `globalName`.
 *  - Drop `imap.user` / `smtp.user` if they equal the email.
 *  - Drop `sig.html: false` (default; only keep when user enabled HTML sigs).
 *  - Keep field order: id → label → email → primary* → imap → smtp → defaultSend
 *    → relayDomains → deliveredToPrefix → identityDomains → syncContacts → sig.
 *    Curated for readability, not alphabetic. */
export function denormalizeAccount(acct, globalName) {
    const domain = (acct.email || "").split("@")[1]?.toLowerCase() || "";
    const provider = PROVIDERS[domain];
    const out = {};
    out.id = acct.id;
    if (acct.label && acct.label !== provider?.label && acct.label !== acct.id)
        out.label = acct.label;
    out.email = acct.email;
    if (acct.name && acct.name !== globalName)
        out.name = acct.name;
    if (acct.primary)
        out.primary = true;
    if (acct.primaryCalendar !== undefined)
        out.primaryCalendar = acct.primaryCalendar;
    if (acct.primaryTasks !== undefined)
        out.primaryTasks = acct.primaryTasks;
    if (acct.primaryContacts !== undefined)
        out.primaryContacts = acct.primaryContacts;
    // imap — keep only fields that differ from provider defaults.
    const imapOut = {};
    if (acct.imap?.host && acct.imap.host !== provider?.imap.host)
        imapOut.host = acct.imap.host;
    if (acct.imap?.user && acct.imap.user !== acct.email)
        imapOut.user = acct.imap.user;
    if (acct.imap?.password)
        imapOut.password = acct.imap.password;
    if (acct.imap?.port && acct.imap.port !== (provider?.imap.port ?? 993))
        imapOut.port = acct.imap.port;
    if (acct.imap?.tls !== undefined && acct.imap.tls !== (provider?.imap.tls ?? true))
        imapOut.tls = acct.imap.tls;
    if (acct.imap?.auth && acct.imap.auth !== (provider?.imap.auth ?? "password"))
        imapOut.auth = acct.imap.auth;
    if (Object.keys(imapOut).length > 0)
        out.imap = imapOut;
    // smtp — same treatment.
    const smtpOut = {};
    if (acct.smtp?.host && acct.smtp.host !== provider?.smtp.host)
        smtpOut.host = acct.smtp.host;
    if (acct.smtp?.user && acct.smtp.user !== acct.email && acct.smtp.user !== acct.imap?.user)
        smtpOut.user = acct.smtp.user;
    if (acct.smtp?.password)
        smtpOut.password = acct.smtp.password;
    if (acct.smtp?.port && acct.smtp.port !== (provider?.smtp.port ?? 587))
        smtpOut.port = acct.smtp.port;
    if (acct.smtp?.tls !== undefined && acct.smtp.tls !== (provider?.smtp.tls ?? true))
        smtpOut.tls = acct.smtp.tls;
    if (acct.smtp?.auth && acct.smtp.auth !== (provider?.smtp.auth ?? "password"))
        smtpOut.auth = acct.smtp.auth;
    if (Object.keys(smtpOut).length > 0)
        out.smtp = smtpOut;
    if (acct.defaultSend)
        out.defaultSend = true;
    if (acct.enabled === false)
        out.enabled = false; // default true → omit
    if (acct.relayDomains && acct.relayDomains.length > 0)
        out.relayDomains = acct.relayDomains;
    if (acct.deliveredToPrefix && acct.deliveredToPrefix.length > 0)
        out.deliveredToPrefix = acct.deliveredToPrefix;
    if (acct.identityDomains && acct.identityDomains.length > 0)
        out.identityDomains = acct.identityDomains;
    // syncContacts default: true for OAuth, false otherwise. Only emit when
    // the user overrode the default.
    const syncContactsDefault = provider?.imap.auth === "oauth2";
    if (acct.syncContacts !== undefined && acct.syncContacts !== syncContactsDefault) {
        out.syncContacts = acct.syncContacts;
    }
    if (acct.signature)
        out.signature = acct.signature;
    if (acct.sig?.text) {
        // html: false is default; only keep the html flag when explicitly true.
        out.sig = acct.sig.html ? { text: acct.sig.text, html: true } : { text: acct.sig.text };
    }
    return out;
}
/** Save account configs */
/** Save accounts — merges with cloud copy by email (multi-client safe).
 *  Writes the lean form via denormalizeAccount so accounts.jsonc stays
 *  compact and human-editable. */
export async function saveAccounts(accounts) {
    // Merge with cloud: keep all accounts, deduplicate by normalized email
    try {
        const cloudContent = await cloudRead("accounts.jsonc");
        if (cloudContent) {
            const cloud = parseJsonc(cloudContent);
            const cloudAccts = cloud?.accounts || (Array.isArray(cloud) ? cloud : []);
            if (cloudAccts.length > 0) {
                const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
                for (const ca of cloudAccts) {
                    if (ca.email && !seen.has(normalizeEmail(ca.email))) {
                        // Cloud entries are already lean — feed through
                        // normalizeAccount to coerce to AccountConfig shape.
                        accounts.push(normalizeAccount(ca));
                        seen.add(normalizeEmail(ca.email));
                    }
                }
            }
        }
    }
    catch { /* cloud read failed — save local version */ }
    // Promote a shared "name" to file level when every account has the same
    // name — keeps the JSONC tidy ({ "name": "Bob Frankston", "accounts": [...] }
    // instead of repeating "name" on each entry).
    const names = new Set(accounts.map(a => a.name).filter(Boolean));
    const globalName = names.size === 1 ? [...names][0] : undefined;
    const lean = accounts.map(a => denormalizeAccount(a, globalName));
    const payload = globalName ? { name: globalName, accounts: lean } : { accounts: lean };
    saveFile("accounts.jsonc", payload);
}
/** Load preferences (shared + local overrides, with legacy fallback) */
export function loadPreferences() {
    let shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
    // Legacy fallback: read ui/sync from settings.jsonc if preferences.jsonc had only defaults
    const legacy = loadLegacySettings();
    if (legacy?.ui)
        shared = { ...shared, ui: { ...shared.ui, ...legacy.ui } };
    if (legacy?.sync)
        shared = { ...shared, sync: { ...shared.sync, ...legacy.sync } };
    const localConfig = readLocalConfig();
    // Local overrides
    if (localConfig.historyDays !== undefined) {
        shared.sync.historyDays = localConfig.historyDays;
    }
    return {
        ui: { ...DEFAULT_PREFERENCES.ui, ...shared.ui },
        sync: { ...DEFAULT_PREFERENCES.sync, ...shared.sync },
        autocomplete: { ...DEFAULT_AUTOCOMPLETE, ...shared.autocomplete },
    };
}
/** Save preferences */
export function savePreferences(prefs) {
    saveFile("preferences.jsonc", prefs);
}
/** Load autocomplete settings */
export function loadAutocomplete() {
    const prefs = loadPreferences();
    return prefs.autocomplete;
}
/** Save autocomplete settings */
export function saveAutocomplete(settings) {
    const prefs = loadPreferences();
    prefs.autocomplete = settings;
    savePreferences(prefs);
}
/** Load remote content allow-list */
export function loadAllowlist() {
    return loadFile("allowlist.jsonc", DEFAULT_ALLOWLIST);
}
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
export async function saveAllowlist(list) {
    // Read current cloud version and merge (other clients may have added entries)
    let merged = { ...list };
    try {
        const cloudContent = await cloudRead("allowlist.jsonc");
        if (cloudContent) {
            const cloud = parseJsonc(cloudContent);
            if (cloud) {
                const mergeArrays = (local, remote) => [...new Set([...local, ...remote])];
                merged = {
                    senders: mergeArrays(list.senders || [], cloud.senders || []),
                    domains: mergeArrays(list.domains || [], cloud.domains || []),
                    recipients: mergeArrays(list.recipients || [], cloud.recipients || []),
                    flaggedSenders: mergeArrays(list.flaggedSenders || [], cloud.flaggedSenders || []),
                    flaggedDomains: mergeArrays(list.flaggedDomains || [], cloud.flaggedDomains || []),
                };
            }
        }
    }
    catch { /* cloud read failed — save local version */ }
    saveFile("allowlist.jsonc", merged);
}
// ── Legacy compatibility ──
function loadLegacySettings() {
    const config = readLocalConfig();
    if (config.settingsPath)
        return readJsonc(resolvePath(config.settingsPath));
    // Try shared dir first, then local
    const sharedDir = getSharedDir();
    const shared = readJsonc(path.join(sharedDir, "settings.jsonc"));
    if (shared)
        return shared;
    return readJsonc(path.join(LOCAL_DIR, "settings.jsonc"));
}
/** Load settings — unified view combining all files (backward compatible) */
export function loadSettings() {
    const accounts = loadAccounts();
    const prefs = loadPreferences();
    const localConfig = readLocalConfig();
    return {
        accounts,
        ui: prefs.ui,
        sync: prefs.sync,
        autocomplete: prefs.autocomplete,
        store: {
            basePath: localConfig.storePath || DEFAULT_STORE_PATH,
            compressionBoundaryDays: 365,
        },
    };
}
/** Save settings — writes to split files */
export async function saveSettings(settings) {
    await saveAccounts(settings.accounts);
    savePreferences({ ui: settings.ui, sync: settings.sync });
}
/** Get the local store base path */
export function getStorePath() {
    const config = readLocalConfig();
    return config.storePath ? resolvePath(config.storePath) : DEFAULT_STORE_PATH;
}
/** Get the local data directory (DB, store, etc.) */
export function getConfigDir() {
    return LOCAL_DIR;
}
/** Get the shared settings directory */
export { getSharedDir };
// detectSharedDir() removed — cloud storage is configured via API (gdrive/onedrive),
// not auto-detected from filesystem mounts. Setup form triggers initCloudConfig().
/** Initialize local config if it doesn't exist */
export function initLocalConfig(sharedDir, storePath) {
    if (fs.existsSync(LOCAL_CONFIG_PATH) && !sharedDir && !storePath)
        return;
    const existing = readLocalConfig();
    // Use explicit sharedDir or preserve existing — no auto-detection.
    // Cloud storage is configured when user adds an account (initCloudConfig).
    const resolvedSharedDir = sharedDir || existing.sharedDir;
    const config = {
        ...existing,
        sharedDir: resolvedSharedDir,
        storePath: storePath || existing.storePath || DEFAULT_STORE_PATH,
    };
    fs.mkdirSync(LOCAL_DIR, { recursive: true });
    atomicWrite(LOCAL_CONFIG_PATH, config);
}
/** Initialize config with Google Drive cloud storage.
 *  Finds or creates the app-owned "mailx" folder via Drive API and stores its ID.
 *  No mount scanning — API only. Existing settings at other paths (e.g., home/.mailx
 *  from Desktop sync) must be migrated manually or via config.jsonc importPath. */
export async function initCloudConfig(provider = "gdrive") {
    const existing = readLocalConfig();
    if (existing.sharedDir)
        return; // Already configured
    // Find or create the settings folder via Drive API — tries "home/.mailx"
    // first (family convention), then "mailx" (default). The found path gets
    // saved so future lookups don't re-scan.
    const folderId = await gDriveFindOrCreateFolder();
    // Detect which path was actually found by reading back from the API
    // (gDriveFindOrCreateFolder logs it). For now use "mailx" as default
    // label — the folderId is what matters for subsequent reads/writes.
    const sharedDir = { provider, path: "home/.mailx", folderId: folderId || undefined };
    const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
    fs.mkdirSync(LOCAL_DIR, { recursive: true });
    atomicWrite(LOCAL_CONFIG_PATH, config);
    pendingCloudConfig = sharedDir;
    console.log(`  Initialized cloud config: ${provider} (folder ID: ${folderId || "pending"})`);
}
const DEFAULT_SETTINGS = {
    accounts: [],
    ui: DEFAULT_PREFERENCES.ui,
    sync: DEFAULT_PREFERENCES.sync,
    autocomplete: DEFAULT_AUTOCOMPLETE,
    store: { basePath: DEFAULT_STORE_PATH, compressionBoundaryDays: 365 },
};
/** Get historyDays for an account: per-account override > system override > shared default */
export function getHistoryDays(accountId) {
    const localConfig = readLocalConfig();
    if (accountId && localConfig.accountOverrides?.[accountId]?.historyDays !== undefined) {
        return localConfig.accountOverrides[accountId].historyDays;
    }
    if (localConfig.historyDays !== undefined)
        return localConfig.historyDays;
    const prefs = loadPreferences();
    return prefs.sync.historyDays || 0;
}
/** Get prefetch setting: download bodies during sync (default true) */
export function getPrefetch() {
    const prefs = loadPreferences();
    return prefs.sync.prefetch !== false;
}
export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
//# sourceMappingURL=index.js.map