import { sql } from 'drizzle-orm'
import type { Order, OrderLine } from '@platform-modules/commerce-orders'
import type { Querier, TransactionalDatabase } from '@platform-modules/db'
import type {
  ChargeRequest,
  ChargeResult,
  DedupStore,
  IntentStore,
  PaymentProvider,
  RefundRequest,
  RefundResult,
} from '@platform-modules/billing'
import type {
  FulfillmentDbSchema,
  FulfillmentNotification,
  FulfillmentPorts,
  StorageAdapter,
} from '@platform-modules/commerce-fulfillment'
import type pg from 'pg'

export type FakeProviderMode =
  | { kind: 'requires_client_action'; clientSecret?: string; providerRef?: string }
  | { kind: 'settled'; providerRef?: string }
  | { kind: 'throw'; error?: unknown }

export interface FakePaymentProvider extends PaymentProvider {
  readonly chargeCallCount: number
  setMode(mode: FakeProviderMode): void
}

export function createFakePaymentProvider(
  initialMode: FakeProviderMode = {
    kind: 'requires_client_action',
    clientSecret: 'sec_test',
    providerRef: 'pi_test',
  },
): FakePaymentProvider {
  let mode = initialMode
  let chargeCallCount = 0

  return {
    provider: 'fake',
    emitsInvoiceOnCharge: false,

    get chargeCallCount() {
      return chargeCallCount
    },

    setMode(next) {
      mode = next
    },

    async charge(req: ChargeRequest): Promise<ChargeResult> {
      chargeCallCount += 1
      if (mode.kind === 'throw') {
        throw mode.error ?? new Error('provider charge failed')
      }
      if (mode.kind === 'settled') {
        return {
          kind: 'settled',
          chargeKey: req.chargeKey,
          providerRef: mode.providerRef ?? 'pi_settled',
          amount: req.amount,
          currency: req.currency,
        }
      }
      return {
        kind: 'requires_client_action',
        chargeKey: req.chargeKey,
        providerRef: mode.providerRef ?? 'pi_test',
        clientSecret: mode.clientSecret ?? 'sec_test',
      }
    },

    async refund(_req: RefundRequest): Promise<RefundResult> {
      return { kind: 'pending' }
    },

    async parseWebhook(): Promise<never> {
      throw new Error('fake provider does not parse webhooks')
    },
  }
}

type FakeIntent = {
  status: 'pending' | 'settled'
  providerRef?: string
  amount: number
  currency: string
}

export interface FakeIntentStore extends IntentStore {
  readonly intents: Map<string, FakeIntent>
  readonly claims: string[]
  setClaimResult(chargeKey: string, result: 'won' | 'pending' | 'settled'): void
}

export function createFakeIntentStore(opts?: { forcePending?: boolean }): FakeIntentStore {
  const intents = new Map<string, FakeIntent>()
  const forced = new Map<string, 'won' | 'pending' | 'settled'>()
  const claims: string[] = []
  const forcePending = opts?.forcePending ?? false

  return {
    intents,
    claims,
    setClaimResult(chargeKey, result) {
      forced.set(chargeKey, result)
      if (result === 'settled') {
        intents.set(chargeKey, { status: 'settled', amount: 0, currency: 'usd' })
      } else if (result === 'pending') {
        intents.set(chargeKey, { status: 'pending', amount: 0, currency: 'usd' })
      }
    },
    async claimIntent(chargeKey, intent) {
      claims.push(chargeKey)
      if (forcePending) {
        return 'pending'
      }
      const forcedResult = forced.get(chargeKey)
      if (forcedResult) {
        return forcedResult
      }
      const existing = intents.get(chargeKey)
      if (existing) {
        return existing.status === 'settled' ? 'settled' : 'pending'
      }
      intents.set(chargeKey, {
        status: 'pending',
        amount: intent.amount,
        currency: intent.currency,
      })
      return 'won'
    },
    async recordProviderRef(chargeKey, providerRef) {
      const row = intents.get(chargeKey)
      if (row) row.providerRef = providerRef
    },
    async markIntentSettled(chargeKey) {
      const row = intents.get(chargeKey)
      if (row) row.status = 'settled'
    },
    async listUnsettledIntents() {
      return [...intents.entries()]
        .filter(([, v]) => v.status === 'pending')
        .map(([chargeKey, v]) => ({
          chargeKey,
          providerRef: v.providerRef,
          amount: v.amount,
          currency: v.currency,
        }))
    },
  }
}

