/**
 * AI-Powered Filtering Service
 *
 * Runs automated filtering rules against projects:
 * - Field-based rules (age checks, category, country, etc.)
 * - Document checks (file existence/types)
 * - AI screening (GPT interprets criteria text, flags spam)
 *
 * GDPR Compliance:
 * - All project data is anonymized before AI processing
 * - Only necessary fields sent to OpenAI
 * - No personal identifiers in prompts or responses
 */

import { getOpenAI, getConfiguredModel, buildCompletionParams } from '@/lib/openai'
import { logAIUsage, extractTokenUsage } from '@/server/utils/ai-usage'
import { classifyAIError, createParseError, logAIError } from './ai-errors'
import { sanitizeUserInput } from '@/server/services/ai-prompt-guard'
import {
  anonymizeProjectsForAI,
  validateAnonymizedProjects,
  toProjectWithRelations,
  type AnonymizedProjectForAI,
  type ProjectAIMapping,
} from './anonymization'
import type { Prisma, FileType, SubmissionSource, PrismaClient } from '@prisma/client'

// ─── Progress Callback Type ─────────────────────────────────────────────────

export type ProgressCallback = (progress: {
  currentBatch: number
  totalBatches: number
  processedCount: number
  tokensUsed?: number
}) => Promise<void>

// ─── Types ──────────────────────────────────────────────────────────────────

export type FieldRuleCondition = {
  field:
    | 'competitionCategory'
    | 'foundedAt'
    | 'country'
    | 'geographicZone'
    | 'tags'
    | 'oceanIssue'
  operator:
    | 'equals'
    | 'not_equals'
    | 'greater_than'
    | 'less_than'
    | 'contains'
    | 'in'
    | 'not_in'
    | 'older_than_years'
    | 'newer_than_years'
    | 'is_empty'
  value: string | number | string[]
}

export type FieldRuleConfig = {
  conditions: FieldRuleCondition[]
  logic: 'AND' | 'OR'
  action: 'PASS' | 'REJECT' | 'FLAG'
}

export type DocumentCheckConfig = {
  requiredFileTypes?: string[] // e.g. ['pdf', 'docx']
  minFileCount?: number
  maxPages?: number // Max pages for ANY file
  maxPagesByFileType?: Record<string, number> // e.g. { "EXECUTIVE_SUMMARY": 2, "PITCH_DECK": 10 }
  action: 'PASS' | 'REJECT' | 'FLAG'
}

export type AIScreeningConfig = {
  criteriaText: string
  action: 'PASS' | 'REJECT' | 'FLAG' | 'AUTO_FILTER' // AUTO_FILTER = reject spam/low-quality, flag borderline
  // Performance settings
  batchSize?: number // Projects per API call (1-50, default 20)
  parallelBatches?: number // Concurrent API calls (1-10, default 1)
  // AUTO_FILTER settings
  autoFilterThreshold?: number // Quality score cutoff (1-10, default 4). Scores at or below are auto-rejected.
}

export type RuleConfig = FieldRuleConfig | DocumentCheckConfig | AIScreeningConfig

export interface RuleResult {
  ruleId: string
  ruleName: string
  ruleType: string
  passed: boolean
  action: 'PASS' | 'REJECT' | 'FLAG'
  reasoning?: string
}

export interface ProjectFilteringResult {
  projectId: string
  outcome: 'PASSED' | 'FILTERED_OUT' | 'FLAGGED'
  ruleResults: RuleResult[]
  aiScreeningJson?: Record<string, unknown>
  awardMatches?: AwardMatchResult[]
}

export type AwardCriteriaInput = {
  awardId: string
  awardName: string
  criteriaText: string
}

export type AwardMatchResult = {
  awardId: string
  eligible: boolean
  confidence: number
  qualityScore: number
  reasoning: string
}

interface ProjectForFiltering {
  id: string
  title: string
  description?: string | null
  competitionCategory?: string | null
  foundedAt?: Date | null
  country?: string | null
  geographicZone?: string | null
  tags: string[]
  oceanIssue?: string | null
  wantsMentorship?: boolean | null
  institution?: string | null
  submissionSource?: SubmissionSource
  submittedAt?: Date | null
  files: Array<{
    id: string
    fileName: string
    fileType?: FileType | null
    size?: number
    pageCount?: number | null
    roundName?: string | null
    isCurrentRound?: boolean
    textContent?: string
  }>
  _count?: {
    teamMembers?: number
    files?: number
  }
}

