require('dotenv').config();
// Session 10 — Sentry must initialize BEFORE express is required so
// the instrumentation hooks attach correctly. Graceful no-op when
// SENTRY_DSN is unset.
const { initSentry, Sentry } = require('./utils/sentry');
initSentry();

const express = require('express');
const cors = require('cors');
const oddsRoutes = require('./routes/odds');
const analyzeRoutes = require('./routes/analyze');
const scanRoutes = require('./routes/scan');
const movementsRoutes = require('./routes/movements');
const alertsRoutes = require('./routes/alerts');
const betsRoutes = require('./routes/bets');
const stripeRoutes = require('./routes/stripe');
const statsRoutes = require('./routes/stats');
const propsRoutes = require('./routes/props');
const waitlistRoutes = require('./routes/waitlist');
const pipelineRoutes = require('./routes/pipeline');
const shareCardRoutes = require('./routes/shareCard');
const pushRoutes = require('./routes/push');
const gradingRoutes = require('./routes/grading');
const correctionRoutes = require('./routes/corrections');
const internalRoutes = require('./routes/internal');
const { missionHeader } = require('./middleware/mission');

const app = express();

// CORS — accept the Next.js frontend on Vercel/production and localhost dev.
// FRONTEND_ORIGINS overrides at deploy time (comma-separated).
const defaultOrigins = [
  'http://localhost:3000',
  'http://localhost:3001',
  'https://vyndr.app',
  'https://www.vyndr.app',
];
const envOrigins = (process.env.FRONTEND_ORIGINS || '').split(',').map((s) => s.trim()).filter(Boolean);
const allowedOrigins = [...new Set([...defaultOrigins, ...envOrigins])];
app.use(
  cors({
    origin(origin, cb) {
      // Allow same-origin (no Origin header) and the configured allowlist.
      // Also allow any *.vercel.app preview for staging.
      if (!origin) return cb(null, true);
      if (allowedOrigins.includes(origin)) return cb(null, true);
      if (/\.vercel\.app$/.test(new URL(origin).hostname)) return cb(null, true);
      return cb(new Error(`Origin ${origin} not allowed`));
    },
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  })
);

// Mission header on all responses
app.use(missionHeader);

// Stripe webhook needs raw body — must be before express.json()
app.use('/api/stripe/webhook', express.raw({ type: 'application/json' }));

// Body parser limit raised to 10MB to accommodate full-slate poller
// payloads. The default 100KB rejected real WNBA slates with 413.
// Per-route limits below can tighten or loosen this for specific paths.
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Health check — public minimal status (Coolify, uptime monitors). Detailed
// adapter + Python service status only with X-VYNDR-Internal-Key.
app.get('/api/health', async (req, res) => {
  const checks = {};

  try {
    const { getRedisClient, isDegraded } = require('./utils/redis');
    if (isDegraded()) throw new Error('degraded');
    await getRedisClient().ping();
    checks.redis = 'ok';
  } catch { checks.redis = 'down'; }

  try {
    const { getSupabaseServiceClient } = require('./utils/supabase');
    const { error } = await getSupabaseServiceClient().from('users').select('id').limit(1);
    checks.supabase = error ? 'error' : 'ok';
  } catch { checks.supabase = 'down'; }

  const healthy = checks.redis === 'ok' && checks.supabase === 'ok';
  const expectedKey = process.env.VYNDR_INTERNAL_KEY;
  const providedKey = req.headers['x-vyndr-internal-key'];

  if (expectedKey && providedKey === expectedKey) {
    try {
      const axios = require('axios');
      const pyUrl = process.env.PYTHON_SERVICE_URL || 'http://localhost:8000';
      await axios.get(`${pyUrl}/health`, { timeout: 3_000 });
      checks.python = 'ok';
    } catch { checks.python = 'down'; }

    checks.adapters = {
      sharpapi:   require('./services/adapters/sharpApiAdapter').configured(),
      propodds:   require('./services/adapters/propOddsAdapter').configured(),
      parlayapi:  require('./services/adapters/parlayApiAdapter').configured(),
      oddspapi:   require('./services/adapters/oddsPapiAdapter').configured(),
      cfbd:       require('./services/adapters/cfbdAdapter').configured(),
      openrouter: require('./services/adapters/openRouterAdapter').configured(),
    };
    checks.engine2_enabled = process.env.ENGINE2_ENABLED === 'true';

    return res.status(healthy ? 200 : 503).json({
      status: healthy ? 'healthy' : 'degraded',
      checks,
      version: require('../package.json').version || '1.0.0',
      uptime: Math.floor(process.uptime()),
    });
  }

  return res.status(healthy ? 200 : 503).json({
    status: healthy ? 'healthy' : 'degraded',
  });
});

