/**
 * AI-Powered Assignment Service
 *
 * Uses GPT to analyze juror expertise and project requirements
 * to generate optimal assignment suggestions.
 */

import { getOpenAI, getConfiguredModel } from '@/lib/openai'
import {
  anonymizeForAI,
  deanonymizeResults,
  validateAnonymization,
  type AnonymizationResult,
} from './anonymization'

// Types for AI assignment
export interface AIAssignmentSuggestion {
  jurorId: string
  projectId: string
  confidenceScore: number // 0-1
  reasoning: string
  expertiseMatchScore: number // 0-1
}

export interface AIAssignmentResult {
  success: boolean
  suggestions: AIAssignmentSuggestion[]
  error?: string
  tokensUsed?: number
  fallbackUsed?: boolean
}

interface JurorForAssignment {
  id: string
  name?: string | null
  email: string
  expertiseTags: string[]
  maxAssignments?: number | null
  _count?: {
    assignments: number
  }
}

interface ProjectForAssignment {
  id: string
  title: string
  description?: string | null
  tags: string[]
  teamName?: string | null
  _count?: {
    assignments: number
  }
}

interface AssignmentConstraints {
  requiredReviewsPerProject: number
  maxAssignmentsPerJuror?: number
  existingAssignments: Array<{
    jurorId: string
    projectId: string
  }>
}

/**
 * System prompt for AI assignment
 */
const ASSIGNMENT_SYSTEM_PROMPT = `You are an expert at matching jury members to projects based on expertise alignment.

Your task is to suggest optimal juror-project assignments that:
1. Match juror expertise tags with project tags and content
2. Distribute workload fairly among jurors
3. Ensure each project gets the required number of reviews
4. Avoid assigning jurors who are already at their limit

For each suggestion, provide:
- A confidence score (0-1) based on how well the juror's expertise matches the project
- An expertise match score (0-1) based purely on tag/content alignment
- A brief reasoning explaining why this is a good match

Return your response as a JSON array of assignments.`

/**
 * Generate AI-powered assignment suggestions
 */
export async function generateAIAssignments(
  jurors: JurorForAssignment[],
  projects: ProjectForAssignment[],
  constraints: AssignmentConstraints
): Promise<AIAssignmentResult> {
  // Anonymize data before sending to AI
  const anonymizedData = anonymizeForAI(jurors, projects)

  // Validate anonymization
  if (!validateAnonymization(anonymizedData)) {
    console.error('Anonymization validation failed, falling back to algorithm')
    return generateFallbackAssignments(jurors, projects, constraints)
  }

  try {
    const openai = await getOpenAI()

    if (!openai) {
      console.log('OpenAI not configured, using fallback algorithm')
      return generateFallbackAssignments(jurors, projects, constraints)
    }

    const suggestions = await callAIForAssignments(
      openai,
      anonymizedData,
      constraints
    )

    // De-anonymize results
    const deanonymizedSuggestions = deanonymizeResults(
      suggestions.map((s) => ({
        ...s,
        jurorId: s.jurorId,
        projectId: s.projectId,
      })),
      anonymizedData.jurorMappings,
      anonymizedData.projectMappings
    ).map((s) => ({
      jurorId: s.realJurorId,
      projectId: s.realProjectId,
      confidenceScore: s.confidenceScore,
      reasoning: s.reasoning,
      expertiseMatchScore: s.expertiseMatchScore,
    }))

    return {
      success: true,
      suggestions: deanonymizedSuggestions,
      fallbackUsed: false,
    }
  } catch (error) {
    console.error('AI assignment failed, using fallback:', error)
    return generateFallbackAssignments(jurors, projects, constraints)
  }
}

/**
 * Call OpenAI API for assignment suggestions
 */
