import { Hono } from 'hono'
import { and, eq, sql } from 'drizzle-orm'
import type { Database } from '@platform-modules/db'
import {
  ingestWebhook,
  type DedupStore,
  type LedgerSeam,
  type PaymentProvider,
  type ProviderEvent,
} from '@platform-modules/billing'
import { appendEntry } from '@platform-modules/ledger'
import { applyVat, fromBasisPoints, IL_VAT_SCHEDULE, resolveVatRate } from '@platform-modules/tax/rates-table'
import { resolveTaxability } from '@platform-modules/tax/taxability'

import {
  issueSettlementInvoice,
  normalizeSettlementInvoiceCurrency,
  normalizeSettlementInvoiceDate,
  type SettlementInvoiceDb,
} from '../lib/billing-doc.js'
import { periodKey, seedPeriodWallet } from '../lib/wallet.js'
import {
  accounts,
  chargeIntents,
  invoiceReferences,
  plugins,
  pressZoneSchema,
  subscriptionPackages,
  subscriptions,
} from '../schema.js'

type SupplyType = 'digital'

type SettlementInvoiceRuntime = Pick<
  SettlementInvoiceDb,
  'morningCredential' | 'morningProvider' | 'invoiceDate' | 'invoiceCurrency' | 'invoiceDocType'
>

type DispatchDb = Database<typeof pressZoneSchema> & SettlementInvoiceRuntime

type SubscriptionDispatchContext = {
  subscriptionRowId: string
  accountId: string
  accountSlug: string
  pluginKey: string
  pluginName: string
  tierKey: string
  creditAllocation: bigint
}

export interface WebhookRouteDeps {
  db: DispatchDb
  paypalProvider: PaymentProvider
  ledger?: LedgerSeam
  supplierCountry?: string
  customerCountry?: string
  supplyType?: SupplyType
}

const DEFAULT_SUPPLIER_COUNTRY = 'IL'
const DEFAULT_CUSTOMER_COUNTRY = 'IL'
const DEFAULT_SUPPLY_TYPE: SupplyType = 'digital'
const ZERO_VAT_RATE = fromBasisPoints(0)
const DEFAULT_LEDGER: LedgerSeam = { appendEntry: appendEntry as LedgerSeam['appendEntry'] }

export function createWebhookRoute({
  db,
  paypalProvider,
  ledger = DEFAULT_LEDGER,
  supplierCountry = DEFAULT_SUPPLIER_COUNTRY,
  customerCountry = DEFAULT_CUSTOMER_COUNTRY,
  supplyType = DEFAULT_SUPPLY_TYPE,
}: WebhookRouteDeps) {
  const route = new Hono()
  const dedupStore = createWebhookDedupStore(db)

  route.post('/webhooks/paypal', (context) =>
    ingestWebhook(context.req.raw, {
      provider: paypalProvider,
      dedupStore,
      ledger,
      db,
      dispatch: async (event) => {
        await dispatchWebhookEvent(db, event, {
          supplierCountry,
          customerCountry,
          supplyType,
        })
      },
    }),
  )

  return route
}

interface DispatchOptions {
  supplierCountry: string
  customerCountry: string
  supplyType: SupplyType
}

