import type { PrismaClient, RoundStatus } from '@prisma/client'
import { createNotification, NotificationTypes } from './in-app-notification'
import { getPresignedUrl } from '@/lib/minio'

export type FinalDocRequirement = {
  id: string
  name: string
  acceptedMimeTypes: string[]
  isRequired: boolean
  uploaded: boolean
  file: { id: string; fileName: string; mimeType: string; bucket: string; objectKey: string; createdAt: Date } | null
}

export type FinalDocumentStatus = {
  roundId: string
  roundName: string
  deadline: Date | null
  deadlinePassed: boolean
  requirements: FinalDocRequirement[]
  allRequiredUploaded: boolean
  hasRequired: boolean // any slot is marked required
  allUploaded: boolean // every listed slot has a file (false when no slots exist)
}

// A LIVE_FINAL round is "open for documents" during the lead-up — while it is
// DRAFT (not yet opened for the live event) or ACTIVE — but not once CLOSED/
// finalized. This decouples document upload + judge review from the ceremony
// actually starting, so the Grand Final round can stay DRAFT until event time.
const OPEN_FINALE_STATUS: RoundStatus[] = ['ROUND_DRAFT', 'ROUND_ACTIVE']

/** Resolve the program's LIVE_FINAL round while it is open for documents (DRAFT or ACTIVE, not closed/finalized), or null. */
export async function getOpenFinaleRound(prisma: PrismaClient, programId: string) {
  return prisma.round.findFirst({
    where: { competition: { programId }, roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null },
    orderBy: { sortOrder: 'desc' },
    select: { id: true, name: true, windowCloseAt: true, configJson: true },
  })
}

/**
 * Whether finalist teams are allowed to upload *revised* documents for the
 * grand final. This is an admin toggle on the LIVE_FINAL round's configJson.
 * When false (default), judges still see the teams' existing prior-round
 * submissions, but teams are not prompted/able to upload anything new.
 */
export function finalistUploadsEnabled(configJson: unknown): boolean {
  return !!(configJson as { allowFinalistRevisedUploads?: boolean } | null)?.allowFinalistRevisedUploads
}

/**
 * Which prior-round FileRequirement ids are visible to finale judges.
 * null = no curation (show all prior files). Empty array = hide all prior
 * files (Grand Final round uploads are always shown regardless).
 */
export function reviewVisibleRequirementIds(configJson: unknown): string[] | null {
  const v = (configJson as { reviewVisibleRequirementIds?: unknown } | null)?.reviewVisibleRequirementIds
  return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : null
}

/**
 * Per-project grand-final document status. Returns null unless the project is
 * enrolled (ProjectRoundState) in the program's active LIVE_FINAL round.
 */
export async function getFinalDocumentStatusForProject(
  prisma: PrismaClient,
  projectId: string,
): Promise<FinalDocumentStatus | null> {
  const project = await prisma.project.findUnique({
    where: { id: projectId },
    select: { id: true, programId: true },
  })
  if (!project) return null

  const round = await getOpenFinaleRound(prisma, project.programId)
  // Banner / upload status only applies when the admin has enabled revised uploads.
  if (!round || !finalistUploadsEnabled(round.configJson)) return null

  const enrolled = await prisma.projectRoundState.findFirst({
    where: { projectId, roundId: round.id },
    select: { id: true },
  })
  if (!enrolled) return null

  const requirements = await prisma.fileRequirement.findMany({
    where: { roundId: round.id },
    orderBy: { sortOrder: 'asc' },
    select: { id: true, name: true, acceptedMimeTypes: true, isRequired: true },
  })

  const files = await prisma.projectFile.findMany({
    where: { projectId, roundId: round.id, requirementId: { in: requirements.map((r) => r.id) } },
    orderBy: { createdAt: 'desc' },
    select: { id: true, requirementId: true, fileName: true, mimeType: true, bucket: true, objectKey: true, createdAt: true },
  })
  const fileByReq = new Map<string, (typeof files)[number]>()
  for (const f of files) if (f.requirementId && !fileByReq.has(f.requirementId)) fileByReq.set(f.requirementId, f)

  const reqStatuses: FinalDocRequirement[] = requirements.map((r) => {
    const f = fileByReq.get(r.id) ?? null
    return {
      id: r.id, name: r.name, acceptedMimeTypes: r.acceptedMimeTypes, isRequired: r.isRequired,
      uploaded: !!f,
      file: f ? { id: f.id, fileName: f.fileName, mimeType: f.mimeType, bucket: f.bucket, objectKey: f.objectKey, createdAt: f.createdAt } : null,
    }
  })

  const required = reqStatuses.filter((r) => r.isRequired)
  const allRequiredUploaded = required.length > 0 && required.every((r) => r.uploaded)
  const hasRequired = required.length > 0
  const allUploaded = reqStatuses.length > 0 && reqStatuses.every((r) => r.uploaded)
  const deadline = round.windowCloseAt ?? null
  return {
    roundId: round.id,
    roundName: round.name,
    deadline,
    deadlinePassed: deadline ? new Date() > deadline : false,
    requirements: reqStatuses,
    allRequiredUploaded,
    hasRequired,
    allUploaded,
  }
}

