import { and, count, desc, eq, inArray, isNull, lt, or } from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import type { ChannelAdapter, ChannelResult, RenderedMessage } from './index.js'
import { clampLimit, decodeCursor, encodeCursor } from './inbox-cursor.js'
import { notifications, type NotificationsSchema } from './inbox-schema.js'

export {
  notifications,
  notificationsSchema,
  notificationsTableSql,
  type NotificationsSchema,
} from './inbox-schema.js'
export { InvalidCursorError } from './inbox-cursor.js'

export interface NotificationRecord {
  id: string
  userId: string
  title: string
  body?: string
  href?: string
  kind?: string
  createdAt: string
  readAt: string | null
}

export interface NotificationPutInput {
  userId: string
  title: string
  body?: string
  href?: string
  kind?: string
}

export interface NotificationListQuery {
  cursor?: string
  limit?: number
  unreadOnly?: boolean
}

export interface NotificationPage {
  items: NotificationRecord[]
  nextCursor: string | null
}

export interface NotificationStore {
  put(input: NotificationPutInput): Promise<NotificationRecord>
  list(userId: string, query?: NotificationListQuery): Promise<NotificationPage>
  unreadCount(userId: string): Promise<number>
  markRead(userId: string, ids: string[] | 'all'): Promise<void>
}

export class InboxValidationError extends Error {
  override readonly name = 'InboxValidationError'
  constructor(
    readonly field: string,
    readonly detail: string,
  ) {
    super(`inbox invalid: ${field} — ${detail}`)
  }
}

const MAX_MARK_IDS = 1000

type Row = typeof notifications.$inferSelect

function deriveCreatedAt(now: () => string): { createdAtMs: number; createdAt: string } {
  const createdAtMs = Date.parse(now())
  if (Number.isNaN(createdAtMs)) {
    throw new RangeError('deriveCreatedAt: now() returned an unparseable timestamp')
  }
  return {
    createdAtMs,
    createdAt: new Date(createdAtMs).toISOString(),
  }
}

function toRecord(row: Row): NotificationRecord {
  return {
    id: row.id,
    userId: row.userId,
    title: row.title,
    ...(row.body != null ? { body: row.body } : {}),
    ...(row.href != null ? { href: row.href } : {}),
    ...(row.kind != null ? { kind: row.kind } : {}),
    createdAt: row.createdAt,
    readAt: row.readAt ?? null,
  }
}

export function createDbNotificationStore(
  db: Querier<NotificationsSchema>,
): NotificationStore {
  return {
    async put(input) {
      const id = crypto.randomUUID()
      const { createdAtMs, createdAt } = deriveCreatedAt(() => new Date().toISOString())

      await db.insert(notifications).values({
        id,
        userId: input.userId,
        title: input.title,
        body: input.body ?? null,
        href: input.href ?? null,
        kind: input.kind ?? null,
        createdAtMs,
        createdAt,
        readAt: null,
      })

      return {
        id,
        userId: input.userId,
        title: input.title,
        ...(input.body != null ? { body: input.body } : {}),
        ...(input.href != null ? { href: input.href } : {}),
        ...(input.kind != null ? { kind: input.kind } : {}),
        createdAt,
        readAt: null,
      }
    },

    async list(userId, query = {}) {
      const limit = clampLimit(query.limit)
      const filters = [eq(notifications.userId, userId)]

      if (query.unreadOnly) {
        filters.push(isNull(notifications.readAt))
      }

      if (query.cursor) {
        const key = decodeCursor(query.cursor)
        filters.push(
          or(
            lt(notifications.createdAtMs, key.createdAtMs),
            and(eq(notifications.createdAtMs, key.createdAtMs), lt(notifications.seq, key.seq))!,
          )!,
        )
      }

      const rows = await db
        .select()
        .from(notifications)
        .where(and(...filters))
        .orderBy(desc(notifications.createdAtMs), desc(notifications.seq))
        .limit(limit + 1)

      const hasMore = rows.length > limit
      const pageRows = hasMore ? rows.slice(0, limit) : rows
      const items = pageRows.map(toRecord)

      let nextCursor: string | null = null
      if (hasMore && pageRows.length > 0) {
        const last = pageRows[pageRows.length - 1]!
        nextCursor = encodeCursor({ createdAtMs: last.createdAtMs, seq: last.seq })
      }

      return { items, nextCursor }
    },

    async unreadCount(userId) {
      const [row] = await db
        .select({ n: count() })
        .from(notifications)
        .where(and(eq(notifications.userId, userId), isNull(notifications.readAt)))
      return row?.n ?? 0
    },

    async markRead(userId, ids) {
      const now = new Date().toISOString()
      if (ids === 'all') {
        await db
          .update(notifications)
          .set({ readAt: now })
          .where(and(eq(notifications.userId, userId), isNull(notifications.readAt)))
        return
      }
      if (ids.length === 0) return
      if (ids.length > MAX_MARK_IDS) {
        throw new InboxValidationError('ids', `exceeds ${MAX_MARK_IDS}`)
      }
      await db
        .update(notifications)
        .set({ readAt: now })
        .where(
          and(
            eq(notifications.userId, userId),
            inArray(notifications.id, ids),
            isNull(notifications.readAt),
          ),
        )
    },
  }
}

export type InboxRecipient = {
  userId: string
  href?: string
  kind?: string
}

/** Persists to NotificationStore — the inbox delivery channel over the shipped notify() pipeline. */
export function createInboxChannel(store: NotificationStore): ChannelAdapter {
  return {
    channel: 'inbox',
    async send(rendered: RenderedMessage, recipient: unknown): Promise<ChannelResult> {
      const r = recipient as InboxRecipient
      if (!r?.userId || typeof r.userId !== 'string') {
        return {
          ok: false,
          error: { message: 'inbox recipient missing userId', retryable: false },
        }
      }

      try {
        const record = await store.put({
          userId: r.userId,
          title: rendered.subject ?? 'notification',
          body: rendered.body,
          href: r.href,
          kind: r.kind,
        })
        return { ok: true, id: record.id }
      } catch {
        return {
          ok: false,
          error: { message: 'inbox delivery failed', retryable: true },
        }
      }
    },
  }
}
