import express from 'express'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import csp, { removeCSPHeaders } from './CSP.mjs'
import Router from '../router.mjs'
import helmet from 'helmet'
import UserSessionsRedis from '../Features/User/UserSessionsRedis.mjs'
import Csrf, { Csrf as CsrfClass } from './Csrf.mjs'
import HttpPermissionsPolicyMiddleware from './HttpPermissionsPolicy.mjs'
import SessionAutostartMiddleware from './SessionAutostartMiddleware.mjs'
import AnalyticsManager from '../Features/Analytics/AnalyticsManager.mjs'
import session from 'express-session'
import CookieMetrics from './CookieMetrics.mjs'
import CustomSessionStore from './CustomSessionStore.mjs'
import bodyParser from './BodyParserWrapper.mjs'
import cookieParser from 'cookie-parser'
import bearerTokenMiddleware from 'express-bearer-token'
import passport from 'passport'
import { Strategy as LocalStrategy } from 'passport-local'
import ReferalConnect from '../Features/Referal/ReferalConnect.mjs'
import RedirectManager from './RedirectManager.mjs'
import translations from './Translations.mjs'
import Views from './Views.mjs'
import Features from './Features.mjs'
import ErrorController from '../Features/Errors/ErrorController.mjs'
import HttpErrorHandler from '../Features/Errors/HttpErrorHandler.mjs'
import UserSessionsManager from '../Features/User/UserSessionsManager.mjs'
import AuthenticationController from '../Features/Authentication/AuthenticationController.mjs'
import SessionManager from '../Features/Authentication/SessionManager.mjs'
import AdminAuthorizationHelper from '../Features/Helpers/AdminAuthorizationHelper.mjs'
import Modules from './Modules.mjs'
import expressLocals from './ExpressLocals.mjs'
import noCache from 'nocache'
import os from 'node:os'
import http from 'node:http'
import { fileURLToPath } from 'node:url'
import serveStaticWrapper from './ServeStaticWrapper.mjs'
import { handleValidationError } from '@overleaf/validation-tools'

const { hasAdminAccess } = AdminAuthorizationHelper
const sessionsRedisClient = UserSessionsRedis.client()

const oneDayInMilliseconds = 86400000

const STATIC_CACHE_AGE = Settings.cacheStaticAssets
  ? oneDayInMilliseconds * 365
  : 0

// Init the session store
const sessionStore = new CustomSessionStore({ client: sessionsRedisClient })

const app = express()

const webRouter = express.Router()
const privateApiRouter = express.Router()
const publicApiRouter = express.Router()

if (Settings.behindProxy) {
  app.set('trust proxy', Settings.trustedProxyIps || true)
  /**
   * Handle the X-Original-Forwarded-For header.
   *
   * The nginx ingress sends us the contents of X-Forwarded-For it received in
   * X-Original-Forwarded-For. Express expects all proxy IPs to be in a comma
   * separated list in X-Forwarded-For.
   */
  app.use((req, res, next) => {
    if (
      req.headers['x-original-forwarded-for'] &&
      req.headers['x-forwarded-for']
    ) {
      req.headers['x-forwarded-for'] =
        req.headers['x-original-forwarded-for'] +
        ', ' +
        req.headers['x-forwarded-for']
    }
    next()
  })
}

// `req.ip` is a getter on the underlying socket.
// The socket details are freed as the connection is dropped -- aka aborted.
// Hence `req.ip` may read `undefined` upon connection drop.
// A couple of places require a valid IP at all times. Cache it!
const ORIGINAL_REQ_IP = Object.getOwnPropertyDescriptor(
  Object.getPrototypeOf(app.request),
  'ip'
).get
Object.defineProperty(app.request, 'ip', {
  configurable: true,
  enumerable: true,
  get() {
    const ip = ORIGINAL_REQ_IP.call(this)
    // Shadow the prototype level getter with a property on the instance.
    // Any future access on `req.ip` will get served by the instance property.
    Object.defineProperty(this, 'ip', { value: ip })
    return ip
  },
})

app.use((req, res, next) => {
  if (req.destroyed) {
    // Request has been aborted already.
    return
  }
  // Implicitly cache the ip, see above.
  if (!req.ip) {
    // Critical connection details are missing.
    return
  }
  next()
})

