// Expense Auto-Categorization System
// LLM classifies transactions, flags anomalies

import prisma from '@/lib/db';
import { logSimpleAction } from './agent-logger';

export interface CategorizationResult {
  expenseId: string;
  description: string;
  amount: number;
  category: string;
  subcategory?: string;
  confidence: number;
  reasoning: string;
  flagged: boolean;
  flagReason?: string;
}

// Category definitions with keywords for rule-based fallback
const CATEGORY_RULES: Record<string, { keywords: string[]; subcategories: string[] }> = {
  'office_supplies': {
    keywords: ['staples', 'office depot', 'amazon', 'paper', 'ink', 'toner', 'supplies'],
    subcategories: ['paper', 'printing', 'furniture', 'equipment', 'general'],
  },
  'software': {
    keywords: ['saas', 'subscription', 'license', 'github', 'slack', 'notion', 'figma', 'adobe', 'microsoft', 'google', 'aws', 'cloud'],
    subcategories: ['productivity', 'development', 'design', 'infrastructure', 'security'],
  },
  'travel': {
    keywords: ['airline', 'hotel', 'uber', 'lyft', 'airbnb', 'expedia', 'flight', 'rental car'],
    subcategories: ['flights', 'lodging', 'ground_transport', 'meals', 'other'],
  },
  'marketing': {
    keywords: ['ads', 'advertising', 'google ads', 'facebook', 'linkedin', 'campaign', 'promotion'],
    subcategories: ['digital_ads', 'content', 'events', 'swag', 'other'],
  },
  'professional_services': {
    keywords: ['consulting', 'legal', 'accounting', 'lawyer', 'attorney', 'cpa', 'audit'],
    subcategories: ['legal', 'accounting', 'consulting', 'recruiting', 'other'],
  },
  'payroll': {
    keywords: ['salary', 'wages', 'bonus', 'payroll', 'gusto', 'rippling', 'adp'],
    subcategories: ['salaries', 'bonuses', 'benefits', 'taxes', 'contractors'],
  },
  'utilities': {
    keywords: ['electric', 'gas', 'water', 'internet', 'phone', 'comcast', 'att', 'verizon'],
    subcategories: ['electricity', 'internet', 'phone', 'other'],
  },
  'rent': {
    keywords: ['rent', 'lease', 'wework', 'office space', 'coworking'],
    subcategories: ['office', 'storage', 'parking'],
  },
  'equipment': {
    keywords: ['computer', 'laptop', 'monitor', 'apple', 'dell', 'lenovo', 'hardware'],
    subcategories: ['computers', 'peripherals', 'furniture', 'other'],
  },
  'meals_entertainment': {
    keywords: ['restaurant', 'doordash', 'grubhub', 'dinner', 'lunch', 'catering'],
    subcategories: ['team_meals', 'client_entertainment', 'catering', 'other'],
  },
};

// Anomaly detection thresholds
const ANOMALY_THRESHOLDS = {
  unusuallyHigh: 5000,        // Flag transactions over this amount
  duplicateWindow: 7,         // Days to check for duplicates
  unusualCategory: ['uncategorized', 'other'],
  weekendTransaction: true,   // Flag weekend transactions
};

// Rule-based categorization (fallback when LLM unavailable)
function categorizeByRules(description: string, amount: number): {
  category: string;
  subcategory: string;
  confidence: number;
  reasoning: string;
} {
  const lowerDesc = description.toLowerCase();
  
  for (const [category, rules] of Object.entries(CATEGORY_RULES)) {
    for (const keyword of rules.keywords) {
      if (lowerDesc.includes(keyword.toLowerCase())) {
        return {
          category,
          subcategory: rules.subcategories[0],
          confidence: 0.75,
          reasoning: `Matched keyword "${keyword}" in description`,
        };
      }
    }
  }
  
  return {
    category: 'uncategorized',
    subcategory: 'review_needed',
    confidence: 0.3,
    reasoning: 'No matching keywords found, requires manual review',
  };
}

