/**
 * AI-Powered Project Tagging Service
 *
 * Analyzes projects and assigns expertise tags automatically.
 *
 * Features:
 * - Single project tagging (on-submit or manual)
 * - Batch tagging with concurrent processing (10 projects/batch, 3 concurrent)
 * - Confidence scores for each tag
 * - Additive only - never removes existing tags
 *
 * GDPR Compliance:
 * - All project data is anonymized before AI processing
 * - Only necessary fields sent to OpenAI
 * - No personal identifiers in prompts or responses
 */

import { prisma } from '@/lib/prisma'
import { getOpenAI, getConfiguredModel, buildCompletionParams, AI_MODELS } from '@/lib/openai'
import { logAIUsage, extractTokenUsage } from '@/server/utils/ai-usage'
import { classifyAIError, createParseError, logAIError } from './ai-errors'
import {
  anonymizeProjectsForAI,
  validateAnonymizedProjects,
  toProjectWithRelations,
  type AnonymizedProjectForAI,
} from './anonymization'

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

export interface TagSuggestion {
  tagId: string
  tagName: string
  confidence: number
  reasoning: string
}

export interface TaggingResult {
  projectId: string
  suggestions: TagSuggestion[]
  applied: TagSuggestion[]
  tokensUsed: number
}

interface AvailableTag {
  id: string
  name: string
  category: string | null
  description: string | null
}

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

const CONFIDENCE_THRESHOLD = 0.5
const DEFAULT_MAX_TAGS = 5
const BATCH_SIZE = 10 // Projects per API call
const BATCH_CONCURRENCY = 3 // Concurrent API calls

// System prompt optimized for single-project tag suggestion
const TAG_SUGGESTION_SYSTEM_PROMPT = `You are an expert at categorizing ocean conservation and sustainability projects.

Analyze the project and suggest the most relevant expertise tags from the provided list.
Consider the project's focus areas, technology, methodology, and domain.

Return JSON with this format:
{
  "suggestions": [
    {
      "tag_name": "exact tag name from list",
      "confidence": 0.0-1.0,
      "reasoning": "brief explanation why this tag fits"
    }
  ]
}

Rules:
- Only suggest tags from the provided list (exact names)
- Order by relevance (most relevant first)
- Confidence should reflect how well the tag matches
- Maximum 7 suggestions per project
- Be conservative - only suggest tags that truly apply`

// System prompt optimized for batch tagging (multiple projects in one call)
const BATCH_TAG_SYSTEM_PROMPT = `You are an expert at categorizing ocean conservation and sustainability projects.

Analyze EACH project and suggest the most relevant expertise tags from the provided list.
Consider each project's focus areas, technology, methodology, and domain.

Return JSON with this format:
{
  "projects": [
    {
      "project_id": "PROJECT_001",
      "suggestions": [
        {
          "tag_name": "exact tag name from list",
          "confidence": 0.0-1.0,
          "reasoning": "brief explanation"
        }
      ]
    }
  ]
}

Rules:
- Only suggest tags from the provided list (exact names)
- Order by relevance (most relevant first)
- Confidence should reflect how well the tag matches
- Maximum 7 suggestions per project
- Be conservative - only suggest tags that truly apply
- Return results for ALL projects provided`

// ─── Helper Functions ────────────────────────────────────────────────────────

/**
 * Get system settings for AI tagging
 */
export async function getTaggingSettings(): Promise<{
  enabled: boolean
  maxTags: number
  confidenceThreshold: number
}> {
  const settings = await prisma.systemSettings.findMany({
    where: {
      key: {
        in: ['ai_tagging_enabled', 'ai_tagging_max_tags', 'ai_tagging_confidence_threshold', 'ai_enabled'],
      },
    },
  })

  const settingsMap = new Map(settings.map((s) => [s.key, s.value]))

  // AI tagging is enabled if:
  // 1. ai_tagging_enabled is explicitly 'true', OR
  // 2. ai_tagging_enabled is not set but ai_enabled is 'true' (fall back to general AI setting)
  const taggingEnabled = settingsMap.get('ai_tagging_enabled')
  const aiEnabled = settingsMap.get('ai_enabled')

  const enabled = taggingEnabled === 'true' || (taggingEnabled === undefined && aiEnabled === 'true')

  return {
    enabled,
    maxTags: parseInt(settingsMap.get('ai_tagging_max_tags') || String(DEFAULT_MAX_TAGS)),
    confidenceThreshold: parseFloat(settingsMap.get('ai_tagging_confidence_threshold') || String(CONFIDENCE_THRESHOLD)),
  }
}