/** Build the reminder notification payload for one finalist team lead. */
async function remindTeam(
  prisma: PrismaClient,
  args: { projectId: string; projectTitle: string; deadline: Date | null; missing: string[]; leadUserId: string },
) {
  await createNotification({
    userId: args.leadUserId,
    type: NotificationTypes.GRAND_FINAL_DOCS_REMINDER,
    title: 'Upload your Grand Final documents',
    message: args.missing.length
      ? `Still needed for "${args.projectTitle}": ${args.missing.join(', ')}.`
      : `Please upload the final documents for "${args.projectTitle}".`,
    linkUrl: `/applicant/documents`, // relative → client-side nav in-app; email renderer absolutizes
    metadata: {
      projectId: args.projectId,
      projectTitle: args.projectTitle,
      deadline: args.deadline?.toISOString(),
      missing: args.missing,
    },
  })
}

/**
 * Manual admin reminder blast. Targets `projectIds` if given, else all finalist
 * teams (enrolled in the active LIVE_FINAL round) with missing required docs.
 */
export async function sendManualFinalDocReminders(
  prisma: PrismaClient,
  opts: { programId: string; projectIds?: string[]; actorId: string },
): Promise<{ sent: number }> {
  const round = await getOpenFinaleRound(prisma, opts.programId)
  if (!round || !finalistUploadsEnabled(round.configJson)) return { sent: 0 }

  const states = await prisma.projectRoundState.findMany({
    where: { roundId: round.id, ...(opts.projectIds ? { projectId: { in: opts.projectIds } } : {}) },
    select: { projectId: true },
  })

  let sent = 0
  for (const { projectId } of states) {
    const status = await getFinalDocumentStatusForProject(prisma, projectId)
    if (!status) continue
    const missing = status.requirements.filter((r) => r.isRequired && !r.uploaded).map((r) => r.name)
    // When projectIds explicitly provided, send regardless; else only if missing docs.
    if (!opts.projectIds && missing.length === 0) continue

    const project = await prisma.project.findUnique({
      where: { id: projectId },
      select: { title: true, teamMembers: { where: { role: 'LEAD' }, take: 1, select: { userId: true } } },
    })
    const leadUserId = project?.teamMembers[0]?.userId
    if (!project || !leadUserId) continue

    await remindTeam(prisma, {
      projectId, projectTitle: project.title, deadline: status.deadline, missing, leadUserId,
    })
    sent++
  }
  return { sent }
}

/**
 * Cron: remind finalist teams (enrolled in an active LIVE_FINAL round) with
 * missing required documents, once, when the deadline is within the configured
 * window. Stamps FinalistConfirmation.finalDocsReminderSentAt.
 */
