import { createHash } from 'node:crypto'
import Member from '../models/member.js'
import Event from '../models/event.js'
import PreRegistration from '../models/preRegistration.js'
import TagSuggestion from '../models/tagSuggestion.js'
import AdminAlertDismissal, { ADMIN_ALERT_TYPES } from '../models/adminAlertDismissal.js'
import { connectDB } from './mongoose.js'

export const ALERT_THRESHOLDS = {
  NO_SLACK_DAYS: 7,
  STUCK_PAYMENT_DAYS: 7,
  PREREG_SELECTED_DAYS: 3,
  DRAFT_IMMINENT_DAYS: 14,
  NEAR_CAPACITY_RATIO: 0.8
}

// Single source of truth for alert presentation. Used by detectors AND the
// dismissed-list endpoint (which has no access to a detector run's output).
export const ALERT_METADATA = {
  slack_invite_failed: { title: 'Slack invites failed', severity: 'critical' },
  no_slack_handle_week: { title: 'Active members without a Slack handle', severity: 'attention' },
  stuck_pending_payment: { title: 'Members stuck in pending payment', severity: 'attention' },
  member_suspended: { title: 'Suspended members', severity: 'attention' },
  preregistrant_selected_not_invited: { title: 'Pre-registrants selected but not invited', severity: 'attention' },
  preregistrant_expired: { title: 'Expired pre-registrant invitations', severity: 'attention' },
  event_draft_imminent: { title: 'Draft events with imminent start', severity: 'critical' },
  event_near_capacity: { title: 'Events approaching capacity', severity: 'attention' },
  tag_suggestions_pending: { title: 'Pending tag suggestions', severity: 'attention' }
}

function alertShell(type) {
  const meta = ALERT_METADATA[type]
  return { type, severity: meta.severity, title: meta.title }
}

const DAY_MS = 24 * 60 * 60 * 1000

function daysAgo(days) {
  return new Date(Date.now() - days * DAY_MS)
}

function daysSince(date) {
  if (!date) return null
  return Math.floor((Date.now() - new Date(date).getTime()) / DAY_MS)
}

export function computeSignature(ids) {
  const normalized = ids
    .map((id) => (id == null ? '' : String(id)))
    .sort()
  const hash = createHash('sha1')
  hash.update(JSON.stringify(normalized))
  return hash.digest('hex')
}

function memberItem(member, sublabel) {
  return {
    id: String(member._id),
    label: member.name,
    sublabel: sublabel ?? member.email,
    href: `/admin/members/${member._id}`
  }
}

export async function detectSlackInviteFailed() {
  await connectDB()
  const members = await Member
    .find({ slackInviteStatus: 'failed' })
    .select('name email')
    .lean()
  return {
    ...alertShell('slack_invite_failed'),
    items: members.map((m) => memberItem(m))
  }
}

export async function detectNoSlackHandleAfterWeek() {
  await connectDB()
  const cutoff = daysAgo(ALERT_THRESHOLDS.NO_SLACK_DAYS)
  const members = await Member
    .find({
      status: 'active',
      createdAt: { $lte: cutoff },
      $or: [
        { slackUserId: { $exists: false } },
        { slackUserId: null },
        { slackUserId: '' }
      ]
    })
    .select('name email createdAt')
    .lean()
  return {
    ...alertShell('no_slack_handle_week'),
    items: members.map((m) =>
      memberItem(m, `${daysSince(m.createdAt)} days since joining`)
    )
  }
}

export async function detectStuckPendingPayment() {
  await connectDB()
  const cutoff = daysAgo(ALERT_THRESHOLDS.STUCK_PAYMENT_DAYS)
  const members = await Member
    .find({
      status: 'pending_payment',
      createdAt: { $lte: cutoff }
    })
    .select('name email createdAt')
    .lean()
  return {
    ...alertShell('stuck_pending_payment'),
    items: members.map((m) =>
      memberItem(m, `${daysSince(m.createdAt)} days stuck`)
    )
  }
}

export async function detectSuspendedMembers() {
  await connectDB()
  const members = await Member
    .find({ status: 'suspended' })
    .select('name email')
    .lean()
  return {
    ...alertShell('member_suspended'),
    items: members.map((m) => memberItem(m))
  }
}

function preRegItem(preReg, sublabel) {
  return {
    id: String(preReg._id),
    label: preReg.name || preReg.email,
    sublabel,
    href: '/admin/pre-registrants'
  }
}