/**
 * Get all active expertise tags
 */
export async function getAvailableTags(): Promise<AvailableTag[]> {
  return prisma.expertiseTag.findMany({
    where: { isActive: true },
    select: {
      id: true,
      name: true,
      category: true,
      description: true,
    },
    orderBy: [{ category: 'asc' }, { sortOrder: 'asc' }],
  })
}

// ─── AI Tagging Core ─────────────────────────────────────────────────────────

/**
 * Call OpenAI to get tag suggestions for a single project
 * Used for on-demand single-project tagging
 */
async function getAISuggestions(
  anonymizedProject: AnonymizedProjectForAI,
  availableTags: AvailableTag[],
  userId?: string
): Promise<{ suggestions: TagSuggestion[]; tokensUsed: number }> {
  const openai = await getOpenAI()
  if (!openai) {
    console.warn('[AI Tagging] OpenAI not configured')
    return { suggestions: [], tokensUsed: 0 }
  }

  // Use QUICK model — tag classification is simple, doesn't need expensive reasoning
  const model = await getConfiguredModel(AI_MODELS.QUICK)

  // Build compact tag list for prompt
  const tagList = availableTags.map((t) => ({
    name: t.name,
    category: t.category,
    description: t.description,
  }))

  const userPrompt = `PROJECT:
${JSON.stringify(anonymizedProject)}

AVAILABLE TAGS:
${JSON.stringify(tagList)}

Suggest relevant tags for this project.`

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

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

    // Log usage
    await logAIUsage({
      userId,
      action: 'PROJECT_TAGGING',
      entityType: 'Project',
      entityId: anonymizedProject.project_id,
      model,
      promptTokens: usage.promptTokens,
      completionTokens: usage.completionTokens,
      totalTokens: usage.totalTokens,
      batchSize: 1,
      itemsProcessed: 1,
      status: 'SUCCESS',
    })

    const content = response.choices[0]?.message?.content
    if (!content) {
      throw new Error('Empty response from AI')
    }

    const parsed = JSON.parse(content) as {
      suggestions: Array<{
        tag_name: string
        confidence: number
        reasoning: string
      }>
    }

    // Map to TagSuggestion format, matching tag names to IDs
    const suggestions: TagSuggestion[] = []
    for (const s of parsed.suggestions || []) {
      const tag = availableTags.find(
        (t) => t.name.toLowerCase() === s.tag_name.toLowerCase()
      )
      if (tag) {
        suggestions.push({
          tagId: tag.id,
          tagName: tag.name,
          confidence: Math.max(0, Math.min(1, s.confidence)),
          reasoning: s.reasoning,
        })
      }
    }

    return { suggestions, tokensUsed: usage.totalTokens }
  } catch (error) {
    if (error instanceof SyntaxError) {
      const parseError = createParseError(error.message)
      logAIError('Tagging', 'getAISuggestions', parseError)
    }

    await logAIUsage({
      userId,
      action: 'PROJECT_TAGGING',
      entityType: 'Project',
      entityId: anonymizedProject.project_id,
      model,
      promptTokens: 0,
      completionTokens: 0,
      totalTokens: 0,
      batchSize: 1,
      itemsProcessed: 0,
      status: 'ERROR',
      errorMessage: error instanceof Error ? error.message : 'Unknown error',
    })

    throw error
  }
}

/**
 * Call OpenAI to get tag suggestions for a batch of projects in one API call.
 * Returns a map of project_id -> TagSuggestion[].
 */