export async function sendDueFinalDocReminders(prisma: PrismaClient): Promise<{ remindersSent: number }> {
  const now = new Date()
  const rounds = await prisma.round.findMany({
    where: { roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null },
    select: { id: true, windowCloseAt: true, configJson: true, competition: { select: { programId: true } } },
  })

  let remindersSent = 0
  for (const round of rounds) {
    if (!round.windowCloseAt) continue
    // Only chase teams to upload when the admin has enabled revised uploads.
    if (!finalistUploadsEnabled(round.configJson)) continue
    const cfg = (round.configJson ?? {}) as { finalDocsReminderHoursBeforeDeadline?: number }
    const windowMs = (cfg.finalDocsReminderHoursBeforeDeadline ?? 48) * 3_600_000
    const isDue = round.windowCloseAt.getTime() <= now.getTime() + windowMs && round.windowCloseAt.getTime() > now.getTime()
    if (!isDue) continue

    const states = await prisma.projectRoundState.findMany({ where: { roundId: round.id }, select: { projectId: true } })
    for (const { projectId } of states) {
      const confirmation = await prisma.finalistConfirmation.findFirst({
        where: { projectId, finalDocsReminderSentAt: null },
        select: { id: true },
      })
      if (!confirmation) continue
      const status = await getFinalDocumentStatusForProject(prisma, projectId)
      if (!status) continue
      const missing = status.requirements.filter((r) => r.isRequired && !r.uploaded).map((r) => r.name)
      if (missing.length === 0) continue

      const project = await prisma.project.findUnique({
        where: { id: projectId },
        select: { title: true, teamMembers: { where: { role: 'LEAD' }, take: 1, select: { userId: true } } },
      })
      const leadUserId = project?.teamMembers[0]?.userId
      if (!project || !leadUserId) continue

      try {
        await remindTeam(prisma, { projectId, projectTitle: project.title, deadline: status.deadline, missing, leadUserId })
        await prisma.finalistConfirmation.update({ where: { id: confirmation.id }, data: { finalDocsReminderSentAt: new Date() } })
        remindersSent++
      } catch (e) {
        console.error('[final-docs] reminder failed for', projectId, e)
      }
    }
  }
  return { remindersSent }
}

export type ReviewFile = {
  id: string
  fileName: string
  mimeType: string
  url: string
  docLabel: string // requirement name if known, else a humanized file type
  roundLabel: string // which round this was submitted in
  roundSort: number
  isFinaleUpload: boolean // uploaded directly to the LIVE_FINAL round (a revised "final")
  createdAt: Date
}
export type ReviewTeam = { projectId: string; teamName: string; category: string | null; files: ReviewFile[] }
export type ReviewPayload = { round: { id: string; name: string; deadline: Date | null }; totalCount: number; teams: ReviewTeam[] }

function humanizeFileType(t: string | null | undefined): string {
  switch (t) {
    case 'EXEC_SUMMARY': return 'Executive Summary'
    case 'BUSINESS_PLAN': return 'Business Plan'
    case 'PRESENTATION': return 'Presentation'
    case 'VIDEO_PITCH': return 'Video Pitch'
    case 'VIDEO': return 'Video'
    case 'SUPPORTING_DOC': return 'Supporting Document'
    default: return 'Document'
  }
}

/**
 * Read-only review payload for finale judges: every finalist team enrolled in
 * the program's LIVE_FINAL round, with ALL of their submitted files across every
 * round (pitch deck, executive summary, business plan, videos, plus any revised
 * finals uploads). Each file carries a server-generated GET presigned URL (1h)
 * so finale judges — who are not assignment-gated through file.getDownloadUrl —
 * can open the documents directly. This is NOT gated on the upload toggle:
 * judges can always review the teams' existing submissions.
 */
