import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'
import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js'
import express from 'express'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { createDb, purgeOAuth } from '@znet/catalog-db'
import { createLogger } from '@znet/shared'
import { magentoFromEnv } from '@znet/magento'
import { callerOf, discoverIssuer, jwtVerifier, staticVerifier } from './auth.js'
import { CatalogApiClient } from './catalog-client.js'
import { loadMcpEnv, SCOPES } from './config.js'
import { MagentoAdminAuthProvider } from './magento-auth/provider.js'
import { parseUserScopes } from './magento-auth/user-scopes.js'
import { rateLimit } from './rate-limit.js'
import { createMcpServer } from './server.js'

const env = loadMcpEnv()
const log = createLogger('mcp-server', env.LOG_LEVEL)
const publicUrl = new URL(env.MCP_PUBLIC_URL)

const { db, close } = createDb(env.DATABASE_URL, { max: 5, applicationName: 'mcp-znet-mcp' })
const deps = {
  db,
  env,
  log,
  catalog: new CatalogApiClient(env.CATALOG_API_URL, env.CATALOG_API_TOKEN),
  magento: magentoFromEnv(env, log, env.MAGENTO_LIVE_TIMEOUT_MS),
}

const app = createMcpExpressApp({
  host: env.MCP_HOST,
  allowedHosts: env.MCP_ALLOWED_HOSTS.length ? env.MCP_ALLOWED_HOSTS : [publicUrl.host, publicUrl.hostname],
})
app.set('trust proxy', 1) // behind Caddy

const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(publicUrl)
let verifier: OAuthTokenVerifier
if (env.AUTH_MODE === 'magento') {
  const provider = new MagentoAdminAuthProvider({
    db,
    magento: deps.magento,
    log,
    secret: env.MCP_AUTH_SECRET!,
    userScopes: parseUserScopes(env.MCP_USER_SCOPES),
    require2fa: env.MAGENTO_ADMIN_2FA === 'google',
    resource: publicUrl,
    accessTtlSeconds: env.MCP_ACCESS_TOKEN_TTL_MIN * 60,
    refreshTtlSeconds: env.MCP_REFRESH_TOKEN_TTL_DAYS * 86_400,
    storeName: env.MCP_STORE_NAME,
  })
  verifier = provider
  // Authorization server: /.well-known/*, /authorize, /token, /register, /revoke
  app.use(mcpAuthRouter({ provider, issuerUrl: new URL(publicUrl.origin), resourceServerUrl: publicUrl, scopesSupported: Object.values(SCOPES), resourceName: env.MCP_STORE_NAME }))
  app.post('/login', express.urlencoded({ extended: false, limit: '16kb' }), (req, res, next) => provider.handleLogin(req, res).catch(next))
  setInterval(() => void purgeOAuth(db).catch(err => log.error({ err }, 'oauth purge failed')), 6 * 3_600_000).unref()
  log.info('OAuth: staff sign in with Magento admin credentials')
} else if (env.AUTH_MODE === 'oauth') {
  const metadata = await discoverIssuer(env.OAUTH_ISSUER!)
  verifier = jwtVerifier(env, metadata)
  // Protected Resource Metadata — tells ChatGPT which authorization server to use.
  app.use(mcpAuthMetadataRouter({ oauthMetadata: metadata, resourceServerUrl: publicUrl, scopesSupported: Object.values(SCOPES), resourceName: 'Znet Magento' }))
  log.info({ issuer: metadata.issuer }, 'OAuth resource server enabled')
} else {
  verifier = staticVerifier(env.MCP_STATIC_TOKENS!)
  log.warn('AUTH_MODE=static — development only')
}

app.get('/health', (_req, res) => void res.json({ ok: true }))

const auth = requireBearerAuth({ verifier, resourceMetadataUrl })
const mcpPath = publicUrl.pathname === '/' ? '/mcp' : publicUrl.pathname

app.post(mcpPath, auth, rateLimit(env.MCP_RATE_LIMIT_PER_MIN), async (req, res) => {
  const server = createMcpServer(deps, callerOf(req.auth))
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true })
  res.on('close', () => {
    void transport.close()
    void server.close()
  })
  try {
    await server.connect(transport)
    await transport.handleRequest(req, res, req.body)
  } catch (err) {
    log.error({ err }, 'mcp request failed')
    if (!res.headersSent) res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' }, id: null })
  }
})

// Stateless server: no SSE stream or session to resume/terminate.
app.all(mcpPath, (_req, res) => {
  res.set('Allow', 'POST').status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null })
})

const httpServer = app.listen(env.MCP_PORT, env.MCP_HOST, () => log.info({ url: env.MCP_PUBLIC_URL, port: env.MCP_PORT }, 'MCP server listening'))

for (const signal of ['SIGTERM', 'SIGINT'] as const) {
  process.on(signal, () => {
    httpServer.close(async () => {
      await close()
      process.exit(0)
    })
  })
}