app.use('/api/odds', oddsRoutes);
app.use('/api/analyze', analyzeRoutes);
app.use('/api/scan', scanRoutes);
app.use('/api/movements', movementsRoutes);
app.use('/api/alerts', alertsRoutes);
app.use('/api/bets', betsRoutes);
// Session 49 — per-user onboarding preferences (auth-gated, user_metadata).
app.use('/api/preferences', require('./routes/preferences'));
app.use('/api/stripe', stripeRoutes);
app.use('/api/stats', statsRoutes);
app.use('/api/props', propsRoutes);
// Session 60 (night2/E) — the scan search box's canonical player resolver.
app.use('/api/players', require('./routes/players'));
// Session 60 (night2/F) — per-user utility reads (Settings scan meter).
app.use('/api/user', require('./routes/user'));
// Session 63 (A1-S4) — the founder's media desk (email-allowlisted).
app.use('/api/desk', require('./routes/desk'));
app.use('/api/waitlist', waitlistRoutes);
app.use('/api/pipeline', pipelineRoutes);
app.use('/api/share-card', shareCardRoutes);
app.use('/api/push', pushRoutes);
// Resolution payloads carry full ESPN box scores plus per-game prop
// arrays. Full-slate WNBA / MLB resolves exceed 2MB in practice — keep
// /api/grading aligned with the global 10MB ceiling. Correction sweep
// stays small (just a window-hours integer + flags).
app.use('/api/grading', express.json({ limit: '10mb' }), gradingRoutes);
app.use('/api/grading', express.json({ limit: '256kb' }), correctionRoutes);
const widgetRoutes = require('./routes/widget');
app.use('/api/widget', widgetRoutes);
// Session 23 — all-day intelligence layer. Free/cheap content surfaces
// that keep the platform alive when odds-api is empty: schedule (ESPN),
// game lines (Tank01), streaks + hot lists (cached game logs), and the
// stat-filtered views over all of them.
const scheduleRoutes = require('./routes/schedule');
app.use('/api/schedule', scheduleRoutes);
// Wave 6 — combat intelligence (MMA/UFC): fight cards + tale-of-the-tape +
// best-effort ML/round-total odds. Read-only, cache-friendly, honest empty
// off-card. NOT in the graded-props pipeline (no settled grades in v1).
const combatRoutes = require('./routes/combat');
app.use('/api/combat', combatRoutes);
app.use('/api/fight', combatRoutes.fightRouter);
// Session 45 — live ticker feed (snapshot exhaust + editorial pins). Public,
// cache-only, never triggers a snapshot.
const tickerRoutes = require('./routes/ticker');
app.use('/api/ticker', tickerRoutes);
// Session 45 — pre-graded slate read (snapshot:{sport}:latest). Public, cache-only.
const snapshotReadRoutes = require('./routes/snapshot');
app.use('/api/snapshot', snapshotReadRoutes);
// Session 51 — Team Hub (roster + archetypes + graded props). Public, cached.
app.use('/api/team', require('./routes/team'));
// A1 Session 11 — LIVE TRACKING: current box-line values for in-progress
// games (free statsapi/ESPN, shared 90s cache). Grades never change in-game
// — this is tracking, labeled as such.
app.use('/api/live', require('./routes/live'));
// Session 55 — self-learning loop: the system's rolling accuracy record
// (settled snapshot grades vs real results). Public, cache-only.
app.use('/api/accuracy', require('./routes/accuracy'));
app.use('/api/ledger', require('./routes/ledger'));
// A1 Session 10 — public ledger profiles: claim a handle, one explicit
// publish toggle, and the ENTIRE settled record on a public page.
app.use('/api/profiles', require('./routes/profiles'));
const gameLinesRoutes = require('./routes/gameLines');
app.use('/api/gamelines', gameLinesRoutes);
const streaksRoutes = require('./routes/streaks');
app.use('/api/streaks', streaksRoutes);
const hotListRoutes = require('./routes/hotlist');
app.use('/api/hotlist', hotListRoutes);
// Wave 2A — offseason/never-dark hub feeds. FREE ESPN news wire + quota-
// disciplined championship futures (both graceful/empty; never error).
app.use('/api/news', require('./routes/news'));
app.use('/api/futures', require('./routes/futures'));
// Session 28 — parlay builder, line-movement views, book comparison.
// All three are zero-credit: parlay math is pure, lines read a Redis
// snapshot history, books read the cached odds props.
const parlayRoutes = require('./routes/parlay');
app.use('/api/parlay', parlayRoutes);
const lineMovementRoutes = require('./routes/lineMovement');
app.use('/api/lines', lineMovementRoutes);
const bookComparisonRoutes = require('./routes/bookComparison');
app.use('/api/books', bookComparisonRoutes);
// Session 29 — content templates: structured social/newsletter content
// generated from live data, degrading gracefully by data level.
const contentRoutes = require('./routes/content');
app.use('/api/content', contentRoutes);
// Session S7 (a1) — THE VYNDR REPORT: public double-opt-in subscribe
// (forwards to the self-hosted Listmonk; graceful no-op without env).
app.use('/api/newsletter', require('./routes/newsletter'));
// A1 S9 — Slip Reader: OCR a bet-slip screenshot into legs (auth +
// per-tier daily quota inside the router). Values are user-slip values.
app.use('/api/slips', require('./routes/slips'));
// Session 18 — internal ops endpoints (admin dashboard triggers,
// shared-key auth via `VYNDR_INTERNAL_KEY`). Never reachable from
// the public surface; the Next.js admin route proxies through with
// the key kept server-side.
app.use('/api/internal', internalRoutes);
// A1 S3 — partner attribution report. Internal-key gated (router-level
// requireInternalAuth); no Next proxy on purpose — never browser-facing.
app.use('/api/partners', require('./routes/partners'));
app.use('/api/founders', require('./routes/founders'));
app.use('/api/hero-prop', require('./routes/heroProp'));
app.use('/api/desk-showcase', require('./routes/deskShowcase'));

// Session 10 — Sentry's Express error handler catches uncaught
// errors from every route mounted above. Must come AFTER routes but
// BEFORE any final express error handler. The noop client makes this
// a safe no-op when SENTRY_DSN is unset.
Sentry.setupExpressErrorHandler(app);

module.exports = app;
