import json
import numpy as np
import pandas as pd
from pathlib import Path
from typing import Dict, List, Tuple
from collections import Counter

# ---------------------------------------------------------------------------
# Constants & Config
# ---------------------------------------------------------------------------
WEIGHT_NICHE = 0.5
WEIGHT_BUDGET = 0.5
TOP_K = 5
MAX_BUDGET_MULTIPLIER = 2.0  # Initial hard filter (e.g. max price < 2x max budget)

EVAL_DIR = Path(__file__).resolve().parent
INFLUENCER_DB_PATH = EVAL_DIR.parent / "influencer" / "influencer_predictions.json"
BUSINESS_PREDS_PATH = EVAL_DIR.parent / "business" / "business_predictions.json"
OUTPUT_CSV = EVAL_DIR / "recommendation_results.csv"
OUTPUT_JSON = EVAL_DIR / "recommendation_results.json"

# ---------------------------------------------------------------------------
# Matching Logic (Matches backend/businesses/services/matching_service.py)
# ---------------------------------------------------------------------------

def calculate_cosine_similarity(probs1: Dict, probs2: Dict) -> float:
    """Calculate cosine similarity between two probability distributions"""
    all_keys = sorted(set(probs1.keys()) | set(probs2.keys()))
    if not all_keys:
        return 0.0

    vec1 = np.array([probs1.get(k, 0.0) for k in all_keys])
    vec2 = np.array([probs2.get(k, 0.0) for k in all_keys])

    dot_product = np.dot(vec1, vec2)
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)

    if norm1 == 0 or norm2 == 0:
        return 0.0

    similarity = dot_product / (norm1 * norm2)
    return max(0.0, min(1.0, similarity))

def calculate_budget_price_overlap(
    budget_min: float, budget_max: float,
    price_min: float, price_max: float
) -> Tuple[float, Dict]:
    """Calculate overlap between business budget and influencer price range."""
    overlap_min = max(budget_min, price_min)
    overlap_max = min(budget_max, price_max)
    overlap_amount = max(0, overlap_max - overlap_min)

    budget_range = budget_max - budget_min
    
    # Score based on overlap percentage of BUDGET
    if budget_range > 0:
        overlap_score = overlap_amount / budget_range
    else:
        overlap_score = 1.0 if overlap_amount > 0 else 0.0

    # Affordability check
    is_affordable = price_min <= budget_max and price_max >= budget_min
    
    if price_max < budget_min:
        price_position = 'too_cheap'
    elif price_min > budget_max:
        price_position = 'too_expensive'
    else:
        price_position = 'within_budget'

    return overlap_score, {
        'overlap_amount': round(overlap_amount, 2),
        'is_affordable': is_affordable,
        'price_position': price_position
    }

def get_top_class(probs: Dict) -> str:
    """Get the class with highest probability"""
    if not probs:
        return None
    return max(probs.items(), key=lambda x: x[1])[0]

# ---------------------------------------------------------------------------
# Main Evaluation
# ---------------------------------------------------------------------------