interface FilteringRuleInput {
  id: string
  name: string
  ruleType: string
  configJson: Prisma.JsonValue
  priority: number
  isActive: boolean
}

// ─── Constants ───────────────────────────────────────────────────────────────

const DEFAULT_BATCH_SIZE = 20
const MAX_BATCH_SIZE = 50
const MIN_BATCH_SIZE = 1
const DEFAULT_PARALLEL_BATCHES = 3
const MAX_PARALLEL_BATCHES = 10

// Structured system prompt for AI screening
const AI_SCREENING_SYSTEM_PROMPT = `You are an expert project screening assistant for an ocean conservation competition.

## Your Role
Evaluate each project against the provided screening criteria. Be objective and base evaluation only on provided data. Your goal is to identify projects that clearly FAIL the criteria — not to nitpick minor issues.

## Output Format
Return a JSON object with this exact structure:
{
  "projects": [
    {
      "project_id": "PROJECT_001",
      "meets_criteria": true/false,
      "confidence": 0.0-1.0,
      "reasoning": "2-3 sentence explanation",
      "quality_score": 1-10,
      "spam_risk": true/false
    }
  ]
}

## Scoring Rubric for quality_score
- 9-10: Exceptional — clearly meets all criteria, well-documented, innovative
- 7-8: Strong — meets most criteria, minor gaps acceptable
- 5-6: Adequate — partially meets criteria, notable gaps
- 3-4: Weak — significant shortcomings against criteria
- 1-2: Poor — does not meet criteria or appears low-quality/spam

## Available Data Per Project
- category: STARTUP or BUSINESS_CONCEPT
- country, region: geographic location (use for regional considerations)
- founded_year: when the company/initiative was founded (use for age checks)
- ocean_issue: the ocean conservation area
- file_count, file_types: uploaded documents summary
- files[]: per-file details (see File Type Reference below) with file_type, page_count (if known), size_kb, detected_lang (ISO 639-3 language code like 'eng', 'fra'), lang_confidence (0-1), round_name (which round the file was submitted for), and is_current_round flag
- description: project summary text
- tags: topic tags
- If document content is provided (text_content field in files), use it for deeper analysis. Pay SPECIAL ATTENTION to files from the current round (is_current_round=true) as they are the most recent and relevant submissions.
- If detected_lang is provided, use it to evaluate language requirements (e.g. 'eng' = English, 'fra' = French). lang_confidence indicates detection reliability.

## File Type Reference
The file_type field uses these codes. When criteria mention document names, match them to the correct code:
- EXEC_SUMMARY = Executive Summary
- PRESENTATION = Pitch Deck / Presentation
- BUSINESS_PLAN = Business Plan
- VIDEO = Video
- VIDEO_PITCH = Video Pitch
- SUPPORTING_DOC = Supporting Document
- OTHER = Other / miscellaneous document

IMPORTANT: Only evaluate page limits against the CORRECT document type. For example, a page limit on "executive summary" applies ONLY to files with file_type=EXEC_SUMMARY, not to pitch decks or other documents.

## How to Interpret Criteria
- Treat criteria as reasonable guidelines written by a human, not as rigid legal rules.
- When criteria say "soft cap", "approximately", "around", "about", or "reasonable amount", be LENIENT. A document that is 1-3 pages over a soft cap still meets criteria. Only flag if it is egregiously over (e.g. double the stated limit).
- When criteria have NO softening language, treat them as firm but still use judgment — a 1-page overage on a hard limit is borderline, not an automatic fail.
- Page count data may be unavailable (null) for some files. If page_count is null, do NOT penalize — only evaluate page limits when page_count is actually provided for that file.
- Focus on the INTENT behind each criterion, not a hyper-literal reading.

## Missing Documents
- If criteria mention document requirements (executive summary, pitch deck, etc.) and the project has ZERO files (file_count=0), this is a CLEAR FAIL. Every competition applicant is expected to upload documents.
- "Where applicable" in criteria refers to edge cases within specific document types — it does NOT mean documents are optional.
- A project with no uploaded documents at all should always have meets_criteria=FALSE unless the criteria explicitly say documents are optional.

## Decision Framework
- Set meets_criteria=TRUE if the project satisfies the core intent of the criteria, even with minor imperfections (e.g. a few pages over a soft cap).
- Set meets_criteria=FALSE when there is a clear, material violation: missing required documents entirely, startup far exceeding age limit, documents entirely in the wrong language, no visible ocean impact, etc.
- When in doubt on a SOFT criterion (page lengths, formatting), set meets_criteria=TRUE with a lower confidence score and note the concern in reasoning.
- When in doubt on a HARD criterion (age limits, language, having documents at all), set meets_criteria=FALSE.
- Mention specific concerns in the reasoning field so the admin can review.

## Guidelines
- Evaluate ONLY against the provided criteria, not your own standards
- A confidence of 1.0 means absolute certainty; 0.5 means borderline
- Flag spam_risk=true for: AI-generated filler text, copied content, or irrelevant submissions
- When criteria differ by category (e.g. stricter for STARTUP vs BUSINESS_CONCEPT), apply the appropriate threshold
- When criteria mention regional considerations (e.g. African projects), use the country/region fields
- Do not include any personal identifiers in reasoning
- If project data is insufficient to evaluate, set confidence below 0.3 and default meets_criteria to TRUE`

