import {
  and,
  desc,
  eq,
  exists,
  lt,
  or,
  sql,
  type SQL,
} from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import { OrderValidationError } from './errors.js'
import { assembleOrder, loadOrdersChildrenBatch } from './order-assembly.js'
import { order, orderLine, type OrdersSchema } from './schema.js'
import {
  ORDER_LIST_DEFAULT_LIMIT,
  ORDER_LIST_MAX_LIMIT,
  isOrderStatus,
  type Actor,
  type Order,
  type OrderListFilter,
  type Page,
} from './types.js'

const CURSOR_SEP = '|'

/**
 * Role precedence for list scope (A6 — applied in SQL WHERE):
 * 1. isAdmin → unscoped (all orders).
 * 2. Else userId and/or vendorId → OR of buyer_user_id match and vendor line EXISTS.
 * 3. Else → empty page (no userId, vendorId, or admin).
 */
function actorHasListScope(actor: Actor): boolean {
  return actor.isAdmin === true || actor.userId !== undefined || actor.vendorId !== undefined
}

function resolveLimit(limit: number | undefined): number {
  const raw = limit ?? ORDER_LIST_DEFAULT_LIMIT
  if (!Number.isFinite(raw) || raw < 1) {
    return ORDER_LIST_DEFAULT_LIMIT
  }
  return Math.min(Math.floor(raw), ORDER_LIST_MAX_LIMIT)
}

function encodeCursor(createdAt: Date, id: string): string {
  return `${createdAt.toISOString()}${CURSOR_SEP}${id}`
}

function decodeCursor(cursor: string): { createdAt: Date; id: string } {
  const sep = cursor.lastIndexOf(CURSOR_SEP)
  if (sep <= 0) {
    throw new OrderValidationError('cursor')
  }
  const createdAtRaw = cursor.slice(0, sep)
  const id = cursor.slice(sep + 1)
  const createdAt = new Date(createdAtRaw)
  if (Number.isNaN(createdAt.getTime())) {
    throw new OrderValidationError('cursor')
  }
  return { createdAt, id }
}

function buildActorScope(q: Querier<OrdersSchema>, actor: Actor): SQL | undefined {
  if (actor.isAdmin === true) {
    return undefined
  }

  const clauses: SQL[] = []

  if (actor.userId !== undefined) {
    clauses.push(eq(order.buyerUserId, actor.userId))
  }

  if (actor.vendorId !== undefined) {
    clauses.push(
      exists(
        q
          .select({ one: sql`1` })
          .from(orderLine)
          .where(
            and(
              eq(orderLine.orderId, order.id),
              eq(orderLine.vendorId, actor.vendorId),
            ),
          ),
      ),
    )
  }

  if (clauses.length === 0) {
    return undefined
  }

  return clauses.length === 1 ? clauses[0]! : or(...clauses)!
}

function buildCursorPredicate(cursor: string): SQL {
  const { createdAt, id } = decodeCursor(cursor)
  return or(
    lt(order.createdAt, createdAt),
    and(eq(order.createdAt, createdAt), lt(order.id, id)),
  )!
}

export async function listOrders(
  q: Querier<OrdersSchema>,
  actor: Actor,
  filter: OrderListFilter = {},
): Promise<Page<Order>> {
  if (!actorHasListScope(actor)) {
    return { items: [], nextCursor: null }
  }

  if (filter.status !== undefined && !isOrderStatus(filter.status)) {
    throw new OrderValidationError('status')
  }

  const limit = resolveLimit(filter.limit)
  const actorScope = buildActorScope(q, actor)

  const predicates: SQL[] = []
  if (actorScope !== undefined) {
    predicates.push(actorScope)
  }
  if (filter.status !== undefined) {
    predicates.push(eq(order.status, filter.status))
  }
  if (filter.cursor !== undefined) {
    predicates.push(buildCursorPredicate(filter.cursor))
  }

  const whereClause = predicates.length === 0 ? undefined : and(...predicates)

  const rows = await q
    .select()
    .from(order)
    .where(whereClause)
    .orderBy(desc(order.createdAt), desc(order.id))
    .limit(limit + 1)

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

  const orderIds = pageRows.map((row) => row.id)
  const { linesByOrderId, splitsByOrderId, stepsByOrderId } =
    await loadOrdersChildrenBatch(q, orderIds)

  const items = pageRows.map((row) =>
    assembleOrder(
      row,
      linesByOrderId.get(row.id) ?? [],
      splitsByOrderId.get(row.id) ?? [],
      stepsByOrderId.get(row.id) ?? {},
    ),
  )

  const last = pageRows[pageRows.length - 1]
  const nextCursor =
    hasMore && last !== undefined ? encodeCursor(last.createdAt, last.id) : null

  return { items, nextCursor }
}
