import Fastify from 'fastify';
import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import rateLimit from '@fastify/rate-limit';
import fastifyStatic from '@fastify/static';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { config } from './config.js';
import { logger } from './utils/logger.js';
import { registerWhatsAppRoutes } from './channels/whatsapp/routes.js';
import { registerGatewayRoutes } from './gateway/routes.js';
import { registerTenantRoutes } from './tenants/routes.js';
import { initKnowledgeGraphs, closeKnowledgeGraphs } from './knowledge/unified.js';
import { registerAuthRoutes } from './auth/routes.js';
import { registerAuthDecorators } from './auth/middleware.js';
import { registerBillingRoutes } from './billing/routes.js';
import { registerOnboardingRoutes } from './onboarding/routes.js';
import { registerWebChatRoutes } from './web-chat/routes.js';
import { registerKnowledgeRoutes } from './knowledge/routes.js';
import { registerCronRoutes } from './cron/routes.js';
import { startCronScheduler, shutdownCronScheduler } from './cron/scheduler.js';
import { bootAutonomy } from './autonomy/boot.js';
import { registerAutonomyRoutes } from './autonomy/routes.js';
import { registerTelegramRoutes } from './channels/telegram/routes.js';
import { registerSlackRoutes } from './channels/slack/routes.js';
import { registerDiscordRoutes } from './channels/discord/routes.js';
import { registerDoctorRoutes } from './doctor/routes.js';
import { registerInsightsRoutes } from './insights/routes.js';
import { registerSubagentRoutes } from './subagents/routes.js';
import { registerModelsRoutes } from './models/routes.js';
import { registerLicenseRoutes } from './security/license-routes.js';
import { startDeliveryRetryWorker, stopDeliveryRetryWorker } from './delivery/retry-worker.js';
import { registerSessionRoutes } from './sessions/routes.js';
import { registerApprovalRoutes } from './approval/routes.js';
import { mcpManager } from './mcp/manager.js';
import { setMcpToolsProvider } from './tools/types.js';
import { initFailover } from './models/failover.js';
// SSE-friendly timeouts. Defaults are way too short for the chat-streaming
// path where a single browser_navigate or send_email tool can legitimately
// take 30–90s — and during that gap the SSE socket stays idle and Fastify's
// default 5s keep-alive + 0 request timeout would terminate the connection
// (which the dashboard surfaces as "خطأ في البث: Load failed").
//   - keepAliveTimeout: how long to hold an idle keep-alive socket
//   - requestTimeout:   max time the WHOLE request body+handler may take
//                       (0 = no limit; we want this for SSE streams that
//                       legitimately run for minutes during long tool chains)
//   - connectionTimeout: socket-level inactivity guard
const app = Fastify({
    logger: false,
    keepAliveTimeout: 5 * 60 * 1000, // 5 minutes
    requestTimeout: 0, // never time out chat-stream
    connectionTimeout: 5 * 60 * 1000, // 5 minutes
});
// ─── Startup Banner ──────────────────────────────────────────────────
// Server-log banner — kept compact and clean. The full ASCII-block "MUASSIS"
// logo lives in the CLI bin (src/cli.ts) and the standalone CLI's banner.ts.
function printBanner() {
    const pkgVersion = (() => {
        try {
            // import.meta.url is dist/index.js at runtime; package.json sits one
            // dir up at <projectRoot>/package.json (dist is flat — outDir=dist,
            // rootDir=src, so dist/index.js → ../package.json). NOTE: previous
            // versions used `require('fs')` here which silently throws "require
            // is not defined" in ESM and fell into the catch — so the banner
            // always showed "vunknown". Fixed by importing readFileSync at the
            // top of the file (ESM-native).
            const url = new URL('../package.json', import.meta.url);
            return JSON.parse(readFileSync(url, 'utf-8')).version || 'unknown';
        }
        catch {
            return 'unknown';
        }
    })();
    const banner = `
═══════════════════════════════════════════════════════════════
   ✦  Al Moasis · المؤسس · The Founder Agent  ✦
   Saudi-Made by TaseesAI · v${pkgVersion}
═══════════════════════════════════════════════════════════════
`;
    console.log(banner);
}
/**
 * Ensure <dataDir>/owner.token exists with a real random value. Idempotent —
 * runs on every boot so v1.0.x installs upgrade automatically. The token
 * replaces the v1.0 "Bearer device" sentinel that accepted any non-empty
 * token (security audit CRITICAL fix).
 */
