/**
 * Tenant subscription API routes — zync-subscription spec (Tasks 8 & 9).
 *
 * Mounted at /api/zync-subscription.
 *
 * GET    /api/zync-subscription            Fetch current subscription + storage info
 * POST   /api/zync-subscription/checkout   Create checkout session
 * DELETE /api/zync-subscription            Cancel subscription
 * POST   /api/zync-subscription/reactivate Reactivate a pending cancellation
 * GET    /api/zync-subscription/invoices   Invoice history via adapter
 * POST   /api/zync-subscription/webhook    Inbound webhook (no session auth; sig-verified)
 *
 * All routes except /webhook require session auth + OWNER or ADMIN role.
 * Zod validation on every route with a body.
 * No raw Drizzle in this file — all DB ops via @zync/db/queries.
 */
import { Hono } from 'hono'
import { z } from 'zod'
import { TenantTier } from '@zync/types'
import type { SessionPayload } from '@zync/types'
import { authMiddleware } from '../middleware/auth'
import { blockDuringImpersonation } from '../middleware/block-impersonation-ops'
import {
  createDb,
  getSubscriptionByTenantId,
  markSubscriptionCanceled,
  reactivateCanceledSubscription,
  activateSubscription,
  getCounterValue,
  countActiveMembers,
  getEnabledModuleIds,
  ackTrialExpiry,
  createNotification,
  getUserEmailAndLocale,
  getTenantById,
} from '@zync/db/queries'
import { getPaymentAdapter, syncTierToTenant } from '@zync/payments'
import { PaymentProviderNotConfiguredError } from '@zync/payments'
import { sendEmail } from '@zync/notifications'
import { getStorageQuotaBytes } from '@zync/storage'
import { getMaxTeamMembers } from '@zync/auth'
import {
  isSubscriptionWebhookDuplicate,
  markSubscriptionWebhookProcessed,
  resolveSubscriptionWebhookEventId,
} from '../lib/subscription-webhook-idempotency'
import { tenantLocaleToBcp47 } from '../lib/invoice-snapshot'
import {
  cancelSubscriptionSchema,
  reactivateSubscriptionSchema,
} from './zync-subscription.validation'
import type { AppEnv } from '../types'

export const zyncSubscriptionRoute = new Hono<AppEnv>()

// ---------------------------------------------------------------------------
// Auth guard: session required + OWNER or ADMIN role
// ---------------------------------------------------------------------------

const OWNER_ADMIN_ROLES = new Set(['OWNER', 'ADMIN'])

function requireOwnerOrAdmin(session: SessionPayload | undefined) {
  if (!session || session.type !== 'user') return false
  return OWNER_ADMIN_ROLES.has(session.role)
}

function requireOwner(session: SessionPayload | undefined) {
  return session?.type === 'user' && session.role === 'OWNER'
}

// All tenant-facing routes require authentication
zyncSubscriptionRoute.use('/checkout', authMiddleware)
zyncSubscriptionRoute.use('/invoices', authMiddleware)
zyncSubscriptionRoute.use('/ack-trial-expiry', authMiddleware)
zyncSubscriptionRoute.use('/reactivate', authMiddleware)
zyncSubscriptionRoute.use('/', authMiddleware)

// admin-impersonation: block destructive ops during impersonation sessions
zyncSubscriptionRoute.use('/checkout', blockDuringImpersonation())
zyncSubscriptionRoute.use('/ack-trial-expiry', blockDuringImpersonation())
zyncSubscriptionRoute.use('/reactivate', blockDuringImpersonation())
zyncSubscriptionRoute.use('/', blockDuringImpersonation())

// ---------------------------------------------------------------------------
// GET /api/zync-subscription
// ---------------------------------------------------------------------------

zyncSubscriptionRoute.get('/', async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (!requireOwnerOrAdmin(session)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const db = createDb(c.env)
  const sub = await getSubscriptionByTenantId(db, session.tid)
  if (!sub) {
    return c.json({ error: 'Subscription not found' }, 404)
  }

  const adapter = getPaymentAdapter(c.env, db)

  // Storage usage info
  const usedBytes = await getCounterValue(db, session.tid, 'storage_bytes', 'all_time')
  const limitBytes = getStorageQuotaBytes(sub.tier as TenantTier)
  const pct = limitBytes > 0 ? Math.round((usedBytes / limitBytes) * 100) : 0
  const [billingPortalUrl, teamMemberCount, enabledModuleIds] = await Promise.all([
    adapter.getBillingPortalUrl(session.tid),
    countActiveMembers(db, session.tid),
    getEnabledModuleIds(db, session.tid),
  ])

  return c.json({
    ...sub,
    billingPortalUrl,
    usage: {
      teamMembers: {
        used: teamMemberCount,
        limit: getMaxTeamMembers(sub.tier as TenantTier),
      },
      activeModules: {
        used: enabledModuleIds.length,
      },
    },
    storage: {
      usedBytes,
      limitBytes,
      pct,
    },
  })
})