async function dispatchWebhookEvent(
  db: DispatchDb,
  event: ProviderEvent,
  options: DispatchOptions,
): Promise<void> {
  if (event.kind === 'other') {
    return
  }

  if (event.kind === 'settlement') {
    const parsedChargeKey = parseSubscriptionChargeKey(event.chargeKey)
    const subscriptionId = event.subscriptionId ?? parsedChargeKey.subscriptionId
    const period = event.period ?? parsedChargeKey.period
    const amountMinor = event.amountMinor ?? BigInt(event.amount)
    const invoiceCurrency = normalizeSettlementInvoiceCurrency(event.currency)
    const subscription = await loadSubscriptionContext(db, subscriptionId)
    const walletOwnerId = periodKey(subscription.accountId, subscription.pluginKey, period)
    const settlementClaim = await claimSettlementDispatch(db, {
      chargeKey: event.chargeKey,
      amountMinor,
      currency: invoiceCurrency,
    })

    if (settlementClaim !== 'won') {
      return
    }

    await seedPeriodWallet(db, walletOwnerId, subscription.creditAllocation)
    await issueDocumentForEvent(db, subscription, {
      amountMinor,
      currency: invoiceCurrency,
      idempotencyKey: `settlement:${event.chargeKey}`,
      docType: 'invoice',
      documentReason: 'subscription settlement',
      options,
    })
    await markSettlementDispatchSettled(db, event.chargeKey)
    return
  }

  const { subscriptionId } = parseSubscriptionChargeKey(event.chargeKey)
  const subscription = await loadSubscriptionContext(db, subscriptionId)
  await issueDocumentForEvent(db, subscription, {
    amountMinor: BigInt(event.amount),
    currency: normalizeSettlementInvoiceCurrency(event.currency),
    idempotencyKey: event.refundKey,
    docType: 'credit_note',
    documentReason: 'subscription refund',
    options,
  })
}

interface IssueDocumentInput {
  amountMinor: bigint
  currency: string
  idempotencyKey: string
  docType: 'invoice' | 'credit_note'
  documentReason: string
  options: DispatchOptions
}

async function issueDocumentForEvent(
  db: DispatchDb,
  subscription: SubscriptionDispatchContext,
  input: IssueDocumentInput,
): Promise<void> {
  const invoiceCurrency = normalizeSettlementInvoiceCurrency(input.currency)
  const invoiceDate = normalizeSettlementInvoiceDate(db.invoiceDate)
  const issueDb = Object.assign(db, {
    invoiceDocType: input.docType,
    invoiceCurrency,
  } as const)
  const result = await issueSettlementInvoice(issueDb, {
    supplier: { country: input.options.supplierCountry },
    customer: {
      country: input.options.customerCountry,
      name: subscription.accountSlug,
    },
    supplyType: input.options.supplyType,
    lineItems: [
      {
        description: `${subscription.pluginName} ${subscription.tierKey} subscription`,
        quantity: 1,
        unitAmountMinor: input.amountMinor,
      },
    ],
    idempotencyKey: input.idempotencyKey,
  })

  if (!result.ok) {
    throw new Error(
      `${input.documentReason} invoice failed: ${result.error.code} ${result.error.message}`,
    )
  }

  const vatAmount = computeVatAmount(
    input.amountMinor,
    input.options.supplierCountry,
    input.options.customerCountry,
    input.options.supplyType,
    invoiceDate,
  )

  await db
    .insert(invoiceReferences)
    .values({
      accountId: subscription.accountId,
      subscriptionId: subscriptionIdFor(subscription),
      morningDocumentId: result.result.documentId,
      documentNumber: result.result.documentNumber,
      documentUrl: result.result.documentUrl,
      docType: input.docType,
      amount: input.amountMinor,
      vatAmount,
      currency: invoiceCurrency,
      idempotencyKey: input.idempotencyKey,
    })
    .onConflictDoNothing({ target: invoiceReferences.idempotencyKey })
}

function subscriptionIdFor(subscription: SubscriptionDispatchContext): string {
  return subscription.subscriptionRowId
}

async function loadSubscriptionContext(
  db: DispatchDb,
  paypalSubscriptionId: string,
): Promise<SubscriptionDispatchContext> {
  const rows = await db
    .select({
      subscriptionRowId: subscriptions.id,
      accountId: subscriptions.accountId,
      accountSlug: accounts.slug,
      pluginKey: subscriptions.pluginKey,
      pluginName: plugins.name,
      tierKey: subscriptions.tierKey,
      creditAllocation: subscriptionPackages.creditAllocation,
    })
    .from(subscriptions)
    .innerJoin(accounts, eq(accounts.id, subscriptions.accountId))
    .innerJoin(plugins, eq(plugins.key, subscriptions.pluginKey))
    .innerJoin(
      subscriptionPackages,
      and(
        eq(subscriptionPackages.pluginKey, subscriptions.pluginKey),
        eq(subscriptionPackages.tierKey, subscriptions.tierKey),
      ),
    )
    .where(eq(subscriptions.paypalSubscriptionId, paypalSubscriptionId))
    .limit(1)

  const row = rows[0]
  if (!row) {
    throw new Error(`subscription not found for PayPal subscription ${paypalSubscriptionId}`)
  }

  return row
}

