/**
 * AccountManager — keeps one SimpleIMAPService + SMTPService per account
 * alive concurrently, supports hot-swapping the "active" account without
 * a server restart, and lets the tool dispatcher route individual calls
 * to a specific account via its `account_id` argument.
 *
 * Design notes
 *   - One ImapFlow connection per account. imapflow's documented pattern
 *     is "N separate clients" (no built-in pool); IDLE auto-runs per
 *     client. Memory is bounded because mailpouch is single-user and
 *     most users have ≤ 3 accounts.
 *   - Lazy connection: services are created per account at AccountManager
 *     construction, but the underlying IMAP socket only opens on first
 *     use. Same for the SMTP transporter (nodemailer is construct-cheap).
 *   - Hot-swap semantics: changing the active account rewires the module-
 *     level `imapService` / `smtpService` references via injected setters.
 *     Callers that were mid-flight keep their existing bindings — ALS or
 *     closure captures of the prior active services continue to work
 *     until the call completes.
 *   - Credential scope: each account's password/SMTP token stays scoped
 *     to its own service instances. Switching accounts never leaks
 *     creds across account boundaries.
 */

import { logger } from "../utils/logger.js";
import { SMTPService } from "../services/smtp-service.js";
import { SimpleIMAPService } from "../services/simple-imap-service.js";
import type { ProtonMailConfig } from "../types/index.js";
import type { AccountSpec } from "./types.js";
import { readRegistry, readRegistryWithSecrets } from "./registry.js";
import { notifications as grantNotifications } from "../agents/notifications.js";
import { EventEmitter } from "events";

export interface AccountServices {
  imap: SimpleIMAPService;
  smtp: SMTPService;
  spec: AccountSpec;
}

/** Build the runtime ProtonMailConfig shape the SMTPService ctor expects. */
function specToRuntimeConfig(spec: AccountSpec): ProtonMailConfig {
  return {
    smtp: {
      host: spec.smtpHost,
      port: spec.smtpPort,
      secure: spec.tlsMode === "ssl",
      username: spec.username,
      password: spec.password,
      smtpToken: spec.smtpToken,
      bridgeCertPath: spec.bridgeCertPath,
      allowInsecureBridge: spec.allowInsecureBridge,
    },
    imap: {
      host: spec.imapHost,
      port: spec.imapPort,
      secure: spec.tlsMode === "ssl",
      username: spec.username,
      password: spec.password,
      bridgeCertPath: spec.bridgeCertPath,
      allowInsecureBridge: spec.allowInsecureBridge,
    },
    debug: false,
    autoStartBridge: spec.autoStartBridge,
    bridgePath: spec.bridgePath,
  };
}

export class AccountManager extends EventEmitter {
  private readonly perAccount = new Map<string, AccountServices>();
  private _activeAccountId = "";

  constructor() {
    super();
    this.rebuildFromRegistry();
  }

  /**
   * Rebuild the account map from the persisted registry. Called at
   * construction and after any setActiveAccount / registry mutation.
   * Preserves in-flight service instances for accounts that still exist;
   * tears down instances for accounts that were deleted; constructs new
   * instances for accounts that were added.
   *
   * Synchronous variant that reads the on-disk state WITHOUT pulling
   * plaintext creds from the keychain — intended for the construction
   * path where we can't block on an async keychain call. Use
   * `rebuildFromRegistryAsync()` (below) when you need the creds
   * populated — main() calls the async version right after boot.
   */
  rebuildFromRegistry(): void {
    const reg = readRegistry();
    const seen = new Set<string>();
    for (const spec of reg.accounts) {
      seen.add(spec.id);
      const existing = this.perAccount.get(spec.id);
      if (existing) {
        // Patch the spec into the existing services so credential
        // changes propagate without a reconnect.
        existing.spec = spec;
        existing.smtp["config"] = specToRuntimeConfig(spec);
        existing.smtp.reinitialize();
        continue;
      }
      const svcs: AccountServices = {
        spec,
        imap: new SimpleIMAPService(),
        smtp: new SMTPService(specToRuntimeConfig(spec)),
      };
      this.perAccount.set(spec.id, svcs);
    }
    // Tear down services for deleted accounts.
    for (const [id, svcs] of this.perAccount) {
      if (seen.has(id)) continue;
      this.perAccount.delete(id);
      svcs.smtp.close().catch(() => {});
      svcs.imap.disconnect().catch(() => {});
    }
    // Ensure activeAccountId points at a real entry.
    if (!this.perAccount.has(reg.activeAccountId) && this.perAccount.size > 0) {
      this._activeAccountId = [...this.perAccount.keys()][0];
    } else {
      this._activeAccountId = reg.activeAccountId;
    }
  }

