import fs from 'node:fs'
import path from 'node:path'

export type BillingPlan = 'free' | 'operator' | 'studio' | 'enterprise'
export type BillingRole = 'viewer' | 'operator' | 'reviewer' | 'admin'
export type BillingEntitlementSource = 'paddle' | 'manual'

export interface BillingEntitlementState {
  email: string
  plan: BillingPlan
  role: BillingRole
  canDownload: boolean
  accessStatus: string
  source: BillingEntitlementSource
  customerId: string | null
  subscriptionId: string | null
  transactionId: string | null
  lastEventId: string
  lastEventType: string
  lastEventAt: string
  updatedAt: string
}

export interface ManualBillingEntitlementInput {
  email: string
  plan: BillingPlan
  role: BillingRole
  canDownload?: boolean
  accessStatus?: string
}

interface BillingStateFile {
  version: 'ht-billing-state/v1'
  updatedAt: string
  processedEvents: Record<string, string>
  accounts: Record<string, BillingEntitlementState>
}

interface PaddleEventEnvelope {
  event_id: string
  event_type: string
  occurred_at: string
  data: Record<string, unknown>
}

interface BillingStateStoreOptions {
  statePath: string
  defaultPlan: BillingPlan
  defaultRole: BillingRole
  productPlanMap?: Record<string, BillingPlan>
  pricePlanMap?: Record<string, BillingPlan>
  now?: () => Date
}

interface BillingEventIdentityCandidates {
  subscriptionIds: string[]
  transactionIds: string[]
  customerIds: string[]
}

interface BillingStateStoreRuntime {
  defaultPlan: BillingPlan
  defaultRole: BillingRole
  productPlanMap: Record<string, BillingPlan>
  pricePlanMap: Record<string, BillingPlan>
  now: () => Date
  loadState: () => BillingStateFile
  saveState: (state: BillingStateFile) => void
}

export interface BillingEventApplyResult {
  kind: 'applied' | 'duplicate' | 'ignored' | 'noop'
  reason:
    | 'processed'
    | 'duplicate_event'
    | 'stale_event'
    | 'unsupported_payload'
    | 'unresolved_account'
  eventId: string
  eventType: string
  email?: string
}

function normalizeEmail(value: unknown): string {
  return String(value || '').trim().toLowerCase()
}

function normalizePlan(value: unknown, fallback: BillingPlan): BillingPlan {
  const plan = String(value || '').trim().toLowerCase()
  if (plan === 'operator' || plan === 'studio' || plan === 'enterprise' || plan === 'free') {
    return plan
  }
  if (plan === 'explorer') {
    return 'free'
  }
  return fallback
}

function normalizeRole(value: unknown, fallback: BillingRole): BillingRole {
  const role = String(value || '').trim().toLowerCase()
  if (role === 'viewer' || role === 'operator' || role === 'reviewer' || role === 'admin') {
    return role
  }
  return fallback
}

function ensureParentDirectory(targetPath: string) {
  fs.mkdirSync(path.dirname(targetPath), { recursive: true })
}

function readJsonFile(targetPath: string): BillingStateFile | null {
  if (!fs.existsSync(targetPath)) {
    return null
  }

  const raw = fs.readFileSync(targetPath, 'utf8')
  return JSON.parse(raw) as BillingStateFile
}

function createEmptyState(nowIso: string): BillingStateFile {
  return {
    version: 'ht-billing-state/v1',
    updatedAt: nowIso,
    processedEvents: {},
    accounts: {},
  }
}

function writeJsonFile(targetPath: string, data: BillingStateFile) {
  ensureParentDirectory(targetPath)
  const tempPath = `${targetPath}.tmp`
  fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8')
  fs.renameSync(tempPath, targetPath)
}

function toIsoString(now: Date): string {
  return now.toISOString()
}

function isLaterEvent(existingOccurredAt: string, nextOccurredAt: string): boolean {
  return Date.parse(nextOccurredAt) > Date.parse(existingOccurredAt)
}

function readStringAtPath(root: unknown, pathSegments: Array<string | number>): string | null {
  let current: unknown = root
  for (const segment of pathSegments) {
    if (current == null) return null
    if (typeof segment === 'number') {
      if (!Array.isArray(current) || current.length <= segment) return null
      current = current[segment]
      continue
    }
    if (typeof current !== 'object' || !(segment in current)) return null
    current = (current as Record<string, unknown>)[segment]
  }

  const value = String(current || '').trim()
  return value ? value : null
}

