import { spawn } from "node:child_process";
import { CONFIG_PATH, loadConfig, readConfigFileSnapshot, resolveGatewayPort, validateConfigObjectWithPlugins, writeConfigFile, } from "../config/config.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime } from "../runtime.js";
import { formatCliCommand } from "../cli/command-format.js";
import { displayPath } from "../utils.js";
import { buildDefaultHookUrl, buildGogWatchServeArgs, buildGogWatchStartArgs, buildTopicPath, DEFAULT_GMAIL_LABEL, DEFAULT_GMAIL_MAX_BYTES, DEFAULT_GMAIL_RENEW_MINUTES, DEFAULT_GMAIL_SERVE_BIND, DEFAULT_GMAIL_SERVE_PATH, DEFAULT_GMAIL_SERVE_PORT, DEFAULT_GMAIL_SUBSCRIPTION, DEFAULT_GMAIL_TOPIC, generateHookToken, mergeHookPresets, normalizeHooksPath, normalizeServePath, parseTopicPath, resolveGmailHookRuntimeConfig, } from "./gmail.js";
import { ensureDependency, ensureGcloudAuth, ensureSubscription, ensureTailscaleEndpoint, ensureTopic, resolveProjectIdFromGogCredentials, runGcloud, } from "./gmail-setup-utils.js";
const DEFAULT_GMAIL_TOPIC_IAM_MEMBER = "serviceAccount:gmail-api-push@system.gserviceaccount.com";
export async function runGmailSetup(opts) {
    await ensureDependency("gcloud", ["--cask", "gcloud-cli"]);
    await ensureDependency("gog", ["gogcli"]);
    if (opts.tailscale !== "off" && !opts.pushEndpoint) {
        await ensureDependency("tailscale", ["tailscale"]);
    }
    await ensureGcloudAuth();
    const configSnapshot = await readConfigFileSnapshot();
    if (!configSnapshot.valid) {
        throw new Error(`Config invalid: ${CONFIG_PATH}`);
    }
    const baseConfig = configSnapshot.config;
    const hooksPath = normalizeHooksPath(baseConfig.hooks?.path);
    const hookToken = opts.hookToken ?? baseConfig.hooks?.token ?? generateHookToken();
    const pushToken = opts.pushToken ?? baseConfig.hooks?.gmail?.pushToken ?? generateHookToken();
    const topicInput = opts.topic ?? baseConfig.hooks?.gmail?.topic ?? DEFAULT_GMAIL_TOPIC;
    const parsedTopic = parseTopicPath(topicInput);
    const topicName = parsedTopic?.topicName ?? topicInput;
    const projectId = opts.project ?? parsedTopic?.projectId ?? (await resolveProjectIdFromGogCredentials());
    // Gmail watch requires the Pub/Sub topic to live in the OAuth client project.
    if (!projectId) {
        throw new Error("GCP project id required (use --project or ensure gog credentials are available)");
    }
    const topicPath = buildTopicPath(projectId, topicName);
    const subscription = opts.subscription ?? DEFAULT_GMAIL_SUBSCRIPTION;
    const label = opts.label ?? DEFAULT_GMAIL_LABEL;
    const hookUrl = opts.hookUrl ??
        baseConfig.hooks?.gmail?.hookUrl ??
        buildDefaultHookUrl(hooksPath, resolveGatewayPort(baseConfig));
    const serveBind = opts.bind ?? DEFAULT_GMAIL_SERVE_BIND;
    const servePort = opts.port ?? DEFAULT_GMAIL_SERVE_PORT;
    const configuredServePath = opts.path ?? baseConfig.hooks?.gmail?.serve?.path;
    const configuredTailscaleTarget = opts.tailscaleTarget ?? baseConfig.hooks?.gmail?.tailscale?.target;
    const normalizedServePath = typeof configuredServePath === "string" && configuredServePath.trim().length > 0
        ? normalizeServePath(configuredServePath)
        : DEFAULT_GMAIL_SERVE_PATH;
    const normalizedTailscaleTarget = typeof configuredTailscaleTarget === "string" && configuredTailscaleTarget.trim().length > 0
        ? configuredTailscaleTarget.trim()
        : undefined;
    const includeBody = opts.includeBody ?? true;
    const maxBytes = opts.maxBytes ?? DEFAULT_GMAIL_MAX_BYTES;
    const renewEveryMinutes = opts.renewEveryMinutes ?? DEFAULT_GMAIL_RENEW_MINUTES;
    const tailscaleMode = opts.tailscale ?? "funnel";
    // Tailscale strips the path before proxying; keep a public path while gog
    // listens on "/" whenever Tailscale is enabled.
    const servePath = normalizeServePath(tailscaleMode !== "off" && !normalizedTailscaleTarget ? "/" : normalizedServePath);
    const tailscalePath = normalizeServePath(opts.tailscalePath ??
        baseConfig.hooks?.gmail?.tailscale?.path ??
        (tailscaleMode !== "off" ? normalizedServePath : servePath));
    await runGcloud(["config", "set", "project", projectId, "--quiet"]);
    await runGcloud([
        "services",
        "enable",
        "gmail.googleapis.com",
        "pubsub.googleapis.com",
        "--project",
        projectId,
        "--quiet",
    ]);
    await ensureTopic(projectId, topicName);
    await runGcloud([
        "pubsub",
        "topics",
        "add-iam-policy-binding",
        topicName,
        "--project",
        projectId,
        "--member",
        DEFAULT_GMAIL_TOPIC_IAM_MEMBER,
        "--role",
        "roles/pubsub.publisher",
        "--quiet",
    ]);
    const pushEndpoint = opts.pushEndpoint
        ? opts.pushEndpoint
        : await ensureTailscaleEndpoint({
            mode: tailscaleMode,
            path: tailscalePath,
            port: servePort,
            target: normalizedTailscaleTarget,
            token: pushToken,
        });
    if (!pushEndpoint) {
        throw new Error("push endpoint required (set --push-endpoint)");
    }
    await ensureSubscription(projectId, subscription, topicName, pushEndpoint);
    await startGmailWatch({
        account: opts.account,
        label,
        topic: topicPath,
    }, true);
    const nextConfig = {
        ...baseConfig,
        hooks: {
            ...baseConfig.hooks,
            enabled: true,
            path: hooksPath,
            token: hookToken,
            presets: mergeHookPresets(baseConfig.hooks?.presets, "gmail"),
            gmail: {
                ...baseConfig.hooks?.gmail,
                account: opts.account,
                label,
                topic: topicPath,
                subscription,
                pushToken,
                hookUrl,
                includeBody,
                maxBytes,
                renewEveryMinutes,
                serve: {
                    ...baseConfig.hooks?.gmail?.serve,
                    bind: serveBind,
                    port: servePort,
                    path: servePath,
                },
                tailscale: {
                    ...baseConfig.hooks?.gmail?.tailscale,
                    mode: tailscaleMode,
                    path: tailscalePath,
                    target: normalizedTailscaleTarget,
                },
            },
        },
    };
    const validated = validateConfigObjectWithPlugins(nextConfig);
    if (!validated.ok) {
        throw new Error(`Config validation failed: ${validated.issues[0]?.message ?? "invalid"}`);
    }
    await writeConfigFile(validated.config);
    const summary = {
        projectId,
        topic: topicPath,
        subscription,
        pushEndpoint,
        hookUrl,
        hookToken,
        pushToken,
        serve: {
            bind: serveBind,
            port: servePort,
            path: servePath,
        },
    };
    if (opts.json) {
        defaultRuntime.log(JSON.stringify(summary, null, 2));
        return;
    }
    defaultRuntime.log("Gmail hooks configured:");
    defaultRuntime.log(`- project: ${projectId}`);
    defaultRuntime.log(`- topic: ${topicPath}`);
    defaultRuntime.log(`- subscription: ${subscription}`);
    defaultRuntime.log(`- push endpoint: ${pushEndpoint}`);
    defaultRuntime.log(`- hook url: ${hookUrl}`);
    defaultRuntime.log(`- config: ${displayPath(CONFIG_PATH)}`);
    defaultRuntime.log(`Next: ${formatCliCommand("openclaw-cn webhooks gmail run")}`);
}
export async function runGmailService(opts) {
    await ensureDependency("gog", ["gogcli"]);
    const config = loadConfig();
    const overrides = {
        account: opts.account,
        topic: opts.topic,
        subscription: opts.subscription,
        label: opts.label,
        hookToken: opts.hookToken,
        pushToken: opts.pushToken,
        hookUrl: opts.hookUrl,
        serveBind: opts.bind,
        servePort: opts.port,
        servePath: opts.path,
        includeBody: opts.includeBody,
        maxBytes: opts.maxBytes,
        renewEveryMinutes: opts.renewEveryMinutes,
        tailscaleMode: opts.tailscale,
        tailscalePath: opts.tailscalePath,
        tailscaleTarget: opts.tailscaleTarget,
    };
    const resolved = resolveGmailHookRuntimeConfig(config, overrides);
    if (!resolved.ok) {
        throw new Error(resolved.error);
    }
    const runtimeConfig = resolved.value;
    if (runtimeConfig.tailscale.mode !== "off") {
        await ensureDependency("tailscale", ["tailscale"]);
        await ensureTailscaleEndpoint({
            mode: runtimeConfig.tailscale.mode,
            path: runtimeConfig.tailscale.path,
            port: runtimeConfig.serve.port,
            target: runtimeConfig.tailscale.target,
        });
    }
    await startGmailWatch(runtimeConfig);
    let shuttingDown = false;
    let child = spawnGogServe(runtimeConfig);
    const renewMs = runtimeConfig.renewEveryMinutes * 60_000;
    const renewTimer = setInterval(() => {
        void startGmailWatch(runtimeConfig);
    }, renewMs);
    const shutdown = () => {
        if (shuttingDown)
            return;
        shuttingDown = true;
        clearInterval(renewTimer);
        child.kill("SIGTERM");
    };
    process.on("SIGINT", shutdown);
    process.on("SIGTERM", shutdown);
    child.on("exit", () => {
        if (shuttingDown)
            return;
        defaultRuntime.log("gog watch serve exited; restarting in 2s");
        setTimeout(() => {
            if (shuttingDown)
                return;
            child = spawnGogServe(runtimeConfig);
        }, 2000);
    });
}
function spawnGogServe(cfg) {
    const args = buildGogWatchServeArgs(cfg);
    defaultRuntime.log(`Starting gog ${args.join(" ")}`);
    return spawn("gog", args, { stdio: "inherit" });
}
async function startGmailWatch(cfg, fatal = false) {
    const args = ["gog", ...buildGogWatchStartArgs(cfg)];
    const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
    if (result.code !== 0) {
        const message = result.stderr || result.stdout || "gog watch start failed";
        if (fatal)
            throw new Error(message);
        defaultRuntime.error(message);
    }
}