async function ensureOwnerToken() {
    try {
        const { existsSync, readFileSync, writeFileSync } = await import('node:fs');
        const { randomBytes, randomUUID } = await import('node:crypto');
        const { join } = await import('node:path');
        const { getDataDir } = await import('./utils/data-dir.js');
        // 1. owner.token — bearer secret matched against incoming requests.
        const tokenPath = join(getDataDir(), 'owner.token');
        if (!existsSync(tokenPath) || readFileSync(tokenPath, 'utf-8').trim().length < 32) {
            writeFileSync(tokenPath, randomBytes(32).toString('hex'), { encoding: 'utf-8', mode: 0o600 });
            logger.info('🔑 owner.token generated');
        }
        // 2. owner.id — the synthetic device user UUID returned by deviceModeUser().
        // authMiddleware refuses to authenticate any request until this exists,
        // so it MUST be created at boot — without it, every API call returns 401
        // and the dashboard can't even fetch the license/tenant state.
        const idPath = join(getDataDir(), 'owner.id');
        if (!existsSync(idPath) || readFileSync(idPath, 'utf-8').trim().length !== 36) {
            writeFileSync(idPath, randomUUID(), { encoding: 'utf-8', mode: 0o600 });
            logger.info('🆔 owner.id generated');
        }
    }
    catch (err) {
        logger.warn({ error: err.message }, '⚠️ owner identity generation failed — auth will reject all requests');
    }
}
/**
 * Periodically heartbeat the license server. Runs on a 24h interval.
 * Failures are logged but never crash the gateway — the license module
 * itself handles offline-grace and read-only fallback.
 */