// ─── Field-Based Rule Evaluation ────────────────────────────────────────────

function evaluateCondition(
  condition: FieldRuleCondition,
  project: ProjectForFiltering
): boolean {
  const { field, operator, value } = condition

  // Get field value from project
  let fieldValue: unknown
  switch (field) {
    case 'competitionCategory':
      fieldValue = project.competitionCategory
      break
    case 'foundedAt':
      fieldValue = project.foundedAt
      break
    case 'country':
      fieldValue = project.country
      break
    case 'geographicZone':
      fieldValue = project.geographicZone
      break
    case 'tags':
      fieldValue = project.tags
      break
    case 'oceanIssue':
      fieldValue = project.oceanIssue
      break
    default:
      return false
  }

  switch (operator) {
    case 'equals':
      return String(fieldValue) === String(value)
    case 'not_equals':
      return String(fieldValue) !== String(value)
    case 'contains':
      if (Array.isArray(fieldValue)) {
        return fieldValue.some((v) =>
          String(v).toLowerCase().includes(String(value).toLowerCase())
        )
      }
      return String(fieldValue || '')
        .toLowerCase()
        .includes(String(value).toLowerCase())
    case 'in':
      if (Array.isArray(value)) {
        return value.includes(String(fieldValue))
      }
      return false
    case 'not_in':
      if (Array.isArray(value)) {
        return !value.includes(String(fieldValue))
      }
      return true
    case 'is_empty':
      if (fieldValue === null || fieldValue === undefined) return true
      if (Array.isArray(fieldValue)) return fieldValue.length === 0
      return String(fieldValue).trim() === ''
    case 'older_than_years': {
      if (!fieldValue || !(fieldValue instanceof Date)) return false
      const yearsAgo = new Date()
      yearsAgo.setFullYear(yearsAgo.getFullYear() - Number(value))
      return fieldValue < yearsAgo
    }
    case 'newer_than_years': {
      if (!fieldValue || !(fieldValue instanceof Date)) return false
      const yearsAgo = new Date()
      yearsAgo.setFullYear(yearsAgo.getFullYear() - Number(value))
      return fieldValue >= yearsAgo
    }
    case 'greater_than':
      return Number(fieldValue) > Number(value)
    case 'less_than':
      return Number(fieldValue) < Number(value)
    default:
      return false
  }
}

export function evaluateFieldRule(
  config: FieldRuleConfig,
  project: ProjectForFiltering
): { passed: boolean; action: 'PASS' | 'REJECT' | 'FLAG' } {
  const results = config.conditions.map((c) => evaluateCondition(c, project))

  const allConditionsMet =
    config.logic === 'AND'
      ? results.every(Boolean)
      : results.some(Boolean)

  if (config.action === 'PASS') {
    return { passed: allConditionsMet, action: config.action }
  }
  return { passed: !allConditionsMet, action: config.action }
}

