import 'dotenv/config'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import express from 'express'
import path from 'node:path'
import supertokens from 'supertokens-node'
import { errorHandler, middleware, type SessionRequest } from 'supertokens-node/framework/express'
import EmailPassword from 'supertokens-node/recipe/emailpassword'
import Session from 'supertokens-node/recipe/session'
import ThirdParty from 'supertokens-node/recipe/thirdparty'
import GithubProvider from 'supertokens-node/lib/build/recipe/thirdparty/providers/github'
import GoogleProvider from 'supertokens-node/lib/build/recipe/thirdparty/providers/google'
import { resolvePublicLaunchStatusSummary } from '../../src/shared/public-launch'
import { buildEmailPasswordApiOverrides } from './auth-session-payload'
import { probeSuperTokensCoreHealth } from './auth-health'
import { createBillingStateStore, type BillingPlan, type BillingRole } from './billing-state'
import { verifyPaddleWebhookSignature } from './paddle-webhook'
import { createReleaseManifestResponse, type ReleaseManifestViewer } from './release-manifest'
import { getRuntimeConfigDiagnostics } from './runtime-config'
import { buildAllowedOriginMatcher, createDesktopLinkStore } from './security'
import { resolveStaticWebsiteConfig } from './static-site'
import { registerStaticWebsiteRoutes } from './static-site-routes'
import { createSessionLikeFromTestPayload, readSignedTestSessionFromRequest, type SessionLike } from './test-session'

const Github = GithubProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GithubProvider>
const Google = GoogleProvider as unknown as (options: { clientId: string; clientSecret: string; scope?: string[] }) => ReturnType<typeof GoogleProvider>
type ThirdPartyProviderList = NonNullable<
  NonNullable<NonNullable<Parameters<typeof ThirdParty.init>[0]>['signInAndUpFeature']>['providers']
>

const PORT = Number(process.env.PORT || 3001)
const SUPERTOKENS_CORE_URI = process.env.SUPERTOKENS_CORE_URI || 'http://localhost:3567'
const API_DOMAIN = process.env.API_DOMAIN || `http://localhost:${PORT}`
const WEBSITE_DOMAIN = process.env.WEBSITE_DOMAIN || 'http://localhost:4273'
const API_BASE_PATH = process.env.API_BASE_PATH || '/auth'
const WEBSITE_BASE_PATH = process.env.WEBSITE_BASE_PATH || '/auth'
const COOKIE_SECURE = String(process.env.COOKIE_SECURE || '').toLowerCase() === 'true'
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''
const ORCID_CLIENT_ID = process.env.ORCID_CLIENT_ID || ''
const ORCID_CLIENT_SECRET = process.env.ORCID_CLIENT_SECRET || ''
const ORCID_IS_SANDBOX = String(process.env.ORCID_IS_SANDBOX || '').toLowerCase() === 'true'
const DEFAULT_PLAN = normalizeBillingPlan(process.env.DEFAULT_PLAN || 'free')
const DEFAULT_ROLE = normalizeBillingRole(process.env.DEFAULT_ROLE || 'operator')
const SUPERTOKENS_HEALTH_TIMEOUT_MS = Number(process.env.SUPERTOKENS_HEALTH_TIMEOUT_MS || 2_000)

const DESKTOP_LINK_TTL_MS = 10 * 60 * 1000
const DESKTOP_LINK_RATE_LIMIT_MAX = Number(process.env.DESKTOP_LINK_RATE_LIMIT_MAX || 5)
const DESKTOP_LINK_RATE_LIMIT_WINDOW_MS = Number(process.env.DESKTOP_LINK_RATE_LIMIT_WINDOW_MS || 15 * 60 * 1000)
const TEST_MODE_ENABLED = String(process.env.TEST_MODE || '').trim().toLowerCase() === 'testing'
const HYPERTWIST_TEST_SESSION_SECRET = process.env.HYPERTWIST_TEST_SESSION_SECRET || ''
const PADDLE_WEBHOOK_SECRET = process.env.PADDLE_WEBHOOK_SECRET || ''
const PADDLE_WEBHOOK_TOLERANCE_MS = Number(process.env.PADDLE_WEBHOOK_TOLERANCE_MS || 5_000)
const BILLING_STATE_PATH = process.env.BILLING_STATE_PATH || path.join(process.cwd(), 'data', 'hypertwist-billing-state.json')
const PADDLE_PRODUCT_PLAN_MAP = parseBillingPlanMap(process.env.PADDLE_PRODUCT_PLAN_MAP)
const PADDLE_PRICE_PLAN_MAP = parseBillingPlanMap(process.env.PADDLE_PRICE_PLAN_MAP)
const staticWebsiteConfig = resolveStaticWebsiteConfig({
  cwd: process.cwd(),
  websiteDistPath: process.env.WEBSITE_DIST_PATH,
  serveStaticWebsite: process.env.SERVE_STATIC_WEBSITE,
})
const runtimeConfigDiagnostics = getRuntimeConfigDiagnostics({
  apiDomain: API_DOMAIN,
  websiteDomain: WEBSITE_DOMAIN,
  superTokensCoreUri: SUPERTOKENS_CORE_URI,
  cookieSecure: COOKIE_SECURE,
  githubClientId: GITHUB_CLIENT_ID,
  githubClientSecret: GITHUB_CLIENT_SECRET,
  googleClientId: GOOGLE_CLIENT_ID,
  googleClientSecret: GOOGLE_CLIENT_SECRET,
  orcidClientId: ORCID_CLIENT_ID,
  orcidClientSecret: ORCID_CLIENT_SECRET,
})

