import {pickPresent} from '@crumbs/core/collections'
import type {
  FriendshipStatus,
  PublicProfile,
} from '@crumbs/core/schemas'
import {and, eq, ilike, inArray, ne, or, sql} from 'drizzle-orm'
import {HTTPException} from 'hono/http-exception'

import {db, schema} from '../db/client'
import {keysetCursorWhere, keysetOrder, toPage} from '../lib/cursor'
import {publicImageUrl} from '../lib/source-image'
import {escapeLike} from '../lib/sql'

export type ProfileRow = {
  id: string
  handle: string
  avatarRef: string | null
  createdAt: Date
}

// the users columns that fill a ProfileRow — shared so ad-hoc selects
// cannot drift from the shape hydrateProfileRows expects
export const profileRowColumns = {
  id: schema.users.id,
  handle: schema.users.handle,
  avatarRef: schema.users.avatarRef,
  createdAt: schema.users.createdAt,
}

const toPublicProfile = (
  row: ProfileRow,
  friendCount: number,
  friendshipStatus: FriendshipStatus,
): PublicProfile => ({
  id: row.id,
  handle: row.handle,
  avatarUrl: publicImageUrl(row.avatarRef),
  createdAt: row.createdAt.toISOString(),
  friendCount,
  friendshipStatus,
})

// the friendship edge between two users, either direction
export const friendshipPairWhere = (a: string, b: string) =>
  or(
    and(
      eq(schema.friendships.requesterId, a),
      eq(schema.friendships.addresseeId, b),
    ),
    and(
      eq(schema.friendships.requesterId, b),
      eq(schema.friendships.addresseeId, a),
    ),
  )

export const areFriends = async (
  a: string,
  b: string,
): Promise<boolean> => {
  const [edge] = await db
    .select({id: schema.friendships.id})
    .from(schema.friendships)
    .where(
      and(
        eq(schema.friendships.status, 'accepted'),
        friendshipPairWhere(a, b),
      ),
    )
    .limit(1)
  return Boolean(edge)
}

// one page of a user's friends as viewer-relative public profiles. keyset
// over the friendship edge so the page order is stable as new friendships
// are added; q narrows to friends whose handle matches
export const listFriendsPage = async ({
  subjectId,
  viewerId,
  limit,
  cursor,
  q,
}: {
  subjectId: string
  viewerId: string
  limit: number
  cursor?: string
  q?: string
}): Promise<{
  friends: PublicProfile[]
  nextCursor: string | null
}> => {
  const edges = await db
    .select({
      id: schema.friendships.id,
      updatedAt: schema.friendships.updatedAt,
      requesterId: schema.friendships.requesterId,
      addresseeId: schema.friendships.addresseeId,
    })
    .from(schema.friendships)
    // the friend side of the edge, whichever direction it was made
    .innerJoin(
      schema.users,
      and(
        ne(schema.users.id, subjectId),
        or(
          eq(schema.users.id, schema.friendships.requesterId),
          eq(schema.users.id, schema.friendships.addresseeId),
        ),
      ),
    )
    .where(
      and(
        eq(schema.friendships.status, 'accepted'),
        or(
          eq(schema.friendships.requesterId, subjectId),
          eq(schema.friendships.addresseeId, subjectId),
        ),
        q
          ? ilike(schema.users.handle, `%${escapeLike(q)}%`)
          : undefined,
        keysetCursorWhere(
          schema.friendships.updatedAt,
          schema.friendships.id,
          cursor,
        ),
      ),
    )
    .orderBy(
      ...keysetOrder(
        schema.friendships.updatedAt,
        schema.friendships.id,
      ),
    )
    .limit(limit + 1)

  const {page, nextCursor} = toPage(edges, limit, (e) => ({
    createdAt: e.updatedAt,
    id: e.id,
  }))
  const otherIds = page.map((e) =>
    e.requesterId === subjectId ? e.addresseeId : e.requesterId,
  )
  if (!otherIds.length) return {friends: [], nextCursor: null}

  return {
    friends: await listProfiles(viewerId, otherIds),
    nextCursor,
  }
}

// ids → viewer-relative public profiles in caller (userIds) order,
// dropping missing users
export const listProfiles = async (
  viewerId: string,
  userIds: string[],
): Promise<PublicProfile[]> => {
  const profiles = await hydrateProfiles(viewerId, userIds)
  return pickPresent(profiles, userIds)
}