async function getAISuggestionsBatch(
  anonymizedProjects: AnonymizedProjectForAI[],
  availableTags: AvailableTag[],
  userId?: string
): Promise<{ suggestionsMap: Map<string, TagSuggestion[]>; tokensUsed: number }> {
  const openai = await getOpenAI()
  if (!openai) {
    console.warn('[AI Tagging] OpenAI not configured')
    return { suggestionsMap: new Map(), tokensUsed: 0 }
  }

  // Use QUICK model — tag classification is simple, doesn't need expensive reasoning
  const model = await getConfiguredModel(AI_MODELS.QUICK)
  const suggestionsMap = new Map<string, TagSuggestion[]>()

  // Build compact tag list (sent once for entire batch)
  const tagList = availableTags.map((t) => ({
    name: t.name,
    category: t.category,
    description: t.description,
  }))

  const userPrompt = `PROJECTS (${anonymizedProjects.length}):
${JSON.stringify(anonymizedProjects)}

AVAILABLE TAGS:
${JSON.stringify(tagList)}

Suggest relevant tags for each project.`

  const MAX_PARSE_RETRIES = 2
  let parseAttempts = 0

  try {
    const params = buildCompletionParams(model, {
      messages: [
        { role: 'system', content: BATCH_TAG_SYSTEM_PROMPT },
        { role: 'user', content: userPrompt },
      ],
      jsonMode: true,
      temperature: 0.1,
      maxTokens: Math.min(4000, anonymizedProjects.length * 500),
    })

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

    // Parse with retry logic
    let parsed: {
      projects: Array<{
        project_id: string
        suggestions: Array<{
          tag_name: string
          confidence: number
          reasoning: string
        }>
      }>
    }

    while (true) {
      try {
        const content = response.choices[0]?.message?.content
        if (!content) throw new Error('Empty response from AI')

        const raw = JSON.parse(content)
        parsed = raw.projects ? raw : { projects: Array.isArray(raw) ? raw : [] }
        break
      } catch (parseError) {
        if (parseError instanceof SyntaxError && parseAttempts < MAX_PARSE_RETRIES) {
          parseAttempts++
          console.warn(`[AI Tagging Batch] JSON parse failed, retrying (${parseAttempts}/${MAX_PARSE_RETRIES})`)
          const retryParams = buildCompletionParams(model, {
            messages: [
              { role: 'system', content: BATCH_TAG_SYSTEM_PROMPT },
              { role: 'user', content: userPrompt + '\n\nIMPORTANT: Please ensure valid JSON output.' },
            ],
            jsonMode: true,
            temperature: 0.1,
            maxTokens: Math.min(4000, anonymizedProjects.length * 500),
          })
          response = await openai.chat.completions.create(retryParams)
          const retryUsage = extractTokenUsage(response)
          totalTokens += retryUsage.totalTokens
          continue
        }
        throw parseError
      }
    }

    // Log usage for the entire batch
    await logAIUsage({
      userId,
      action: 'PROJECT_TAGGING',
      entityType: 'Project',
      model,
      promptTokens: usage.promptTokens,
      completionTokens: usage.completionTokens,
      totalTokens,
      batchSize: anonymizedProjects.length,
      itemsProcessed: parsed.projects?.length || 0,
      status: 'SUCCESS',
    })

    // Map results back to TagSuggestion format
    for (const projectResult of parsed.projects || []) {
      const suggestions: TagSuggestion[] = []
      for (const s of projectResult.suggestions || []) {
        const tag = availableTags.find(
          (t) => t.name.toLowerCase() === s.tag_name.toLowerCase()
        )
        if (tag) {
          suggestions.push({
            tagId: tag.id,
            tagName: tag.name,
            confidence: Math.max(0, Math.min(1, s.confidence)),
            reasoning: s.reasoning || '',
          })
        }
      }
      suggestionsMap.set(projectResult.project_id, suggestions)
    }

    return { suggestionsMap, tokensUsed: totalTokens }
  } catch (error) {
    if (error instanceof SyntaxError) {
      const parseError = createParseError(error.message)
      logAIError('Tagging', 'getAISuggestionsBatch', parseError)
    }

    const classified = classifyAIError(error)
    logAIError('Tagging', 'getAISuggestionsBatch', classified)

    await logAIUsage({
      userId,
      action: 'PROJECT_TAGGING',
      entityType: 'Project',
      model: 'unknown',
      promptTokens: 0,
      completionTokens: 0,
      totalTokens: 0,
      batchSize: anonymizedProjects.length,
      itemsProcessed: 0,
      status: 'ERROR',
      errorMessage: error instanceof Error ? error.message : 'Unknown error',
    })

    throw error
  }
}

// ─── Public API ──────────────────────────────────────────────────────────────

/**
 * Tag a single project with AI-suggested expertise tags
 *
 * Behavior:
 * - Only applies tags with confidence >= 0.5
 * - Additive only - never removes existing tags
 * - Respects maxTags setting
 */