async function callAIForAssignments(
  openai: Awaited<ReturnType<typeof getOpenAI>>,
  anonymizedData: AnonymizationResult,
  constraints: AssignmentConstraints
): Promise<AIAssignmentSuggestion[]> {
  if (!openai) {
    throw new Error('OpenAI client not available')
  }

  // Build the user prompt
  const userPrompt = buildAssignmentPrompt(anonymizedData, constraints)

  const model = await getConfiguredModel()

  const response = await openai.chat.completions.create({
    model,
    messages: [
      { role: 'system', content: ASSIGNMENT_SYSTEM_PROMPT },
      { role: 'user', content: userPrompt },
    ],
    response_format: { type: 'json_object' },
    temperature: 0.3, // Lower temperature for more consistent results
    max_tokens: 4000,
  })

  const content = response.choices[0]?.message?.content

  if (!content) {
    throw new Error('No response from AI')
  }

  // Parse the response
  const parsed = JSON.parse(content) as {
    assignments: Array<{
      juror_id: string
      project_id: string
      confidence_score: number
      expertise_match_score: number
      reasoning: string
    }>
  }

  return (parsed.assignments || []).map((a) => ({
    jurorId: a.juror_id,
    projectId: a.project_id,
    confidenceScore: Math.min(1, Math.max(0, a.confidence_score)),
    expertiseMatchScore: Math.min(1, Math.max(0, a.expertise_match_score)),
    reasoning: a.reasoning,
  }))
}

/**
 * Build the prompt for AI assignment
 */
function buildAssignmentPrompt(
  data: AnonymizationResult,
  constraints: AssignmentConstraints
): string {
  const { jurors, projects } = data

  // Map existing assignments to anonymous IDs
  const jurorIdMap = new Map(
    data.jurorMappings.map((m) => [m.realId, m.anonymousId])
  )
  const projectIdMap = new Map(
    data.projectMappings.map((m) => [m.realId, m.anonymousId])
  )

  const anonymousExisting = constraints.existingAssignments
    .map((a) => ({
      jurorId: jurorIdMap.get(a.jurorId),
      projectId: projectIdMap.get(a.projectId),
    }))
    .filter((a) => a.jurorId && a.projectId)

  return `## Jurors Available
${JSON.stringify(jurors, null, 2)}

## Projects to Assign
${JSON.stringify(projects, null, 2)}

## Constraints
- Each project needs ${constraints.requiredReviewsPerProject} reviews
- Maximum assignments per juror: ${constraints.maxAssignmentsPerJuror || 'No limit'}
- Existing assignments to avoid duplicating:
${JSON.stringify(anonymousExisting, null, 2)}

## Instructions
Generate optimal juror-project assignments. Return a JSON object with an "assignments" array where each assignment has:
- juror_id: The anonymous juror ID
- project_id: The anonymous project ID
- confidence_score: 0-1 confidence in this match
- expertise_match_score: 0-1 expertise alignment score
- reasoning: Brief explanation (1-2 sentences)

Focus on matching expertise tags with project tags and descriptions. Distribute assignments fairly.`
}

/**
 * Fallback algorithm-based assignment when AI is unavailable
 */
