"""
Influencer Zero-Shot Classification & Enrichment Evaluation

Evaluates the ML service's influencer classification pipeline against
real influencer data from Final__data.csv.

This script performs a full evaluation:
1. Predicts Niche & Popularity using LLaMA (ModelPredictor)
2. Enriches data with Estimated Pricing & Quality Metrics
3. Generates Human-Readable Explanations (Niche/Tier)
4. Saves a comprehensive CSV dataset ready for recommendation matching.

Outputs:
- `evaluation_results.json`: Full detailed results
- `influencer_predictions.csv`: The "Cleaned CSV" with all metrics (replaces enrich_results.py output)

Usage:
    cd ml_service
    python -m evaluation.influencer.evaluate_influencer --sample-size 20
"""

import argparse
import json
import logging
import os
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path

import pandas as pd
import numpy as np

# Add roots to path so we can import app and backend modules
PROJECT_ROOT = str(Path(__file__).resolve().parents[3])
ML_SERVICE_ROOT = str(Path(__file__).resolve().parents[2])

if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)
if ML_SERVICE_ROOT not in sys.path:
    sys.path.insert(0, ML_SERVICE_ROOT)

# Load .env from ml_service root (needed for GROQ_API_KEY)
from dotenv import load_dotenv
load_dotenv(Path(ML_SERVICE_ROOT) / ".env")

from app.constants import POPULARITY_TIERS, NICHE_CLASSES, POPULARITY_CLASSES
from app.models.predictor import ModelPredictor
# Import pricing and service logic 
from app.utils.pricing import calculate_estimated_price, get_tier_from_budget

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
EVAL_DIR = Path(__file__).resolve().parent
DATA_FILE = EVAL_DIR / "Final__data.csv"
CHECKPOINT_FILE = EVAL_DIR / "results_checkpoint.json"
RESULTS_FILE = EVAL_DIR / "influencer_predictions.json"
OUTPUT_CSV = EVAL_DIR / "influencer_predictions.csv" 

# Keyword map for heuristic niche validation
NICHE_KEYWORDS = {
    "Fashion": ["fashion", "style", "outfit", "ootd", "clothing", "designer", "model", "vogue", "wear", "dress"],
    "Fitness": ["fitness", "gym", "workout", "training", "muscle", "bodybuilding", "crossfit", "athlete", "exercise"],
    "Food": ["food", "recipe", "cook", "chef", "baking", "restaurant", "foodie", "cuisine", "eat", "kitchen"],
    "Travel": ["travel", "adventure", "explore", "wanderlust", "trip", "destination", "tourism", "backpack", "globe"],
    "Tech": ["tech", "technology", "coding", "developer", "software", "programming", "gadget", "ai", "startup", "digital"],
    "Beauty": ["beauty", "makeup", "skincare", "cosmetics", "glam", "tutorial", "nail", "hair", "salon"],
    "Lifestyle": ["lifestyle", "daily", "life", "living", "home", "decor", "motivation", "inspiration"],
    "Gaming": ["gaming", "gamer", "esports", "twitch", "playstation", "xbox", "nintendo", "game", "streamer"],
    "Sports": ["sports", "football", "soccer", "basketball", "cricket", "tennis", "baseball", "nfl", "nba"],
    "Music": ["music", "singer", "song", "album", "concert", "band", "musician", "producer", "dj", "rap", "hiphop"],
    "Art": ["art", "artist", "painting", "drawing", "illustration", "gallery", "creative", "sculpture"],
    "Education": ["education", "learn", "teach", "school", "university", "professor", "tutor", "study"],
    "Business": ["business", "entrepreneur", "ceo", "founder", "startup", "company", "marketing", "brand"],
    "Health": ["health", "wellness", "mental", "nutrition", "yoga", "meditation", "holistic", "doctor", "nurse"],
    "Parenting": ["parent", "mom", "dad", "baby", "child", "family", "motherhood", "fatherhood", "kids"],
    "Photography": ["photo", "photographer", "photography", "camera", "lens", "portrait", "landscape"],
    "Comedy": ["comedy", "funny", "humor", "comedian", "jokes", "laugh", "meme", "satire"],
}

