import http from 'http'
import { Server as SocketIOServer } from 'socket.io'
import type { Socket } from 'socket.io'
import cron from 'node-cron'
import { redis } from './lib/redis'
import { prisma } from './lib/prisma'
import { assertStorageConfiguration } from './lib/storage'
import { createApp, corsOrigins } from './app'
import { verifyAnyActorToken } from './security/tokens'
import { getSessionCookieName } from './security/sessionCookies'
import { sendNotification } from './services/notificationService'
import {
  runTrialExpirationJob,
  runPaymentPendingTimeoutJob,
  runPastDueTimeoutJob,
  runSuspensionTimeoutJob,
  runPeriodEndCancellationJob,
} from './modules/subscriptions/subscription.service'

const app    = createApp()
const server = http.createServer(app)
assertStorageConfiguration()

// ─── Socket.io ────────────────────────────────────────────────
const io = new SocketIOServer(server, {
  cors: { origin: corsOrigins, credentials: true, methods: ['GET', 'POST'] },
})


function readCookieFromHeader(cookieHeader: string | undefined, name: string): string | null {
  if (!cookieHeader) return null

  for (const chunk of cookieHeader.split(';')) {
    const [rawName, ...rawValue] = chunk.trim().split('=')
    if (rawName === name) return decodeURIComponent(rawValue.join('='))
  }

  return null
}

function getSocketSessionToken(socket: Socket): string | undefined {
  const explicitToken = socket.handshake.auth?.token
  if (typeof explicitToken === 'string' && explicitToken.trim()) return explicitToken.trim()

  const cookieHeader = socket.request.headers.cookie
  return (
    readCookieFromHeader(cookieHeader, getSessionCookieName('employee')) ??
    readCookieFromHeader(cookieHeader, getSessionCookieName('admin')) ??
    readCookieFromHeader(cookieHeader, getSessionCookieName('renter')) ??
    undefined
  )
}

// Authenticate socket connections via JWT before joining user rooms
io.use((socket, next) => {
  const token = getSocketSessionToken(socket)
  if (!token) return next() // unauthenticated connections allowed; they just don't join rooms
  try {
    const payload = verifyAnyActorToken(token)
    ;(socket as any).authenticatedUserId = payload.sub
    next()
  } catch {
    next(new Error('invalid_token'))
  }
})

io.on('connection', (socket) => {
  const userId = (socket as any).authenticatedUserId as string | undefined
  if (userId) {
    socket.join(`user:${userId}`)
  }
})

// Redis pub/sub → broadcast to connected clients
const subscriber = redis.duplicate()
subscriber.psubscribe('notifications:*', (err) => {
  if (err) console.error('[Redis] Subscribe error:', err)
})
subscriber.on('pmessage', (_pattern, channel, message) => {
  try {
    const parsed = JSON.parse(message)
    const userId = channel.replace('notifications:', '')
    io.to(`user:${userId}`).emit('notification', parsed)
  } catch (err) {
    console.error('[Redis] Invalid notification message:', err)
  }
})

// ─── Scheduled jobs ───────────────────────────────────────────