// LLM-based categorization
async function categorizeWithLLM(description: string, amount: number, vendor?: string): Promise<{
  category: string;
  subcategory: string;
  confidence: number;
  reasoning: string;
}> {
  const apiKey = process.env.ABACUSAI_API_KEY;
  if (!apiKey) {
    return categorizeByRules(description, amount);
  }
  
  const categories = Object.keys(CATEGORY_RULES).join(', ');
  
  const prompt = `You are an expense categorization AI. Categorize this business expense:

Description: ${description}
Amount: $${amount.toFixed(2)}
${vendor ? `Vendor: ${vendor}` : ''}

Available categories: ${categories}

Respond with JSON only:
{
  "category": "category_name",
  "subcategory": "subcategory_name",
  "confidence": 0.95,
  "reasoning": "Brief explanation"
}`;

  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 30_000); // 30s timeout for categorization
    const response = await fetch('https://routellm.abacus.ai/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: prompt }],
        temperature: 0.1,
        max_tokens: 200,
      }),
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      console.error('[ExpenseCategorizer] LLM API error, falling back to rules');
      return categorizeByRules(description, amount);
    }
    
    const data = await response.json();
    const content = data.choices?.[0]?.message?.content || '';
    
    // Parse JSON from response
    const jsonMatch = content.match(/\{[\s\S]*\}/);
    if (jsonMatch) {
      const parsed = JSON.parse(jsonMatch[0]);
      return {
        category: parsed.category || 'uncategorized',
        subcategory: parsed.subcategory || 'general',
        confidence: parsed.confidence || 0.8,
        reasoning: parsed.reasoning || 'LLM categorization',
      };
    }
    
    return categorizeByRules(description, amount);
  } catch (error) {
    console.error('[ExpenseCategorizer] Error:', error);
    return categorizeByRules(description, amount);
  }
}

// Check for anomalies
async function detectAnomalies(expense: {
  description: string;
  amount: number;
  date: Date;
  vendor?: string;
  category: string;
}): Promise<{ flagged: boolean; reasons: string[] }> {
  const reasons: string[] = [];
  
  // Check unusually high amount
  if (expense.amount > ANOMALY_THRESHOLDS.unusuallyHigh) {
    reasons.push(`Unusually high amount: $${expense.amount.toFixed(2)}`);
  }
  
  // Check for potential duplicates
  const windowStart = new Date(expense.date);
  windowStart.setDate(windowStart.getDate() - ANOMALY_THRESHOLDS.duplicateWindow);
  
  const potentialDuplicates = await prisma.expense.findMany({
    where: {
      amount: expense.amount,
      date: { gte: windowStart, lte: expense.date },
      vendor: expense.vendor,
    },
  });
  
  if (potentialDuplicates.length > 0) {
    reasons.push(`Potential duplicate: ${potentialDuplicates.length} similar transaction(s) in last ${ANOMALY_THRESHOLDS.duplicateWindow} days`);
  }
  
  // Check weekend transaction
  const dayOfWeek = expense.date.getDay();
  if (ANOMALY_THRESHOLDS.weekendTransaction && (dayOfWeek === 0 || dayOfWeek === 6)) {
    reasons.push('Weekend transaction');
  }
  
  // Check uncategorized
  if (ANOMALY_THRESHOLDS.unusualCategory.includes(expense.category)) {
    reasons.push(`Requires manual categorization: ${expense.category}`);
  }
  
  return {
    flagged: reasons.length > 0,
    reasons,
  };
}

