import { sql } from '@zync/db'
import type { Db } from '@zync/db/queries'
import {
  countPushSubscriptionsForUser,
  deletePushSubscriptionByEndpoint,
  getNotificationPreferences,
  getPushSubscriptionsForUser,
  getUserTelegramChatId,
  getUserTelegramPrefs,
} from '@zync/db/queries'
import type { DeliverableNotification, Env } from '@zync/types'
import {
  notify,
  type ChannelAdapter,
  type ChannelResult,
  type DedupStore,
  type NotifyContext,
  type NotifyEvent,
  type PreferenceStore,
  type RenderedMessage,
} from '@platform-modules/notifications'
import { sendWebPush } from './web-push/send'
import { loadAdapterCredential } from './credentials'

const DEDUP_PREFIX = 'notifications:'
const DEFAULT_DEDUP_TTL_SECONDS = 24 * 60 * 60

type NotificationDedupRow = {
  key: string
  status: string
  firstSeenAt: Date
  processedAt: Date | null
  expiresAt: Date | null
  payload: unknown
}

type TelegramRecipient = {
  chatId: string
  token: string
  buttons: DeliverableNotification['actionButtons']
}

type WebPushRecipient = {
  subscriptions: Array<{
    endpoint: string
    p256dh: string
    auth: string
  }>
  payload: {
    title: string
    body: string
    url: string
    tag: string
    type: DeliverableNotification['type']
  }
}

export type HostInjectedNotificationDedupStore = DedupStore & {
  list(): Promise<NotificationDedupRow[]>
}

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

function buildTelegramPayload(rendered: RenderedMessage, recipient: TelegramRecipient) {
  const buttons =
    recipient.buttons?.map((button) => (
      button.url
        ? { text: button.label, url: button.url }
        : { text: button.label, callback_data: button.callbackAction ?? button.label }
    )) ?? []

  return {
    chat_id: recipient.chatId,
    text: `<b>${escapeHtml(rendered.subject ?? rendered.body)}</b>\n${escapeHtml(rendered.body)}`,
    parse_mode: 'HTML' as const,
    ...(buttons.length > 0 ? { reply_markup: { inline_keyboard: [buttons] } } : {}),
  }
}

function buildNotificationDedupKey(userId: string, notification: DeliverableNotification): string {
  if (notification.entityId) {
    return `${notification.type}:${notification.entityType ?? 'items'}:${notification.entityId}:${userId}`
  }
  return `${notification.type}:${userId}:${notification.title}:${notification.body}`
}

export function createNotificationPreferenceStore(
  db: Db,
  tenantId: string,
): PreferenceStore {
  return {
    async getEnabledChannels(userId, eventType) {
      const [telegramPrefs, notificationPrefs, pushCount] = await Promise.all([
        getUserTelegramPrefs(db, userId, tenantId),
        getNotificationPreferences(db, tenantId, userId),
        countPushSubscriptionsForUser(db, userId, tenantId),
      ])

      const channels: string[] = []
      if (telegramPrefs?.telegramTypes.includes(eventType)) {
        channels.push('telegram')
      }
      if (pushCount > 0 && notificationPrefs.inApp) {
        channels.push('push')
      }
      return channels
    },
  }
}

export function createNotificationDedupStore(
  db: Db,
  now: () => Date = () => new Date(),
): HostInjectedNotificationDedupStore {
  return {
    async seen(key) {
      const rows = await db.execute<{ key: string }>(sql`
        SELECT key
        FROM job_idempotency_keys
        WHERE key = ${`${DEDUP_PREFIX}${key}`}
          AND (expires_at IS NULL OR expires_at > ${now()})
        LIMIT 1
      `)
      return rows.length > 0
    },
    async mark(key, ttl = DEFAULT_DEDUP_TTL_SECONDS) {
      const current = now()
      const expiresAt = ttl > 0 ? new Date(current.getTime() + ttl * 1000) : null
      await db.execute(sql`
        INSERT INTO job_idempotency_keys ("key", "status", "first_seen_at", "processed_at", "expires_at", "payload")
        VALUES (${`${DEDUP_PREFIX}${key}`}, 'processed', ${current}, ${current}, ${expiresAt}, null::jsonb)
        ON CONFLICT ("key") DO UPDATE
        SET status = 'processed',
            processed_at = EXCLUDED.processed_at,
            expires_at = EXCLUDED.expires_at
      `)
    },
    async list() {
      return db.execute<NotificationDedupRow>(sql`
        SELECT
          key,
          status,
          first_seen_at AS "firstSeenAt",
          processed_at AS "processedAt",
          expires_at AS "expiresAt",
          payload
        FROM job_idempotency_keys
        WHERE key LIKE ${`${DEDUP_PREFIX}%`}
          AND (expires_at IS NULL OR expires_at > ${now()})
        ORDER BY first_seen_at ASC
      `)
    },
  }
}

