import { and, eq, sql } from 'drizzle-orm'
import { Hono, type MiddlewareHandler } from 'hono'
import { listAudit, logAudit, type AuditEvent, type ListAuditFilter } from '@platform-modules/audit'
import type { VerifiedApiKey } from '@platform-modules/auth/api-keys'
import { createCustomEngine } from '@platform-modules/auth/engine-custom'
import type { Querier, TransactionalDatabase } from '@platform-modules/db'
import { createEntitlements } from '@platform-modules/entitlements'
import type { InvoiceDocumentResult } from '@platform-modules/invoicing'
import { MorningProvider, type MorningCredentials } from '@platform-modules/invoicing/morning'
import { PaypalProvider, type PaypalCreds } from '@platform-modules/billing/paypal'
import { createPluginGuards, createPluginRoute } from './routes/plugin.js'
import { createSubscribeRoute } from './routes/subscribe.js'
import { createWebhookRoute } from './routes/webhooks.js'
import { createAccountRoute } from './routes/account.js'
import { createStaffRoute } from './routes/staff.js'
import { createOauthRoute } from './routes/oauth.js'
import { createSessionRoute } from './routes/session.js'
import type { PressZoneBindings, QueueBinding } from './config.js'
import { loadConfig } from './config.js'
import { getDb, type DbEnv } from './db.js'
import { AppError } from './errors.js'
import { fail, onError } from './http.js'
import { issueSettlementInvoice } from './lib/billing-doc.js'
import { createRateLimit } from './mw/rate-limit.js'
import { requireAuth } from './mw/auth.js'
import { healthRoute } from './routes/health.js'
import {
  chargeIntents,
  invoiceDocument,
  ledgerEntries,
  members,
  pressZoneSchema,
  plugins,
  refundReservations,
  subscriptionPackages,
  subscriptions,
  walletBalances,
} from './schema.js'
import { type QueueJob, runCron } from './cron.js'
import { consumeQueue } from './queue.js'

export interface ExecutionContextLike {
  waitUntil(promise: Promise<unknown>): void
}

export interface ScheduledEventLike {
  cron: string
  scheduledTime: number
}

export interface QueueMessageLike<T = unknown> {
  body: T
}

export interface QueueBatchLike<T = unknown> {
  messages: Array<QueueMessageLike<T>>
  queue?: QueueBinding<T>
}

type AppContext = {
  Bindings: PressZoneBindings
}

type RuntimeDb = Querier<typeof pressZoneSchema> &
  TransactionalDatabase<typeof pressZoneSchema>

type RuntimeBindings = PressZoneBindings & {
  MORNING_API_PASS?: string
  MORNING_API_USER?: string
  MORNING_COMPANY_ID?: string
  PAYPAL_CLIENT_ID?: string
  PAYPAL_CLIENT_SECRET?: string
  PAYPAL_WEBHOOK_ID?: string
}

type MountedRoute = Hono<any, any, any>

export interface CreateAppOptions {
  oauthRoute?: MountedRoute
  webhookRoute?: MountedRoute
  subscribeRoute?: MountedRoute
  pluginRoute?: MountedRoute
  accountRoute?: MountedRoute
  staffRoute?: MountedRoute
  sessionRoute?: MountedRoute
  protectedMiddleware?: MiddlewareHandler<any>
}

function createBaseApp() {
  const app = new Hono<AppContext>()

  app.onError(onError)
  app.notFound(() => fail('NOT_FOUND', 'Route not found', 404))
  app.use(
    '*',
    createRateLimit<PressZoneBindings>({
      namespace: (env) => env?.RATE_LIMIT_KV,
      limit: 60,
      windowMs: 60_000,
      key: (context) =>
        context.req.header('cf-connecting-ip') ??
        context.req.header('x-forwarded-for') ??
        'anonymous',
    }),
  )

  return app
}