export interface PgDedupStore extends DedupStore {
  readonly claims: string[]
  readonly marked: string[]
}

export function createPgDedupStore(pool: pg.Pool): PgDedupStore {
  const claims: string[] = []
  const marked: string[] = []

  return {
    claims,
    marked,

    async claim(eventId: string): Promise<'won' | 'lost'> {
      claims.push(eventId)
      const res = await pool.query(
        `
        INSERT INTO webhook_events (event_id, claimed_at)
        VALUES ($1, 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
        `,
        [eventId],
      )
      return res.rowCount && res.rowCount > 0 ? 'won' : 'lost'
    },

    async markProcessed(eventId: string): Promise<void> {
      marked.push(eventId)
      await pool.query(
        `UPDATE webhook_events SET processed_at = now() WHERE event_id = $1`,
        [eventId],
      )
    },
  }
}

export interface FakeStorageAdapter extends StorageAdapter {
  readonly puts: Array<{ key: string; body: Uint8Array | ArrayBuffer }>
}

function createFakeStorage(): FakeStorageAdapter {
  const puts: Array<{ key: string; body: Uint8Array | ArrayBuffer }> = []
  return {
    puts,
    async put(key, body) {
      puts.push({ key, body })
    },
    async signedUrl(key, ttlSeconds) {
      return {
        url: `https://fake-storage.test/${encodeURIComponent(key)}`,
        expiresAt: new Date(Date.now() + ttlSeconds * 1000),
      }
    },
  }
}

export interface FakeFulfillmentPortsOptions {
  db: TransactionalDatabase<FulfillmentDbSchema>
  resolveBlobKey?: (line: OrderLine) => Promise<string>
  notify?: (notification: FulfillmentNotification) => Promise<void>
}

export interface FakeFulfillmentPorts extends FulfillmentPorts {
  readonly notifications: FulfillmentNotification[]
  readonly storage: FakeStorageAdapter
  setResolveBlobKey(fn: (line: OrderLine) => Promise<string>): void
}

export function fakeFulfillmentPorts(
  options: FakeFulfillmentPortsOptions,
): FakeFulfillmentPorts {
  const notifications: FulfillmentNotification[] = []
  const storage = createFakeStorage()
  let resolveBlobKeyFn =
    options.resolveBlobKey ?? (async (line: OrderLine) => `blob:${line.id}`)

  return {
    db: options.db,
    storage,
    get notifications() {
      return notifications
    },
    setResolveBlobKey(fn) {
      resolveBlobKeyFn = fn
    },
    resolveBlobKey: (line) => resolveBlobKeyFn(line),
    resolveShippingAddress: async (_order: Order) => {
      throw new Error('resolveShippingAddress must not be called in digital slice tests')
    },
    notify: async (notification) => {
      notifications.push(notification)
      await options.notify?.(notification)
    },
  }
}

export async function countAccessGrants(db: Querier<FulfillmentDbSchema>, orderId: string): Promise<number> {
  const res = await db.execute(sql`
    SELECT COUNT(*)::int AS count FROM access_grant WHERE order_id = ${orderId}::uuid
  `)
  const rows = (Array.isArray(res) ? res : (res as { rows?: Array<{ count: number }> }).rows) ?? []
  return Number(rows[0]?.count ?? 0)
}