export function createTelegramChannelAdapter(): ChannelAdapter {
  return {
    channel: 'telegram',
    async send(rendered, recipient) {
      const target = recipient as TelegramRecipient
      const response = await fetch(`https://api.telegram.org/bot${target.token}/sendMessage`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(buildTelegramPayload(rendered, target)),
      })
      if (!response.ok) {
        return {
          ok: false,
          error: {
            message: `Telegram sendMessage failed: ${response.status} ${await response.text()}`,
            retryable: response.status >= 500,
          },
        }
      }
      return { ok: true }
    },
  }
}

export function createWebPushChannelAdapter(
  db: Db,
  env: Pick<Env, 'VAPID_PRIVATE_KEY' | 'VAPID_PUBLIC_KEY'>,
): ChannelAdapter {
  return {
    channel: 'push',
    async send(_rendered, recipient) {
      const target = recipient as WebPushRecipient
      const results = await Promise.allSettled(
        target.subscriptions.map((subscription) => sendWebPush(
          subscription,
          target.payload,
          env.VAPID_PRIVATE_KEY,
          env.VAPID_PUBLIC_KEY,
        )),
      )

      const pruneOps: Promise<void>[] = []
      let delivered = false
      let firstFailure: string | null = null

      for (const result of results) {
        if (result.status === 'fulfilled') {
          if (result.value.gone) {
            pruneOps.push(deletePushSubscriptionByEndpoint(db, result.value.endpoint))
            continue
          }
          if (result.value.status < 300) {
            delivered = true
            continue
          }
          firstFailure ??= `Web Push failed: ${result.value.status}`
          continue
        }
        firstFailure ??= result.reason instanceof Error ? result.reason.message : String(result.reason)
      }

      if (pruneOps.length > 0) {
        await Promise.allSettled(pruneOps)
      }

      if (delivered) {
        return { ok: true }
      }

      return {
        ok: false,
        error: {
          message: firstFailure ?? 'No push subscriptions',
          retryable: true,
        },
      }
    },
  }
}

export async function buildPlatformNotificationRecipients(
  db: Db,
  env: Pick<Env, 'INTEGRATION_ENCRYPTION_KEY'>,
  tenantId: string,
  userId: string,
  notification: DeliverableNotification,
): Promise<NotifyEvent['recipients']> {
  const [chatId, token, subscriptions] = await Promise.all([
    getUserTelegramChatId(db, userId, tenantId),
    loadAdapterCredential(db, tenantId, 'telegram', env.INTEGRATION_ENCRYPTION_KEY),
    getPushSubscriptionsForUser(db, userId, tenantId),
  ])

  const recipients: NotifyEvent['recipients'] = {}

  if (chatId && token) {
    recipients.telegram = {
      chatId,
      token,
      buttons: notification.actionButtons,
    } satisfies TelegramRecipient
  }

  if (subscriptions.length > 0) {
    recipients.push = {
      subscriptions: subscriptions.map((subscription) => ({
        endpoint: subscription.endpoint,
        p256dh: subscription.p256dh,
        auth: subscription.auth,
      })),
      payload: {
        title: notification.title,
        body: notification.body,
        url: notification.entityId
          ? `/${notification.entityType ?? 'items'}/${notification.entityId}`
          : '/',
        tag: `${notification.type}_${notification.entityId ?? userId}`,
        type: notification.type,
      },
    } satisfies WebPushRecipient
  }

  return recipients
}

export async function deliverPlatformNotification(
  userId: string,
  notification: DeliverableNotification,
  db: Db,
  env: Pick<Env, 'INTEGRATION_ENCRYPTION_KEY' | 'VAPID_PRIVATE_KEY' | 'VAPID_PUBLIC_KEY'>,
  tenantId: string,
  opts?: {
    dedup?: HostInjectedNotificationDedupStore
    preferences?: PreferenceStore
    retry?: NotifyContext['retry']
  },
) {
  const recipients = await buildPlatformNotificationRecipients(db, env, tenantId, userId, notification)

  return notify(
    {
      id: buildNotificationDedupKey(userId, notification),
      type: notification.type,
      userId,
      template: {
        subject: notification.title,
        body: notification.body,
      },
      data: {},
      recipients,
    },
    {
      adapters: [
        createTelegramChannelAdapter(),
        createWebPushChannelAdapter(db, env),
      ],
      preferences: opts?.preferences ?? createNotificationPreferenceStore(db, tenantId),
      dedup: opts?.dedup ?? createNotificationDedupStore(db),
      retry: opts?.retry,
    },
  )
}

export { buildTelegramPayload, buildNotificationDedupKey }