function normalizeBillingPlan(value: string): BillingPlan {
  const plan = value.trim().toLowerCase()
  if (plan === 'operator' || plan === 'studio' || plan === 'enterprise' || plan === 'free') {
    return plan
  }
  if (plan === 'explorer') {
    return 'free'
  }
  return 'free'
}

function normalizeBillingRole(value: string): BillingRole {
  const role = value.trim().toLowerCase()
  if (role === 'viewer' || role === 'operator' || role === 'reviewer' || role === 'admin') {
    return role
  }
  return 'operator'
}

function parseBillingPlanMap(rawValue: string | undefined): Record<string, BillingPlan> {
  const trimmed = String(rawValue || '').trim()
  if (!trimmed) {
    return {}
  }

  try {
    const parsed = JSON.parse(trimmed) as Record<string, string>
    return Object.fromEntries(
      Object.entries(parsed)
        .filter(([key]) => String(key || '').trim().length > 0)
        .map(([key, value]) => [key, normalizeBillingPlan(String(value || ''))]),
    )
  } catch {
    console.warn('[billing] failed to parse plan map JSON; ignoring configured map')
    return {}
  }
}

function resolveAccountState(email: string, payload: Record<string, unknown>) {
  const entitlement = billingStateStore.getEntitlementByEmail(email)
  const plan = entitlement?.plan ?? normalizeBillingPlan(String(payload.plan || DEFAULT_PLAN))
  const role = entitlement?.role ?? normalizeBillingRole(String(payload.role || DEFAULT_ROLE))
  const canDownload = entitlement ? entitlement.canDownload : plan !== 'free'

  return {
    plan,
    role,
    canDownload,
    billing: {
      source: entitlement?.source || 'session',
      access_status: entitlement?.accessStatus || 'session-default',
      can_download: canDownload,
      subscription_id: entitlement?.subscriptionId || null,
      customer_id: entitlement?.customerId || null,
      transaction_id: entitlement?.transactionId || null,
      last_event_id: entitlement?.lastEventId || null,
      last_event_type: entitlement?.lastEventType || null,
      last_event_at: entitlement?.lastEventAt || null,
      updated_at: entitlement?.updatedAt || null,
      state_path: entitlement ? billingStateStore.getStatePath() : null,
    },
  }
}

function createAnonymousReleaseManifestViewer(): ReleaseManifestViewer {
  return {
    authenticated: false,
    canDownload: false,
    plan: null,
    role: null,
    accessStatus: null,
  }
}

async function getRequestSession(req: SessionRequest, res: express.Response): Promise<SessionLike | null> {
  if (TEST_MODE_ENABLED && HYPERTWIST_TEST_SESSION_SECRET.trim()) {
    const testPayload = readSignedTestSessionFromRequest(req, HYPERTWIST_TEST_SESSION_SECRET)
    if (testPayload) {
      return createSessionLikeFromTestPayload(testPayload)
    }
  }

  return (await Session.getSession(req, res, { sessionRequired: false })) ?? null
}