// Categorize a single expense
export async function categorizeExpense(
  expenseId: string,
  useLLM: boolean = true
): Promise<CategorizationResult> {
  const expense = await prisma.expense.findUnique({
    where: { id: expenseId },
  });
  
  if (!expense) throw new Error(`Expense ${expenseId} not found`);
  
  // Categorize
  const categorization = useLLM
    ? await categorizeWithLLM(expense.description, expense.amount, expense.vendor || undefined)
    : categorizeByRules(expense.description, expense.amount);
  
  // Detect anomalies
  const anomalyCheck = await detectAnomalies({
    description: expense.description,
    amount: expense.amount,
    date: expense.date,
    vendor: expense.vendor || undefined,
    category: categorization.category,
  });
  
  // Update expense record
  await prisma.expense.update({
    where: { id: expenseId },
    data: {
      category: categorization.category,
      subcategory: categorization.subcategory,
      status: anomalyCheck.flagged ? 'flagged' : 'categorized',
      aiCategorized: true,
      aiConfidence: categorization.confidence,
      aiReasoning: categorization.reasoning,
      flagReason: anomalyCheck.flagged ? anomalyCheck.reasons.join('; ') : null,
    },
  });
  
  // Log action
  await logSimpleAction({
    actionType: 'detect_anomaly',
    targetEntity: 'Expense',
    targetId: expenseId,
    input: { description: expense.description, amount: expense.amount },
    output: { category: categorization.category, confidence: categorization.confidence, flagged: anomalyCheck.flagged },
    confidence: categorization.confidence,
    anomalyDetected: anomalyCheck.flagged,
    anomalyType: anomalyCheck.flagged ? 'expense_anomaly' : undefined,
  });
  
  return {
    expenseId,
    description: expense.description,
    amount: expense.amount,
    category: categorization.category,
    subcategory: categorization.subcategory,
    confidence: categorization.confidence,
    reasoning: categorization.reasoning,
    flagged: anomalyCheck.flagged,
    flagReason: anomalyCheck.flagged ? anomalyCheck.reasons.join('; ') : undefined,
  };
}

// Process all uncategorized expenses with concurrency-limited parallelism
const CATEGORIZATION_CONCURRENCY = 5;

export async function processUncategorizedExpenses(): Promise<CategorizationResult[]> {
  const uncategorized = await prisma.expense.findMany({
    where: {
      OR: [
        { status: 'pending' },
        { category: 'uncategorized' },
        { aiCategorized: false },
      ],
    },
    take: 50,
  });
  
  const results: CategorizationResult[] = [];
  
  // Process in concurrency-limited batches
  for (let i = 0; i < uncategorized.length; i += CATEGORIZATION_CONCURRENCY) {
    const batch = uncategorized.slice(i, i + CATEGORIZATION_CONCURRENCY);
    const batchResults = await Promise.allSettled(
      batch.map((expense: any) => categorizeExpense(expense.id, true))
    );
    
    for (const result of batchResults) {
      if (result.status === 'fulfilled') {
        results.push(result.value);
      } else {
        console.error('[ExpenseCategorizer] Failed to categorize expense:', result.reason);
      }
    }
  }
  
  return results;
}

// Get expense stats
export async function getExpenseStats(): Promise<{
  totalExpenses: number;
  categorized: number;
  uncategorized: number;
  flagged: number;
  byCategory: { category: string; count: number; total: number }[];
  avgConfidence: number;
}> {
  const [all, categorized, uncategorized, flagged] = await Promise.all([
    prisma.expense.count(),
    prisma.expense.count({ where: { status: 'categorized' } }),
    prisma.expense.count({ where: { OR: [{ status: 'pending' }, { category: 'uncategorized' }] } }),
    prisma.expense.count({ where: { status: 'flagged' } }),
  ]);
  
  const byCategory = await prisma.expense.groupBy({
    by: ['category'],
    _count: true,
    _sum: { amount: true },
  });
  
  const withConfidence = await prisma.expense.findMany({
    where: { aiConfidence: { not: null } },
    select: { aiConfidence: true },
  });
  
  const avgConfidence = withConfidence.length > 0
    ? withConfidence.reduce((sum: number, e: { aiConfidence: number | null }) => sum + (e.aiConfidence || 0), 0) / withConfidence.length
    : 0;
  
  return {
    totalExpenses: all,
    categorized,
    uncategorized,
    flagged,
    byCategory: byCategory.map((c: any) => ({
      category: c.category,
      count: c._count,
      total: c._sum.amount || 0,
    })),
    avgConfidence,
  };
}