export function createApp(options: CreateAppOptions = {}) {
  const app = createBaseApp()

  app.route('', healthRoute)

  if (options.oauthRoute) {
    app.route('', options.oauthRoute)
  }

  if (options.webhookRoute) {
    app.route('/api', options.webhookRoute)
  }

  if (options.pluginRoute) {
    app.route('/api', options.pluginRoute)
  }

  if (options.sessionRoute) {
    app.route('/api/account', options.sessionRoute)
  }

  if (
    options.protectedMiddleware &&
    (options.subscribeRoute || options.accountRoute || options.staffRoute)
  ) {
    const protectedGroup = new Hono<AppContext>()
    protectedGroup.use('*', options.protectedMiddleware)

    if (options.subscribeRoute) {
      protectedGroup.route('/subscribe', options.subscribeRoute)
    }

    if (options.accountRoute) {
      protectedGroup.route('/account', options.accountRoute)
    }

    if (options.staffRoute) {
      protectedGroup.route('/staff', options.staffRoute)
    }

    app.route('/api', protectedGroup)
  }

  return app
}

function requireSecret(value: string | undefined, name: string): string {
  if (!value) {
    throw new Error(`Missing required binding: ${name}`)
  }

  return value
}

function createMorningCredential(env: RuntimeBindings): MorningCredentials {
  return {
    apiUser: requireSecret(env.MORNING_API_USER, 'MORNING_API_USER'),
    apiPass: requireSecret(env.MORNING_API_PASS, 'MORNING_API_PASS'),
    companyId: requireSecret(env.MORNING_COMPANY_ID, 'MORNING_COMPANY_ID'),
  }
}

function createPaypalProvider(env: RuntimeBindings): PaypalProvider {
  return new PaypalProvider({
    clientId: requireSecret(env.PAYPAL_CLIENT_ID, 'PAYPAL_CLIENT_ID'),
    clientSecret: requireSecret(env.PAYPAL_CLIENT_SECRET, 'PAYPAL_CLIENT_SECRET'),
    webhookId: requireSecret(env.PAYPAL_WEBHOOK_ID, 'PAYPAL_WEBHOOK_ID'),
  } satisfies PaypalCreds)
}

function createProtectedMiddleware(
  db: RuntimeDb,
  authEngine: ReturnType<typeof createCustomEngine>,
): MiddlewareHandler {
  return requireAuth({
    authEngine,
    db,
    resolveApiKeyPrincipal: (verified) => resolveApiKeyPrincipal(db, verified),
    resolveSessionAccount: (principal) => resolveSessionAccount(db, principal.userId),
    tenancy: {
      resolveCapabilities: (userId, tenantId) => resolveCapabilities(db, userId, tenantId),
    },
  })
}

async function resolveSessionAccount(db: RuntimeDb, userId: string): Promise<string | null> {
  const memberships = await db
    .select({ accountId: members.accountId })
    .from(members)
    .where(and(eq(members.userId, userId), eq(members.status, 'active')))
    .limit(2)

  return memberships.length === 1 ? memberships[0]?.accountId ?? null : null
}

async function resolveCapabilities(
  db: RuntimeDb,
  userId: string,
  tenantId: string,
): Promise<string[]> {
  const [membership] = await db
    .select({
      roleKey: members.roleKey,
      status: members.status,
    })
    .from(members)
    .where(and(eq(members.accountId, tenantId), eq(members.userId, userId)))
    .limit(1)

  if (!membership || membership.status !== 'active') {
    return []
  }

  // v1 has no per-role permission grant — any active member gets every
  // permission the account's entitled plugins expose. Entitlement (does the
  // account hold the plugin) stays the separate, real access gate.
  const pluginRows = await db.select({ permissionCatalog: plugins.permissionCatalog }).from(plugins)
  return pluginRows.flatMap((row) => row.permissionCatalog)
}

async function resolveApiKeyPrincipal(
  db: RuntimeDb,
  verified: VerifiedApiKey,
): Promise<{ account: string; userId: string; scopes: string[] } | null> {
  const memberships = await db
    .select({
      accountId: members.accountId,
    })
    .from(members)
    .where(and(eq(members.userId, verified.owner), eq(members.status, 'active')))
    .limit(2)

  const membership = memberships[0]
  if (memberships.length !== 1 || !membership) {
    return null
  }

  return {
    account: membership.accountId,
    userId: verified.owner,
    scopes: [...verified.scopes],
  }
}