# Rate limit: seconds between API calls
API_DELAY = 0.5

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)-7s | %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def get_ground_truth_tier(followers: float) -> str:
    """Determine popularity tier from follower count using POPULARITY_TIERS."""
    if pd.isna(followers) or followers < 0:
        return "Unknown"
    for tier, bounds in POPULARITY_TIERS.items():
        if bounds["min"] <= followers < bounds["max"]:
            return tier
    return "Mega"  # fallback for exactly inf boundary


def build_text_context(row: pd.Series) -> str:
    """Combine biography + caption + hashtags for keyword matching."""
    parts = []
    for col in ["biography", "recent_caption", "recent_hashtags"]:
        val = row.get(col, "")
        if pd.notna(val) and str(val).strip():
            parts.append(str(val).lower())
    return " ".join(parts)


def detect_niche_keywords(text: str) -> list[str]:
    """Return list of niches whose keywords appear in text."""
    matched = []
    for niche, keywords in NICHE_KEYWORDS.items():
        if any(kw in text for kw in keywords):
            matched.append(niche)
    return matched


def load_checkpoint() -> dict:
    """Load previously saved checkpoint if it exists."""
    if CHECKPOINT_FILE.exists():
        with open(CHECKPOINT_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}


def save_checkpoint(data: dict):
    """Save checkpoint to disk."""
    with open(CHECKPOINT_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, default=str)


# ---------------------------------------------------------------------------
# Data Loading
# ---------------------------------------------------------------------------

def load_and_prepare_data(sample_size: int | None = None, seed: int = 42) -> pd.DataFrame:
    """Load Final__data.csv and prepare for evaluation."""
    logger.info(f"Loading data from {DATA_FILE}")
    df = pd.read_csv(DATA_FILE)
    logger.info(f"Raw data shape: {df.shape}")

    # Drop fully-NaN rows
    df = df.dropna(how="all").reset_index(drop=True)
    
    # Drop rows missing username
    df = df.dropna(subset=["username"]).reset_index(drop=True)

    # Fill missing text fields
    for col in ["biography", "recent_caption", "recent_hashtags"]:
        df[col] = df[col].fillna("")

    # Fill missing numeric fields with 0
    numeric_cols = ["followers", "following", "posts_count", "total_likes",
                    "total_comments", "avg_likes", "avg_comments",
                    "avg_video_views", "engagement_rate_%"]
    for col in numeric_cols:
        if col in df.columns:
            df[col] = df[col].fillna(0)

    # Convert verified to bool
    df["verified"] = df["verified"].map(
        lambda v: True if str(v).lower() in ("true", "1", "yes") else False
    )

    # Compute benchmark popularity tier (follower-count based)
    df["benchmark_tier"] = df["followers"].apply(get_ground_truth_tier)

    # Remove duplicate usernames – keep first
    df = df.drop_duplicates(subset=["username"], keep="first").reset_index(drop=True)

    # Sample
    if sample_size and sample_size < len(df):
        df = df.sample(n=sample_size, random_state=seed).reset_index(drop=True)
        logger.info(f"Sampled {sample_size} rows for evaluation")

    return df


# ---------------------------------------------------------------------------
# Enrichment Logic (Pricing & Explanations)
# ---------------------------------------------------------------------------