export async function listFinalistDocumentsForReview(prisma: PrismaClient, programId: string): Promise<ReviewPayload> {
  const round = await getOpenFinaleRound(prisma, programId)
  if (!round) return { round: { id: '', name: '', deadline: null }, totalCount: 0, teams: [] }

  // Admin curation: which prior-round documents judges may see (null = all).
  const visibleIds = reviewVisibleRequirementIds(round.configJson)

  const states = await prisma.projectRoundState.findMany({
    where: { roundId: round.id },
    select: { project: { select: { id: true, title: true, teamName: true, competitionCategory: true } } },
  })
  const projectIds = states.map((s) => s.project.id)

  // Every file these teams have submitted, in any round.
  const allFiles = await prisma.projectFile.findMany({
    where: { projectId: { in: projectIds } },
    orderBy: { createdAt: 'desc' },
    select: {
      id: true, projectId: true, fileName: true, mimeType: true, fileType: true, requirementId: true,
      bucket: true, objectKey: true, createdAt: true, roundId: true,
      requirement: { select: { name: true, round: { select: { name: true, sortOrder: true } } } },
    },
  })

  // Resolve round names for files attached directly to a round (no requirement).
  const directRoundIds = [...new Set(allFiles.filter((f) => f.roundId && !f.requirement).map((f) => f.roundId!))]
  const directRounds = directRoundIds.length
    ? await prisma.round.findMany({ where: { id: { in: directRoundIds } }, select: { id: true, name: true, sortOrder: true } })
    : []
  const roundById = new Map(directRounds.map((r) => [r.id, r]))

  const filesByProject = new Map<string, ReviewFile[]>()
  for (const f of allFiles) {
    const isFinaleUpload = f.roundId === round.id
    // Curated mode: prior-round files must match a selected requirement; finale uploads always pass.
    if (!isFinaleUpload && visibleIds !== null && (!f.requirementId || !visibleIds.includes(f.requirementId))) continue
    const r = f.requirement?.round ?? (f.roundId ? roundById.get(f.roundId) : null)
    const rf: ReviewFile = {
      id: f.id,
      fileName: f.fileName,
      mimeType: f.mimeType,
      url: await getPresignedUrl(f.bucket, f.objectKey, 'GET', 3600),
      docLabel: f.requirement?.name?.trim() || humanizeFileType(f.fileType),
      roundLabel: r?.name ?? '—',
      roundSort: r?.sortOrder ?? -1,
      isFinaleUpload,
      createdAt: f.createdAt,
    }
    const list = filesByProject.get(f.projectId)
    if (list) list.push(rf)
    else filesByProject.set(f.projectId, [rf])
  }

  const teams: ReviewTeam[] = states.map(({ project }) => ({
    projectId: project.id,
    teamName: project.teamName || project.title,
    category: project.competitionCategory,
    files: (filesByProject.get(project.id) ?? []).sort(
      (a, b) => b.roundSort - a.roundSort || a.docLabel.localeCompare(b.docLabel),
    ),
  }))

  teams.sort((a, b) => (a.category || '').localeCompare(b.category || '') || a.teamName.localeCompare(b.teamName))
  return { round: { id: round.id, name: round.name, deadline: round.windowCloseAt ?? null }, totalCount: teams.length, teams }
}

export type ReviewDocSlot = {
  requirementId: string
  name: string
  roundName: string
  roundSort: number
  fileCount: number
}

/**
 * Distinct prior-round document slots (FileRequirements) that the finalist
 * teams have files for — the options offered in the admin "documents shown to
 * judges" picker. Excludes the finale round's own slots (those uploads are
 * always visible to judges) and files without a requirement.
 */
export async function listReviewVisibilityOptions(prisma: PrismaClient, programId: string): Promise<ReviewDocSlot[]> {
  const round = await getOpenFinaleRound(prisma, programId)
  if (!round) return []
  const states = await prisma.projectRoundState.findMany({ where: { roundId: round.id }, select: { projectId: true } })
  const files = await prisma.projectFile.findMany({
    where: { projectId: { in: states.map((s) => s.projectId) }, requirement: { roundId: { not: round.id } } },
    select: {
      requirementId: true,
      requirement: { select: { name: true, round: { select: { name: true, sortOrder: true } } } },
    },
  })
  const slots = new Map<string, ReviewDocSlot>()
  for (const f of files) {
    if (!f.requirementId || !f.requirement) continue
    const existing = slots.get(f.requirementId)
    if (existing) existing.fileCount++
    else slots.set(f.requirementId, {
      requirementId: f.requirementId,
      name: f.requirement.name.trim(),
      roundName: f.requirement.round.name,
      roundSort: f.requirement.round.sortOrder,
      fileCount: 1,
    })
  }
  return [...slots.values()].sort((a, b) => a.roundSort - b.roundSort || a.name.localeCompare(b.name))
}

/** True if user is admin or a member of the program's open LIVE_FINAL jury group (DRAFT or ACTIVE). */
export async function userCanReviewFinals(prisma: PrismaClient, userId: string, userRole: string, programId: string): Promise<boolean> {
  if (userRole === 'SUPER_ADMIN' || userRole === 'PROGRAM_ADMIN') return true
  const round = await prisma.round.findFirst({
    where: { competition: { programId }, roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null },
    orderBy: { sortOrder: 'desc' },
    select: { juryGroupId: true },
  })
  if (!round?.juryGroupId) return false
  const member = await prisma.juryGroupMember.findFirst({ where: { juryGroupId: round.juryGroupId, userId }, select: { id: true } })
  return !!member
}