async function setMemberRole(
  db: RuntimeDb,
  tenantId: string,
  userId: string,
  roleKey: string,
) {
  const [updated] = await db
    .update(members)
    .set({ roleKey })
    .where(and(eq(members.accountId, tenantId), eq(members.userId, userId)))
    .returning({
      tenantId: members.accountId,
      userId: members.userId,
      roleKey: members.roleKey,
      status: members.status,
    })

  if (!updated) {
    throw new Error(`member not found: ${tenantId}/${userId}`)
  }

  return {
    ...updated,
    status: updated.status as 'active' | 'frozen' | 'pending_approval',
  }
}

async function adjustCredits(
  db: RuntimeDb,
  input: {
    walletKey: string
    amount: bigint
    idempotencyKey: string
    mode: 'delta' | 'set'
  },
): Promise<{ balance: bigint }> {
  return db.transaction(async (tx) => {
    const [claim] = await tx
      .insert(ledgerEntries)
      .values({
        delta: input.amount,
        reason: `staff_credit_adjust:${input.mode}`,
        ref: { kind: 'staff_credit_adjust', walletKey: input.walletKey, mode: input.mode },
        idempotencyKey: input.idempotencyKey,
      })
      .onConflictDoNothing()
      .returning({ id: ledgerEntries.id })

    if (claim) {
      const updates =
        input.mode === 'delta'
          ? {
              balance: sql`${walletBalances.balance} + ${input.amount}`,
              updatedAt: new Date(),
            }
          : {
              balance: input.amount,
              updatedAt: new Date(),
            }

      const [row] = await tx
        .update(walletBalances)
        .set(updates)
        .where(eq(walletBalances.ownerId, input.walletKey))
        .returning({ balance: walletBalances.balance })

      if (row) {
        return row
      }

      const [inserted] = await tx
        .insert(walletBalances)
        .values({
          ownerId: input.walletKey,
          balance: input.amount,
          updatedAt: new Date(),
        })
        .returning({ balance: walletBalances.balance })

      if (!inserted) {
        throw new Error(`wallet not created: ${input.walletKey}`)
      }
      return inserted
    }

    const [existing] = await tx
      .select({ balance: walletBalances.balance })
      .from(walletBalances)
      .where(eq(walletBalances.ownerId, input.walletKey))
      .limit(1)

    if (!existing) {
      throw new Error(`wallet not found: ${input.walletKey}`)
    }
    return existing
  })
}

async function issueCreditNote(
  db: RuntimeDb,
  env: RuntimeBindings,
  input: {
    chargeKey: string
    refundId: string
    refundKey: string
    amountMinor: bigint
    currency: string
    customer: {
      name: string
      email?: string
      taxId?: string
    }
  },
): Promise<InvoiceDocumentResult> {
  const provider = new MorningProvider()
  const result = await issueSettlementInvoice(
    Object.assign(db, {
      morningCredential: createMorningCredential(env),
      morningProvider: provider,
      invoiceCurrency: input.currency,
      invoiceDocType: 'credit_note' as const,
    }),
    {
      supplier: { country: 'IL' },
      customer: {
        country: 'IL',
        ...input.customer,
      },
      supplyType: 'digital',
      lineItems: [
        {
          description: `Refund ${input.chargeKey} (${input.refundId})`,
          quantity: 1,
          unitAmountMinor: input.amountMinor,
        },
      ],
      idempotencyKey: input.refundKey,
    },
  )

  if (!result.ok) {
    throw new Error(`${result.error.code}: ${result.error.message}`)
  }

  return result.result
}

function walletAccount(walletKey: string): string {
  const [account] = walletKey.split(':', 1)
  if (!account) {
    throw new Error(`invalid wallet key: ${walletKey}`)
  }
  return account
}