def enrich_data(record: dict) -> dict:
    """
    Add pricing, quality metrics, and explanations to the prediction record.
    Matches logic from enrich_results.py and backend services.
    """
    # 1. Calculate Estimated Price using Detailed Hybrid Model
    pricing_result = calculate_estimated_price(
        followers=record["followers"],
        engagement_rate=record.get("engagement_rate", 0),
        verified=record["verified"],
        avg_likes=record.get("avg_likes", 0),
        avg_comments=record.get("avg_comments", 0),
        llm_predicted_tier=record["predicted_popularity"]
    )
    
    price_min = pricing_result['estimated_min']
    price_max = pricing_result['estimated_max']
    quality_multiplier = pricing_result['quality_multiplier']
    is_quality_adjusted = pricing_result['is_quality_adjusted']
    base_price_per_1k = pricing_result['base_price_per_1k']
    clamped_tier = pricing_result['llm_tier']
    
    # Update record with clamped tier to prevent reports from showing hallucinations
    record["predicted_popularity"] = clamped_tier
    
    # 2. Compute Quality Metrics
    quality_ratio = 0.0
    if record["followers"] > 0:
        quality_ratio = (record.get("avg_likes", 0) + record.get("avg_comments", 0)) / record["followers"]
        
    comment_to_like_ratio = 0.0
    if record.get("avg_likes", 0) > 0:
        comment_to_like_ratio = record.get("avg_comments", 0) / record.get("avg_likes", 0)

    # 3. Generate Explanations (Using Service Logic usually, but here replicating for standalone eval)
    # Niche Explanation
    niche_exp = (f"Classified as {record['predicted_niche']} based on bio keywords and content analysis. "
                 f"Confidence score: {record.get('niche_confidence', 0):.2f}.")
    
    # Tier Explanation - USE CLAMPED TIER
    tier_exp = (f"Identified as {clamped_tier} Tier influencer with {record['followers']:,} followers "
                f"and {record.get('engagement_rate', 0):.2f}% engagement rate.")

    # Update record
    record["estimated_price_min"] = price_min
    record["estimated_price_max"] = price_max
    record["quality_multiplier"] = quality_multiplier
    record["is_quality_adjusted"] = is_quality_adjusted
    record["base_price_per_1k"] = base_price_per_1k
    record["quality_ratio"] = round(quality_ratio, 6)
    record["comment_to_like_ratio"] = round(comment_to_like_ratio, 6)
    record["niche_classification_explanation"] = niche_exp
    record["tier_classification_explanation"] = tier_exp
    
    return record


# ---------------------------------------------------------------------------
# Evaluation Runner
# ---------------------------------------------------------------------------