def main():
    print("Starting Recommendation Evaluation...")
    
    # 1. Load Data
    if not INFLUENCER_DB_PATH.exists():
        print(f"Error: Database not found at {INFLUENCER_DB_PATH}")
        return
    if not BUSINESS_PREDS_PATH.exists():
        print(f"Error: Business predictions not found at {BUSINESS_PREDS_PATH}")
        return
        
    with open(INFLUENCER_DB_PATH, "r", encoding="utf-8") as f:
        # Merged format has "records" key
        influencer_data = json.load(f).get("records", [])
    print(f"Loaded {len(influencer_data)} influencer profiles (The Database)")
        
    with open(BUSINESS_PREDS_PATH, "r", encoding="utf-8") as f:
        business_cases = json.load(f)
    print(f"Loaded {len(business_cases)} business cases (The Queries)")
    
    all_recommendations = []
    summary_stats = {
        "total_cases": len(business_cases),
        "niche_matches": 0,
        "budget_matches": 0,
        "total_recommendations": 0
    }
    
    # 2. Run Matching for Each Business
    for case in business_cases:
        business_name = case["business_name"]
        budget_min = case["budget_min"]
        budget_max = case["budget_max"]
        
        # Get predictions
        biz_pred = case["prediction"]
        biz_niche_probs = biz_pred.get("niche_probabilities", {})
        biz_top_niche = get_top_class(biz_niche_probs)
        
        print(f"\nMatching for '{business_name}'...")
        print(f"  Target: {biz_top_niche} | Budget: ${budget_min}-${budget_max}")
        
        # --- STAGE 1: Strict Filter by Niche + Budget ---
        # We NO LONGER back off. If no matches found, we return empty.
        candidates = []
        for inf in influencer_data:
            inf_top_niche = get_top_class(inf.get("niche_probabilities", {}))
            inf_price_min = inf.get("estimated_price_min", 0)
            
            # Intersection: Niche Match AND strict budget cap (2x budget_max)
            if inf_top_niche == biz_top_niche:
                if inf_price_min <= (budget_max * MAX_BUDGET_MULTIPLIER):
                    candidates.append(inf)
        
        print(f"  Stage 1: Found {len(candidates)} affordable candidates in niche '{biz_top_niche}'")
            
        if not candidates:
            print(f"  Warning: No matches found for '{business_name}' that fit both niche and budget.")
                
        # --- STAGE 2: Score Candidates ---
        # (scoring logic remains the same)
        scored_matches = []
        for inf in candidates:
            # 1. Niche Similarity
            niche_score = calculate_cosine_similarity(
                biz_niche_probs, 
                inf.get("niche_probabilities", {})
            )
            
            # 2. Budget/Price Overlap
            inf_price_min = inf.get("estimated_price_min", 0)
            inf_price_max = inf.get("estimated_price_max", 0)
            
            budget_score, price_info = calculate_budget_price_overlap(
                budget_min, budget_max, 
                inf_price_min, inf_price_max
            )
            
            # 3. Total Weighted Score
            total_score = (WEIGHT_NICHE * niche_score) + (WEIGHT_BUDGET * budget_score)
            
            scored_matches.append({
                "influencer": inf,
                "score": total_score,
                "niche_score": niche_score,
                "budget_score": budget_score,
                "price_info": price_info
            })
            
        # Sort and take Top K
        scored_matches.sort(key=lambda x: x["score"], reverse=True)
        top_matches = scored_matches[:TOP_K]
        
        # Store results
        for rank, match in enumerate(top_matches, 1):
            inf = match["influencer"]
            price_info = match["price_info"]
            
            rec_record = {
                "business_name": business_name,
                "business_niche": biz_top_niche,
                "business_budget": f"${budget_min}-${budget_max}",
                
                "rank": rank,
                "match_score": round(match["score"], 4),
                
                "influencer_handle": inf["username"],
                "influencer_niche": inf["predicted_niche"],
                "influencer_tier": inf["predicted_popularity"],
                "influencer_followers": inf["followers"],
                "influencer_price": f"${inf['estimated_price_min']}-${inf['estimated_price_max']}",
                
                "niche_score": round(match["niche_score"], 4),
                "budget_score": round(match["budget_score"], 4),
                "price_position": price_info["price_position"],
                
                # Full explanations for review
                "niche_explanation": inf.get("niche_classification_explanation", ""),
                "tier_explanation": inf.get("tier_classification_explanation", "")
            }
            all_recommendations.append(rec_record)
            
            # Stats
            summary_stats["total_recommendations"] += 1
            if biz_top_niche == inf["predicted_niche"]:
                summary_stats["niche_matches"] += 1
            if price_info["is_affordable"]:
                summary_stats["budget_matches"] += 1

    # 3. Save Results
    df_results = pd.DataFrame(all_recommendations)
    df_results.to_csv(OUTPUT_CSV, index=False)
    print(f"\nSaved detailed recommendations to {OUTPUT_CSV}")
    
    # 4. Print Summary
    print(f"\n{'='*60}")
    print("RECOMMENDATION EVALUATION SUMMARY")
    print(f"{'='*60}")
    print(f"Total Business Cases: {summary_stats['total_cases']}")
    print(f"Top-{TOP_K} Matches Generated: {summary_stats['total_recommendations']}")
    
    if summary_stats['total_recommendations'] > 0:
        niche_match_rate = (summary_stats['niche_matches'] / summary_stats['total_recommendations']) * 100
        budget_match_rate = (summary_stats['budget_matches'] / summary_stats['total_recommendations']) * 100
        
        print(f"Niche Consistency Rate: {niche_match_rate:.1f}%")
        print(f"Budget Fit Rate: {budget_match_rate:.1f}%")
    else:
        print("Niche Consistency Rate: N/A (No recommendations generated)")
        print("Budget Fit Rate: N/A (No recommendations generated)")
    
    # Print a few examples
    print(f"\n--- Example Matches ---")
    if not df_results.empty:
        examples = df_results[df_results['rank'] == 1].head(3)
        for _, row in examples.iterrows():
            print(f"\nBusiness: {row['business_name']} ({row['business_budget']})")
            print(f"  -> Top Match: {row['influencer_handle']} ({row['influencer_tier']})")
            print(f"  -> Price: {row['influencer_price']} | Score: {row['match_score']}")
            print(f"  -> Rationale: {row['niche_explanation'][:100]}...")

if __name__ == "__main__":
    main()