function subscriptionIdFromChargeKey(chargeKey: string): string {
  const [subscriptionId] = chargeKey.split(':', 1)
  if (!subscriptionId) {
    throw new Error(`invalid charge key: ${chargeKey}`)
  }
  return subscriptionId
}

async function loadRecordedRefund(
  db: Querier<typeof pressZoneSchema>,
  input: {
    refundKey: string
    walletKey: string
  },
): Promise<
  | {
      balance: bigint
      creditNote: InvoiceDocumentResult
    }
  | undefined
> {
  const [wallet] = await db
    .select({ balance: walletBalances.balance })
    .from(walletBalances)
    .where(eq(walletBalances.ownerId, input.walletKey))
    .limit(1)
  const [document] = await db
    .select()
    .from(invoiceDocument)
    .where(eq(invoiceDocument.idempotencyKey, input.refundKey))
    .limit(1)

  if (!wallet || !document) {
    return undefined
  }

  return {
    balance: wallet.balance,
    creditNote: {
      documentId: document.documentId,
      documentNumber: document.documentNumber,
      documentUrl: document.documentUrl,
    },
  }
}

export async function reserveRefund(
  db: RuntimeDb,
  input: {
    chargeKey: string
    refundKey: string
    walletKey: string
    amountMinor: bigint
    currency: string
  },
): Promise<
  | { status: 'reserved' }
  | { status: 'completed'; balance: bigint; creditNote: InvoiceDocumentResult }
> {
  return db.transaction(async (tx) => {
    const [charge] = await tx
      .select({
        amount: chargeIntents.amount,
        currency: chargeIntents.currency,
      })
      .from(chargeIntents)
      .where(eq(chargeIntents.chargeKey, input.chargeKey))
      .limit(1)

    if (!charge) {
      throw new Error(`charge not found: ${input.chargeKey}`)
    }

    const accountId = walletAccount(input.walletKey)
    const subscriptionId = subscriptionIdFromChargeKey(input.chargeKey)
    const [subscription] = await tx
      .select({ accountId: subscriptions.accountId, currency: subscriptionPackages.currency })
      .from(subscriptions)
      .innerJoin(
        subscriptionPackages,
        and(
          eq(subscriptionPackages.pluginKey, subscriptions.pluginKey),
          eq(subscriptionPackages.tierKey, subscriptions.tierKey),
        ),
      )
      .where(
        and(
          eq(subscriptions.paypalSubscriptionId, subscriptionId),
          eq(subscriptions.accountId, accountId),
        ),
      )
      .limit(1)

    if (!subscription || subscription.currency !== charge.currency) {
      throw new Error(`wallet ${input.walletKey} does not match charge ${input.chargeKey}`)
    }

    if (charge.currency !== input.currency) {
      throw new AppError('BAD_REQUEST', 'currency does not match the original charge', 400)
    }

    const [reservationClaim] = await tx
      .insert(refundReservations)
      .values({
        refundKey: input.refundKey,
        chargeKey: input.chargeKey,
        walletKey: input.walletKey,
        amountMinor: input.amountMinor,
        currency: input.currency,
        status: 'pending',
      })
      .onConflictDoNothing()
      .returning({ refundKey: refundReservations.refundKey })

    if (!reservationClaim) {
      const [existingReservation] = await tx
        .select({
          amountMinor: refundReservations.amountMinor,
          chargeKey: refundReservations.chargeKey,
          currency: refundReservations.currency,
          status: refundReservations.status,
          walletKey: refundReservations.walletKey,
        })
        .from(refundReservations)
        .where(eq(refundReservations.refundKey, input.refundKey))
        .limit(1)

      if (!existingReservation) {
        throw new Error(`reservation not found after conflict: ${input.refundKey}`)
      }

      if (
        existingReservation.chargeKey !== input.chargeKey ||
        existingReservation.walletKey !== input.walletKey ||
        existingReservation.amountMinor !== input.amountMinor ||
        existingReservation.currency !== input.currency
      ) {
        throw new AppError('BAD_REQUEST', 'refundId does not match the original refund request', 400)
      }

      if (existingReservation.status === 'completed') {
        const existingRefund = await loadRecordedRefund(tx, {
          refundKey: input.refundKey,
          walletKey: input.walletKey,
        })
        if (!existingRefund) {
          throw new Error(`completed refund missing artifacts: ${input.refundKey}`)
        }
        return { status: 'completed', ...existingRefund }
      }

      return { status: 'reserved' }
    }

    const [updatedCharge] = await tx
      .update(chargeIntents)
      .set({
        refundReservedMinor: sql`${chargeIntents.refundReservedMinor} + ${input.amountMinor}`,
      })
      .where(
        and(
          eq(chargeIntents.chargeKey, input.chargeKey),
          sql`${chargeIntents.refundReservedMinor} + ${input.amountMinor} <= ${chargeIntents.amount}`,
        ),
      )
      .returning({ chargeKey: chargeIntents.chargeKey })

    if (!updatedCharge) {
      throw new AppError('BAD_REQUEST', 'refund exceeds captured charge amount', 400)
    }

    return { status: 'reserved' }
  })
}