// ---------------------------------------------------------------------------
// POST /api/zync-subscription/checkout
// ---------------------------------------------------------------------------

const checkoutBodySchema = z.object({
  tier: z.enum(['freelancer', 'business', 'enterprise', 'white_label']),
  period: z.enum(['monthly', 'annual']),
})

const ackTrialExpiryBodySchema = z.object({}).strict()

zyncSubscriptionRoute.post('/checkout', async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (!requireOwnerOrAdmin(session)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const body = await c.req.json().catch(() => null)
  const parsed = checkoutBodySchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'invalid_body', issues: parsed.error.issues }, 400)
  }
  const { tier, period } = parsed.data

  // Reject non-self-service tiers
  const selfServiceTiers: string[] = ['freelancer', 'business']
  if (!selfServiceTiers.includes(tier)) {
    return c.json(
      {
        error: 'not_self_service',
        message: 'Enterprise and White Label require manual provisioning. Contact sales.',
      },
      422,
    )
  }

  const db = createDb(c.env)
  const adapter = getPaymentAdapter(c.env, db)

  try {
    const result = await adapter.createCheckoutSession(
      session.tid,
      tier as TenantTier,
      period,
    )
    return c.json(result)
  } catch (err) {
    if (err instanceof PaymentProviderNotConfiguredError) {
      return c.json(
        {
          error: 'payment_provider_not_configured',
          message: 'Payment provider not yet configured — contact support.',
        },
        503,
      )
    }
    throw err
  }
})

// ---------------------------------------------------------------------------
// POST /api/zync-subscription/ack-trial-expiry
// ---------------------------------------------------------------------------

zyncSubscriptionRoute.post('/ack-trial-expiry', async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (!requireOwnerOrAdmin(session)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const body = await c.req.json().catch(() => null)
  const parsed = ackTrialExpiryBodySchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'invalid_body', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)
  const updated = await ackTrialExpiry(db, session.tid)

  const trialExpiryAckAt =
    updated?.trialExpiryAckAt ??
    (await getSubscriptionByTenantId(db, session.tid))?.trialExpiryAckAt ??
    null

  return c.json({
    ok: true,
    trialExpiryAckAt: trialExpiryAckAt ? trialExpiryAckAt.toISOString() : null,
  })
})

// ---------------------------------------------------------------------------
// DELETE /api/zync-subscription
// ---------------------------------------------------------------------------

zyncSubscriptionRoute.delete('/', authMiddleware, async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (!requireOwner(session)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const body = await c.req.json().catch(() => ({}))
  const parsed = cancelSubscriptionSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'invalid_body', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)
  const subscription = await getSubscriptionByTenantId(db, session.tid)
  if (!subscription) {
    return c.json({ error: 'Subscription not found' }, 404)
  }
  if (
    (subscription.status !== 'active' && subscription.status !== 'trialing') ||
    subscription.tier === 'freelancer'
  ) {
    return c.json({ error: 'subscription_not_cancellable' }, 409)
  }

  const adapter = getPaymentAdapter(c.env, db)
  const { effectiveDate } = await adapter.cancelSubscription(session.tid)
  const canceledAt = new Date()
  const updated = await markSubscriptionCanceled(db, session.tid, {
    canceledAt,
    cancellationReason: parsed.data.cancellationReason ?? null,
    cancellationReasonFreetext: parsed.data.cancellationReasonFreetext ?? null,
  })
  if (!updated) {
    return c.json({ error: 'Subscription not found' }, 404)
  }

  const periodEndValue = effectiveDate ?? updated.currentPeriodEnd ?? null
  const periodEnd =
    periodEndValue instanceof Date
      ? periodEndValue.toISOString()
      : typeof periodEndValue === 'string'
        ? periodEndValue
        : null

  const [ownerContact, tenant] = await Promise.all([
    getUserEmailAndLocale(db, session.sub, session.tid),
    getTenantById(db, session.tid),
  ])
  const tenantSettings = (tenant?.settings as Record<string, unknown> | null) ?? null
  const tenantLocale = tenantLocaleToBcp47(
    typeof tenantSettings?.locale === 'string' ? tenantSettings.locale : ownerContact?.locale,
  )

  if (ownerContact?.email) {
    void Promise.resolve(
      sendEmail(
        {
          to: ownerContact.email,
          templateKey: 'subscription_cancelled',
          locale: tenantLocale,
          vars: {
            subject: 'Your Zync Business subscription has been cancelled',
            title: 'Your Zync Business subscription has been cancelled',
            body: periodEnd
              ? `Your Business plan will stay active until ${new Date(periodEnd).toLocaleDateString(tenantLocale)}. Your data stays safe. You can reactivate anytime.`
              : 'Your Business plan has been cancelled. Your data stays safe. You can reactivate anytime.',
            cancellationDate: canceledAt.toLocaleDateString(tenantLocale),
            periodEnd: periodEnd ? new Date(periodEnd).toLocaleDateString(tenantLocale) : '',
            reactivateUrl: '/settings/plan',
          },
        },
        c.env,
      ),
    ).catch((error) => {
      console.error('subscription cancellation email failed', error)
    })
  }

  void Promise.resolve(
    createNotification(db, {
      tenantId: session.tid,
      userId: session.sub,
      type: 'subscription_cancelled',
      titleKey: 'notification.subscription_cancelled.title',
      bodyKey: 'notification.subscription_cancelled.body',
      params: { periodEnd: periodEnd ?? '' },
      entityType: 'subscription',
      entityId: updated.id,
    }),
  ).catch((error) => {
    console.error('subscription cancellation notification failed', error)
  })

  return c.json({
    status: 'canceled',
    canceledAt: updated.canceledAt?.toISOString() ?? canceledAt.toISOString(),
    periodEnd,
  })
})