export async function tagProject(
  projectId: string,
  userId?: string
): Promise<TaggingResult> {
  const settings = await getTaggingSettings()
  if (!settings.enabled) {
    return {
      projectId,
      suggestions: [],
      applied: [],
      tokensUsed: 0,
    }
  }

  // Fetch project with needed fields
  const project = await prisma.project.findUnique({
    where: { id: projectId },
    include: {
      projectTags: true,
      files: { select: { fileType: true } },
      _count: { select: { teamMembers: true, files: true } },
    },
  })

  if (!project) {
    throw new Error(`Project not found: ${projectId}`)
  }

  // Get available tags
  const availableTags = await getAvailableTags()
  if (availableTags.length === 0) {
    return {
      projectId,
      suggestions: [],
      applied: [],
      tokensUsed: 0,
    }
  }

  // Anonymize project data
  const projectWithRelations = toProjectWithRelations(project)
  const { anonymized, mappings } = anonymizeProjectsForAI([projectWithRelations], 'FILTERING')

  // Validate anonymization
  if (!validateAnonymizedProjects(anonymized)) {
    throw new Error('GDPR compliance check failed: PII detected in anonymized data')
  }

  // Get AI suggestions
  const { suggestions, tokensUsed } = await getAISuggestions(
    anonymized[0],
    availableTags,
    userId
  )

  // Filter by confidence threshold from settings
  const validSuggestions = suggestions.filter(
    (s) => s.confidence >= settings.confidenceThreshold
  )

  // Get existing tag IDs to avoid duplicates
  const existingTagIds = new Set(project.projectTags.map((pt) => pt.tagId))

  // Calculate how many more tags we can add
  const currentTagCount = project.projectTags.length
  const remainingSlots = Math.max(0, settings.maxTags - currentTagCount)

  // Filter out existing tags and limit to remaining slots
  const newSuggestions = validSuggestions
    .filter((s) => !existingTagIds.has(s.tagId))
    .slice(0, remainingSlots)

  // Apply new tags
  const applied: TagSuggestion[] = []
  for (const suggestion of newSuggestions) {
    try {
      await prisma.projectTag.create({
        data: {
          projectId,
          tagId: suggestion.tagId,
          confidence: suggestion.confidence,
          source: 'AI',
        },
      })
      applied.push(suggestion)
    } catch (error) {
      // Skip if tag already exists (race condition)
      console.warn(`[AI Tagging] Failed to apply tag ${suggestion.tagName}: ${error}`)
    }
  }

  return {
    projectId,
    suggestions,
    applied,
    tokensUsed,
  }
}

/**
 * Tag a batch of projects using batched API calls with concurrency.
 * Much more efficient than tagging one-by-one for bulk operations.
 *
 * @param projects Array of { id, projectTags } to tag
 * @param userId The user initiating the tagging
 * @param onProgress Callback for progress updates
 * @returns Array of TaggingResult
 */