export async function detectPreRegistrantSelectedNotInvited() {
  await connectDB()
  const cutoff = daysAgo(ALERT_THRESHOLDS.PREREG_SELECTED_DAYS)
  const preRegs = await PreRegistration
    .find({
      status: 'selected',
      updatedAt: { $lte: cutoff }
    })
    .select('name email updatedAt')
    .lean()
  return {
    ...alertShell('preregistrant_selected_not_invited'),
    items: preRegs.map((p) =>
      preRegItem(p, `${p.email} — ${daysSince(p.updatedAt)} days selected`)
    )
  }
}

export async function detectPreRegistrantExpired() {
  await connectDB()
  const preRegs = await PreRegistration
    .find({ status: 'expired' })
    .select('name email updatedAt')
    .lean()
  return {
    ...alertShell('preregistrant_expired'),
    items: preRegs.map((p) =>
      preRegItem(p, `${p.email} — expired ${daysSince(p.updatedAt)} days ago`)
    )
  }
}

function eventItem(ev, sublabel) {
  return {
    id: String(ev._id),
    label: ev.title,
    sublabel,
    href: '/admin/events'
  }
}

export async function detectDraftEventsImminent() {
  await connectDB()
  const now = new Date()
  const horizon = new Date(now.getTime() + ALERT_THRESHOLDS.DRAFT_IMMINENT_DAYS * DAY_MS)
  const events = await Event
    .find({
      isVisible: false,
      isCancelled: { $ne: true },
      startDate: { $gte: now, $lte: horizon }
    })
    .select('title startDate')
    .lean()
  return {
    ...alertShell('event_draft_imminent'),
    items: events.map((ev) => {
      const days = Math.max(0, Math.ceil((new Date(ev.startDate).getTime() - Date.now()) / DAY_MS))
      return eventItem(ev, `Starts in ${days} days`)
    })
  }
}

export async function detectEventsNearCapacity() {
  await connectDB()
  const now = new Date()
  const events = await Event
    .find({
      'tickets.enabled': true,
      'tickets.capacity.total': { $gt: 0 },
      isCancelled: { $ne: true },
      startDate: { $gte: now }
    })
    .select('title startDate tickets registrations')
    .lean()

  const matched = events
    .map((ev) => {
      const total = ev.tickets?.capacity?.total
      if (!total) return null
      const reserved = ev.tickets?.capacity?.reserved || 0
      const taken = (ev.registrations?.length || 0) + reserved
      const ratio = taken / total
      if (ratio < ALERT_THRESHOLDS.NEAR_CAPACITY_RATIO) return null
      return eventItem(ev, `${taken} / ${total} seats taken`)
    })
    .filter(Boolean)

  return {
    ...alertShell('event_near_capacity'),
    items: matched
  }
}

export async function detectPendingTagSuggestions() {
  await connectDB()
  const suggestions = await TagSuggestion
    .find({ status: 'pending' })
    .select('_id')
    .lean()
  if (suggestions.length === 0) {
    return {
      ...alertShell('tag_suggestions_pending'),
      items: []
    }
  }
  return {
    ...alertShell('tag_suggestions_pending'),
    items: [
      {
        id: 'tag-suggestions',
        label: `${suggestions.length} pending tag suggestion${suggestions.length === 1 ? '' : 's'}`,
        sublabel: 'Review and approve in Tags',
        href: '/admin/tags'
      }
    ],
    // signature is computed from the underlying suggestion ids, not the rendered item id
    signatureIds: suggestions.map((s) => String(s._id))
  }
}

const DETECTORS = [
  detectSlackInviteFailed,
  detectNoSlackHandleAfterWeek,
  detectStuckPendingPayment,
  detectSuspendedMembers,
  detectPreRegistrantSelectedNotInvited,
  detectPreRegistrantExpired,
  detectDraftEventsImminent,
  detectEventsNearCapacity,
  detectPendingTagSuggestions
]

function signatureForAlert(alert) {
  if (Array.isArray(alert.signatureIds)) {
    return computeSignature(alert.signatureIds)
  }
  return computeSignature(alert.items.map((item) => item.id))
}

export async function computeAllAlerts(adminId) {
  await connectDB()

  const [rawAlerts, dismissals] = await Promise.all([
    Promise.all(DETECTORS.map((fn) => fn())),
    AdminAlertDismissal.find({ adminId }).lean()
  ])

  const dismissedByType = new Map()
  for (const d of dismissals) {
    dismissedByType.set(d.alertType, d.signature)
  }

  const result = []
  for (const alert of rawAlerts) {
    if (!alert.items || alert.items.length === 0) continue
    const signature = signatureForAlert(alert)
    if (dismissedByType.get(alert.type) === signature) continue
    const { signatureIds, ...publicFields } = alert
    result.push({
      ...publicFields,
      count: alert.items.length,
      signature
    })
  }
  return result
}