// ─── Document Check Evaluation ──────────────────────────────────────────────

export function evaluateDocumentRule(
  config: DocumentCheckConfig,
  project: ProjectForFiltering
): { passed: boolean; action: 'PASS' | 'REJECT' | 'FLAG' } {
  const files = project.files || []

  if (config.minFileCount !== undefined && files.length < config.minFileCount) {
    return { passed: false, action: config.action }
  }

  if (config.requiredFileTypes && config.requiredFileTypes.length > 0) {
    const fileExtensions = files.map((f) => {
      const ext = f.fileName.split('.').pop()?.toLowerCase()
      return ext || ''
    })
    const hasAllTypes = config.requiredFileTypes.every((type) =>
      fileExtensions.some((ext) => ext === type.toLowerCase())
    )
    if (!hasAllTypes) {
      return { passed: false, action: config.action }
    }
  }

  // Check global max pages (any file exceeding limit fails)
  if (config.maxPages !== undefined) {
    const overLimit = files.some((f) => f.pageCount != null && f.pageCount > config.maxPages!)
    if (overLimit) {
      return { passed: false, action: config.action }
    }
  }

  // Check per-fileType max pages (e.g. EXECUTIVE_SUMMARY: 2, PITCH_DECK: 10)
  if (config.maxPagesByFileType && Object.keys(config.maxPagesByFileType).length > 0) {
    for (const file of files) {
      if (!file.fileType || file.pageCount == null) continue
      const limit = config.maxPagesByFileType[file.fileType]
      if (limit !== undefined && file.pageCount > limit) {
        return { passed: false, action: config.action }
      }
    }
  }

  return { passed: true, action: config.action }
}

// ─── AI Screening ───────────────────────────────────────────────────────────

interface AIScreeningResult {
  meetsCriteria: boolean
  confidence: number
  reasoning: string
  qualityScore: number
  spamRisk: boolean
  awardMatches?: AwardMatchResult[]
}

/**
 * Execute AI screening on a batch of projects
 */