  /**
   * Async rebuild that fills credentials from the OS keychain. Called
   * by main() right after boot and after any Accounts-tab mutation
   * that lands via the settings-UI server — ensures the in-memory
   * services have the passwords the user persisted, even though the
   * on-disk config blanks them.
   */
  async rebuildFromRegistryAsync(): Promise<void> {
    const reg = await readRegistryWithSecrets();
    const seen = new Set<string>();
    for (const spec of reg.accounts) {
      seen.add(spec.id);
      const existing = this.perAccount.get(spec.id);
      if (existing) {
        existing.spec = spec;
        existing.smtp["config"] = specToRuntimeConfig(spec);
        existing.smtp.reinitialize();
        continue;
      }
      const svcs: AccountServices = {
        spec,
        imap: new SimpleIMAPService(),
        smtp: new SMTPService(specToRuntimeConfig(spec)),
      };
      this.perAccount.set(spec.id, svcs);
    }
    for (const [id, svcs] of this.perAccount) {
      if (seen.has(id)) continue;
      this.perAccount.delete(id);
      svcs.smtp.close().catch(() => {});
      svcs.imap.disconnect().catch(() => {});
    }
    if (!this.perAccount.has(reg.activeAccountId) && this.perAccount.size > 0) {
      this._activeAccountId = [...this.perAccount.keys()][0];
    } else {
      this._activeAccountId = reg.activeAccountId;
    }
  }

  /** The account currently wired into the module-level service references. */
  activeAccountId(): string { return this._activeAccountId; }

  /** Services for whichever account is currently active. */
  getActive(): AccountServices {
    const svcs = this.perAccount.get(this._activeAccountId);
    if (!svcs) throw new Error(`No account services for active id ${this._activeAccountId}`);
    return svcs;
  }

  /** Services for a specific account (by id). Throws on unknown id. */
  getForAccount(accountId: string): AccountServices {
    const svcs = this.perAccount.get(accountId);
    if (!svcs) throw new Error(`Unknown account id: ${accountId}`);
    return svcs;
  }

  /** Enumerate all accounts the manager knows about. */
  list(): AccountServices[] { return [...this.perAccount.values()]; }

  /**
   * Hot-swap the active account. Rewires the active pointer and emits
   * "active-changed" so any subscribers (module-level re-bindings, tray
   * updaters) can react. No service teardown — the prior account's
   * clients remain warm for future per-call routing.
   */
  async setActive(accountId: string): Promise<void> {
    if (!this.perAccount.has(accountId)) throw new Error(`Unknown account id: ${accountId}`);
    const prev = this._activeAccountId;
    if (prev === accountId) return;
    this._activeAccountId = accountId;
    logger.info(`Active account hot-swapped: ${prev} → ${accountId}`, "AccountManager");
    this.emit("active-changed", { prev, next: accountId, services: this.getActive() });
    // A grant-style notification so the tray/UI pick it up alongside agent events.
    grantNotifications.emit("active-account-changed", { prev, next: accountId });
  }