zyncSubscriptionRoute.post('/reactivate', async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (!requireOwner(session)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const body = await c.req.json().catch(() => ({}))
  const parsed = reactivateSubscriptionSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'invalid_body', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)
  const subscription = await getSubscriptionByTenantId(db, session.tid)
  if (!subscription) {
    return c.json({ error: 'Subscription not found' }, 404)
  }

  const periodEnded =
    subscription.currentPeriodEnd instanceof Date
      ? subscription.currentPeriodEnd.getTime() <= Date.now()
      : false
  if (
    subscription.status !== 'canceled' ||
    subscription.tier === 'freelancer' ||
    periodEnded
  ) {
    return c.json(
      {
        error: 'subscription_not_reactivatable',
        message: 'This subscription must use the full upgrade flow.',
      },
      409,
    )
  }

  const updated = await reactivateCanceledSubscription(db, session.tid)
  if (!updated) {
    return c.json({ error: 'Subscription not found' }, 404)
  }

  return c.json({
    status: 'active',
    canceledAt: null,
  })
})

// ---------------------------------------------------------------------------
// GET /api/zync-subscription/invoices
// ---------------------------------------------------------------------------

zyncSubscriptionRoute.get('/invoices', async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (!requireOwnerOrAdmin(session)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const db = createDb(c.env)
  const adapter = getPaymentAdapter(c.env, db)
  const invoices = await adapter.getInvoiceHistory(session.tid)

  return c.json(invoices)
})

// ---------------------------------------------------------------------------
// POST /api/zync-subscription/webhook (no session auth; signature-verified)
// ---------------------------------------------------------------------------

zyncSubscriptionRoute.post('/webhook', async (c) => {
  const db = createDb(c.env)
  const adapter = getPaymentAdapter(c.env, db)

  // Read raw body + provider signature header
  const rawBody = await c.req.text()
  const signature = c.req.header('x-payment-signature') ?? ''

  let event
  try {
    event = await adapter.handleWebhook(rawBody, signature)
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error'
    // NullAdapter throws → 501; invalid signature → 400
    if (message.includes('does not support webhooks')) {
      return c.json({ error: 'not_implemented' }, 501)
    }
    return c.json({ error: 'invalid_signature', message }, 400)
  }

  const eventId = await resolveSubscriptionWebhookEventId(event, rawBody)

  if (await isSubscriptionWebhookDuplicate(c.env.KV, eventId)) {
    return c.json({ received: true, duplicate: true })
  }

  // Handle activating events
  if (event.type === 'subscription.updated' || event.type === 'invoice.paid') {
    const payload = event.payload as Record<string, unknown> | null

    await activateSubscription(db, event.tenantId, {
      adapterSubscriptionId: payload?.subscription_id as string | undefined,
      adapterCustomerId: payload?.customer_id as string | undefined,
      currentPeriodStart: payload?.period_start
        ? new Date(payload.period_start as string)
        : undefined,
      currentPeriodEnd: payload?.period_end
        ? new Date(payload.period_end as string)
        : undefined,
      tier: payload?.tier as string | undefined,
    })

    // Re-fetch and sync tier
    const sub = await getSubscriptionByTenantId(db, event.tenantId)
    if (sub) {
      await syncTierToTenant(db, event.tenantId, sub.tier as TenantTier)
    }
  }

  await markSubscriptionWebhookProcessed(c.env.KV, eventId)

  return c.json({ received: true })
})