async function processAIBatch(
  openai: NonNullable<Awaited<ReturnType<typeof getOpenAI>>>,
  model: string,
  criteriaText: string,
  anonymized: AnonymizedProjectForAI[],
  mappings: ProjectAIMapping[],
  userId?: string,
  entityId?: string,
  awards?: AwardCriteriaInput[]
): Promise<{
  results: Map<string, AIScreeningResult>
  tokensUsed: number
}> {
  const results = new Map<string, AIScreeningResult>()
  let tokensUsed = 0

  // Sanitize user-supplied criteria
  const { sanitized: safeCriteria } = sanitizeUserInput(criteriaText)

  // Build system prompt — dynamically append awards block if any
  let systemPrompt = AI_SCREENING_SYSTEM_PROMPT
  if (awards && awards.length > 0) {
    const awardsList = awards
      .map((a, i) => `${i + 1}. ${a.awardName}: ${a.criteriaText}`)
      .join('\n')
    systemPrompt += `

## Special Award Evaluation (Additional Task)
In addition to the main screening, evaluate each project against these special awards.
Award eligibility is independent of the main screening outcome — a project can fail screening but still match an award.

### Awards
${awardsList}

Add an "award_matches" array to each project result:
"award_matches": [
  {
    "award_index": 1,
    "eligible": true/false,
    "confidence": 0.0-1.0,
    "quality_score": 0-100,
    "reasoning": "1-2 sentence explanation"
  }
]
Include one entry per award for every project. quality_score (0-100) measures fit for ranking.`
  }

  // Build user prompt with clear structure
  const userPrompt = `## Screening Criteria
The admin has defined the following requirements. Evaluate each project against ALL of these criteria:

${safeCriteria}

## Projects to Evaluate (${anonymized.length} total)
${JSON.stringify(anonymized)}

Evaluate each project and return JSON with your assessment.`

  const MAX_PARSE_RETRIES = 2
  let parseAttempts = 0
  let response: Awaited<ReturnType<typeof openai.chat.completions.create>>
  const maxTokens = Math.min(8000, 4000 + (awards?.length ?? 0) * 500)

  try {
    const params = buildCompletionParams(model, {
      messages: [
        { role: 'system', content: systemPrompt },
        { role: 'user', content: userPrompt },
      ],
      jsonMode: true,
      temperature: 0.1,
      maxTokens,
    })

    response = await openai.chat.completions.create(params)
    const usage = extractTokenUsage(response)
    tokensUsed = usage.totalTokens

    await logAIUsage({
      userId,
      action: 'FILTERING',
      entityType: 'Stage',
      entityId,
      model,
      promptTokens: usage.promptTokens,
      completionTokens: usage.completionTokens,
      totalTokens: usage.totalTokens,
      batchSize: anonymized.length,
      itemsProcessed: anonymized.length,
      status: 'SUCCESS',
    })

    // Parse with retry logic
    let parsed: {
      projects: Array<{
        project_id: string
        meets_criteria: boolean
        confidence: number
        reasoning: string
        quality_score: number
        spam_risk: boolean
        award_matches?: Array<{
          award_index: number
          eligible: boolean
          confidence: number
          quality_score: number
          reasoning: string
        }>
      }>
    }

    while (true) {
      try {
        const content = response.choices[0]?.message?.content
        if (!content) {
          throw new Error('Empty response from AI')
        }
        parsed = JSON.parse(content)
        break
      } catch (parseError) {
        if (parseError instanceof SyntaxError && parseAttempts < MAX_PARSE_RETRIES) {
          parseAttempts++
          console.warn(`[AI Filtering] JSON parse failed, retrying (${parseAttempts}/${MAX_PARSE_RETRIES})`)

          // Retry the API call with hint
          const retryParams = buildCompletionParams(model, {
            messages: [
              { role: 'system', content: systemPrompt },
              { role: 'user', content: userPrompt + '\n\nIMPORTANT: Please ensure valid JSON output.' },
            ],
            jsonMode: true,
            temperature: 0.1,
            maxTokens,
          })
          response = await openai.chat.completions.create(retryParams)
          const retryUsage = extractTokenUsage(response)
          tokensUsed += retryUsage.totalTokens
          continue
        }
        throw parseError
      }
    }

    // Map results back to real IDs
    for (const result of parsed.projects || []) {
      const mapping = mappings.find((m) => m.anonymousId === result.project_id)
      if (mapping) {
        // Map award matches back to real award IDs
        let awardMatches: AwardMatchResult[] | undefined
        if (awards && awards.length > 0 && result.award_matches) {
          awardMatches = []
          for (const match of result.award_matches) {
            const award = awards[(match.award_index ?? 1) - 1]
            if (award) {
              awardMatches.push({
                awardId: award.awardId,
                eligible: match.eligible,
                confidence: match.confidence,
                qualityScore: Math.max(0, Math.min(100, match.quality_score ?? 0)),
                reasoning: match.reasoning ?? '',
              })
            }
          }
        }

        results.set(mapping.realId, {
          meetsCriteria: result.meets_criteria,
          confidence: result.confidence,
          reasoning: result.reasoning,
          qualityScore: result.quality_score,
          spamRisk: result.spam_risk,
          awardMatches: awardMatches && awardMatches.length > 0 ? awardMatches : undefined,
        })
      }
    }
  } catch (error) {
    // Check if parse error
    if (error instanceof SyntaxError) {
      const parseError = createParseError(error.message)
      logAIError('Filtering', 'batch processing', parseError)

      await logAIUsage({
        userId,
        action: 'FILTERING',
        entityType: 'Stage',
        entityId,
        model,
        promptTokens: 0,
        completionTokens: 0,
        totalTokens: tokensUsed,
        batchSize: anonymized.length,
        itemsProcessed: 0,
        status: 'ERROR',
        errorMessage: parseError.message,
      })

      // Flag all for manual review
      for (const mapping of mappings) {
        results.set(mapping.realId, {
          meetsCriteria: false,
          confidence: 0,
          reasoning: 'AI response parse error — flagged for manual review',
          qualityScore: 5,
          spamRisk: false,
        })
      }
    } else {
      throw error // Re-throw for outer catch
    }
  }

  return { results, tokensUsed }
}