function parseSubscriptionChargeKey(chargeKey: string): { subscriptionId: string; period: string } {
  const separatorIndex = chargeKey.lastIndexOf(':')
  if (separatorIndex <= 0 || separatorIndex >= chargeKey.length - 1) {
    throw new Error(`invalid subscription chargeKey: ${chargeKey}`)
  }

  return {
    subscriptionId: chargeKey.slice(0, separatorIndex),
    period: chargeKey.slice(separatorIndex + 1),
  }
}

function computeVatAmount(
  amountMinor: bigint,
  supplierCountry: string,
  customerCountry: string,
  supplyType: SupplyType,
  invoiceDate?: string,
): bigint {
  const treatment = resolveTaxability({
    supplier: { country: supplierCountry },
    customer: { country: customerCountry },
    supplyType,
  }).treatment

  const rate =
    treatment === 'standard'
      ? resolveVatRate(IL_VAT_SCHEDULE, invoiceDate ?? todayInIsrael())
      : ZERO_VAT_RATE

  return applyVat(amountMinor, rate).vat
}

function todayInIsrael(): 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}`
}

async function claimSettlementDispatch(
  db: DispatchDb,
  input: { chargeKey: string; amountMinor: bigint; currency: string },
): Promise<'won' | 'pending' | 'settled'> {
  const claimed = await db.execute(sql`
    INSERT INTO charge_intents (charge_key, amount, currency, status, claimed_at, settled_at)
    VALUES (${input.chargeKey}, ${input.amountMinor}, ${input.currency}, 'pending', now(), NULL)
    ON CONFLICT (charge_key) DO UPDATE
      SET amount = EXCLUDED.amount,
          currency = EXCLUDED.currency,
          claimed_at = now()
      WHERE charge_intents.status = 'pending'
        AND charge_intents.claimed_at < now() - INTERVAL '5 min'
    RETURNING charge_key
  `)

  if (extractCount(claimed) > 0) {
    return 'won'
  }

  const rows = await db
    .select({ status: chargeIntents.status })
    .from(chargeIntents)
    .where(eq(chargeIntents.chargeKey, input.chargeKey))
    .limit(1)

  const status = rows[0]?.status
  if (status === 'settled') {
    return 'settled'
  }

  return 'pending'
}

async function markSettlementDispatchSettled(db: DispatchDb, chargeKey: string): Promise<void> {
  await db
    .update(chargeIntents)
    .set({
      status: 'settled',
      settledAt: new Date(),
    })
    .where(eq(chargeIntents.chargeKey, chargeKey))
}

function createWebhookDedupStore(db: DispatchDb): DedupStore {
  return {
    async claim(eventId: string): Promise<'won' | 'lost'> {
      const result = await db.execute(sql`
        INSERT INTO webhook_events (event_id, claimed_at)
        VALUES (${eventId}, now())
        ON CONFLICT (event_id) DO UPDATE SET claimed_at = now()
          WHERE webhook_events.processed_at IS NULL
            AND (webhook_events.claimed_at IS NULL
                 OR webhook_events.claimed_at < now() - INTERVAL '5 min')
        RETURNING event_id
      `)
      return extractCount(result) > 0 ? 'won' : 'lost'
    },

    async markProcessed(eventId: string): Promise<void> {
      await db.execute(sql`
        UPDATE webhook_events
        SET processed_at = now()
        WHERE event_id = ${eventId}
      `)
    },
  }
}

function extractCount(result: unknown): number {
  const rows =
    (Array.isArray(result)
      ? result
      : (result as { rows?: Array<Record<string, unknown> | undefined> }).rows) ?? []
  return rows.length
}