// accepted friends' user ids, either edge direction
export const listFriendIds = async (
  userId: string,
): Promise<string[]> => {
  const rows = await db
    .select({
      requesterId: schema.friendships.requesterId,
      addresseeId: schema.friendships.addresseeId,
    })
    .from(schema.friendships)
    .where(
      and(
        eq(schema.friendships.status, 'accepted'),
        or(
          eq(schema.friendships.requesterId, userId),
          eq(schema.friendships.addresseeId, userId),
        ),
      ),
    )
  return rows.map((r) =>
    r.requesterId === userId ? r.addresseeId : r.requesterId,
  )
}

// viewers may read an author's content iff self or accepted friends. the one
// guard for the app's central privacy rule; returns the resolved status for
// callers that shape their response with it
export const requireViewableAuthor = async (
  viewerId: string,
  authorId: string,
): Promise<FriendshipStatus> => {
  const status: FriendshipStatus =
    viewerId === authorId
      ? 'self'
      : ((await loadFriendshipStatuses(viewerId, [authorId])).get(
          authorId,
        ) ?? 'none')
  if (status !== 'self' && status !== 'friends')
    throw new HTTPException(403, {message: 'not_friends'})
  return status
}

const loadFriendCounts = async (
  userIds: string[],
): Promise<Map<string, number>> => {
  const out = new Map<string, number>()
  if (!userIds.length) return out
  const rows = await db
    .select({
      userId: schema.users.id,
      friendCount: sql<number>`count(${schema.friendships.id})::int`,
    })
    .from(schema.users)
    .leftJoin(
      schema.friendships,
      and(
        eq(schema.friendships.status, 'accepted'),
        or(
          eq(schema.friendships.requesterId, schema.users.id),
          eq(schema.friendships.addresseeId, schema.users.id),
        ),
      ),
    )
    .where(inArray(schema.users.id, userIds))
    .groupBy(schema.users.id)
  for (const r of rows) out.set(r.userId, r.friendCount)
  return out
}

const loadFriendshipStatuses = async (
  userId: string,
  otherIds: string[],
): Promise<Map<string, FriendshipStatus>> => {
  const out = new Map<string, FriendshipStatus>()
  if (!otherIds.length) return out
  const rows = await db
    .select()
    .from(schema.friendships)
    .where(
      or(
        and(
          eq(schema.friendships.requesterId, userId),
          inArray(schema.friendships.addresseeId, otherIds),
        ),
        and(
          eq(schema.friendships.addresseeId, userId),
          inArray(schema.friendships.requesterId, otherIds),
        ),
      ),
    )
  for (const r of rows) {
    const other =
      r.requesterId === userId ? r.addresseeId : r.requesterId
    const status: FriendshipStatus =
      r.status === 'accepted'
        ? 'friends'
        : r.requesterId === userId
          ? 'pending_outgoing'
          : 'pending_incoming'
    out.set(other, status)
  }
  return out
}

// hydrate already-selected profile rows into public profiles keyed by id: the
// one batched friend-count + viewer-relative status (self included) fetch that
// every profile-list endpoint shares. callers map their own ordered rows
// through the returned map, so ordering stays with the caller
export const hydrateProfileRows = async (
  viewerId: string,
  rows: ProfileRow[],
): Promise<Map<string, PublicProfile>> => {
  const out = new Map<string, PublicProfile>()
  if (!rows.length) return out
  const ids = rows.map((r) => r.id)
  const [counts, statuses] = await Promise.all([
    loadFriendCounts(ids),
    loadFriendshipStatuses(viewerId, ids),
  ])
  for (const r of rows) {
    out.set(
      r.id,
      toPublicProfile(
        r,
        counts.get(r.id) ?? 0,
        r.id === viewerId ? 'self' : (statuses.get(r.id) ?? 'none'),
      ),
    )
  }
  return out
}

// hydrate by id, selecting the user rows first
export const hydrateProfiles = async (
  viewerId: string,
  userIds: string[],
): Promise<Map<string, PublicProfile>> => {
  if (!userIds.length) return new Map()
  const rows = await db
    .select(profileRowColumns)
    .from(schema.users)
    .where(inArray(schema.users.id, userIds))
  return hydrateProfileRows(viewerId, rows)
}