function createOrcidProvider() {
  const orcidBase = ORCID_IS_SANDBOX ? 'https://sandbox.orcid.org' : 'https://orcid.org'

  return {
    id: 'orcid',
    name: 'ORCID',
    get(redirectURI: string | undefined, authCodeFromRequest: string | undefined) {
      return {
        accessTokenAPI: {
          url: `${orcidBase}/oauth/token`,
          params: {
            client_id: ORCID_CLIENT_ID,
            client_secret: ORCID_CLIENT_SECRET,
            grant_type: 'authorization_code',
            redirect_uri: redirectURI || '',
            code: authCodeFromRequest || '',
          },
        },
        authorisationRedirect: {
          url: `${orcidBase}/oauth/authorize`,
          params: {
            client_id: ORCID_CLIENT_ID,
            response_type: 'code',
            scope: '/authenticate',
            redirect_uri: redirectURI || '',
          },
        },
        getUserInfo: async (tokenAPIResponse: {
          access_token?: string
          orcid?: string
          name?: string
          email?: string
        }) => {
          const orcidId = String(tokenAPIResponse.orcid || '').trim()
          const fallbackEmailLocalPart = orcidId.replace(/\W/g, '') || 'orcid-user'

          return {
            thirdPartyUserId: orcidId,
            email: {
              id: tokenAPIResponse.email
                ? String(tokenAPIResponse.email).trim().toLowerCase()
                : `${fallbackEmailLocalPart}@orcid.placeholder`,
              isVerified: true,
            },
            rawUserInfoFromProvider: {
              fromUserInfoAPI: {
                orcidId,
                name: String(tokenAPIResponse.name || '').trim(),
              },
              fromIdTokenPayload: {},
            },
          }
        },
      }
    },
  }
}

const recipeList = [
  EmailPassword.init({
    signUpFeature: {
      formFields: [
        { id: 'email' },
        { id: 'password' },
      ],
    },
    override: {
      apis: (originalImplementation) => buildEmailPasswordApiOverrides(originalImplementation, {
        defaultPlan: DEFAULT_PLAN,
        defaultRole: DEFAULT_ROLE,
      }),
    },
  }),
  Session.init({
    cookieSecure: COOKIE_SECURE,
    cookieSameSite: COOKIE_SECURE ? 'none' : 'lax',
  }),
]

const thirdPartyProviders: Array<ReturnType<typeof Github> | ReturnType<typeof Google> | ReturnType<typeof createOrcidProvider>> = []
if (GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET) {
  thirdPartyProviders.push(Github({
    clientId: GITHUB_CLIENT_ID,
    clientSecret: GITHUB_CLIENT_SECRET,
    scope: ['read:user', 'user:email'],
  }))
}
if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
  thirdPartyProviders.push(Google({
    clientId: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    scope: ['openid', 'email', 'profile'],
  }))
}
if (ORCID_CLIENT_ID && ORCID_CLIENT_SECRET) {
  thirdPartyProviders.push(createOrcidProvider())
}

if (thirdPartyProviders.length > 0) {
  recipeList.splice(1, 0, ThirdParty.init({
    signInAndUpFeature: {
      providers: thirdPartyProviders as ThirdPartyProviderList,
    },
    override: {
      apis: (originalImplementation) => ({
        ...originalImplementation,
        signInUpPOST: async (input) => {
          const response = await originalImplementation.signInUpPOST!(input)
          if (response.status === 'OK') {
            await response.session.mergeIntoAccessTokenPayload({
              email: response.user.emails[0] || '',
              plan: DEFAULT_PLAN,
              role: DEFAULT_ROLE,
            })
          }
          return response
        },
      }),
    },
  }))
}

supertokens.init({
  framework: 'express',
  supertokens: {
    connectionURI: SUPERTOKENS_CORE_URI,
  },
  appInfo: {
    appName: 'HyperTwist',
    apiDomain: API_DOMAIN,
    websiteDomain: WEBSITE_DOMAIN,
    apiBasePath: API_BASE_PATH,
    websiteBasePath: WEBSITE_BASE_PATH,
  },
  recipeList,
})

const matchesAllowedOrigin = buildAllowedOriginMatcher(WEBSITE_DOMAIN)
const desktopLinkStore = createDesktopLinkStore({
  ttlMs: DESKTOP_LINK_TTL_MS,
  rateLimitMax: DESKTOP_LINK_RATE_LIMIT_MAX,
  rateLimitWindowMs: DESKTOP_LINK_RATE_LIMIT_WINDOW_MS,
})
const billingStateStore = createBillingStateStore({
  statePath: BILLING_STATE_PATH,
  defaultPlan: DEFAULT_PLAN,
  defaultRole: DEFAULT_ROLE,
  productPlanMap: PADDLE_PRODUCT_PLAN_MAP,
  pricePlanMap: PADDLE_PRICE_PLAN_MAP,
})

setInterval(() => {
  desktopLinkStore.cleanupExpiredTokens()
}, 60_000).unref()

const app = express()

