import { eq, sql } from 'drizzle-orm'
import { Hono, type Context, type MiddlewareHandler } from 'hono'

import { requireAuth, type AuthPrincipal, type RequireAuthOptions } from '../mw/auth.js'
import { requirePluginAccess } from '../mw/access.js'
import { fail, ok } from '../http.js'
import { getDb } from '../db.js'
import { debitCredits, periodKey } from '../lib/wallet.js'
import { walletBalances } from '../schema.js'
import type { PressZoneBindings } from '../config.js'

type PluginVariables = {
  account?: string
  capabilities: string[]
  principal: AuthPrincipal
}

type PluginEnv<TBindings extends object = PressZoneBindings> = {
  Bindings: TBindings
  Variables: PluginVariables
}

type PluginContext<TBindings extends object> = Context<PluginEnv<TBindings>>
type WalletDb = Parameters<typeof debitCredits>[0]

export interface CreatePluginRouteOptions<
  TBindings extends object,
> {
  access: MiddlewareHandler<PluginEnv<TBindings>>
  auth: MiddlewareHandler<PluginEnv<TBindings>>
  cost: bigint
  currentPeriod: () => string
  execute: (context: PluginContext<TBindings>) => Promise<unknown> | unknown
  getDb?: (env: TBindings) => WalletDb
  plugin: string
}

const bindAccount: MiddlewareHandler<PluginEnv> = async (context, next) => {
  context.set('account', context.get('principal').account)
  await next()
}

async function refundCredits(db: WalletDb, key: string, amount: bigint): Promise<void> {
  await db
    .update(walletBalances)
    .set({
      balance: sql`${walletBalances.balance} + ${amount}`,
      updatedAt: new Date(),
    })
    .where(eq(walletBalances.ownerId, key))
}

export function createPluginGuards<
  TBindings extends object = PressZoneBindings,
  S extends Record<string, unknown> = Record<string, never>,
>(options: {
  auth: RequireAuthOptions<TBindings, S>
  entitlement: string
  permission: string
}): [
  MiddlewareHandler<PluginEnv<TBindings>>,
  MiddlewareHandler<PluginEnv<TBindings>>,
  MiddlewareHandler<PluginEnv<TBindings>>,
] {
  return [
    requireAuth(options.auth) as unknown as MiddlewareHandler<PluginEnv<TBindings>>,
    bindAccount as unknown as MiddlewareHandler<PluginEnv<TBindings>>,
    requirePluginAccess(options.entitlement, options.permission) as unknown as MiddlewareHandler<
      PluginEnv<TBindings>
    >,
  ]
}

export function createPluginRoute<TBindings extends object = PressZoneBindings>(
  options: CreatePluginRouteOptions<TBindings>,
) {
  const app = new Hono<PluginEnv<TBindings>>()
  const resolveDb = options.getDb ?? ((env: TBindings) => getDb(env as never) as WalletDb)

  app.use('*', options.auth)
  app.use('*', bindAccount as unknown as MiddlewareHandler<PluginEnv<TBindings>>)
  app.use('*', options.access)
  app.post(`/api/plugin/${options.plugin}`, async (context) => {
    const principal = context.get('principal')
    const walletKey = periodKey(principal.account, options.plugin, options.currentPeriod())
    const db = resolveDb(context.env)
    const debit = await debitCredits(db, walletKey, options.cost)

    if (!debit.ok) {
      return fail('INSUFFICIENT_CREDITS', 'Insufficient credits', 402)
    }

    try {
      const data = await options.execute(context)
      return ok(data)
    } catch (error) {
      await refundCredits(db, walletKey, options.cost)
      throw error
    }
  })

  return app
}
