// EmailService — orchestrates connected mailboxes: OAuth connect flow,
// account persistence, token storage/refresh, and provider-agnostic ops.
import { randomBytes } from "node:crypto";
import { and, desc, eq, isNull } from "drizzle-orm";
import { emailAccounts } from "../../db/schema.js";
import { uuidv7 } from "../../db/uuidv7.js";
import { GmailClient } from "./gmail.js";
import { OutlookClient } from "./outlook.js";
import { buildAuthorizeUrl, createPkce, envClientCredentials, exchangeCode, refreshTokens, } from "./oauth.js";
const TOKEN_SECRET_TYPE = "email-account";
const APP_SECRET_TYPE = "email-oauth-app";
const PROVIDERS = ["gmail", "outlook"];
export class EmailService {
    db;
    secrets;
    clients = {
        gmail: new GmailClient(),
        outlook: new OutlookClient(),
    };
    pending = new Map();
    constructor(db, secrets) {
        this.db = db;
        this.secrets = secrets;
    }
    // ── OAuth client credentials ───────────────────────────────────────
    /** Resolve OAuth app credentials: per-user secret first, then env. */
    resolveCredentials(provider, userId) {
        const clientId = this.secrets.getValue(userId, APP_SECRET_TYPE, `${provider}-client-id`);
        const clientSecret = this.secrets.getValue(userId, APP_SECRET_TYPE, `${provider}-client-secret`);
        if (clientId && clientSecret)
            return { clientId, clientSecret };
        return envClientCredentials(provider);
    }
    /** Which providers are ready to connect (have OAuth app credentials). */
    configuredProviders(userId) {
        return {
            gmail: !!this.resolveCredentials("gmail", userId),
            outlook: !!this.resolveCredentials("outlook", userId),
        };
    }
    saveAppCredentials(userId, provider, creds) {
        this.secrets.save({
            userId,
            type: APP_SECRET_TYPE,
            key: `${provider}-client-id`,
            label: `${provider} OAuth client id`,
            value: creds.clientId,
        });
        this.secrets.save({
            userId,
            type: APP_SECRET_TYPE,
            key: `${provider}-client-secret`,
            label: `${provider} OAuth client secret`,
            value: creds.clientSecret,
        });
    }
    // ── Connect flow ───────────────────────────────────────────────────
    startConnect(params) {
        if (!PROVIDERS.includes(params.provider)) {
            throw new Error(`Unsupported email provider: ${params.provider}`);
        }
        const credentials = this.resolveCredentials(params.provider, params.userId);
        if (!credentials) {
            throw new Error(`${params.provider} is not configured. Add its OAuth client id/secret (env ` +
                `${params.provider === "gmail" ? "GMAIL" : "OUTLOOK"}_OAUTH_CLIENT_ID/SECRET or in Email settings).`);
        }
        const state = randomBytes(24).toString("base64url");
        const { verifier, challenge } = createPkce();
        this.pending.set(state, {
            userId: params.userId,
            provider: params.provider,
            codeVerifier: verifier,
            redirectUri: params.redirectUri,
            createdAt: Date.now(),
        });
        this.gcPending();
        const authUrl = buildAuthorizeUrl({
            provider: params.provider,
            credentials,
            redirectUri: params.redirectUri,
            state,
            codeChallenge: challenge,
        });
        return { authUrl };
    }
    async completeConnect(params) {
        const pending = this.pending.get(params.state);
        if (!pending)
            throw new Error("OAuth state is invalid or expired. Please retry connecting.");
        this.pending.delete(params.state);
        const credentials = this.resolveCredentials(pending.provider, pending.userId);
        if (!credentials)
            throw new Error(`${pending.provider} OAuth credentials are no longer configured.`);
        const tokens = await exchangeCode({
            provider: pending.provider,
            credentials,
            redirectUri: pending.redirectUri,
            code: params.code,
            codeVerifier: pending.codeVerifier,
        });
        const email = await this.clients[pending.provider].getProfileEmail(tokens.accessToken);
        if (!email)
            throw new Error("Could not read the account email address from the provider.");
        const account = this.upsertAccount(pending.userId, pending.provider, email);
        this.storeTokens(account.id, pending.userId, tokens);
        return account;
    }
    // ── Account persistence ────────────────────────────────────────────
    listAccounts(userId) {
        return this.db
            .select()
            .from(emailAccounts)
            .where(userId ? eq(emailAccounts.userId, userId) : isNull(emailAccounts.userId))
            .orderBy(desc(emailAccounts.updatedAt))
            .all()
            .map(rowToAccount);
    }
    getAccount(userId, accountId) {
        const row = this.db
            .select()
            .from(emailAccounts)
            .where(and(eq(emailAccounts.id, accountId), userId ? eq(emailAccounts.userId, userId) : isNull(emailAccounts.userId)))
            .get();
        return row ? rowToAccount(row) : null;
    }
    /** Resolve an account by id, or fall back to the user's first account. */
    resolveAccount(userId, accountId) {
        if (accountId) {
            const acct = this.getAccount(userId, accountId);
            if (!acct)
                throw new Error(`Email account ${accountId} not found.`);
            return acct;
        }
        const accounts = this.listAccounts(userId);
        const first = accounts[0];
        if (!first)
            throw new Error("No email account connected. Connect Gmail or Outlook first.");
        return first;
    }
    disconnect(userId, accountId) {
        const acct = this.getAccount(userId, accountId);
        if (!acct)
            return false;
        this.db.delete(emailAccounts).where(eq(emailAccounts.id, accountId)).run();
        const secretId = this.findTokenSecretId(userId, accountId);
        if (secretId)
            this.secrets.delete(secretId, userId);
        return true;
    }
    upsertAccount(userId, provider, email) {
        const now = new Date().toISOString();
        const existing = this.db
            .select()
            .from(emailAccounts)
            .where(and(userId ? eq(emailAccounts.userId, userId) : isNull(emailAccounts.userId), eq(emailAccounts.provider, provider), eq(emailAccounts.email, email)))
            .get();
        if (existing) {
            this.db
                .update(emailAccounts)
                .set({ status: "connected", error: null, updatedAt: now })
                .where(eq(emailAccounts.id, existing.id))
                .run();
            return rowToAccount({ ...existing, status: "connected", error: null, updatedAt: now });
        }
        const id = uuidv7();
        const row = {
            id,
            userId: userId ?? null,
            provider,
            email,
            displayName: email,
            status: "connected",
            error: null,
            createdAt: now,
            updatedAt: now,
        };
        this.db.insert(emailAccounts).values(row).run();
        return rowToAccount(row);
    }
    // ── Token storage / refresh ────────────────────────────────────────
    storeTokens(accountId, userId, tokens) {
        this.secrets.save({
            userId,
            type: TOKEN_SECRET_TYPE,
            key: accountId,
            label: `Email tokens (${accountId})`,
            value: JSON.stringify(tokens),
        });
    }
    loadTokens(accountId, userId) {
        const raw = this.secrets.getValue(userId, TOKEN_SECRET_TYPE, accountId);
        if (!raw)
            return null;
        try {
            return JSON.parse(raw);
        }
        catch {
            return null;
        }
    }
    findTokenSecretId(userId, accountId) {
        const list = this.secrets.list(userId, TOKEN_SECRET_TYPE);
        return list.find((s) => s.key === accountId)?.id ?? null;
    }
    async validAccessToken(account) {
        const tokens = this.loadTokens(account.id, account.userId);
        if (!tokens)
            throw new Error("No stored credentials for this account. Reconnect it.");
        if (tokens.expiresAt - Date.now() > 60_000)
            return tokens.accessToken;
        if (!tokens.refreshToken)
            throw new Error("Access token expired and no refresh token is available. Reconnect.");
        const credentials = this.resolveCredentials(account.provider, account.userId);
        if (!credentials)
            throw new Error(`${account.provider} OAuth credentials are not configured.`);
        const refreshed = await refreshTokens({
            provider: account.provider,
            credentials,
            refreshToken: tokens.refreshToken,
        });
        this.storeTokens(account.id, account.userId, refreshed);
        return refreshed.accessToken;
    }
    // ── Provider-agnostic operations ───────────────────────────────────
    async listMessages(userId, accountId, opts) {
        const account = this.resolveAccount(userId, accountId);
        const token = await this.validAccessToken(account);
        const messages = await this.clients[account.provider].listMessages(token, opts);
        return { account, messages };
    }
    async getMessage(userId, accountId, id) {
        const account = this.resolveAccount(userId, accountId);
        const token = await this.validAccessToken(account);
        return this.clients[account.provider].getMessage(token, id);
    }
    async sendMessage(userId, accountId, input) {
        const account = this.resolveAccount(userId, accountId);
        const token = await this.validAccessToken(account);
        return this.clients[account.provider].sendMessage(token, input);
    }
    async tagMessage(userId, accountId, id, add, remove) {
        const account = this.resolveAccount(userId, accountId);
        const token = await this.validAccessToken(account);
        await this.clients[account.provider].tagMessage(token, id, add, remove);
    }
    async deleteMessage(userId, accountId, id) {
        const account = this.resolveAccount(userId, accountId);
        const token = await this.validAccessToken(account);
        await this.clients[account.provider].deleteMessage(token, id);
    }
    async listFolders(userId, accountId) {
        const account = this.resolveAccount(userId, accountId);
        const token = await this.validAccessToken(account);
        return this.clients[account.provider].listFolders(token);
    }
    gcPending() {
        const cutoff = Date.now() - 10 * 60_000;
        for (const [state, p] of this.pending) {
            if (p.createdAt < cutoff)
                this.pending.delete(state);
        }
    }
}
function rowToAccount(row) {
    return {
        id: row.id,
        userId: row.userId,
        provider: row.provider,
        email: row.email,
        displayName: row.displayName,
        status: row.status,
        error: row.error,
        createdAt: row.createdAt,
        updatedAt: row.updatedAt,
    };
}
//# sourceMappingURL=service.js.map