  /**
   * Inject credentials freshly loaded from the OS keychain into every
   * account that currently has an empty password slot. Patches the in-memory
   * spec, the per-account SMTPService.config, and calls reinitialize() so
   * the SMTP transporter is rebuilt with the credentials. IMAP picks up the
   * new password automatically through connectAll() (which reads from the
   * patched spec).
   *
   * Context: credentials are stored in the OS keychain (under
   * service=mailpouch, account=bridge-password) rather than in the config
   * file so ~/.mailpouch.json can be mode-0600 without also being a plaintext
   * secret store. The AccountManager constructs services from the file-level
   * registry at module load, BEFORE main() has read the keychain, which
   * means every account spec starts with password = "". Without this
   * propagation step the SMTP transporter auths with empty credentials and
   * Bridge rejects with "Please configure the login" / "Missing credentials
   * for PLAIN". This is specifically the regression the multi-account
   * rollout introduced — the singleton-service world had module-level
   * reinitialize() reading a single shared config that main() populated
   * directly; per-account isolation broke that path.
   *
   * Accounts that already had a password (explicit in the file or
   * previously propagated) are left alone; passing an empty string does
   * nothing. Safe to call at boot after keychain load, and after every
   * settings-UI credential save.
   */
  applyKeychainCredentials(password: string, smtpToken?: string): void {
    if (!password && !smtpToken) return;
    for (const [id, svcs] of this.perAccount) {
      let mutated = false;
      // Password: overwrite whenever the new value differs (covers both
      // "empty → set after boot-time keychain load" and "rotated → replace
      // old value after a settings-UI save"). Never overwrite to empty —
      // that would nuke a good in-memory credential after a save that
      // left the password field blank on purpose (user updated username
      // only, for instance).
      if (password && password !== svcs.spec.password) {
        svcs.spec.password = password;
        mutated = true;
      }
      if (smtpToken !== undefined && smtpToken !== svcs.spec.smtpToken) {
        svcs.spec.smtpToken = smtpToken;
        mutated = true;
      }
      if (mutated) {
        svcs.smtp["config"] = specToRuntimeConfig(svcs.spec);
        svcs.smtp.reinitialize();
        logger.debug(`Applied keychain credentials to account "${id}"`, "AccountManager");
      }
    }
  }

  /**
   * Flush stale IMAP credentials and reconnect with the new password for every
   * account that now holds it, so a Settings → Connection save takes effect
   * without a server restart. SMTP is already refreshed synchronously by
   * applyKeychainCredentials (reinitialize); IMAP needs an async reconnect.
   * Best-effort per account; never throws.
   */
  async reloadImapCredentials(password: string): Promise<void> {
    if (!password) return;
    await Promise.all(
      [...this.perAccount.values()]
        .filter((svcs) => svcs.spec.password === password)
        .map((svcs) =>
          svcs.imap.reloadCredentials(password).catch((err: unknown) =>
            logger.warn(`IMAP credential reload failed for account "${svcs.spec.id}"`, "AccountManager", err),
          ),
        ),
    );
  }

  /** Cleanly tear down every account's services. Called on shutdown. */
  async closeAll(): Promise<void> {
    for (const svcs of this.perAccount.values()) {
      try { await svcs.smtp.close(); } catch { /* ignore */ }
      try { await svcs.imap.disconnect(); } catch { /* ignore */ }
    }
  }

  /**
   * Warm IMAP connections for every known account. Called at boot so IDLE
   * runs against every configured mailbox — otherwise non-active accounts
   * only connect on their first per-tool call, which means new-mail events
   * sit in the Proton server until the agent asks for them.
   *
   * Failures are logged per-account but do not stop the loop; a single
   * broken account shouldn't block the others. Returns per-account
   * success/failure so the caller can surface a summary.
   */
  async connectAll(): Promise<Array<{ id: string; ok: boolean; error?: string }>> {
    const results: Array<{ id: string; ok: boolean; error?: string }> = [];
    for (const [id, svcs] of this.perAccount) {
      const s = svcs.spec;
      try {
        await svcs.imap.connect(
          s.imapHost,
          s.imapPort,
          s.username,
          s.password,
          s.bridgeCertPath,
          s.tlsMode === "ssl",
          !!s.allowInsecureBridge,
        );
        logger.info(`IMAP connected for account "${id}"`, "AccountManager");
        results.push({ id, ok: true });
      } catch (err: unknown) {
        const msg = err instanceof Error ? err.message : String(err);
        logger.warn(`IMAP connect failed for account "${id}": ${msg}`, "AccountManager");
        results.push({ id, ok: false, error: msg });
      }
    }
    return results;
  }
}

// ─── Module singleton accessor ────────────────────────────────────────────
// index.ts constructs the manager during server bootstrap; the settings
// server imports this getter so it can trigger hot-swaps on /api/accounts/
// activate without a circular dep or explicit wiring.

let _singleton: AccountManager | null = null;
export function registerAccountManager(mgr: AccountManager): void {
  _singleton = mgr;
}
export function getAccountManager(): AccountManager | null {
  return _singleton;
}