function readRecordAtPath(root: unknown, pathSegments: Array<string | number>): Record<string, unknown> | null {
  let current: unknown = root
  for (const segment of pathSegments) {
    if (current == null) return null
    if (typeof segment === 'number') {
      if (!Array.isArray(current) || current.length <= segment) return null
      current = current[segment]
      continue
    }
    if (typeof current !== 'object' || !(segment in current)) return null
    current = (current as Record<string, unknown>)[segment]
  }

  return current && typeof current === 'object' && !Array.isArray(current)
    ? current as Record<string, unknown>
    : null
}

function readEmailFromEventData(data: Record<string, unknown>): string {
  const candidates = [
    ['customer', 'email'],
    ['customer', 'email_address'],
    ['custom_data', 'email'],
    ['custom_data', 'user_email'],
    ['custom_data', 'account_email'],
    ['custom_data', 'operator_email'],
    ['custom_data', 'hypertwist_account_email'],
    ['billing_details', 'email'],
  ]

  for (const candidate of candidates) {
    const value = readStringAtPath(data, candidate)
    const email = normalizeEmail(value)
    if (email) {
      return email
    }
  }

  return ''
}

function readAllCandidateIds(data: Record<string, unknown>, key: 'subscriptionId' | 'transactionId' | 'customerId') {
  const pathCandidates: Record<typeof key, Array<Array<string | number>>> = {
    subscriptionId: [
      ['subscription_id'],
      ['subscription', 'id'],
      ['data', 'subscription_id'],
      ['data', 'subscription', 'id'],
    ],
    transactionId: [
      ['transaction_id'],
      ['transaction', 'id'],
      ['data', 'transaction_id'],
      ['data', 'transaction', 'id'],
    ],
    customerId: [
      ['customer_id'],
      ['customer', 'id'],
      ['data', 'customer_id'],
    ],
  }

  const values = new Set<string>()
  for (const candidate of pathCandidates[key]) {
    const value = readStringAtPath(data, candidate)
    if (value) {
      values.add(value)
    }
  }
  return Array.from(values)
}

function readPlanFromCustomData(data: Record<string, unknown>, fallback: BillingPlan): BillingPlan {
  const customData = readRecordAtPath(data, ['custom_data'])
  if (!customData) {
    return fallback
  }

  return normalizePlan(
    customData.plan
      ?? customData.ht_plan
      ?? customData.hypertwist_plan
      ?? customData.account_plan,
    fallback,
  )
}

function readRoleFromCustomData(data: Record<string, unknown>, fallback: BillingRole): BillingRole {
  const customData = readRecordAtPath(data, ['custom_data'])
  if (!customData) {
    return fallback
  }

  return normalizeRole(
    customData.role
      ?? customData.ht_role
      ?? customData.hypertwist_role
      ?? customData.account_role,
    fallback,
  )
}

function resolvePlanFromItemMaps(
  data: Record<string, unknown>,
  productPlanMap: Record<string, BillingPlan>,
  pricePlanMap: Record<string, BillingPlan>,
): BillingPlan | null {
  const items = Array.isArray(data.items) ? data.items : []
  for (const item of items) {
    if (!item || typeof item !== 'object') continue
    const record = item as Record<string, unknown>
    const priceId = readStringAtPath(record, ['price', 'id']) ?? readStringAtPath(record, ['price_id'])
    if (priceId && pricePlanMap[priceId]) {
      return pricePlanMap[priceId]
    }

    const productId =
      readStringAtPath(record, ['product', 'id'])
      ?? readStringAtPath(record, ['product_id'])
      ?? readStringAtPath(record, ['price', 'product_id'])
    if (productId && productPlanMap[productId]) {
      return productPlanMap[productId]
    }
  }

  return null
}

function inferAccessStatus(eventType: string, data: Record<string, unknown>): string {
  const explicitStatus = readStringAtPath(data, ['status'])
  if (explicitStatus) {
    return explicitStatus
  }

  if (eventType === 'subscription.trialing') return 'trialing'
  if (eventType === 'subscription.activated' || eventType === 'subscription.resumed') return 'active'
  if (eventType === 'subscription.paused') return 'paused'
  if (eventType === 'subscription.canceled') return 'canceled'
  if (eventType === 'subscription.past_due' || eventType === 'transaction.past_due' || eventType === 'transaction.payment_failed') return 'past_due'
  if (eventType === 'transaction.completed' || eventType === 'transaction.paid') return 'completed'
  if (eventType === 'transaction.canceled') return 'canceled'

  return 'unknown'
}

function deriveCanDownload(plan: BillingPlan, accessStatus: string): boolean {
  if (plan === 'free') {
    return false
  }

  return accessStatus === 'active'
    || accessStatus === 'trialing'
    || accessStatus === 'completed'
    || accessStatus === 'paid'
}