if (Settings.exposeHostname) {
  const HOSTNAME = os.hostname()
  app.use((req, res, next) => {
    res.setHeader('X-Served-By', HOSTNAME)
    next()
  })
}

webRouter.use(
  serveStaticWrapper(
    fileURLToPath(new URL('../../../public', import.meta.url)),
    {
      maxAge: STATIC_CACHE_AGE,
      setHeaders: removeCSPHeaders,
    }
  )
)

app.set('views', fileURLToPath(new URL('../../views', import.meta.url)))
app.set('view engine', 'pug')

if (Settings.enabledServices.includes('web')) {
  if (Settings.enablePugCache || app.get('env') !== 'development') {
    logger.debug('enabling view cache for production or acceptance tests')
    app.enable('view cache')
  }
  if (Settings.precompilePugTemplatesAtBootTime) {
    logger.debug('precompiling views for web in production environment')
    await Views.precompileViews(app)
  }
  Modules.loadViewIncludes(app)
}

app.use(metrics.http.monitor(logger))

await Modules.applyMiddleware(app, 'appMiddleware')
app.use(bodyParser.urlencoded({ extended: true, limit: '2mb' }))
app.use(bodyParser.json({ limit: Settings.max_json_request_size }))
// add explicit name for telemetry
app.use(bearerTokenMiddleware())

if (Settings.blockCrossOriginRequests) {
  app.use(Csrf.blockCrossOriginRequests())
}

if (Settings.useHttpPermissionsPolicy) {
  const httpPermissionsPolicy = new HttpPermissionsPolicyMiddleware(
    Settings.httpPermissions
  )
  logger.debug('adding permissions policy config', Settings.httpPermissions)
  webRouter.use(httpPermissionsPolicy.middleware)
}

RedirectManager.apply(webRouter)

if (!Settings.security.sessionSecret) {
  throw new Error('No SESSION_SECRET provided.')
}

const sessionSecrets = [
  Settings.security.sessionSecret,
  Settings.security.sessionSecretUpcoming,
  Settings.security.sessionSecretFallback,
].filter(Boolean)

webRouter.use(cookieParser(sessionSecrets))
webRouter.use(CookieMetrics.middleware)
SessionAutostartMiddleware.applyInitialMiddleware(webRouter)
await Modules.applyMiddleware(webRouter, 'sessionMiddleware', {
  store: sessionStore,
})
webRouter.use(
  session({
    resave: false,
    saveUninitialized: false,
    secret: sessionSecrets,
    proxy: Settings.behindProxy,
    cookie: {
      domain: Settings.cookieDomain,
      maxAge: Settings.cookieSessionLength, // in milliseconds, see https://github.com/expressjs/session#cookiemaxage
      secure: Settings.secureCookie,
      sameSite: Settings.sameSiteCookie,
    },
    store: sessionStore,
    key: Settings.cookieName,
    rolling: Settings.cookieRollingSession === true,
  })
)

if (Features.hasFeature('saas')) {
  webRouter.use(AnalyticsManager.analyticsIdMiddleware)
}

// passport
webRouter.use(passport.initialize())
webRouter.use(passport.session())

passport.use(
  new LocalStrategy(
    {
      passReqToCallback: true,
      usernameField: 'email',
      passwordField: 'password',
    },
    AuthenticationController.doPassportLogin
  )
)
passport.serializeUser(AuthenticationController.serializeUser)
passport.deserializeUser(AuthenticationController.deserializeUser)

Modules.hooks.fire('passportSetup', passport, err => {
  if (err != null) {
    logger.err({ err }, 'error setting up passport in modules')
  }
})

await Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter)

webRouter.csrf = new CsrfClass()
webRouter.use(webRouter.csrf.middleware)
webRouter.use(translations.setLangBasedOnDomainMiddleware)

if (Settings.cookieRollingSession) {
  // Measure expiry from last request, not last login
  webRouter.use((req, res, next) => {
    if (!req.session.noSessionCallback) {
      req.session.touch()
      if (SessionManager.isUserLoggedIn(req.session)) {
        UserSessionsManager.touch(
          SessionManager.getSessionUser(req.session),
          err => {
            if (err) {
              logger.err({ err }, 'error extending user session')
            }
          }
        )
      }
    }
    next()
  })
}