export async function executeAIScreening(
  config: AIScreeningConfig,
  projects: ProjectForFiltering[],
  userId?: string,
  entityId?: string,
  onProgress?: ProgressCallback,
  onBatchComplete?: (batchResults: Map<string, AIScreeningResult>) => Promise<void>,
  awards?: AwardCriteriaInput[]
): Promise<Map<string, AIScreeningResult>> {
  const results = new Map<string, AIScreeningResult>()

  try {
    const openai = await getOpenAI()
    if (!openai) {
      console.warn('[AI Filtering] OpenAI not configured')
      for (const p of projects) {
        results.set(p.id, {
          meetsCriteria: false,
          confidence: 0,
          reasoning: 'AI screening unavailable — flagged for manual review',
          qualityScore: 5,
          spamRisk: false,
        })
      }
      return results
    }

    const model = await getConfiguredModel()

    // Get batch settings from config
    const batchSize = Math.min(
      MAX_BATCH_SIZE,
      Math.max(MIN_BATCH_SIZE, config.batchSize ?? DEFAULT_BATCH_SIZE)
    )
    const parallelBatches = Math.min(
      MAX_PARALLEL_BATCHES,
      Math.max(1, config.parallelBatches ?? DEFAULT_PARALLEL_BATCHES)
    )

    console.log(`[AI Filtering] Using model: ${model} for ${projects.length} projects (batch size: ${batchSize}, parallel: ${parallelBatches})`)

    // Convert and anonymize projects
    const projectsWithRelations = projects.map(toProjectWithRelations)
    const { anonymized, mappings } = anonymizeProjectsForAI(projectsWithRelations, 'FILTERING')

    // Validate anonymization
    if (!validateAnonymizedProjects(anonymized)) {
      console.error('[AI Filtering] Anonymization validation failed')
      throw new Error('GDPR compliance check failed: PII detected in anonymized data')
    }

    let totalTokens = 0
    const totalBatches = Math.ceil(anonymized.length / batchSize)
    let processedBatches = 0

    // Create batch chunks for parallel processing
    const batches: Array<{ anon: typeof anonymized; maps: typeof mappings; index: number }> = []
    for (let i = 0; i < anonymized.length; i += batchSize) {
      batches.push({
        anon: anonymized.slice(i, i + batchSize),
        maps: mappings.slice(i, i + batchSize),
        index: batches.length,
      })
    }

    // Process batches in parallel chunks
    for (let i = 0; i < batches.length; i += parallelBatches) {
      const parallelChunk = batches.slice(i, i + parallelBatches)

      console.log(`[AI Filtering] Processing batches ${i + 1}-${Math.min(i + parallelBatches, batches.length)} of ${totalBatches} (${parallelChunk.length} in parallel)`)

      // Run parallel batches concurrently
      const batchPromises = parallelChunk.map(async (batch) => {
        const { results: batchResults, tokensUsed } = await processAIBatch(
          openai,
          model,
          config.criteriaText,
          batch.anon,
          batch.maps,
          userId,
          entityId,
          awards
        )
        return { batchResults, tokensUsed, index: batch.index }
      })

      const parallelResults = await Promise.all(batchPromises)

      // Merge results from all parallel batches
      for (const { batchResults, tokensUsed } of parallelResults) {
        totalTokens += tokensUsed
        for (const [id, result] of batchResults) {
          results.set(id, result)
        }
        processedBatches++
      }

      // Emit batch results for streaming
      if (onBatchComplete) {
        const chunkResults = new Map<string, AIScreeningResult>()
        for (const { batchResults: br } of parallelResults) {
          for (const [id, result] of br) {
            chunkResults.set(id, result)
          }
        }
        await onBatchComplete(chunkResults)
      }

      // Report progress after each parallel chunk
      if (onProgress) {
        await onProgress({
          currentBatch: processedBatches,
          totalBatches,
          processedCount: Math.min(processedBatches * batchSize, anonymized.length),
          tokensUsed: totalTokens,
        })
      }
    }

    console.log(`[AI Filtering] Completed. Total tokens: ${totalTokens}`)

  } catch (error) {
    const classified = classifyAIError(error)
    logAIError('Filtering', 'executeAIScreening', classified)

    await logAIUsage({
      userId,
      action: 'FILTERING',
      entityType: 'Stage',
      entityId,
      model: 'unknown',
      promptTokens: 0,
      completionTokens: 0,
      totalTokens: 0,
      batchSize: projects.length,
      itemsProcessed: 0,
      status: 'ERROR',
      errorMessage: classified.message,
    })

    // Flag all for manual review with error info
    for (const p of projects) {
      results.set(p.id, {
        meetsCriteria: false,
        confidence: 0,
        reasoning: `AI screening error: ${classified.message}`,
        qualityScore: 5,
        spamRisk: false,
      })
    }
  }

  return results
}