export async function releaseRefundReservation(
  db: RuntimeDb,
  input: { refundKey: string },
): Promise<void> {
  await db.transaction(async (tx) => {
    const [reservation] = await tx
      .select({
        amountMinor: refundReservations.amountMinor,
        chargeKey: refundReservations.chargeKey,
        status: refundReservations.status,
      })
      .from(refundReservations)
      .where(eq(refundReservations.refundKey, input.refundKey))
      .limit(1)

    if (!reservation || reservation.status === 'completed') {
      return
    }

    await tx
      .update(chargeIntents)
      .set({
        refundReservedMinor: sql`${chargeIntents.refundReservedMinor} - ${reservation.amountMinor}`,
      })
      .where(eq(chargeIntents.chargeKey, reservation.chargeKey))

    await tx.delete(refundReservations).where(eq(refundReservations.refundKey, input.refundKey))
  })
}

async function recordRefundCredit(
  db: RuntimeDb,
  env: RuntimeBindings,
  input: {
    chargeKey: string
    refundId: string
    refundKey: string
    providerRefundKey: string
    walletKey: string
    amountMinor: bigint
    currency: string
    customer: {
      name: string
      email?: string
      taxId?: string
    }
  },
) {
  const creditNote = await issueCreditNote(db, env, input)
  const balance = await db.transaction(async (tx) => {
    const [claim] = await tx
      .insert(ledgerEntries)
      .values({
        delta: input.amountMinor,
        currency: input.currency,
        reason: 'staff_refund',
        ref: {
          kind: 'staff_refund',
          chargeKey: input.chargeKey,
          refundId: input.refundId,
          walletKey: input.walletKey,
          currency: input.currency,
          providerRefundKey: input.providerRefundKey,
        },
        idempotencyKey: input.refundKey,
      })
      .onConflictDoNothing()
      .returning({ id: ledgerEntries.id })

    if (claim) {
      await tx
        .insert(walletBalances)
        .values({
          ownerId: input.walletKey,
          balance: input.amountMinor,
          updatedAt: new Date(),
        })
        .onConflictDoUpdate({
          target: walletBalances.ownerId,
          set: {
            balance: sql`${walletBalances.balance} + ${input.amountMinor}`,
            updatedAt: new Date(),
          },
        })
    }

    await tx
      .update(refundReservations)
      .set({
        status: 'completed',
        providerRefundKey: input.providerRefundKey,
        completedAt: new Date(),
      })
      .where(eq(refundReservations.refundKey, input.refundKey))

    const [wallet] = await tx
      .select({ balance: walletBalances.balance })
      .from(walletBalances)
      .where(eq(walletBalances.ownerId, input.walletKey))
      .limit(1)
    if (!wallet) {
      throw new Error(`wallet not found: ${input.walletKey}`)
    }
    return wallet.balance
  })

  return { balance, creditNote }
}