webRouter.use(ReferalConnect.use)
await expressLocals(webRouter, privateApiRouter, publicApiRouter)
webRouter.use(SessionAutostartMiddleware.invokeCallbackMiddleware)

webRouter.use(function checkIfSiteClosed(req, res, next) {
  if (Settings.siteIsOpen) {
    next()
  } else if (hasAdminAccess(SessionManager.getSessionUser(req.session))) {
    next()
  } else {
    HttpErrorHandler.maintenance(req, res)
  }
})

webRouter.use(function checkIfEditorClosed(req, res, next) {
  if (Settings.editorIsOpen) {
    next()
  } else if (req.url.indexOf('/admin') === 0) {
    next()
  } else {
    HttpErrorHandler.maintenance(req, res)
  }
})

webRouter.use(AuthenticationController.validateAdmin)

// add security headers using Helmet
const noCacheMiddleware = noCache()
webRouter.use((req, res, next) => {
  const isProjectPage = /^\/project\/[a-f0-9]{24}$/.test(req.path)
  if (isProjectPage) {
    // always set no-cache headers on a project page, as it could be an anonymous token viewer
    return noCacheMiddleware(req, res, next)
  }

  const isProjectFile = /^\/project\/[a-f0-9]{24}\/file\/[a-f0-9]{24}$/.test(
    req.path
  )
  if (isProjectFile) {
    // don't set no-cache headers on a project file, as it's immutable and can be cached (privately)
    return next()
  }
  const isProjectBlob = /^\/project\/[a-f0-9]{24}\/blob\/[a-f0-9]{40}$/.test(
    req.path
  )
  if (isProjectBlob) {
    // don't set no-cache headers on a project blobs, as they are immutable and can be cached (privately)
    return next()
  }

  const isWikiContent = /^\/learn(-scripts)?(\/|$)/i.test(req.path)
  if (isWikiContent) {
    // don't set no-cache headers on wiki content, as it's immutable and can be cached (publicly)
    return next()
  }

  const isLoggedIn = SessionManager.isUserLoggedIn(req.session)
  if (isLoggedIn) {
    // always set no-cache headers for authenticated users (apart from project files, above)
    return noCacheMiddleware(req, res, next)
  }

  // allow other responses (anonymous users, except for project pages) to be cached
  return next()
})

webRouter.use(
  helmet({
    // note that more headers are added by default
    dnsPrefetchControl: false,
    referrerPolicy: { policy: 'origin-when-cross-origin' },
    hsts: false,
    // Disabled because it's impractical to include every resource via CORS or
    // with the magic CORP header
    crossOriginEmbedderPolicy: false,
    // We need to be able to share the context of some popups. For example,
    // when Recurly opens Paypal in a popup.
    crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
    // Disabled because it's not a security header and has possibly-unwanted
    // effects
    originAgentCluster: false,
    // We have custom handling for CSP below, so Helmet's default is disabled
    contentSecurityPolicy: false,
  })
)

// add CSP header to HTML-rendering routes, if enabled
if (Settings.csp && Settings.csp.enabled) {
  logger.debug('adding CSP header to rendered routes', Settings.csp)
  app.use(csp(Settings.csp))
}

logger.debug('creating HTTP server'.yellow)
const server = http.createServer(app)

// provide settings for separate web and api processes
if (Settings.enabledServices.includes('api')) {
  logger.debug({}, 'providing api router')
  app.use(privateApiRouter)
  app.use(handleValidationError)
  app.use(ErrorController.handleApiError)
}

if (Settings.enabledServices.includes('web')) {
  logger.debug({}, 'providing web router')
  app.use(publicApiRouter) // public API goes with web router for public access
  app.use(handleValidationError)
  app.use(ErrorController.handleApiError)
  app.use(webRouter)
  app.use(handleValidationError)
  app.use(ErrorController.handleError)
}

metrics.injectMetricsRoute(webRouter)
metrics.injectMetricsRoute(privateApiRouter)

const beforeRouterInitialize = performance.now()
await Router.initialize(webRouter, privateApiRouter, publicApiRouter)
metrics.gauge('web_startup', performance.now() - beforeRouterInitialize, 1, {
  path: 'Router.initialize',
})

export default { app, server }