def run_evaluation(df: pd.DataFrame, predictor: ModelPredictor) -> list[dict]:
    """Run predictions on every row and collect results."""
    results = []
    checkpoint = load_checkpoint()
    already_done = set(checkpoint.get("completed_usernames", []))

    total = len(df)
    logger.info(f"Running evaluation on {total} influencers...")

    for idx, row in df.iterrows():
        username = row["username"]

        # Decide whether to skip based on checkpoint
        should_skip = False
        if username in already_done:
            # Find the existing result
            existing_res = next((r for r in checkpoint.get("results", []) if r["username"] == username), None)
            
            if existing_res:
                # RE-EVALUATE IF:
                # 1. Prediction was a failure (parse_success=False)
                # 2. Predicted values are None
                # 3. Misclassification detected: Nano tier but > 1,000,000 followers
                is_failed = not existing_res.get("parse_success", False)
                is_missing_data = existing_res.get("predicted_popularity") is None or existing_res.get("predicted_niche") is None
                is_misclassified_nano = (existing_res.get("predicted_popularity") == "Nano" and existing_res.get("followers", 0) > 1000000)
                
                if not (is_failed or is_missing_data or is_misclassified_nano):
                    should_skip = True
        
        if should_skip:
            logger.info(f"[{idx+1}/{total}] Skipping {username} (checkpointed & valid)")
            results.append(existing_res)
            continue

        logger.info(f"[{idx+1}/{total}] Predicting for @{username} "
                     f"(followers={int(row['followers']):,})")

        start_time = time.time()
        try:
            # 1. Prediction
            prediction = predictor.predict_influencer(
                biography=str(row["biography"]),
                recent_captions=str(row["recent_caption"]),
                recent_hashtags=str(row["recent_hashtags"]),
                followers=int(row["followers"]),
                following=int(row["following"]),
                posts_count=int(row["posts_count"]),
                verified=bool(row["verified"]),
                avg_likes=float(row["avg_likes"]),
                avg_comments=float(row["avg_comments"]),
                avg_video_views=float(row["avg_video_views"]),
                engagement_rate=float(row["engagement_rate_%"]),
            )
            elapsed = time.time() - start_time

            niche_probs = prediction["niche_probabilities"]
            popularity_probs = prediction["popularity_probabilities"]
            
            # Determine top predictions
            sorted_niches = sorted(niche_probs.items(), key=lambda x: x[1], reverse=True)
            sorted_pops = sorted(popularity_probs.items(), key=lambda x: x[1], reverse=True)

            # Base Result
            result = {
                "username": username,
                "followers": int(row["followers"]),
                "benchmark_tier": row["benchmark_tier"],
                
                # Prediction
                "predicted_niche": sorted_niches[0][0],
                "niche_confidence": round(sorted_niches[0][1], 4),
                "niche_top3": {k: round(v, 4) for k, v in sorted_niches[:3]},
                "niche_probabilities": niche_probs, # Needed for matching
                
                "predicted_popularity": sorted_pops[0][0],
                "popularity_confidence": round(sorted_pops[0][1], 4),
                "popularity_top2": {k: round(v, 4) for k, v in sorted_pops[:2]},
                
                # Metadata
                "response_time_s": round(elapsed, 3),
                "parse_success": True,
                "error": None,
                "has_biography": len(str(row["biography"]).strip()) > 0,
                "has_captions": len(str(row["recent_caption"]).strip()) > 0,
                "has_hashtags": len(str(row["recent_hashtags"]).strip()) > 0,
                "verified": bool(row["verified"]),
                
                # Raw inputs needed for enrichment
                "engagement_rate": float(row["engagement_rate_%"]),
                "avg_likes": float(row["avg_likes"]),
                "avg_comments": float(row["avg_comments"]),
            }
            
            # 2. Enrichment (Pricing & Explanations)
            result = enrich_data(result)

        except Exception as e:
            elapsed = time.time() - start_time
            logger.error(f"  ERROR for @{username}: {e}")
            result = {
                "username": username,
                "followers": int(row["followers"]),
                "benchmark_tier": row["benchmark_tier"],
                "predicted_niche": None,
                "niche_confidence": 0.0,
                "predicted_popularity": None,
                "response_time_s": round(elapsed, 3),
                "parse_success": False,
                "error": str(e),
            }

        results.append(result)

        # Save checkpoint every 5 rows
        if (idx + 1) % 5 == 0:
            save_checkpoint({
                "completed_usernames": [r["username"] for r in results],
                "results": results,
                "last_updated": datetime.now().isoformat(),
            })
            logger.info(f"  Checkpoint saved ({len(results)} results)")

        # Rate limit
        time.sleep(API_DELAY)

    # Final checkpoint
    save_checkpoint({
        "completed_usernames": [r["username"] for r in results],
        "results": results,
        "last_updated": datetime.now().isoformat(),
    })

    return results


# ---------------------------------------------------------------------------
# Metrics Computation
# ---------------------------------------------------------------------------

def compute_metrics(results: list[dict], df: pd.DataFrame) -> dict:
    """Compute all evaluation metrics from results."""
    successful = [r for r in results if r.get("parse_success")]
    failed = [r for r in results if not r.get("parse_success")]

    metrics = {
        "total_samples": len(results),
        "successful_predictions": len(successful),
        "failed_predictions": len(failed),
        "parse_success_rate": round(len(successful) / len(results) * 100, 2) if results else 0,
    }

    if not successful:
        return metrics

    # ── Prediction Shifts (LLM vs Followers) ──
    shifted = [r for r in successful if r["predicted_popularity"] != r["benchmark_tier"]]
    
    metrics["tier_shifts"] = {
        "total_shifts": len(shifted),
        "shift_rate": round(len(shifted) / len(successful) * 100, 2),
    }

    # ── Niche Confidence Distribution ──
    niche_confs = [r["niche_confidence"] for r in successful]
    metrics["niche"] = {
        "avg_confidence": round(sum(niche_confs) / len(niche_confs), 4),
        "distribution": dict(Counter(r["predicted_niche"] for r in successful).most_common()),
    }
    
    return metrics