export function generateFallbackAssignments(
  jurors: JurorForAssignment[],
  projects: ProjectForAssignment[],
  constraints: AssignmentConstraints
): AIAssignmentResult {
  const suggestions: AIAssignmentSuggestion[] = []
  const existingSet = new Set(
    constraints.existingAssignments.map((a) => `${a.jurorId}:${a.projectId}`)
  )

  // Track assignments per juror and project
  const jurorAssignments = new Map<string, number>()
  const projectAssignments = new Map<string, number>()

  // Initialize counts from existing assignments
  for (const assignment of constraints.existingAssignments) {
    jurorAssignments.set(
      assignment.jurorId,
      (jurorAssignments.get(assignment.jurorId) || 0) + 1
    )
    projectAssignments.set(
      assignment.projectId,
      (projectAssignments.get(assignment.projectId) || 0) + 1
    )
  }

  // Also include current assignment counts
  for (const juror of jurors) {
    const current = juror._count?.assignments || 0
    jurorAssignments.set(
      juror.id,
      Math.max(jurorAssignments.get(juror.id) || 0, current)
    )
  }

  for (const project of projects) {
    const current = project._count?.assignments || 0
    projectAssignments.set(
      project.id,
      Math.max(projectAssignments.get(project.id) || 0, current)
    )
  }

  // Sort projects by need (fewest assignments first)
  const sortedProjects = [...projects].sort((a, b) => {
    const aCount = projectAssignments.get(a.id) || 0
    const bCount = projectAssignments.get(b.id) || 0
    return aCount - bCount
  })

  // For each project, find best matching jurors
  for (const project of sortedProjects) {
    const currentProjectAssignments = projectAssignments.get(project.id) || 0
    const neededReviews = Math.max(
      0,
      constraints.requiredReviewsPerProject - currentProjectAssignments
    )

    if (neededReviews === 0) continue

    // Score all available jurors
    const scoredJurors = jurors
      .filter((juror) => {
        // Check not already assigned
        if (existingSet.has(`${juror.id}:${project.id}`)) return false

        // Check not at limit
        const currentAssignments = jurorAssignments.get(juror.id) || 0
        const maxAssignments =
          juror.maxAssignments ?? constraints.maxAssignmentsPerJuror ?? Infinity
        if (currentAssignments >= maxAssignments) return false

        return true
      })
      .map((juror) => ({
        juror,
        score: calculateExpertiseScore(juror.expertiseTags, project.tags),
        loadScore: calculateLoadScore(
          jurorAssignments.get(juror.id) || 0,
          juror.maxAssignments ?? constraints.maxAssignmentsPerJuror ?? 10
        ),
      }))
      .sort((a, b) => {
        // Combined score: 60% expertise, 40% load balancing
        const aTotal = a.score * 0.6 + a.loadScore * 0.4
        const bTotal = b.score * 0.6 + b.loadScore * 0.4
        return bTotal - aTotal
      })

    // Assign top jurors
    for (let i = 0; i < Math.min(neededReviews, scoredJurors.length); i++) {
      const { juror, score } = scoredJurors[i]

      suggestions.push({
        jurorId: juror.id,
        projectId: project.id,
        confidenceScore: score,
        expertiseMatchScore: score,
        reasoning: generateFallbackReasoning(
          juror.expertiseTags,
          project.tags,
          score
        ),
      })

      // Update tracking
      existingSet.add(`${juror.id}:${project.id}`)
      jurorAssignments.set(juror.id, (jurorAssignments.get(juror.id) || 0) + 1)
      projectAssignments.set(
        project.id,
        (projectAssignments.get(project.id) || 0) + 1
      )
    }
  }

  return {
    success: true,
    suggestions,
    fallbackUsed: true,
  }
}

/**
 * Calculate expertise match score based on tag overlap
 */
function calculateExpertiseScore(
  jurorTags: string[],
  projectTags: string[]
): number {
  if (jurorTags.length === 0 || projectTags.length === 0) {
    return 0.5 // Neutral score if no tags
  }

  const jurorTagsLower = new Set(jurorTags.map((t) => t.toLowerCase()))
  const matchingTags = projectTags.filter((t) =>
    jurorTagsLower.has(t.toLowerCase())
  )

  // Score based on percentage of project tags matched
  const matchRatio = matchingTags.length / projectTags.length

  // Boost for having expertise, even if not all match
  const hasExpertise = matchingTags.length > 0 ? 0.2 : 0

  return Math.min(1, matchRatio * 0.8 + hasExpertise)
}

/**
 * Calculate load balancing score (higher score = less loaded)
 */
function calculateLoadScore(currentLoad: number, maxLoad: number): number {
  if (maxLoad === 0) return 0
  const utilization = currentLoad / maxLoad
  return Math.max(0, 1 - utilization)
}

/**
 * Generate reasoning for fallback assignments
 */
function generateFallbackReasoning(
  jurorTags: string[],
  projectTags: string[],
  score: number
): string {
  const jurorTagsLower = new Set(jurorTags.map((t) => t.toLowerCase()))
  const matchingTags = projectTags.filter((t) =>
    jurorTagsLower.has(t.toLowerCase())
  )

  if (matchingTags.length > 0) {
    return `Expertise match: ${matchingTags.join(', ')}. Match score: ${(score * 100).toFixed(0)}%.`
  }

  if (score >= 0.5) {
    return `Assigned for workload balance. No direct expertise match but available capacity.`
  }

  return `Assigned to ensure project coverage.`
}