function currentDateInIsrael(): string {
  const parts = new Intl.DateTimeFormat('en', {
    timeZone: 'Asia/Jerusalem',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(new Date())
  const values = Object.fromEntries(parts.map((part) => [part.type, part.value]))
  return `${values.year}-${values.month}-${values.day}`
}

function createRuntimeApp(env: RuntimeBindings) {
  const db = getDb(env as DbEnv) as RuntimeDb
  const paypalProvider = createPaypalProvider(env)
  const config = loadConfig(env)
  const authEngine = createCustomEngine({
    db,
    schema: pressZoneSchema,
    jwtSecrets: [config.authSessionSecret],
    pepper: {
      currentVersion: 'v1',
      secrets: { v1: config.authPepper },
    },
  })
  const protectedMiddleware = createProtectedMiddleware(db, authEngine)
  const [pluginAuth, _bindAccount, pluginAccess] = createPluginGuards({
    auth: {
      authEngine: createCustomEngine({
        db,
        schema: pressZoneSchema,
        jwtSecrets: [loadConfig(env).authSessionSecret],
        pepper: {
          currentVersion: 'v1',
          secrets: {
            v1: loadConfig(env).authPepper,
          },
        },
      }),
      db,
      resolveApiKeyPrincipal: (verified) => resolveApiKeyPrincipal(db, verified),
      tenancy: {
        resolveCapabilities: (userId, tenantId) => resolveCapabilities(db, userId, tenantId),
      },
    },
    entitlement: 'plugin:translate',
    permission: 'translate.write',
  })

  return createApp({
    sessionRoute: createSessionRoute({ authEngine }),
    oauthRoute: createOauthRoute({ db }),
    webhookRoute: createWebhookRoute({
      db: Object.assign(db, {
        morningCredential: createMorningCredential(env),
        morningProvider: new MorningProvider(),
        invoiceDate: currentDateInIsrael(),
      }),
      paypalProvider,
    }),
    subscribeRoute: createSubscribeRoute({
      db,
      provider: paypalProvider,
    }),
    pluginRoute: createPluginRoute({
      auth: pluginAuth,
      access: pluginAccess,
      cost: 1n,
      currentPeriod: () => currentDateInIsrael().slice(0, 7),
      execute: async () => ({ ok: true }),
      getDb: () => db,
      plugin: 'translate',
    }),
    accountRoute: createAccountRoute({
      db,
      tenancy: {
        setMemberRole: (tenantId, userId, roleKey) => setMemberRole(db, tenantId, userId, roleKey),
      },
    }),
    staffRoute: createStaffRoute({
      adjustCredits: ({ walletKey, amount, idempotencyKey, mode }) =>
        adjustCredits(db, { walletKey, amount, idempotencyKey, mode }),
      audit: (event: AuditEvent) => logAudit(db, event).then(() => undefined),
      allowedTiers: ['free', 'pro', 'enterprise'],
      reserveRefund: (input) => reserveRefund(db, input),
      releaseRefundReservation: (input) => releaseRefundReservation(db, input),
      recordRefundCredit: (input) => recordRefundCredit(db, env, input),
      listAudit: (filter: ListAuditFilter) => listAudit(db, filter),
      refundProvider: paypalProvider,
      setTier: (account, tier) =>
        createEntitlements({ db }).setTier(account, {
          capability: 'plugin:translate',
          tier,
        }),
    }),
    protectedMiddleware,
  })
}

export const fetch = (
  request: Request,
  env: RuntimeBindings,
  executionContext: ExecutionContextLike,
) => createRuntimeApp(env).fetch(request, env, executionContext as never)

export function scheduled(
  event: ScheduledEventLike,
  env: RuntimeBindings,
  context: ExecutionContextLike,
): Promise<void> {
  return runCron(event, env, context as never)
}

export function queue(
  batch: QueueBatchLike<QueueJob>,
  env: RuntimeBindings,
  context: ExecutionContextLike,
): Promise<void> {
  return consumeQueue(batch, env, context as never)
}