app.post('/api/billing/paddle/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = Buffer.isBuffer(req.body) ? req.body.toString() : ''
  const verificationResult = verifyPaddleWebhookSignature({
    rawBody,
    signatureHeader: req.headers['paddle-signature'],
    secretKey: PADDLE_WEBHOOK_SECRET,
    toleranceMs: PADDLE_WEBHOOK_TOLERANCE_MS,
  })

  if (!verificationResult.ok) {
    const statusCode = verificationResult.error === 'missing_secret'
      ? 503
      : verificationResult.error === 'expired_signature'
        ? 408
        : 400
    console.warn('[billing] rejected paddle webhook', {
      error: verificationResult.error,
      bodyBytes: Buffer.byteLength(rawBody),
    })
    return res.status(statusCode).json({ error: verificationResult.error })
  }

  const applyResult = billingStateStore.applyVerifiedPaddleEvent(rawBody)
  console.log('[billing] verified paddle webhook received', {
    bodyBytes: Buffer.byteLength(rawBody),
    timestamp: verificationResult.timestamp,
    applyResult,
  })
  return res.json({ received: true, verified: true, apply_result: applyResult })
})

app.use(cors({
  origin(origin, callback) {
    if (!origin || matchesAllowedOrigin(origin)) {
      callback(null, true)
      return
    }
    callback(new Error('forbidden_origin'))
  },
  credentials: true,
  allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
}))

app.use(express.json())
app.use(cookieParser())
app.use(middleware())

app.get('/health', (_req, res) => {
  res.json({ ok: true, service: 'hypertwist-auth-server' })
})

app.get('/api/auth/health', async (_req, res) => {
  const superTokensHealth = await probeSuperTokensCoreHealth({
    connectionUri: SUPERTOKENS_CORE_URI,
    apiDomain: API_DOMAIN,
    websiteDomain: WEBSITE_DOMAIN,
    timeoutMs: SUPERTOKENS_HEALTH_TIMEOUT_MS,
  })

  const billing = {
    statePath: billingStateStore.getStatePath(),
    processedEventCount: billingStateStore.getProcessedEventCount(),
    pricePlanMapConfigured: Object.keys(PADDLE_PRICE_PLAN_MAP).length > 0,
    productPlanMapConfigured: Object.keys(PADDLE_PRODUCT_PLAN_MAP).length > 0,
    webhookSecretConfigured: Boolean(PADDLE_WEBHOOK_SECRET),
  }
  const runtime = {
    mode: runtimeConfigDiagnostics.mode,
    public_origin_ready: runtimeConfigDiagnostics.publicOriginReady,
    cookie_secure: runtimeConfigDiagnostics.cookieSecure,
    api_domain: runtimeConfigDiagnostics.apiDomain,
    website_domain: runtimeConfigDiagnostics.websiteDomain,
    warnings: runtimeConfigDiagnostics.warnings,
    errors: runtimeConfigDiagnostics.errors,
  }
  const publicManifest = createReleaseManifestResponse(createAnonymousReleaseManifestViewer()).manifest
  const launch = resolvePublicLaunchStatusSummary({
    manifest: publicManifest,
    health: {
      billing,
      runtime,
    },
  })

  res.json({
    ok: true,
    service: 'hypertwist-auth-server',
    supertokens: {
      configured: superTokensHealth.configured,
      reachable: superTokensHealth.reachable,
      ready: superTokensHealth.ready,
      apiVersion: superTokensHealth.apiVersion,
      error: superTokensHealth.error,
      oauth: {
        github: Boolean(GITHUB_CLIENT_ID && GITHUB_CLIENT_SECRET),
        google: Boolean(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET),
        orcid: Boolean(ORCID_CLIENT_ID && ORCID_CLIENT_SECRET),
      },
    },
    fallback: {
      enabled: true,
      active: !superTokensHealth.ready,
      reason: superTokensHealth.ready ? null : (superTokensHealth.error || 'not_ready'),
    },
    billing,
    runtime,
    launch,
  })
})

app.get('/api/releases/manifest', async (req: SessionRequest, res) => {
  let viewer = createAnonymousReleaseManifestViewer()

  const session = await getRequestSession(req, res)
  if (session) {
    const payload = session.getAccessTokenPayload()
    const email = String(payload.email || '')

    viewer = {
      authenticated: true,
      canDownload: false,
      plan: String(payload.plan || '') || null,
      role: String(payload.role || '') || null,
      accessStatus: null,
    }

    if (email) {
      const resolvedAccount = resolveAccountState(email, payload)
      if (payload.plan !== resolvedAccount.plan || payload.role !== resolvedAccount.role) {
        await session.mergeIntoAccessTokenPayload({
          plan: resolvedAccount.plan,
          role: resolvedAccount.role,
        })
      }

      viewer = {
        authenticated: true,
        canDownload: resolvedAccount.canDownload,
        plan: resolvedAccount.plan,
        role: resolvedAccount.role,
        accessStatus: resolvedAccount.billing.access_status,
      }
    }
  }

  return res.json(createReleaseManifestResponse(viewer))
})