function parsePaddleEvent(rawBody: string): PaddleEventEnvelope | null {
  try {
    const parsed = JSON.parse(rawBody) as Partial<PaddleEventEnvelope>
    if (
      !parsed
      || typeof parsed !== 'object'
      || typeof parsed.event_id !== 'string'
      || typeof parsed.event_type !== 'string'
      || typeof parsed.occurred_at !== 'string'
      || !parsed.data
      || typeof parsed.data !== 'object'
      || Array.isArray(parsed.data)
    ) {
      return null
    }

    return parsed as PaddleEventEnvelope
  } catch {
    return null
  }
}

function readPrimaryEventId(envelope: PaddleEventEnvelope, prefix: 'subscription' | 'transaction'): string {
  const primaryId = readStringAtPath(envelope.data, ['id'])
  return primaryId && envelope.event_type.startsWith(`${prefix}.`) ? primaryId : ''
}

function buildEventIdentityCandidates(envelope: PaddleEventEnvelope): BillingEventIdentityCandidates {
  return {
    subscriptionIds: [
      readPrimaryEventId(envelope, 'subscription'),
      ...readAllCandidateIds(envelope.data, 'subscriptionId'),
    ].filter((value): value is string => Boolean(value)),
    transactionIds: [
      readPrimaryEventId(envelope, 'transaction'),
      ...readAllCandidateIds(envelope.data, 'transactionId'),
    ].filter((value): value is string => Boolean(value)),
    customerIds: readAllCandidateIds(envelope.data, 'customerId'),
  }
}

function findExistingAccount(
  state: BillingStateFile,
  email: string,
  ids: BillingEventIdentityCandidates,
): BillingEntitlementState | null {
  if (email && state.accounts[email]) {
    return state.accounts[email]
  }

  for (const account of Object.values(state.accounts)) {
    if (
      (account.subscriptionId && ids.subscriptionIds.includes(account.subscriptionId))
      || (account.transactionId && ids.transactionIds.includes(account.transactionId))
      || (account.customerId && ids.customerIds.includes(account.customerId))
    ) {
      return account
    }
  }

  return null
}

function markProcessedEvent(
  state: BillingStateFile,
  envelope: PaddleEventEnvelope,
  saveState: (state: BillingStateFile) => void,
) {
  state.processedEvents[envelope.event_id] = envelope.occurred_at
  saveState(state)
}

function buildNextEntitlementState({
  envelope,
  existingAccount,
  ids,
  resolvedEmail,
  runtime,
}: {
  envelope: PaddleEventEnvelope
  existingAccount: BillingEntitlementState | null
  ids: BillingEventIdentityCandidates
  resolvedEmail: string
  runtime: Pick<BillingStateStoreRuntime, 'defaultPlan' | 'defaultRole' | 'productPlanMap' | 'pricePlanMap' | 'now'>
}): BillingEntitlementState {
  const planFromItems = resolvePlanFromItemMaps(envelope.data, runtime.productPlanMap, runtime.pricePlanMap)
  const nextPlan = planFromItems
    ?? readPlanFromCustomData(envelope.data, existingAccount?.plan ?? runtime.defaultPlan)
  const nextRole = readRoleFromCustomData(envelope.data, existingAccount?.role ?? runtime.defaultRole)
  const accessStatus = inferAccessStatus(envelope.event_type, envelope.data)

  return {
    email: resolvedEmail,
    plan: nextPlan,
    role: nextRole,
    canDownload: deriveCanDownload(nextPlan, accessStatus),
    accessStatus,
    source: 'paddle',
    customerId: ids.customerIds[0] || existingAccount?.customerId || null,
    subscriptionId: ids.subscriptionIds[0] || existingAccount?.subscriptionId || null,
    transactionId: ids.transactionIds[0] || existingAccount?.transactionId || null,
    lastEventId: envelope.event_id,
    lastEventType: envelope.event_type,
    lastEventAt: envelope.occurred_at,
    updatedAt: toIsoString(runtime.now()),
  }
}

