import { and, eq } from 'drizzle-orm'
import { Hono } from 'hono'
import { createEntitlements } from '@platform-modules/entitlements'
import type { Transaction, TransactionalDatabase } from '@platform-modules/db'
import type { PaypalProvider } from '@platform-modules/billing/paypal'

import { AppError } from '../errors.js'
import { ok } from '../http.js'
import { getDb, type DbEnv } from '../db.js'
import { periodKey, seedPeriodWallet } from '../lib/wallet.js'
import {
  type PressZoneSchema,
  subscriptionPackages,
  subscriptions,
} from '../schema.js'

type PressZoneDb = TransactionalDatabase<PressZoneSchema>
type PressZoneTx = Transaction<PressZoneSchema>
type SubscribeProvider = Pick<PaypalProvider, 'createSubscription'>

export interface SubscribeRequestBody {
  pluginKey: string
  tierKey: string
}

export interface SubscribeAccountInput extends SubscribeRequestBody {
  accountId: string
  db: PressZoneDb
  provider: SubscribeProvider
  now?: () => Date
}

export interface SubscribeAccountResult {
  accountId: string
  pluginKey: string
  tierKey: string
  status: string
  paypalSubscriptionId: string
  currentPeriod: string
  walletKey: string
}

type RouteEnv = {
  Bindings: DbEnv
  Variables: {
    principal: {
      account: string
    }
  }
}

function requireBodyString(value: unknown, field: keyof SubscribeRequestBody): string {
  if (typeof value !== 'string' || value.trim() === '') {
    throw new AppError('BAD_REQUEST', `Missing required field: ${field}`, 400)
  }

  return value.trim()
}

function subscribeIdempotencyKey(accountId: string, pluginKey: string, tierKey: string): string {
  return `subscribe:${accountId}:${pluginKey}:${tierKey}`
}

function currentPeriod(now: Date): string {
  return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}`
}

function periodStart(period: string): Date {
  const match = /^(\d{4})-(\d{2})$/.exec(period)
  if (!match) {
    throw new AppError('SUBSCRIPTION_PERIOD_INVALID', `Invalid subscription period: ${period}`, 502)
  }

  const year = Number(match[1])
  const monthIndex = Number(match[2]) - 1
  return new Date(Date.UTC(year, monthIndex, 1))
}

function nextPeriodStart(period: string): Date {
  const start = periodStart(period)
  return new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, 1))
}

function asTransactional(tx: PressZoneTx): TransactionalDatabase<PressZoneSchema> {
  return Object.assign(tx, {
    transaction: async <T>(fn: (nestedTx: PressZoneTx) => Promise<T>) => fn(tx),
  }) as unknown as TransactionalDatabase<PressZoneSchema>
}

export async function subscribeAccount(
  input: SubscribeAccountInput,
): Promise<SubscribeAccountResult> {
  const [pkg] = await input.db
    .select()
    .from(subscriptionPackages)
    .where(
      and(
        eq(subscriptionPackages.pluginKey, input.pluginKey),
        eq(subscriptionPackages.tierKey, input.tierKey),
      ),
    )
    .limit(1)

  if (!pkg) {
    throw new AppError('SUBSCRIPTION_PACKAGE_NOT_FOUND', 'Subscription package not found', 404)
  }

  const [existing] = await input.db
    .select()
    .from(subscriptions)
    .where(
      and(
        eq(subscriptions.accountId, input.accountId),
        eq(subscriptions.pluginKey, input.pluginKey),
      ),
    )
    .limit(1)

  if (existing && existing.tierKey !== input.tierKey) {
    throw new AppError(
      'SUBSCRIPTION_TIER_CONFLICT',
      'Subscription already exists for a different tier',
      409,
    )
  }

  if (existing?.currentPeriodStart && existing.paypalSubscriptionId) {
    const existingPeriod = currentPeriod(existing.currentPeriodStart)
    const walletKey = periodKey(input.accountId, input.pluginKey, existingPeriod)

    await input.db.transaction(async (tx) => {
      await createEntitlements({ db: asTransactional(tx) }).setTier(input.accountId, {
        capability: `plugin:${input.pluginKey}`,
        tier: input.tierKey,
      })
      await seedPeriodWallet(tx, walletKey, pkg.creditAllocation)
    })

    return {
      accountId: input.accountId,
      pluginKey: input.pluginKey,
      tierKey: input.tierKey,
      status: existing.status,
      paypalSubscriptionId: existing.paypalSubscriptionId,
      currentPeriod: existingPeriod,
      walletKey,
    }
  }

  const created = await input.provider.createSubscription({
    idempotencyKey: subscribeIdempotencyKey(input.accountId, input.pluginKey, input.tierKey),
    planId: pkg.paypalPlanId,
    currentPeriod: currentPeriod((input.now ?? (() => new Date()))()),
  })
  const walletKey = periodKey(input.accountId, input.pluginKey, created.currentPeriod)
  const updatedAt = (input.now ?? (() => new Date()))()

  await input.db.transaction(async (tx) => {
    await tx
      .insert(subscriptions)
      .values({
        accountId: input.accountId,
        pluginKey: input.pluginKey,
        tierKey: input.tierKey,
        status: created.status,
        currentPeriodStart: periodStart(created.currentPeriod),
        currentPeriodEnd: nextPeriodStart(created.currentPeriod),
        paypalSubscriptionId: created.id,
        updatedAt,
      })
      .onConflictDoUpdate({
        target: [subscriptions.accountId, subscriptions.pluginKey],
        set: {
          tierKey: input.tierKey,
          status: created.status,
          currentPeriodStart: periodStart(created.currentPeriod),
          currentPeriodEnd: nextPeriodStart(created.currentPeriod),
          paypalSubscriptionId: created.id,
          updatedAt,
        },
      })

    await createEntitlements({ db: asTransactional(tx) }).setTier(input.accountId, {
      capability: `plugin:${input.pluginKey}`,
      tier: input.tierKey,
    })
    await seedPeriodWallet(tx, walletKey, pkg.creditAllocation)
  })

  return {
    accountId: input.accountId,
    pluginKey: input.pluginKey,
    tierKey: input.tierKey,
    status: created.status,
    paypalSubscriptionId: created.id,
    currentPeriod: created.currentPeriod,
    walletKey,
  }
}

export function createSubscribeRoute(options: { provider: SubscribeProvider; db?: PressZoneDb }) {
  const route = new Hono<RouteEnv>()

  route.post('/', async (context) => {
    const body = (await context.req.json()) as Partial<SubscribeRequestBody>
    const principal = context.get('principal')
    const db = options.db ?? (getDb(context.env) as PressZoneDb)
    const result = await subscribeAccount({
      accountId: principal.account,
      db,
      pluginKey: requireBodyString(body.pluginKey, 'pluginKey'),
      provider: options.provider,
      tierKey: requireBodyString(body.tierKey, 'tierKey'),
    })

    return ok(result)
  })

  return route
}