app.get('/api/auth/me', async (req: SessionRequest, res) => {
  const session = await getRequestSession(req, res)
  if (!session) {
    return res.status(401).json({ error: 'unauthorized' })
  }

  const payload = session.getAccessTokenPayload()
  const email = String(payload.email || '')
  if (!email) {
    return res.status(401).json({ error: 'session_missing_email' })
  }

  const resolvedAccount = resolveAccountState(email, payload)
  if (payload.plan !== resolvedAccount.plan || payload.role !== resolvedAccount.role) {
    await session.mergeIntoAccessTokenPayload({
      plan: resolvedAccount.plan,
      role: resolvedAccount.role,
    })
  }

  return res.json({
    user: {
      id: session.getUserId(),
      email,
      name: payload.name || email.split('@')[0],
      plan: resolvedAccount.plan,
      role: resolvedAccount.role,
      can_download: resolvedAccount.canDownload,
      billing: resolvedAccount.billing,
      auth_method: 'supertokens',
    },
    mode: 'supertokens',
  })
})

app.post('/api/auth/logout', async (req: SessionRequest, res) => {
  const session = await getRequestSession(req, res)
  if (session) {
    await session.revokeSession()
  }
  return res.json({ ok: true })
})

app.post('/api/auth/desktop-link', async (req: SessionRequest, res) => {
  const origin = String(req.headers.origin || req.headers.referer || '')
  if (!matchesAllowedOrigin(origin)) {
    return res.status(403).json({ error: 'forbidden_origin' })
  }

  const session = await getRequestSession(req, res)
  if (!session) {
    return res.status(401).json({ error: 'unauthorized' })
  }

  const payload = session.getAccessTokenPayload()
  const email = String(payload.email || '')
  if (!email) {
    return res.status(401).json({ error: 'session_missing_email' })
  }
  const resolvedAccount = resolveAccountState(email, payload)

  const issueResult = desktopLinkStore.issueToken({
    rateLimitKey: `desktop-link:${session.getUserId()}`,
    userId: session.getUserId(),
    email,
    plan: resolvedAccount.plan,
    role: resolvedAccount.role,
    canDownload: resolvedAccount.canDownload,
    accessStatus: resolvedAccount.billing.access_status,
  })
  if (!issueResult.ok) {
    return res.status(429).json({ error: issueResult.error })
  }

  return res.json({ ok: true, token: issueResult.token, expires_at: issueResult.expiresAt })
})

app.get('/api/auth/desktop-link/verify', (req, res) => {
  const token = String(req.query.token || '')
  const record = desktopLinkStore.verifyAndConsumeToken(token)
  if (!record) {
    return res.status(404).json({ error: 'invalid_token' })
  }

  return res.json({
    ok: true,
    email: record.email,
    plan: record.plan,
    role: record.role,
    can_download: record.canDownload,
    access_status: record.accessStatus,
  })
})

app.use(errorHandler())
registerStaticWebsiteRoutes(app, staticWebsiteConfig)

app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
  console.error('[hypertwist-auth-server] unhandled error', error)
  res.status(500).json({ error: 'internal_server_error' })
})

app.listen(PORT, () => {
  console.log(`HyperTwist auth server listening on ${API_DOMAIN}`)
  console.log(`Frontend origin: ${WEBSITE_DOMAIN}`)
  console.log(`SuperTokens core: ${SUPERTOKENS_CORE_URI}`)
  if (staticWebsiteConfig.enabled) {
    console.log(`[hypertwist-auth-server] serving website build from ${staticWebsiteConfig.distPath}`)
  } else if (staticWebsiteConfig.reason === 'forced_missing') {
    console.warn(`[hypertwist-auth-server] SERVE_STATIC_WEBSITE requested, but ${staticWebsiteConfig.indexHtmlPath} is missing`)
  } else {
    console.log(`[hypertwist-auth-server] no built website bundle detected at ${staticWebsiteConfig.indexHtmlPath}; api-only mode remains active`)
  }
  if (runtimeConfigDiagnostics.errors.length > 0 || runtimeConfigDiagnostics.warnings.length > 0) {
    console.warn('[hypertwist-auth-server] runtime configuration diagnostics', runtimeConfigDiagnostics)
  }
})