function scheduleLicenseHeartbeat() {
    const tick = async () => {
        try {
            const { loadAndVerifyLicense, heartbeat, storeSignedLicense, getLicenseStatus } = await import('./security/license.js');
            const { getDataDir } = await import('./utils/data-dir.js');
            const dir = getDataDir();
            const license = loadAndVerifyLicense(dir);
            if (!license)
                return; // unactivated — no heartbeat needed
            try {
                const refreshed = await heartbeat(license);
                storeSignedLicense(dir, refreshed);
                logger.debug({ state: getLicenseStatus(dir).state }, 'license heartbeat ok');
            }
            catch (err) {
                const msg = err.message;
                if (msg === 'REVOKED') {
                    logger.error('🚨 License revoked by server. Read-only mode on next status check.');
                }
                else {
                    logger.warn({ error: msg }, 'license heartbeat failed (will retry in 24h)');
                }
            }
        }
        catch (err) {
            logger.debug({ err: err.message }, 'license heartbeat skipped');
        }
    };
    // Initial heartbeat after 30s (give the gateway time to settle), then every 24h.
    setTimeout(() => { void tick(); }, 30_000);
    setInterval(() => { void tick(); }, 24 * 60 * 60 * 1000);
}
// ─── Main ────────────────────────────────────────────────────────────
async function start() {
    printBanner();
    // ── Single-instance guard ──────────────────────────────────────────────
    // A background LaunchAgent daemon (com.taseesai.almoasis.gateway) keeps the
    // gateway — and therefore all autonomy / self-improvement crons — running
    // 24/7 even when the desktop window is closed. When the desktop app later
    // launches, it spawns its OWN gateway child. If that child ran a full boot
    // it would fire autonomy twice and write the SQLite DB concurrently. So:
    // if a gateway is already serving /health on our port, defer to it and exit
    // cleanly BEFORE any boot work. The desktop splash polls /health and uses
    // the already-running daemon transparently.
    try {
        // Only defer to a gateway that is genuinely OURS. A foreign server on the
        // same port (e.g. another app like OpenClaw, which many users run) also
        // answers /health — blindly deferring to it would route the desktop
        // webview's chat calls to a server with no /api routes → "error 404" on
        // every message. Parse the body and require our own service identity.
        const peer = await fetch(`http://127.0.0.1:${config.PORT}/health`, {
            signal: AbortSignal.timeout(900),
        }).then((r) => (r.ok ? r.json() : null)).catch(() => null);
        if (peer && peer.service === 'muassis-gateway') {
            logger.info(`✓ Al Moasis gateway already running on :${config.PORT} — deferring to it (no double-boot). Autonomy keeps running there.`);
            process.exit(0);
        }
        if (peer) {
            // Port is held by a DIFFERENT application. We cannot bind it; warn
            // loudly so the user frees the port instead of seeing a cryptic 404.
            logger.error(`🚫 Port ${config.PORT} is in use by another app (its /health is not Al Moasis: ${JSON.stringify(peer).slice(0, 120)}). Quit that app (e.g. OpenClaw) and reopen Al Moasis, or set PORT to a free port.`);
        }
    }
    catch { /* nothing listening — proceed with full boot below */ }
    await ensureOwnerToken();
    scheduleLicenseHeartbeat();
    // Boot-time code integrity check. Compares dist/* SHA-256 hashes against
    // a manifest baked at build time. Tampered installs WARN loudly + journal,
    // but boot continues — this is forensic signal, not a hard gate.
    try {
        const { verifyCodeIntegrity } = await import('./security/hardening.js');
        const r = await verifyCodeIntegrity();
        if (!r.ok && (r.tampered.length > 0 || r.missing.length > 0)) {
            logger.warn({
                tampered: r.tampered,
                missing: r.missing,
            }, '🚨 CODE INTEGRITY: dist/ files differ from manifest. Check for tampering.');
        }
    }
    catch { /* best-effort */ }
    // CORS — gateway is bound to 127.0.0.1 loopback only, so any origin
    // making a request must already be running on the same machine. The real
    // defense is the Bearer-token auth gate (owner.token). We allow:
    //   - The Tauri desktop webview (tauri://localhost / tauri.localhost)
    //   - Loopback browser pages (http://localhost:* / http://127.0.0.1:*)
    //   - The TaseesAI marketing properties
    //   - Anything if MUASSIS_DEV_CORS=1
    // Block anything else (e.g. evil-site.com) to keep CSRF surface narrow.
    const allowDevCors = process.env.MUASSIS_DEV_CORS === '1';
    await app.register(cors, {
        origin: (origin, callback) => {
            if (allowDevCors || !origin)
                return callback(null, true);
            const isLoopback = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin) ||
                /^tauri:\/\/localhost$/.test(origin) ||
                /^https?:\/\/tauri\.localhost$/.test(origin);
            const allowed = [
                'https://muassis.ai',
                'https://app.muassis.ai',
                'https://almoasis.ai',
                'https://app.almoasis.ai',
            ];
            if (isLoopback || allowed.includes(origin))
                return callback(null, true);
            callback(null, false);
        },
        credentials: false,
    });
    // Multipart — رفع الملفات
    await app.register(multipart, {
        limits: {
            fileSize: 100 * 1024 * 1024, // 100 MB — owners attach screenshots, PDFs, recordings
            files: 10,
        },
    });
    // Rate limit — global default; sensitive routes override via config.rateLimit.
    // Loopback owner traffic was hitting the prior 200/min ceiling on busy
    // sessions (poll + heartbeat + chat + suggestions all share the budget),
    // producing 429s the owner read as "the app is broken". 1500/min keeps
    // headroom for both the dashboard and an active WhatsApp tenant.
    await app.register(rateLimit, {
        global: true,
        max: 1500,
        timeWindow: '1 minute',
        skipOnError: true,
        // Loopback is the owner's own machine — don't rate-limit yourself.
        allowList: (req) => req.url === '/health' || req.ip === '127.0.0.1' || req.ip === '::1',
        errorResponseBuilder: (_req, ctx) => ({
            success: false,
            error: 'تم تجاوز الحد المسموح — Rate limit exceeded',
            retryAfterSeconds: Math.ceil(ctx.ttl / 1000),
        }),
    });
    // Auth decorators — تسجيل request.user و request.tenant
    registerAuthDecorators(app);
    // Serve the bundled dashboard at /dashboard (and / redirects to it).
    // Located alongside the package — dist/index.js → ../dashboard.
    const __dirname = dirname(fileURLToPath(import.meta.url));
    const dashboardRoot = join(__dirname, '..', 'dashboard');
    await app.register(fastifyStatic, {
        root: dashboardRoot,
        prefix: '/dashboard/',
        decorateReply: false,
    });
    // (v3.0.1) Parallel preview of the pixel-perfect rewrite at /dashboard-v2/.
    // Live dashboard keeps working; new one is opt-in via URL until validated.
    const dashboardV2Root = join(__dirname, '..', 'dashboard-v2');
    await app.register(fastifyStatic, {
        root: dashboardV2Root,
        prefix: '/dashboard-v2/',
        decorateReply: false,
    });
    // Serve docs/ and legal/ as static markdown so the dashboard's "guide
    // links" resolve cleanly. Read-only — no auth check (these are public-
    // domain docs the customer can also read offline from the npm tarball).
    const docsRoot = join(__dirname, '..', 'docs');
    const legalRoot = join(__dirname, '..', 'legal');
    await app.register(fastifyStatic, {
        root: docsRoot,
        prefix: '/docs/',
        decorateReply: false,
    });
    await app.register(fastifyStatic, {
        root: legalRoot,
        prefix: '/legal/',
        decorateReply: false,
    });
    app.get('/', async (_req, reply) => reply.redirect('/dashboard-v2/'));
    // Keep the legacy /dashboard/ path as a fallback so the new dashboard
    // can deep-link tabs (WhatsApp, Settings, Memory, etc.) back to the
    // working legacy UI until each gets a pixel-perfect rewrite of its own.
    // Health check — version is read at module load and stays consistent
    // with package.json (was hardcoded "1.0.3" while package said 1.0.4).
    const healthVersion = (() => {
        try {
            const url = new URL('../package.json', import.meta.url);
            return JSON.parse(readFileSync(url, 'utf-8')).version || 'unknown';
        }
        catch {
            return 'unknown';
        }
    })();
    app.get('/health', async () => ({
        status: 'ok',
        service: 'muassis-gateway',
        version: healthVersion,
        timestamp: new Date().toISOString(),
    }));
    // ─── Public Routes (no auth required) ───
    await registerAuthRoutes(app); // /api/auth/*
    await registerGatewayRoutes(app); // gateway routes
    await registerWhatsAppRoutes(app); // /webhook/whatsapp
    // ─── Protected Routes ───
    await registerTenantRoutes(app); // /api/tenants/*
    await registerBillingRoutes(app); // /api/billing/*
    await registerOnboardingRoutes(app); // /api/onboarding/*
    await registerWebChatRoutes(app); // /api/chat — web-channel agent chat
    await registerKnowledgeRoutes(app); // /api/knowledge/graph
    await registerCronRoutes(app); // /api/cron/*
    await registerSessionRoutes(app); // /api/sessions/*
    await registerApprovalRoutes(app); // /api/approvals/*
    await registerAutonomyRoutes(app); // /api/autonomy/* (agenda, journal, policies, identity, escalations)
    await registerTelegramRoutes(app); // /api/channels/telegram/*
    await registerSlackRoutes(app); // /api/channels/slack/*
    await registerDiscordRoutes(app); // /api/channels/discord/*
    await registerDoctorRoutes(app); // /api/doctor
    await registerInsightsRoutes(app); // /api/insights/*
    await registerSubagentRoutes(app); // /api/subagents/*
    await registerModelsRoutes(app); // /api/models/* (provider catalog + BYOK keys)
    await registerLicenseRoutes(app); // /api/license/* (activation, heartbeat, status)
    // Initialize MCP servers — each is spawned over stdio, we list its
    // tools, and merge them into the agent's toolbelt. Best-effort: a
    // broken server doesn't block boot.
    try {
        logger.info('🔌 Starting MCP servers...');
        await mcpManager.init();
        setMcpToolsProvider(() => mcpManager.getToolDefinitions());
        const status = mcpManager.status();
        const totalTools = status.reduce((s, srv) => s + srv.toolCount, 0);
        logger.info({ servers: status.map(s => ({ name: s.name, running: s.running, tools: s.toolCount })), totalTools }, `🔌 MCP ready (${status.filter(s => s.running).length}/${status.length} servers, ${totalTools} tools)`);
    }
    catch (err) {
        logger.warn({ error: err instanceof Error ? err.message : String(err) }, '⚠️ MCP init failed — continuing without external MCP tools');
    }
    // Initialize the model failover chain. Default = NIM-only (bundled key +
    // qwen3-next-80b). Ollama is detected but only wired in if the operator
    // explicitly opts in with MUASSIS_OLLAMA_FALLBACK=1 — we don't want the
    // out-of-the-box customer flow to depend on local model downloads.
    try {
        await initFailover();
    }
    catch (err) {
        logger.debug({ err: err instanceof Error ? err.message : String(err) }, 'Failover init errored — chain stays NIM-only');
    }
    // Initialize knowledge graphs
    try {
        logger.info('🧠 Initializing knowledge graphs...');
        await initKnowledgeGraphs();
        logger.info('🧠 Knowledge graphs ready');
    }
    catch (err) {
        logger.warn({
            error: err instanceof Error ? err.message : String(err),
        }, '⚠️ Knowledge graphs initialization failed — continuing without graph intelligence');
    }
    // Boot the autonomy layer FIRST — seeds default jobs/policies + auto-starts
    // WhatsApp. Must run before the cron scheduler so the seeded jobs are
    // visible on the scheduler's initial poll instead of waiting 60s.
    // (Audit C7) Tool dispatch sanity check — fails loud at boot if a TOOLS
    // entry lacks a dispatch case in executor.ts.
    try {
        const { auditToolDispatch } = await import('./tools/executor.js');
        const audit = await auditToolDispatch();
        if (audit.missing.length > 0) {
            logger.error({ missing: audit.missing }, '🛑 tool dispatch audit failed — tools registered but not dispatched');
        }
        else {
            logger.info({ covered: audit.covered, orphanedCount: audit.orphaned?.length ?? 0 }, '✅ tool dispatch audit passed');
        }
    }
    catch (err) {
        logger.warn({ err: err.message }, 'tool dispatch audit threw');
    }
    try {
        logger.info('🤖 Booting autonomy layer...');
        await bootAutonomy();
        logger.info('🤖 Autonomy ready');
    }
    catch (err) {
        logger.warn({
            error: err instanceof Error ? err.message : String(err),
        }, '⚠️ Autonomy boot failed — gateway continues but agent will be passive');
    }
    // Start cron scheduler — followups, daily reports, Ramadan-hours flips,
    // PLUS the autonomy-seeded defaults (morning brief, agenda check-ins, etc).
    try {
        logger.info('🕒 Starting cron scheduler...');
        await startCronScheduler();
        logger.info('🕒 Cron scheduler ready');
    }
    catch (err) {
        logger.warn({
            error: err instanceof Error ? err.message : String(err),
        }, '⚠️ Cron scheduler failed to start — scheduled jobs will not fire');
    }
    // Start delivery retry worker. Drains failed outbound messages so a
    // transient WhatsApp socket disconnect or webhook 5xx doesn't silently
    // lose customer messages. First pass runs immediately to flush any rows
    // queued during the previous downtime.
    try {
        startDeliveryRetryWorker();
    }
    catch (err) {
        logger.warn({
            error: err instanceof Error ? err.message : String(err),
        }, '⚠️ Delivery retry worker failed to start');
    }
    // Start server
    try {
        await app.listen({ port: config.PORT, host: config.HOST });
        // Log which path supplied the NIM key. Three states:
        //   • own key      → user has their own NVIDIA_API_KEY in env or .env
        //   • bundled key  → fallback shipped with the npm package (testing only)
        //   • no key       → shared TaseesAI proxy (Vercel 25s timeout — fragile)
        const usingProxy = !config.NVIDIA_NIM_API_KEY;
        if (usingProxy) {
            logger.warn(`⚠️  No NVIDIA key found and no bundled fallback decoded — using shared TaseesAI proxy ` +
                `(${config.NVIDIA_NIM_BASE_URL}). Big tool turns may time out.`);
        }
        else if (process.env.MUASSIS_USING_BUNDLED_KEY === '1') {
            logger.info(`🤖 NIM backend: ${config.NVIDIA_NIM_BASE_URL} (TaseesAI bundled key — testing fallback)`);
        }
        else {
            logger.info(`🤖 NIM backend: ${config.NVIDIA_NIM_BASE_URL} (own key)`);
        }
        logger.info(`🚀 Al Moasis · المؤسس gateway running on http://${config.HOST}:${config.PORT}`);
        // Print a tight, friendly banner so the customer sees one URL to click.
        // The dashboard auto-fetches the local owner token from
        // /api/auth/local-token (loopback-only) — no manual paste needed.
        const dashboardUrl = `http://${config.HOST === '0.0.0.0' ? '127.0.0.1' : config.HOST}:${config.PORT}/dashboard/`;
        console.log('');
        console.log('  ╭───────────────────────────────────────────────────────────╮');
        console.log('  │                                                           │');
        console.log('  │   ✦  Al Moasis · المؤسس is ready  ✦                       │');
        console.log('  │                                                           │');
        console.log(`  │   👉  ${dashboardUrl.padEnd(48)} │`);
        console.log('  │                                                           │');
        console.log('  │   ⌘+click the link above to open the dashboard.           │');
        console.log('  │   The token is auto-loaded — no copy/paste needed.        │');
        console.log('  │                                                           │');
        console.log('  ╰───────────────────────────────────────────────────────────╯');
        console.log('');
        // Detail logs (only on debug-level) — kept for ops/support
        logger.debug(`📱 WhatsApp webhook: http://${config.HOST}:${config.PORT}/webhook/whatsapp`);
        logger.debug(`🏥 Health: http://${config.HOST}:${config.PORT}/health`);
        logger.debug(`🌍 Environment: ${config.NODE_ENV}`);
    }
    catch (err) {
        logger.fatal(err, 'Failed to start Al Moasis gateway');
        process.exit(1);
    }
}
// ─── Graceful Shutdown ───────────────────────────────────────────────
async function shutdown(signal) {
    logger.info(`${signal} received — shutting down المؤسس Gateway...`);
    try {
        await app.close();
        logger.info('HTTP server closed');
        // Shut down MCP server child processes cleanly so we don't leak.
        await mcpManager.shutdown().catch(err => {
            logger.warn({ error: err instanceof Error ? err.message : String(err) }, 'Error stopping MCP servers');
        });
        await closeKnowledgeGraphs().catch(err => {
            logger.warn({
                error: err instanceof Error ? err.message : String(err),
            }, 'Error closing knowledge graphs');
        });
        logger.info('Knowledge graphs closed');
        try {
            shutdownCronScheduler();
        }
        catch (err) {
            logger.warn({
                error: err instanceof Error ? err.message : String(err),
            }, 'Error stopping cron scheduler');
        }
        try {
            stopDeliveryRetryWorker();
        }
        catch (err) {
            logger.warn({
                error: err instanceof Error ? err.message : String(err),
            }, 'Error stopping delivery retry worker');
        }
        logger.info('👋 المؤسس Gateway shut down gracefully');
        process.exit(0);
    }
    catch (err) {
        logger.error({ error: err }, 'Error during shutdown');
        process.exit(1);
    }
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// ─── Unhandled Error Handlers ────────────────────────────────────────
process.on('unhandledRejection', (reason, promise) => {
    logger.error({
        reason: reason instanceof Error ? reason.message : String(reason),
    }, 'Unhandled promise rejection');
});
process.on('uncaughtException', (error) => {
    logger.fatal({
        error: error.message,
        stack: error.stack,
    }, 'Uncaught exception — shutting down');
    process.exit(1);
});
start();
//# sourceMappingURL=index.js.map