import("@triliumnext/core");

import { erase } from "@triliumnext/core";
import compression from "compression";
import cookieParser from "cookie-parser";
import ejs from "ejs";
import express from "express";
import { auth } from "express-openid-connect";
import helmet from "helmet";
import { t } from "i18next";
import path from "path";
import favicon from "serve-favicon";
import type serveStatic from "serve-static";

import assets from "./routes/assets.js";
import custom from "./routes/custom.js";
import error_handlers from "./routes/error_handlers.js";
import mcpRoutes from "./routes/mcp.js";
import routes from "./routes/routes.js";
import config from "./services/config.js";
import log from "./services/log.js";
import openID from "./services/open_id.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";

// Allow serving assets even if the installation path contains a hidden (dot-prefixed) directory.
const STATIC_OPTIONS: serveStatic.ServeStaticOptions = { dotfiles: "allow" };

// Capacitor WebView origins for the mobile app — baked in so the mobile client
// can sync against any Trilium server without per-server CORS configuration.
const MOBILE_APP_ORIGINS = ["https://localhost", "capacitor://localhost"];
const DEFAULT_CORS_METHODS = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
const DEFAULT_CORS_HEADERS = "Content-Type, Authorization, trilium-cred, pageCount, pageIndex, requestId";

export default async function buildApp() {
    const app = express();

    const publicDir = isDev ? path.join(getResourceDir(), "../dist/public") : path.join(getResourceDir(), "public");
    const publicAssetsDir = path.join(publicDir, "assets");
    const assetsDir = RESOURCE_DIR;

    // view engine setup
    app.set("views", path.join(assetsDir, "views"));
    app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
    app.set("view engine", "ejs");

    setupCors(app);

    app.use((_req, res, next) => {
        res.locals.t = t;
        return next();
    });

    if (!utils.isElectron) {
        app.use(compression({
            // Skip compression for SSE endpoints to enable real-time streaming
            filter: (req, res) => {
                // Skip compression for SSE-capable endpoints
                if (req.path === "/api/llm-chat/stream" || req.path === "/mcp") {
                    return false;
                }
                return compression.filter(req, res);
            }
        }));
    }

    let resourcePolicy = config["Network"]["corsResourcePolicy"] as 'same-origin' | 'same-site' | 'cross-origin' | undefined;
    if(resourcePolicy !== 'same-origin' && resourcePolicy !== 'same-site' && resourcePolicy !== 'cross-origin') {
        log.error(`Invalid CORS Resource Policy value: '${resourcePolicy}', defaulting to 'same-origin'`);
        resourcePolicy = 'same-origin';
    }

    app.use(
        helmet({
            hidePoweredBy: false, // errors out in electron
            contentSecurityPolicy: false,
            crossOriginResourcePolicy: {
                policy: resourcePolicy
            },
            crossOriginEmbedderPolicy: false
        })
    );

    app.use(express.text({ limit: "500mb" }));
    app.use(express.json({ limit: "500mb" }));
    app.use(express.raw({ limit: "500mb" }));
    app.use(express.urlencoded({ extended: false }));
    app.use(cookieParser());

    // MCP is registered before session/auth middleware — it uses its own
    // localhost-only guard and does not require Trilium authentication.
    mcpRoutes.register(app);

    app.use(express.static(path.join(publicDir, "root"), STATIC_OPTIONS));
    app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest"), STATIC_OPTIONS));
    app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"), STATIC_OPTIONS));
    app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png"), STATIC_OPTIONS));

    const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js");
    app.use(sessionParser);
    startSessionCleanup();
    app.use(favicon(path.join(assetsDir, isDev ? "icon-dev.ico" : "icon.ico")));

    if (openID.isOpenIDEnabled())
        app.use(auth(openID.generateOAuthConfig()));

    await assets.register(app);
    routes.register(app);
    custom.register(app);
    error_handlers.register(app);

    const { sync, consistency_checks, scheduler } = await import("@triliumnext/core");
    sync.startSyncTimer();

    consistency_checks.startConsistencyChecks();
    scheduler.startScheduler();

    erase.startScheduledCleanup();

    if (utils.isElectron) {
        (await import("@electron/remote/main/index.js")).initialize();
    }

    return app;
}

function setupCors(app: express.Express) {
    const configuredOrigins = (config["Network"]["corsAllowOrigin"] || "")
        .split(",")
        .map(o => o.trim())
        .filter(Boolean);
    const allowedOrigins = new Set([...MOBILE_APP_ORIGINS, ...configuredOrigins]);

    app.use((req, res, next) => {
        const requestOrigin = req.get("origin");
        if (requestOrigin && allowedOrigins.has(requestOrigin)) {
            res.header("Access-Control-Allow-Origin", requestOrigin);
            res.header("Access-Control-Allow-Credentials", "true");
            res.header("Vary", "Origin");
            res.header("Access-Control-Allow-Methods", config["Network"]["corsAllowMethods"] || DEFAULT_CORS_METHODS);
            res.header("Access-Control-Allow-Headers", config["Network"]["corsAllowHeaders"] || DEFAULT_CORS_HEADERS);

            if (req.method === "OPTIONS") {
                res.sendStatus(204);
                return;
            }
        }
        return next();
    });
}