// Daily: flag expiring/expired licenses
cron.schedule('0 8 * * *', async () => {
  const customers = await prisma.customer.findMany({ where: { licenseExpiry: { not: null } } })
  for (const c of customers) {
    if (!c.licenseExpiry) continue
    const daysLeft = Math.ceil((c.licenseExpiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
    const expired = c.licenseExpiry <= new Date()
    const expiring = !expired && daysLeft < 90
    if (expired !== c.licenseExpired || expiring !== c.licenseExpiringSoon) {
      await prisma.customer.update({ where: { id: c.id }, data: { licenseExpired: expired, licenseExpiringSoon: expiring, licenseValidationStatus: expired ? 'EXPIRED' : expiring ? 'EXPIRING' : 'VALID' } })
    }
  }
})

// Hourly: expire trials that ended without payment
cron.schedule('0 * * * *', async () => {
  const n = await runTrialExpirationJob()
  if (n > 0) console.log(`[subscription] trial_expiration: ${n} expired`)
})

// Hourly: payment_pending → past_due after 7 days
cron.schedule('15 * * * *', async () => {
  const n = await runPaymentPendingTimeoutJob()
  if (n > 0) console.log(`[subscription] payment_pending_timeout: ${n} moved to past_due`)
})

// Hourly: past_due → suspended after 7 days
cron.schedule('30 * * * *', async () => {
  const n = await runPastDueTimeoutJob()
  if (n > 0) console.log(`[subscription] past_due_timeout: ${n} suspended`)
})

// Daily: suspended → cancelled after 16 days; and period-end cancellations
cron.schedule('0 1 * * *', async () => {
  const nSuspend = await runSuspensionTimeoutJob()
  const nPeriod  = await runPeriodEndCancellationJob()
  if (nSuspend > 0) console.log(`[subscription] suspension_timeout: ${nSuspend} cancelled`)
  if (nPeriod > 0)  console.log(`[subscription] period_end_cancel: ${nPeriod} cancelled`)
})

// Daily: send trial-ending reminders (3 days before trial end)
cron.schedule('0 9 * * *', async () => {
  const soon = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000)
  const subscriptions = await prisma.subscription.findMany({
    where: { status: 'TRIALING', trialEndAt: { lte: soon, gte: new Date() } },
    include: { company: { include: { employees: { where: { role: 'OWNER' } } } } },
  })
  for (const sub of subscriptions) {
    const owner = sub.company.employees[0]
    if (owner) {
      await sendNotification({
        type: 'SUBSCRIPTION_TRIAL_ENDING',
        companyId: sub.companyId,
        employeeId: owner.id,
        channels: ['IN_APP'],
        templateKey: 'subscription.trial_ending',
        templateVariables: {
          trialEndDate: sub.trialEndAt ?? new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
        },
      }).catch((err) => {
        console.error('[Notifications] Failed to create trial ending reminder:', err?.message ?? String(err))
      })
    }
  }
})

// Daily: notify companies about upcoming and overdue vehicle maintenance (date- and odometer-based).
// Repeats every day until the owner logs a new service entry that pushes the due date/mileage into the future.
cron.schedule('0 8 * * *', async () => {
  const now = new Date()
  const in30Days = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)
  const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)

  // Fetch all candidate logs (date-due or has an odometer target), ordered newest-first per vehicle+type.
  // We keep only the LATEST log per vehicle+type so that once the owner logs a new service the
  // old overdue log is superseded and notifications stop automatically.
  const allCandidates = await prisma.maintenanceLog.findMany({
    where: {
      OR: [
        { nextDueAt: { lte: in30Days } },
        { nextDueMileage: { not: null } },
      ],
    },
    include: { vehicle: { include: { company: { include: { employees: { where: { role: { in: ['OWNER', 'MANAGER'] }, isActive: true }, take: 1 } } } } } },
    orderBy: { performedAt: 'desc' },
  })

  // Keep only the most-recent log per vehicle+type combination
  const latestByKey = new Map<string, typeof allCandidates[number]>()
  for (const log of allCandidates) {
    const key = `${log.vehicleId}:${log.type}`
    if (!latestByKey.has(key)) latestByKey.set(key, log)
  }

  for (const log of latestByKey.values()) {
    const vehicle = log.vehicle
    const company = vehicle.company
    const recipient = company.employees[0]
    if (!recipient) continue

    // Determine date-based urgency
    let isOverdueByDate = false
    let daysLeft: number | null = null
    let dueSoonByDate = false
    if (log.nextDueAt) {
      daysLeft = Math.ceil((log.nextDueAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
      isOverdueByDate = log.nextDueAt <= now
      dueSoonByDate = !isOverdueByDate && daysLeft <= 30
    }

    // Determine odometer-based urgency
    let isOverdueByOdometer = false
    let kmLeft: number | null = null
    let dueSoonByOdometer = false
    if (log.nextDueMileage != null && vehicle.mileage != null) {
      kmLeft = log.nextDueMileage - vehicle.mileage
      isOverdueByOdometer = kmLeft <= 0
      dueSoonByOdometer = !isOverdueByOdometer && kmLeft <= 500
    }

    // Skip if the latest log is no longer due (owner has updated it)
    const isOverdue = isOverdueByDate || isOverdueByOdometer
    const isDueSoon = !isOverdue && (dueSoonByDate || dueSoonByOdometer)
    if (!isOverdue && !isDueSoon) continue

    // Dedup: don't send more than once per day for the same log
    const alreadySent = await prisma.notification.findFirst({
      where: {
        type: 'VEHICLE_MAINTENANCE_DUE',
        companyId: company.id,
        createdAt: { gte: oneDayAgo },
        data: { path: ['maintenanceLogId'], equals: log.id },
      },
    })
    if (alreadySent) continue

    // Build human-readable description
    const dueParts: string[] = []
    if (isOverdueByDate) dueParts.push(`overdue since ${log.nextDueAt!.toLocaleDateString()}`)
    else if (dueSoonByDate && daysLeft != null) dueParts.push(`due in ${daysLeft} day${daysLeft === 1 ? '' : 's'}`)
    if (isOverdueByOdometer) dueParts.push(`overdue by odometer (${Math.abs(kmLeft!).toLocaleString()} km ago)`)
    else if (dueSoonByOdometer && kmLeft != null) dueParts.push(`${kmLeft.toLocaleString()} km remaining`)

    const title = isOverdue
      ? `Overdue: ${log.type} — ${vehicle.make} ${vehicle.model}`
      : `${log.type} due soon — ${vehicle.make} ${vehicle.model}`
    const body = `${log.type} for ${vehicle.make} ${vehicle.model} (${vehicle.licensePlate}): ${dueParts.join('; ')}. Please log the service to dismiss this reminder.`

    await prisma.notification.create({
      data: {
        type: 'VEHICLE_MAINTENANCE_DUE',
        title,
        body,
        data: {
          vehicleId: vehicle.id,
          maintenanceLogId: log.id,
          maintenanceType: log.type,
          isOverdue,
          daysLeft,
          kmLeft,
          isOverdueByDate,
          isOverdueByOdometer,
        },
        companyId: company.id,
        employeeId: recipient.id,
        channel: 'IN_APP',
        status: 'DELIVERED',
      },
    })
  }
})

// ─── Start ────────────────────────────────────────────────────
const PORT = Number(process.env.API_PORT ?? 4000)
const HOST = process.env.API_HOST ?? '0.0.0.0'

server.listen(PORT, HOST, () => {
  console.log(`[API] Server running on ${HOST}:${PORT}`)
})

export { app, io }
