import type { Access, BaseFilter, ClientUser, PayloadRequest, Where } from 'payload'

/**
 * Assignment fields on the Users collection: each holds the ids of the
 * documents a Gläubiger (role 'user') is allowed to edit.
 */
export type AssignmentField = 'groups' | 'parishes' | 'pages'

/**
 * Normalize a user's assignment field to an array of id strings.
 * The fields use maxDepth: 0 so values are plain ids, but handle
 * populated objects defensively.
 */
export const getAssignedIds = (user: unknown, field: AssignmentField): string[] => {
  const value = (user as { [K in AssignmentField]?: unknown } | null | undefined)?.[field]
  return toIdArray(value)
}

/** Normalize any relationship value (single or hasMany, id or populated doc) to id strings. */
export const toIdArray = (value: unknown): string[] => {
  if (value == null) return []
  const arr = Array.isArray(value) ? value : [value]
  return arr.map((v) =>
    typeof v === 'object' && v !== null ? String((v as { id: unknown }).id) : String(v),
  )
}

const isStaff = (user: { roles?: unknown } | null | undefined): boolean =>
  user?.roles === 'admin' || user?.roles === 'employee'

/**
 * Update access: admin/employee always; role 'user' only for documents
 * they are assigned to. The collection-level probe (no id) is granted
 * only if the user has at least one assignment.
 */
export const isStaffOrAssigned =
  (field: AssignmentField): Access =>
  ({ req: { user }, id }) => {
    if (!user) return false
    if (user.roles === 'admin' || user.roles === 'employee') return true
    if (user.roles !== 'user') return false
    const ids = getAssignedIds(user, field)
    if (ids.length === 0) return false
    if (id === undefined) return true
    return ids.includes(String(id))
  }

/**
 * Admin base filter: staff see everything (null = unfiltered); role 'user'
 * sees only assigned documents. An empty id list matches nothing.
 */
export const filterToAssigned =
  (field: AssignmentField): BaseFilter =>
  ({ req: { user } }) => {
    if (!user || user.roles !== 'user') return null
    return { id: { in: getAssignedIds(user, field) } }
  }

/**
 * admin.hidden: hide the collection from role 'user' unless they have at
 * least one assignment; never hidden for staff.
 */
export const hideUnlessAssigned =
  (field: AssignmentField) =>
  ({ user }: { user: ClientUser }): boolean => {
    if (!user || user.roles !== 'user') return false
    return getAssignedIds(user, field).length === 0
  }

/**
 * admin.hidden: hide the collection from role 'user' unless they have at
 * least one assignment in ANY of the given fields.
 */
export const hideUnlessAssignedAny =
  (fields: AssignmentField[]) =>
  ({ user }: { user: ClientUser }): boolean => {
    if (!user || user.roles !== 'user') return false
    return fields.every((field) => getAssignedIds(user, field).length === 0)
  }

/** True if at least one of the given ids is in the user's assignment field. */
export const hasAssignedMatch = (user: unknown, ids: unknown, field: AssignmentField): boolean => {
  const assigned = getAssignedIds(user, field)
  return toIdArray(ids).some((id) => assigned.includes(id))
}

/**
 * defaultValue for hasMany relationship fields: when a Gläubiger has exactly
 * one assignment, pre-fill it on new documents. Staff get no pre-fill.
 */
export const defaultToOnlyAssigned =
  (field: AssignmentField) =>
  ({ user }: { user: unknown }): string[] | undefined => {
    if ((user as { roles?: unknown } | null)?.roles !== 'user') return undefined
    const ids = getAssignedIds(user, field)
    return ids.length === 1 ? ids : undefined
  }

/**
 * Ids in `value` that are neither carried over from `previousValue` nor among
 * the user's own assignments — i.e. foreign connections the user is trying to
 * ADD. Existing foreign connections (set by staff) are not flagged.
 */
export const unassignedAdditions = (
  user: unknown,
  field: AssignmentField,
  value: unknown,
  previousValue: unknown,
): string[] => {
  const assigned = getAssignedIds(user, field)
  const previous = toIdArray(previousValue)
  return toIdArray(value).filter((id) => !previous.includes(id) && !assigned.includes(id))
}

// ---------------------------------------------------------------------------
// Worship: a worship doc has no parish field — only `location` (church).
// A Gläubiger's scope is the union of churches of their assigned parishes.
// ---------------------------------------------------------------------------

