/**
 * 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' // REJECT = auto-filter-out, FLAG = flag for human review
  // Performance settings
  batchSize?: number // Projects per API call (1-50, default 20)
  parallelBatches?: number // Concurrent API calls (1-10, default 1)
}

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>
}

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 = 1
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.

## 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 with file_type, page_count (if known), size_kb, 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.

## 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`

// ─── 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
}

/**
 * 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
): 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 optimized prompt
  const userPrompt = `CRITERIA: ${safeCriteria}
PROJECTS: ${JSON.stringify(anonymized)}
Evaluate and return JSON.`

  const MAX_PARSE_RETRIES = 2
  let parseAttempts = 0
  let response: Awaited<ReturnType<typeof openai.chat.completions.create>>

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

    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
      }>
    }

    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: AI_SCREENING_SYSTEM_PROMPT },
              { role: 'user', content: userPrompt + '\n\nIMPORTANT: Please ensure valid JSON output.' },
            ],
            jsonMode: true,
            temperature: 0.1,
            maxTokens: 4000,
          })
          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) {
        results.set(mapping.realId, {
          meetsCriteria: result.meets_criteria,
          confidence: result.confidence,
          reasoning: result.reasoning,
          qualityScore: result.quality_score,
          spamRisk: result.spam_risk,
        })
      }
    }
  } 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
): 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
        )
        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++
      }

      // 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
): Promise<ProjectFilteringResult[]> {
  const activeRules = rules
    .filter((r) => r.isActive)
    .sort((a, b) => a.priority - b.priority)

  // Separate AI screening rules (need batch processing)
  const aiRules = activeRules.filter((r) => r.ruleType === 'AI_SCREENING')
  const nonAiRules = activeRules.filter((r) => r.ruleType !== 'AI_SCREENING')

  // Pre-compute AI screening results if needed
  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)
    aiResults.set(aiRule.id, screeningResults)
  }

  // Evaluate each project
  const results: ProjectFilteringResult[] = []

  for (const project of projects) {
    const ruleResults: RuleResult[] = []
    let hasFailed = false
    let hasFlagged = false

    // Evaluate non-AI rules
    for (const rule of nonAiRules) {
      let result: { passed: boolean; action: 'PASS' | 'REJECT' | 'FLAG' }

      if (rule.ruleType === 'FIELD_BASED') {
        const config = rule.configJson as unknown as FieldRuleConfig
        result = evaluateFieldRule(config, project)
      } else if (rule.ruleType === 'DOCUMENT_CHECK') {
        const config = rule.configJson as unknown as DocumentCheckConfig
        result = evaluateDocumentRule(config, 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
      }
    }

    // Evaluate AI rules
    for (const aiRule of aiRules) {
      const ruleScreening = aiResults.get(aiRule.id)
      const screening = ruleScreening?.get(project.id)

      if (screening) {
        const passed = screening.meetsCriteria && !screening.spamRisk
        const aiConfig = aiRule.configJson as unknown as AIScreeningConfig
        const aiAction = aiConfig?.action || 'FLAG'
        ruleResults.push({
          ruleId: aiRule.id,
          ruleName: aiRule.name,
          ruleType: 'AI_SCREENING',
          passed,
          action: aiAction,
          reasoning: screening.reasoning,
        })

        if (!passed) {
          if (aiAction === 'REJECT') hasFailed = true
          else hasFlagged = true
        }
      }
    }

    // Determine overall outcome
    let outcome: 'PASSED' | 'FILTERED_OUT' | 'FLAGGED'
    if (hasFailed) {
      outcome = 'FILTERED_OUT'
    } else if (hasFlagged) {
      outcome = 'FLAGGED'
    } else {
      outcome = 'PASSED'
    }

    // Collect AI screening data
    const aiScreeningData: Record<string, unknown> = {}
    for (const aiRule of aiRules) {
      const screening = aiResults.get(aiRule.id)?.get(project.id)
      if (screening) {
        aiScreeningData[aiRule.id] = screening
      }
    }

    results.push({
      projectId: project.id,
      outcome,
      ruleResults,
      aiScreeningJson:
        Object.keys(aiScreeningData).length > 0 ? aiScreeningData : undefined,
    })
  }

  return results
}