function buildManualEntitlementState({
  existingAccount,
  input,
  nowIso,
}: {
  existingAccount: BillingEntitlementState | null
  input: ManualBillingEntitlementInput
  nowIso: string
}): BillingEntitlementState {
  const normalizedEmail = normalizeEmail(input.email)
  const normalizedPlan = normalizePlan(input.plan, 'free')
  const normalizedRole = normalizeRole(input.role, 'viewer')
  const resolvedCanDownload =
    typeof input.canDownload === 'boolean'
      ? input.canDownload
      : normalizedPlan !== 'free'
  const resolvedAccessStatus = String(
    input.accessStatus
      || (resolvedCanDownload ? 'active' : normalizedPlan === 'free' ? 'free' : 'manual-review'),
  ).trim().toLowerCase()
  const manualEventId = `manual:${normalizedEmail}:${Date.parse(nowIso)}`

  return {
    email: normalizedEmail,
    plan: normalizedPlan,
    role: normalizedRole,
    canDownload: resolvedCanDownload,
    accessStatus: resolvedAccessStatus,
    source: 'manual',
    customerId: existingAccount?.customerId || null,
    subscriptionId: existingAccount?.subscriptionId || null,
    transactionId: existingAccount?.transactionId || null,
    lastEventId: manualEventId,
    lastEventType: 'manual.entitlement.set',
    lastEventAt: nowIso,
    updatedAt: nowIso,
  }
}

function applyParsedPaddleEvent(
  envelope: PaddleEventEnvelope,
  runtime: BillingStateStoreRuntime,
): BillingEventApplyResult {
  const state = runtime.loadState()
  if (state.processedEvents[envelope.event_id]) {
    return {
      kind: 'duplicate',
      reason: 'duplicate_event',
      eventId: envelope.event_id,
      eventType: envelope.event_type,
    }
  }

  const email = readEmailFromEventData(envelope.data)
  const ids = buildEventIdentityCandidates(envelope)
  const existingAccount = findExistingAccount(state, email, ids)

  if (existingAccount && !isLaterEvent(existingAccount.lastEventAt, envelope.occurred_at)) {
    markProcessedEvent(state, envelope, runtime.saveState)
    return {
      kind: 'ignored',
      reason: 'stale_event',
      eventId: envelope.event_id,
      eventType: envelope.event_type,
      email: existingAccount.email,
    }
  }

  const resolvedEmail = email || existingAccount?.email || ''
  if (!resolvedEmail) {
    markProcessedEvent(state, envelope, runtime.saveState)
    return {
      kind: 'noop',
      reason: 'unresolved_account',
      eventId: envelope.event_id,
      eventType: envelope.event_type,
    }
  }

  state.accounts[resolvedEmail] = buildNextEntitlementState({
    envelope,
    existingAccount,
    ids,
    resolvedEmail,
    runtime,
  })
  markProcessedEvent(state, envelope, runtime.saveState)
  return {
    kind: 'applied',
    reason: 'processed',
    eventId: envelope.event_id,
    eventType: envelope.event_type,
    email: resolvedEmail,
  }
}

export function createBillingStateStore({
  statePath,
  defaultPlan,
  defaultRole,
  productPlanMap = {},
  pricePlanMap = {},
  now = () => new Date(),
}: BillingStateStoreOptions) {
  function loadState(): BillingStateFile {
    const loaded = readJsonFile(statePath)
    if (!loaded || loaded.version !== 'ht-billing-state/v1') {
      return createEmptyState(toIsoString(now()))
    }
    return loaded
  }

  function saveState(state: BillingStateFile) {
    state.updatedAt = toIsoString(now())
    writeJsonFile(statePath, state)
  }

  function getEntitlementByEmail(email: string): BillingEntitlementState | null {
    const normalizedEmail = normalizeEmail(email)
    if (!normalizedEmail) {
      return null
    }

    const state = loadState()
    return state.accounts[normalizedEmail] || null
  }

  function applyVerifiedPaddleEvent(rawBody: string): BillingEventApplyResult {
    const envelope = parsePaddleEvent(rawBody)
    if (!envelope) {
      return {
        kind: 'noop',
        reason: 'unsupported_payload',
        eventId: 'unknown',
        eventType: 'unknown',
      }
    }

    return applyParsedPaddleEvent(envelope, {
      defaultPlan,
      defaultRole,
      productPlanMap,
      pricePlanMap,
      now,
      loadState,
      saveState,
    })
  }

  function getProcessedEventCount() {
    return Object.keys(loadState().processedEvents).length
  }

  function setManualEntitlement(input: ManualBillingEntitlementInput): BillingEntitlementState {
    const normalizedEmail = normalizeEmail(input.email)
    if (!normalizedEmail) {
      throw new Error('Manual entitlement email is required.')
    }

    const state = loadState()
    const nowIso = toIsoString(now())
    const existingAccount = state.accounts[normalizedEmail] || null
    const nextState = buildManualEntitlementState({
      existingAccount,
      input: {
        ...input,
        email: normalizedEmail,
      },
      nowIso,
    })

    state.accounts[normalizedEmail] = nextState
    saveState(state)
    return nextState
  }

  return {
    applyVerifiedPaddleEvent,
    getEntitlementByEmail,
    getProcessedEventCount,
    getStatePath: () => statePath,
    setManualEntitlement,
  }
}