// ─── Main Execution ─────────────────────────────────────────────────────────

export async function executeFilteringRules(
  rules: FilteringRuleInput[],
  projects: ProjectForFiltering[],
  userId?: string,
  roundId?: string,
  onProgress?: ProgressCallback,
  onResultsBatch?: (results: ProjectFilteringResult[]) => Promise<void>,
  awards?: AwardCriteriaInput[]
): Promise<ProjectFilteringResult[]> {
  const activeRules = rules
    .filter((r) => r.isActive)
    .sort((a, b) => a.priority - b.priority)

  const aiRules = activeRules.filter((r) => r.ruleType === 'AI_SCREENING')
  const nonAiRules = activeRules.filter((r) => r.ruleType !== 'AI_SCREENING')

  // Pre-evaluate non-AI rules for all projects (instant)
  const nonAiEval = new Map<string, { ruleResults: RuleResult[]; hasFailed: boolean; hasFlagged: boolean }>()
  for (const project of projects) {
    const ruleResults: RuleResult[] = []
    let hasFailed = false
    let hasFlagged = false

    // Built-in hard check: projects with zero files always fail
    const fileCount = project.files?.length ?? 0
    if (fileCount === 0) {
      ruleResults.push({
        ruleId: '__builtin_zero_files',
        ruleName: 'Required Documents Check',
        ruleType: 'DOCUMENT_CHECK',
        passed: false,
        action: 'REJECT',
      })
      hasFailed = true
    }

    for (const rule of nonAiRules) {
      let result: { passed: boolean; action: 'PASS' | 'REJECT' | 'FLAG' }
      if (rule.ruleType === 'FIELD_BASED') {
        result = evaluateFieldRule(rule.configJson as unknown as FieldRuleConfig, project)
      } else if (rule.ruleType === 'DOCUMENT_CHECK') {
        result = evaluateDocumentRule(rule.configJson as unknown as DocumentCheckConfig, project)
      } else {
        continue
      }

      ruleResults.push({
        ruleId: rule.id,
        ruleName: rule.name,
        ruleType: rule.ruleType,
        passed: result.passed,
        action: result.action,
      })
      if (!result.passed) {
        if (result.action === 'REJECT') hasFailed = true
        if (result.action === 'FLAG') hasFlagged = true
      }
    }
    nonAiEval.set(project.id, { ruleResults, hasFailed, hasFlagged })
  }

  // Helper: combine non-AI + AI results for a single project
  function computeProjectResult(
    projectId: string,
    aiRuleResults: Array<{ ruleId: string; ruleName: string; passed: boolean; action: string; reasoning?: string }>,
    aiScreeningData: Record<string, unknown>,
    awardMatches?: AwardMatchResult[]
  ): ProjectFilteringResult {
    const nonAi = nonAiEval.get(projectId)!
    const ruleResults: RuleResult[] = [...nonAi.ruleResults]
    let hasFailed = nonAi.hasFailed
    let hasFlagged = nonAi.hasFlagged

    for (const ar of aiRuleResults) {
      ruleResults.push({
        ruleId: ar.ruleId,
        ruleName: ar.ruleName,
        ruleType: 'AI_SCREENING',
        passed: ar.passed,
        action: ar.action as 'PASS' | 'REJECT' | 'FLAG',
        reasoning: ar.reasoning,
      })
      if (!ar.passed) {
        if (ar.action === 'REJECT') hasFailed = true
        else hasFlagged = true
      }
    }

    return {
      projectId,
      outcome: hasFailed ? 'FILTERED_OUT' : hasFlagged ? 'FLAGGED' : 'PASSED',
      ruleResults,
      aiScreeningJson: Object.keys(aiScreeningData).length > 0 ? aiScreeningData : undefined,
      awardMatches: awardMatches && awardMatches.length > 0 ? awardMatches : undefined,
    }
  }

  // No AI rules → compute all results immediately
  if (aiRules.length === 0) {
    const results = projects.map((p) => computeProjectResult(p.id, [], {}))
    if (onResultsBatch) await onResultsBatch(results)
    return results
  }

  // Single AI rule → stream results per batch
  if (aiRules.length === 1) {
    const aiRule = aiRules[0]
    const config = aiRule.configJson as unknown as AIScreeningConfig
    const allResults: ProjectFilteringResult[] = []

    await executeAIScreening(config, projects, userId, roundId, onProgress, async (batchAIResults) => {
      const batchResults: ProjectFilteringResult[] = []
      for (const [projectId, aiResult] of batchAIResults) {
        let passed: boolean
        let aiAction: string
        if (config.action === 'AUTO_FILTER') {
          const threshold = config.autoFilterThreshold ?? 4
          if (aiResult.spamRisk || aiResult.qualityScore <= threshold) {
            // Clear spam/junk — auto-reject
            passed = false
            aiAction = 'REJECT'
          } else if (!aiResult.meetsCriteria) {
            // Borderline — flag for human review
            passed = false
            aiAction = 'FLAG'
          } else {
            passed = true
            aiAction = 'FLAG'
          }
        } else {
          passed = aiResult.meetsCriteria && !aiResult.spamRisk
          aiAction = config.action || 'FLAG'
        }
        batchResults.push(
          computeProjectResult(
            projectId,
            [{ ruleId: aiRule.id, ruleName: aiRule.name, passed, action: aiAction, reasoning: aiResult.reasoning }],
            { [aiRule.id]: aiResult },
            aiResult.awardMatches
          )
        )
      }
      allResults.push(...batchResults)
      if (onResultsBatch) await onResultsBatch(batchResults)
    }, awards)

    return allResults
  }

  // Multiple AI rules → run all sequentially, then compute (no per-batch streaming)
  const aiResults = new Map<string, Map<string, AIScreeningResult>>()
  for (const aiRule of aiRules) {
    const config = aiRule.configJson as unknown as AIScreeningConfig
    const screeningResults = await executeAIScreening(config, projects, userId, roundId, onProgress, undefined, awards)
    aiResults.set(aiRule.id, screeningResults)
  }

  const results: ProjectFilteringResult[] = []
  for (const project of projects) {
    const aiRuleResults: Array<{ ruleId: string; ruleName: string; passed: boolean; action: string; reasoning?: string }> = []
    const aiScreeningData: Record<string, unknown> = {}

    for (const aiRule of aiRules) {
      const screening = aiResults.get(aiRule.id)?.get(project.id)
      if (screening) {
        const aiConfig = aiRule.configJson as unknown as AIScreeningConfig
        let passed: boolean
        let aiAction: string
        if (aiConfig?.action === 'AUTO_FILTER') {
          const threshold = aiConfig.autoFilterThreshold ?? 4
          if (screening.spamRisk || screening.qualityScore <= threshold) {
            passed = false
            aiAction = 'REJECT'
          } else if (!screening.meetsCriteria) {
            passed = false
            aiAction = 'FLAG'
          } else {
            passed = true
            aiAction = 'FLAG'
          }
        } else {
          passed = screening.meetsCriteria && !screening.spamRisk
          aiAction = aiConfig?.action || 'FLAG'
        }
        aiRuleResults.push({ ruleId: aiRule.id, ruleName: aiRule.name, passed, action: aiAction, reasoning: screening.reasoning })
        aiScreeningData[aiRule.id] = screening
      }
    }
    // Merge award matches from all AI rules (dedup by awardId, first wins)
    const mergedAwardMatches: AwardMatchResult[] = []
    const seenAwardIds = new Set<string>()
    for (const aiRule of aiRules) {
      const screening = aiResults.get(aiRule.id)?.get(project.id)
      if (screening?.awardMatches) {
        for (const m of screening.awardMatches) {
          if (!seenAwardIds.has(m.awardId)) {
            seenAwardIds.add(m.awardId)
            mergedAwardMatches.push(m)
          }
        }
      }
    }
    results.push(computeProjectResult(project.id, aiRuleResults, aiScreeningData, mergedAwardMatches))
  }

  if (onResultsBatch) await onResultsBatch(results)
  return results
}
