// admin-server.mjs
//
// Standalone memind_adm service — the back-office process.
//
// Serves the platform super-admin API (/admin-api/*) and the plaza operations
// console API (/api/ops/v1/*) on its own port, isolated from the public
// user-facing server (server.mjs). In production this is fronted by
// gadm.tkmind.cn; in dev the ops SPA (:3002) proxies to it directly.
//
// It trusts the shared session cookie (H5_COOKIE_DOMAIN=.tkmind.cn), so login
// continues to happen on the main app domain — this process only reads the
// session and authorizes admin/ops roles. No daemons, no public traffic.
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseCookies } from './auth.mjs';
import { USER_COOKIE } from './user-auth.mjs';
import { createAdminServices } from './admin-bootstrap.mjs';
import { createAdminApi, createOpsApi } from './admin-routes.mjs';
import { buildNetworkGuard, parseList } from './admin-guard.mjs';
import { sendError } from './api-response.mjs';
import { isLocalDevHostname } from './scripts/local-test-config.mjs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Mirror server.mjs env loading so the admin service can run standalone.
function loadEnvFile(filePath) {
  if (!fs.existsSync(filePath)) return;
  for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const eq = trimmed.indexOf('=');
    if (eq < 0) continue;
    const key = trimmed.slice(0, eq).trim();
    const value = trimmed.slice(eq + 1).trim();
    if (!process.env[key]) process.env[key] = value;
  }
}

loadEnvFile(path.join(__dirname, '../../.env.local'));
loadEnvFile(path.join(__dirname, '.env'));

const PORT = Number(process.env.ADMIN_PORT ?? 8082);
const HOST = process.env.ADMIN_HOST ?? '127.0.0.1';
const PUBLIC_HOST = process.env.ADMIN_PUBLIC_HOST ?? 'gadm.tkmind.cn';

const getToken = (req) => parseCookies(req.get('cookie'))[USER_COOKIE];

// Resolve the shared session cookie into req.currentUser, else 401.
function resolveSessionUser(userAuth) {
  return async (req, res, next) => {
    const token = getToken(req);
    const me = token ? await userAuth.getMe(token) : null;
    if (!me) return res.status(401).json({ message: '未授权，请重新登录' });
    req.currentUser = me;
    next();
  };
}

// ---- Console registry ----------------------------------------------------
// Each back-office console is an independently mountable unit. A process serves
// whichever subset ADMIN_CONSOLES names (default: all). Splitting into separate
// processes later is purely a config change — run one instance with
// ADMIN_CONSOLES=admin and another with ADMIN_CONSOLES=ops; no code changes.
const CONSOLES = {
  admin: {
    label: 'platform super-admin',
    mountPath: '/admin-api',
    routeHint: '/admin-api/*',
    envPrefix: 'ADMIN_API', // ADMIN_API_ALLOWED_HOSTS / ADMIN_API_IP_ALLOWLIST
    build: ({ jsonBody, services }) =>
      // The admin router gates currentUser itself, then requireAdmin per route.
      createAdminApi({
        jsonBody,
        getToken,
        userAuth: services.userAuth,
        llmProviderService: services.llmProviderService,
        plazaPosts: services.plazaPosts,
        plazaOps: services.plazaOps,
        wechatAdmin: services.wechatAdmin,
        subscriptionService: services.subscriptionService,
      }),
  },
  ops: {
    label: 'plaza operations console',
    mountPath: '/api',
    routeHint: '/api/ops/v1/*',
    envPrefix: 'OPS_API', // OPS_API_ALLOWED_HOSTS / OPS_API_IP_ALLOWLIST
    build: ({ jsonBody, services }) => {
      const router = express.Router();
      router.use(resolveSessionUser(services.userAuth));
      router.use('/ops/v1', createOpsApi({ jsonBody, plazaOps: services.plazaOps }));
      return router;
    },
  },
};

function csrfOriginCheck(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  const host = req.get('host');
  const origin = req.get('origin');
  const referer = req.get('referer');
  if (!origin && !referer) return next();
  const requestHostname = (host ?? '').split(':')[0];
  const allowed = [origin, referer].some((value) => {
    if (!value) return false;
    try {
      const source = new URL(value);
      if (source.host === host) return true;
      if (source.hostname === PUBLIC_HOST) return true;
      return isLocalDevHostname(requestHostname) && isLocalDevHostname(source.hostname);
    } catch {
      return false;
    }
  });
  if (!allowed) {
    return sendError(res, req, 403, 'csrf_failed', '来源校验失败');
  }
  return next();
}

function resolveEnabledConsoles() {
  const enabled = parseList(process.env.ADMIN_CONSOLES || 'admin,ops');
  if (!enabled.length) throw new Error('ADMIN_CONSOLES resolved to an empty set');
  const unknown = enabled.filter((key) => !CONSOLES[key]);
  if (unknown.length) {
    throw new Error(
      `unknown ADMIN_CONSOLES: ${unknown.join(', ')} (valid: ${Object.keys(CONSOLES).join(', ')})`,
    );
  }
  return enabled;
}

async function main() {
  const enabled = resolveEnabledConsoles();
  const services = await createAdminServices({ h5Root: __dirname });

  const app = express();
  app.set('trust proxy', true);
  app.disable('x-powered-by');

  const jsonBody = express.json({ limit: '1mb' });
  const ctx = { jsonBody, services };

  app.get('/healthz', (_req, res) =>
    res.json({ ok: true, service: 'memind_adm', consoles: enabled }),
  );

  const summary = [];
  for (const key of enabled) {
    const console_ = CONSOLES[key];
    const allowedHosts = parseList(process.env[`${console_.envPrefix}_ALLOWED_HOSTS`]);
    const ipAllowlist = parseList(process.env[`${console_.envPrefix}_IP_ALLOWLIST`]);
    const guard = buildNetworkGuard({
      allowedHosts,
      ipAllowlist,
      onDeny: (res, req, reason) =>
        sendError(
          res,
          req,
          403,
          'forbidden_network',
          reason === 'host' ? '主机不在白名单' : 'IP 不在白名单',
        ),
    });

    app.use(console_.mountPath, csrfOriginCheck);
    if (guard) app.use(console_.mountPath, guard);
    app.use(console_.mountPath, console_.build(ctx));

    summary.push({ key, console_, allowedHosts, ipAllowlist, guarded: Boolean(guard) });
  }

  app.use((req, res) => res.status(404).json({ message: 'not found' }));

  if (services.subscriptionService) {
    const subExpiryTimer = setInterval(async () => {
      try {
        const n = await services.subscriptionService.expireStaleSubscriptions();
        if (n > 0) console.log(`Expired ${n} stale subscription(s)`);
      } catch (err) {
        console.warn('Subscription expiry check failed:', err);
      }
    }, 60 * 60 * 1000);
    subExpiryTimer.unref?.();
  }

  app.listen(PORT, HOST, () => {
    console.log(`memind_adm @ http://${HOST}:${PORT}  (public host: ${PUBLIC_HOST})`);
    for (const { console_, allowedHosts, ipAllowlist, guarded } of summary) {
      const fence = guarded
        ? ` [hosts: ${allowedHosts.join(',') || '*'} | ip: ${ipAllowlist.join(',') || '*'}]`
        : '';
      console.log(`  ${console_.routeHint.padEnd(16)} ${console_.label}${fence}`);
    }
  });
}

main().catch((err) => {
  console.error('admin-server failed to start:', err);
  process.exit(1);
});