/** Resolve the user's assigned parishes to the union of their church ids. */
export const resolveAssignedChurchIds = async (
  req: PayloadRequest,
  user: unknown,
): Promise<string[]> => {
  const parishIds = getAssignedIds(user, 'parishes')
  if (parishIds.length === 0) return []
  const { docs } = await req.payload.find({
    collection: 'parish',
    where: { id: { in: parishIds } },
    depth: 0,
    pagination: false,
    select: { churches: true },
    req,
  })
  return [...new Set(docs.flatMap((doc) => toIdArray((doc as { churches?: unknown }).churches)))]
}

/**
 * Worship create access. The admin UI probes create permission without data;
 * the actual create always carries data, where `location` is enforced (and
 * additionally validated on the field for a German error message).
 */
export const canCreateAssignedWorship = (): Access =>
  async ({ req, data }) => {
    const user = req.user
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    const churchIds = await resolveAssignedChurchIds(req, user)
    if (churchIds.length === 0) return false
    if (data?.location === undefined) return true
    return churchIds.includes(String(data.location))
  }

/** Worship update/delete access: staff always; role 'user' only within their churches. */
export const canMutateAssignedWorship = (): Access =>
  async ({ req }) => {
    const user = req.user
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    const churchIds = await resolveAssignedChurchIds(req, user)
    if (churchIds.length === 0) return false
    return { location: { in: churchIds } } as Where
  }

/** Worship admin list filter: staff unfiltered; role 'user' only their churches. */
export const filterWorshipToAssigned = (): BaseFilter =>
  async ({ req }) => {
    const user = req.user
    if (!user || user.roles !== 'user') return null
    return { location: { in: await resolveAssignedChurchIds(req, user) } }
  }

// ---------------------------------------------------------------------------
// Events: connected to groups AND/OR parishes; one own match grants access.
// ---------------------------------------------------------------------------

/**
 * Where matching events with at least one assigned group OR parish.
 * `prefix: 'event.'` targets the parent event from the occurrence collection.
 * Returns false when the user has no assignments at all.
 */
export const eventMatchWhere = (user: unknown, prefix = ''): false | Where => {
  const groupIds = getAssignedIds(user, 'groups')
  const parishIds = getAssignedIds(user, 'parishes')
  const or: Where[] = []
  if (groupIds.length > 0) or.push({ [`${prefix}group`]: { in: groupIds } })
  if (parishIds.length > 0) or.push({ [`${prefix}parish`]: { in: parishIds } })
  if (or.length === 0) return false
  return { or }
}

/**
 * Event create access: staff always; role 'user' with at least one group or
 * parish assignment. Which connections the new event must carry is enforced
 * by the combined validate on the `group` field.
 */
export const canCreateAssignedEvent = (): Access =>
  ({ req: { user } }) => {
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    return (
      getAssignedIds(user, 'groups').length > 0 || getAssignedIds(user, 'parishes').length > 0
    )
  }

/** Event update/delete access: staff always; role 'user' via one-match Where. */
export const canMutateAssignedEvent = (): Access =>
  ({ req: { user } }) => {
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    return eventMatchWhere(user)
  }

/** Events admin list filter: staff unfiltered; role 'user' one-match filter. */
export const filterEventsToAssigned = (): BaseFilter =>
  ({ req: { user } }) => {
    if (!user || user.roles !== 'user') return null
    const where = eventMatchWhere(user)
    return where === false ? { id: { in: [] } } : where
  }

/** Occurrence update access: whoever may edit the parent event may toggle its occurrences. */
export const canMutateOccurrenceOfAssignedEvent = (): Access =>
  ({ req: { user } }) => {
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    return eventMatchWhere(user, 'event.')
  }

// ---------------------------------------------------------------------------
// Announcements: scoped by the `parish` field (hasMany), not by church.
// ---------------------------------------------------------------------------

/**
 * Announcement create access: staff always; role 'user' with at least one
 * parish assignment. Which parishes the new announcement may carry is
 * enforced by the validate on the `parish` field.
 */
export const canCreateAssignedAnnouncement = (): Access =>
  ({ req: { user } }) => {
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    return getAssignedIds(user, 'parishes').length > 0
  }

/** Announcement update/delete access: staff always; role 'user' only their parishes. */
export const canMutateAssignedAnnouncement = (): Access =>
  ({ req: { user } }) => {
    if (!user) return false
    if (isStaff(user)) return true
    if (user.roles !== 'user') return false
    const parishIds = getAssignedIds(user, 'parishes')
    if (parishIds.length === 0) return false
    return { parish: { in: parishIds } } as Where
  }

/** Announcements admin list filter: staff unfiltered; role 'user' only their parishes. */
export const filterAnnouncementsToAssigned = (): BaseFilter =>
  ({ req: { user } }) => {
    if (!user || user.roles !== 'user') return null
    return { parish: { in: getAssignedIds(user, 'parishes') } }
  }