def print_report(metrics: dict):
    """Print a formatted evaluation report to console."""
    print("\n" + "=" * 72)
    print("  INFLUENCER EVALUATION REPORT")
    print("=" * 72)

    print(f"\n{'Total Samples:':<30} {metrics['total_samples']}")
    print(f"{'Successful Predictions:':<30} {metrics['successful_predictions']}")
    print(f"{'Parse Success Rate:':<30} {metrics['parse_success_rate']:.1f}%")

    if "tier_shifts" in metrics:
        shifts = metrics["tier_shifts"]
        print(f"\n{'Quality Adjustments:':<30} {shifts['total_shifts']} instances ({shifts['shift_rate']:.1f}% of data)")
        print(f"{'  (Predictions shifted away from follower-count tiers due to quality data)'}")

    if "niche" in metrics:
        niche = metrics["niche"]
        print(f"{'Avg Confidence:':<30} {niche['avg_confidence']:.2%}")
        print(f"\n  Niche Distribution:")
        for niche_name, count in sorted(niche["distribution"].items(), key=lambda x: -x[1]):
            bar = "█" * count
            print(f"    {niche_name:<15} {count:>3}  {bar}")

    print("\n" + "=" * 72)
    print("  EVALUATION COMPLETE")
    print("=" * 72 + "\n")


def save_results(results: list[dict], metrics: dict):
    """Save full results and summary CSV."""
    # JSON results
    output = {
        "evaluation_date": datetime.now().isoformat(),
        "metrics": metrics,
        "records": results, # Store as records for easy loading
    }
    with open(RESULTS_FILE, "w", encoding="utf-8") as f:
        json.dump(output, f, indent=2, default=str)
    logger.info(f"Full results saved to {RESULTS_FILE}")

    # Comprehensive CSV (The CLEANED CSV)
    df_results = pd.DataFrame(results)
    
    # Flatten/Cleanup for CSV
    cols_to_keep = [
        "username", "followers", "verified", "benchmark_tier",
        "predicted_niche", "niche_confidence", 
        "predicted_popularity", "popularity_confidence",
        "estimated_price_min", "estimated_price_max",
        "engagement_rate", "avg_likes", "avg_comments", "quality_ratio",
        "niche_classification_explanation", "tier_classification_explanation",
        "biography", "recent_captions", "recent_hashtags"
    ]
    # Filter only existing cols
    export_cols = [c for c in cols_to_keep if c in df_results.columns]
    
    df_results[export_cols].to_csv(OUTPUT_CSV, index=False)
    logger.info(f"Cleaned Predictions CSV saved to {OUTPUT_CSV}")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Evaluate influencer zero-shot classification & enrichment"
    )
    parser.add_argument(
        "--sample-size", type=int, default=50,
        help="Number of influencers to evaluate (default: 50, use 0 for all)"
    )
    parser.add_argument(
        "--seed", type=int, default=42,
        help="Random seed for sampling (default: 42)"
    )
    parser.add_argument(
        "--resume", action="store_true",
        help="Resume from checkpoint if available"
    )
    parser.add_argument(
        "--delay", type=float, default=0.5,
        help="Seconds between API calls (default: 0.5)"
    )
    args = parser.parse_args()

    global API_DELAY
    API_DELAY = args.delay

    if not args.resume and CHECKPOINT_FILE.exists():
        CHECKPOINT_FILE.unlink()
        logger.info("Cleared previous checkpoint")

    # Load data
    sample = args.sample_size if args.sample_size > 0 else None
    df = load_and_prepare_data(sample_size=sample, seed=args.seed)

    # Initialize predictor
    logger.info("Initializing ModelPredictor (Groq API)...")
    predictor = ModelPredictor()
    logger.info("ModelPredictor ready!")

    # Run evaluation (prediction + enrichment)
    results = run_evaluation(df, predictor)

    # Compute metrics
    metrics = compute_metrics(results, df)

    # Print report
    print_report(metrics)

    # Save outputs
    save_results(results, metrics)

    # Cleanup
    if CHECKPOINT_FILE.exists():
        CHECKPOINT_FILE.unlink()
        logger.info("Checkpoint cleaned up")

if __name__ == "__main__":
    main()