export async function tagProjectsBatch(
  projects: Array<{
    id: string
    title: string
    projectTags: Array<{ tagId: string }>
  }>,
  userId: string,
  onProgress?: (processed: number, total: number) => Promise<void>
): Promise<{ results: TaggingResult[]; totalTokens: number }> {
  const settings = await getTaggingSettings()
  if (!settings.enabled) {
    return { results: [], totalTokens: 0 }
  }

  const availableTags = await getAvailableTags()
  if (availableTags.length === 0) {
    return { results: [], totalTokens: 0 }
  }

  // Fetch full project data for all projects at once (single DB query)
  const fullProjects = await prisma.project.findMany({
    where: { id: { in: projects.map((p) => p.id) } },
    include: {
      projectTags: true,
      files: { select: { fileType: true } },
      _count: { select: { teamMembers: true, files: true } },
    },
  })

  const projectMap = new Map(fullProjects.map((p) => [p.id, p]))

  // Anonymize all projects at once
  const projectsWithRelations = fullProjects.map(toProjectWithRelations)
  const { anonymized, mappings } = anonymizeProjectsForAI(projectsWithRelations, 'FILTERING')

  if (!validateAnonymizedProjects(anonymized)) {
    throw new Error('GDPR compliance check failed: PII detected in anonymized data')
  }

  // Build mapping from anonymous ID to real project
  const anonToRealMap = new Map<string, string>()
  for (const mapping of mappings) {
    anonToRealMap.set(mapping.anonymousId, mapping.realId)
  }

  // Split into batches
  const batches: AnonymizedProjectForAI[][] = []
  for (let i = 0; i < anonymized.length; i += BATCH_SIZE) {
    batches.push(anonymized.slice(i, i + BATCH_SIZE))
  }

  const allResults: TaggingResult[] = []
  let totalTokens = 0
  let processedCount = 0

  // Process batches with concurrency
  for (let i = 0; i < batches.length; i += BATCH_CONCURRENCY) {
    const concurrentBatches = batches.slice(i, i + BATCH_CONCURRENCY)

    const batchPromises = concurrentBatches.map(async (batch) => {
      try {
        const { suggestionsMap, tokensUsed } = await getAISuggestionsBatch(
          batch,
          availableTags,
          userId
        )
        return { suggestionsMap, tokensUsed, error: null }
      } catch (error) {
        console.error('[AI Tagging Batch] Batch failed:', error)
        return { suggestionsMap: new Map<string, TagSuggestion[]>(), tokensUsed: 0, error }
      }
    })

    const batchResults = await Promise.all(batchPromises)

    // Process results from all concurrent batches
    for (const { suggestionsMap, tokensUsed } of batchResults) {
      totalTokens += tokensUsed

      for (const [anonId, suggestions] of suggestionsMap) {
        const realId = anonToRealMap.get(anonId)
        if (!realId) continue

        const project = projectMap.get(realId)
        if (!project) continue

        // Filter by confidence
        const validSuggestions = suggestions.filter(
          (s) => s.confidence >= settings.confidenceThreshold
        )

        // Get existing tags
        const existingTagIds = new Set(project.projectTags.map((pt) => pt.tagId))
        const currentTagCount = project.projectTags.length
        const remainingSlots = Math.max(0, settings.maxTags - currentTagCount)

        const newSuggestions = validSuggestions
          .filter((s) => !existingTagIds.has(s.tagId))
          .slice(0, remainingSlots)

        // Apply tags
        const applied: TagSuggestion[] = []
        for (const suggestion of newSuggestions) {
          try {
            await prisma.projectTag.create({
              data: {
                projectId: realId,
                tagId: suggestion.tagId,
                confidence: suggestion.confidence,
                source: 'AI',
              },
            })
            applied.push(suggestion)
          } catch {
            // Skip duplicates
          }
        }

        allResults.push({
          projectId: realId,
          suggestions,
          applied,
          tokensUsed: 0, // Token tracking is per-batch, not per-project
        })

        processedCount++
      }
    }

    // Report progress after each concurrent chunk
    if (onProgress) {
      await onProgress(processedCount, projects.length)
    }
  }

  return { results: allResults, totalTokens }
}

/**
 * Get tag suggestions for a project without applying them
 * Useful for preview/review before applying
 */
export async function getTagSuggestions(
  projectId: string,
  userId?: string
): Promise<TagSuggestion[]> {
  // Fetch project
  const project = await prisma.project.findUnique({
    where: { id: projectId },
    include: {
      files: { select: { fileType: true } },
      _count: { select: { teamMembers: true, files: true } },
    },
  })

  if (!project) {
    throw new Error(`Project not found: ${projectId}`)
  }

  // Get available tags
  const availableTags = await getAvailableTags()
  if (availableTags.length === 0) {
    return []
  }

  // Anonymize project data
  const projectWithRelations = toProjectWithRelations(project)
  const { anonymized } = anonymizeProjectsForAI([projectWithRelations], 'FILTERING')

  // Validate anonymization
  if (!validateAnonymizedProjects(anonymized)) {
    throw new Error('GDPR compliance check failed')
  }

  // Get AI suggestions
  const { suggestions } = await getAISuggestions(anonymized[0], availableTags, userId)

  return suggestions
}

/**
 * Manually add a tag to a project
 */
export async function addProjectTag(
  projectId: string,
  tagId: string
): Promise<void> {
  await prisma.projectTag.upsert({
    where: { projectId_tagId: { projectId, tagId } },
    create: { projectId, tagId, source: 'MANUAL', confidence: 1.0 },
    update: { source: 'MANUAL', confidence: 1.0 },
  })
}

/**
 * Remove a tag from a project
 */
export async function removeProjectTag(
  projectId: string,
  tagId: string
): Promise<void> {
  await prisma.projectTag.deleteMany({
    where: { projectId, tagId },
  })
}
