import 'dotenv/config'
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { createNodeWebSocket } from '@hono/node-ws'
import { getConnInfo } from '@hono/node-server/conninfo'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { showRoutes } from 'hono/dev'
import { auth } from './lib/auth.ts'
import { createOcppHandler } from './ocpp/handler.ts'
import { verifyOcppPassword } from './lib/ocpp-auth.ts'
import { useDrizzle } from './lib/db.ts'
import { chargePoint } from './db/schema.ts'
import { eq } from 'drizzle-orm'
import statsRoutes from './routes/stats.ts'
import statsChartRoutes from './routes/stats-chart.ts'
import chargePointRoutes from './routes/charge-points.ts'
import transactionRoutes from './routes/transactions.ts'
import idTagRoutes from './routes/id-tags.ts'
import userRoutes from './routes/users.ts'
import setupRoutes from './routes/setup.ts'
import tariffRoutes from './routes/tariff.ts'
import settingsRoutes from './routes/settings.ts'

import type { HonoEnv } from './types/hono.ts'

const app = new Hono<HonoEnv>()

const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })

app.use(logger())

app.use('*', async (c, next) => {
  const session = await auth.api.getSession({ headers: c.req.raw.headers })
  if (!session) {
    c.set('user', null)
    c.set('session', null)
    await next()
    return
  }
  c.set('user', session.user)
  c.set('session', session.session)
  await next()
})

app.use(
  '/api/*',
  cors({
    origin: process.env.WEB_ORIGIN ?? '*',
    allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    allowHeaders: ['Content-Type', 'Authorization'],
    exposeHeaders: ['Content-Length'],
    credentials: true,
  }),
)

app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw))

// REST API routes
app.route('/api/stats', statsRoutes)
app.route('/api/stats/chart', statsChartRoutes)
app.route('/api/charge-points', chargePointRoutes)
app.route('/api/transactions', transactionRoutes)
app.route('/api/id-tags', idTagRoutes)
app.route('/api/users', userRoutes)
app.route('/api/setup', setupRoutes)
app.route('/api/tariff', tariffRoutes)
app.route('/api/settings', settingsRoutes)

app.get('/api', (c) => {
  const user = c.get('user')
  const session = c.get('session')

  const payload = {
    platform: 'Helios CSMS',
    message: 'ok',
  }

  if (user) {
    Object.assign(payload, { user })
  }

  if (session) {
    Object.assign(payload, { session })
  }

  return c.json(payload)
})

app.get(
  '/ocpp/:chargePointId',
  async (c, next) => {
    const chargePointId = c.req.param('chargePointId')
    const authHeader = c.req.header('Authorization')

    if (!authHeader?.startsWith('Basic ')) {
      c.header('WWW-Authenticate', 'Basic realm="OCPP"')
      return c.json({ error: 'Unauthorized' }, 401)
    }

    let id: string, password: string
    try {
      const decoded = atob(authHeader.slice(6))
      const colonIdx = decoded.indexOf(':')
      if (colonIdx === -1) throw new Error('Invalid format')
      id = decoded.slice(0, colonIdx)
      password = decoded.slice(colonIdx + 1)
    } catch {
      return c.json({ error: 'Invalid Authorization header' }, 400)
    }

    if (id !== chargePointId) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const db = useDrizzle()
    const [cp] = await db
      .select({ passwordHash: chargePoint.passwordHash })
      .from(chargePoint)
      .where(eq(chargePoint.chargePointIdentifier, chargePointId))
      .limit(1)

    if (!cp?.passwordHash || !(await verifyOcppPassword(password, cp.passwordHash))) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    await next()
  },
  upgradeWebSocket((c) => {
    const chargePointId = c.req.param('chargePointId')
    const connInfo = getConnInfo(c)
    return createOcppHandler(chargePointId, connInfo.remote.address)
  }),
)

showRoutes(app, {
  verbose: true,
})

const server = serve(
  {
    fetch: app.fetch,
    port: 3001,
  },
  (info) => {
    console.log(`Server is running on http://localhost:${info.port}`)
  },
)

injectWebSocket(server)
