# ==============================================
# ADCP API — FastAPI Implementation
# Implements AdCP v2.3.0 specification
# ==============================================
from fastapi import FastAPI, HTTPException, Query, Body
from fastapi.middleware.cors import CORSMiddleware
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from pymongo import MongoClient
from bson import ObjectId
import os

# ==============================================
# MongoDB Connection
# ==============================================
MONGO_URI = "mongodb://admin:admin123@mongodb:27017/mcp_adcp_db?authSource=admin"
client = MongoClient(MONGO_URI)
db = client["mcp_acdp_db"]

# ==============================================
# FastAPI Setup
# ==============================================
app = FastAPI(
    title="ADCP API",
    version="2.3.0",
    description="Ad Context Protocol compliant API for TV advertising",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ==============================================
# ADCP Pydantic Models
# ==============================================

class ADCPResponse(BaseModel):
    protocol: str = "adcp"
    version: str = "2.3.0"
    status: str  # completed, input-required, working, failed
    timestamp: str
    data: Optional[Any] = None
    message: Optional[str] = None

class ProductDiscoveryRequest(BaseModel):
    query: str
    channel: Optional[str] = None
    date_from: Optional[str] = None
    date_to: Optional[str] = None
    filters: Optional[Dict] = {}

class MediaBuyRequest(BaseModel):
    name: str
    advertiser: str
    packages: List[Dict]  # List of {package_id: str}
    start_date: str
    end_date: str
    budget: float
    currency: str = "MAD"
    objectives: List[str] = ["reach"]
    kpis: Dict = {}

class SignalDiscoveryRequest(BaseModel):
    query: str
    signal_types: List[str] = ["audience", "contextual"]
    providers: Optional[List[str]] = None
    filters: Optional[Dict] = {}

class SignalActivationRequest(BaseModel):
    signal_id: str
    platforms: List[Dict]  # List of {platform_id: str}
    config: Dict = {}

class CreativeSyncRequest(BaseModel):
    media_buy_id: str
    creatives: List[Dict]  # List of {url: str}
    assignments: Dict = {}

# ==============================================
# Helper Functions
# ==============================================

def serialize_doc(doc):
    """Convert MongoDB documents to JSON-safe objects."""
    if "_id" in doc:
        doc["_id"] = str(doc["_id"])
    return doc

def create_adcp_response(status: str, data: Any = None, message: str = None) -> Dict:
    """Create ADCP-compliant response"""
    return {
        "protocol": "adcp",
        "version": "2.3.0",
        "status": status,
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "data": data,
        "message": message
    }

def transform_adbreak_to_product(adbreak, program, channel, price_data):
    """Transform ad break to ADCP Product format"""
    return {
        "product_id": adbreak["ad_break_id"],
        "name": f"{program['title']} - Ad Break",
        "type": "linear_tv",
        "format": "video",
        "channel": {
            "id": channel["channel_id"],
            "name": channel["channel_name"],
            "language": channel.get("language", "ar"),
            "country": channel.get("country", "MA")
        },
        "program": {
            "id": program["program_id"],
            "title": program["title"],
            "category": program.get("category", "general"),
            "start_time": program["start_time"].isoformat(),
            "end_time": program["end_time"].isoformat()
        },
        "placement": {
            "start_time": adbreak["start_time"].isoformat(),
            "duration_seconds": adbreak["duration_seconds"],
            "position": adbreak.get("position", "mid-roll")
        },
        "pricing": {
            "base_price": price_data.get("base_price", 0) if price_data else 0,
            "currency": price_data.get("currency", "MAD") if price_data else "MAD",
            "pricing_model": price_data.get("pricing_model", "cpm") if price_data else "cpm"
        },
        "availability": {
            "available": not adbreak.get("is_sold", False),
            "inventory_type": "guaranteed"
        },
        "metadata": {
            "ad_break_id": adbreak["ad_break_id"],
            "program_id": program["program_id"],
            "channel_id": channel["channel_id"]
        }
    }

# ==============================================
# LEGACY ROUTES (Backward Compatibility)
# ==============================================

@app.get("/api/v1/channels")
def get_channels():
    """List all channels."""
    channels = list(db['channels'].find())
    return [serialize_doc(c) for c in channels]

@app.get("/api/v1/adbreaks")
def get_adbreaks(available: Optional[bool] = None):
    """List all ad breaks with optional availability filter."""
    query = {}
    if available is not None:
        query["is_sold"] = not available  # available=True means is_sold=False
    adbreaks = db['ad_breaks'].find(query)
    return [serialize_doc(c) for c in adbreaks]

@app.get("/api/v1/channels/{channel_id}")
def get_channel(channel_id: str):
    """Get single channel by ID."""
    channel = db.channels.find_one({"channel_id": channel_id})
    if not channel:
        raise HTTPException(status_code=404, detail="Channel not found")
    return serialize_doc(channel)

@app.get("/api/v1/programs")
def get_programs(channel: Optional[str] = None, date: Optional[str] = None):
    """Fetch EPG programs by channel and optional date."""
    query = {}
    if channel:
        query["channel_id"] = channel
    if date:
        try:
            start = datetime.fromisoformat(date)
            end = datetime.fromisoformat(date)
            end = end.replace(hour=23, minute=59, second=59)
            query["start_time"] = {"$gte": start, "$lte": end}
        except ValueError:
            raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")

    programs = list(db.programs.find(query))
    return [serialize_doc(p) for p in programs]

@app.get("/api/v1/programs/{program_id}")
def get_program(program_id: str):
    program = db.programs.find_one({"program_id": program_id})
    if not program:
        raise HTTPException(status_code=404, detail="Program not found")
    return serialize_doc(program)

@app.get("/api/v1/inventory")
def get_inventory(
    channel: str = Query(..., description="Channel ID (e.g., al_aoula)"),
    date: str = Query(..., description="Date in YYYY-MM-DD"),
    region: Optional[str] = Query(None, description="Region (e.g., France)")
):
    """Return ad inventory by channel/date and optional region."""
    try:
        start = datetime.fromisoformat(date)
        end = start.replace(hour=23, minute=59, second=59)
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")

    programs = list(db.programs.find({
        "channel_id": channel,
        "start_time": {"$gte": start, "$lte": end}
    }))
    program_ids = [p["program_id"] for p in programs]

    if not program_ids:
        return []

    ad_breaks = list(db.ad_breaks.find({"program_id": {"$in": program_ids}}))
    results = []

    for ad in ad_breaks:
        ad_data = serialize_doc(ad)
        price = db.inventory_prices.find_one({"inventory_id": ad["ad_break_id"]})
        if price:
            ad_data["price"] = serialize_doc(price)
        if region:
            audience = db.audience_data.find_one({
                "inventory_id": ad["ad_break_id"],
                "region": {"$regex": f"^{region}$", "$options": "i"}
            })
            if audience:
                ad_data["audience"] = serialize_doc(audience)
        results.append(ad_data)

    return results

@app.get("/api/v1/pricing")
def get_pricing(inventory_id: str = Query(..., description="Inventory/Ad Break ID")):
    """Get pricing info for a given ad break (inventory)."""
    price = db.inventory_prices.find_one({"inventory_id": inventory_id})
    if not price:
        raise HTTPException(status_code=404, detail="Pricing not found for this inventory ID")
    return serialize_doc(price)

@app.post("/api/v1/book_ad")
def book_ad(inventory_id: str = Body(..., embed=True)):
    """Mark an ad break as sold."""
    ad_break = db.ad_breaks.find_one({"ad_break_id": inventory_id})
    if not ad_break:
        raise HTTPException(status_code=404, detail="Ad break not found")

    if ad_break.get("is_sold", False):
        raise HTTPException(status_code=400, detail="This ad is already sold")

    db.ad_breaks.update_one({"ad_break_id": inventory_id}, {"$set": {"is_sold": True}})
    return {"message": "Ad booked successfully", "inventory_id": inventory_id}

@app.get("/api/v1/audience")
def get_audience(inventory_id: Optional[str] = None, region: Optional[str] = None):
    """Fetch audience data by inventory or region."""
    query = {}
    if inventory_id:
        query["inventory_id"] = inventory_id
    if region:
        query["region"] = {"$regex": f"^{region}$", "$options": "i"}

    data = list(db.audience_data.find(query))
    return [serialize_doc(d) for d in data]

# ==============================================
# ADCP ROUTES — Media Buy Protocol
# ==============================================

@app.post("/api/v1/adcp/products")
def adcp_get_products(request: ProductDiscoveryRequest):
    """
    ADCP Task: get_products
    Natural language product discovery across inventory
    """
    try:
        # Parse date range
        date_from = datetime.fromisoformat(request.date_from) if request.date_from else datetime.now()
        date_to = datetime.fromisoformat(request.date_to) if request.date_to else date_from + timedelta(days=7)
        
        # Build query
        query = {
            "start_time": {"$gte": date_from, "$lte": date_to}
        }
        if request.channel:
            query["channel_id"] = request.channel
        
        # Get programs
        programs = list(db.programs.find(query))
        program_ids = [p["program_id"] for p in programs]
        
        # Get available ad breaks
        ad_breaks = list(db.ad_breaks.find({
            "program_id": {"$in": program_ids},
            "is_sold": False
        }))
        
        # Transform to ADCP Products
        products = []
        for adbreak in ad_breaks:
            program = next((p for p in programs if p["program_id"] == adbreak["program_id"]), None)
            if not program:
                continue
                
            channel = db.channels.find_one({"channel_id": program["channel_id"]})
            if not channel:
                continue
                
            price_data = db.inventory_prices.find_one({"inventory_id": adbreak["ad_break_id"]})
            
            # Apply budget filters
            if request.filters.get("max_budget"):
                if price_data and price_data.get("base_price", 0) > request.filters["max_budget"]:
                    continue
            
            product = transform_adbreak_to_product(adbreak, program, channel, price_data)
            products.append(product)
        
        return create_adcp_response(
            status="completed",
            data={
                "products": products,
                "total": len(products),
                "query": request.query
            },
            message=f"Found {len(products)} products matching '{request.query}'"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Product discovery failed: {str(e)}"
        )

@app.post("/api/v1/adcp/media-buy")
def adcp_create_media_buy(request: MediaBuyRequest):
    """
    ADCP Task: create_media_buy
    Create a new media buy campaign
    """
    try:
        # Extract package IDs
        package_ids = [pkg["package_id"] for pkg in request.packages]
        
        # Verify all packages are available
        ad_breaks = list(db.ad_breaks.find({
            "ad_break_id": {"$in": package_ids}
        }))
        
        if len(ad_breaks) != len(package_ids):
            return create_adcp_response(
                status="failed",
                message="Some packages are not available"
            )
        
        # Check if any are already sold
        sold = [ab for ab in ad_breaks if ab.get("is_sold", False)]
        if sold:
            return create_adcp_response(
                status="input-required",
                data={"unavailable_packages": [ab["ad_break_id"] for ab in sold]},
                message="Some packages are already sold. Please review."
            )
        
        # Create media buy record
        media_buy_id = f"mb_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        media_buy = {
            "media_buy_id": media_buy_id,
            "name": request.name,
            "advertiser": request.advertiser,
            "packages": package_ids,
            "start_date": datetime.fromisoformat(request.start_date),
            "end_date": datetime.fromisoformat(request.end_date),
            "budget": request.budget,
            "currency": request.currency,
            "objectives": request.objectives,
            "kpis": request.kpis,
            "status": "active",
            "created_at": datetime.utcnow(),
            "updated_at": datetime.utcnow()
        }
        
        db.media_buys.insert_one(media_buy)
        
        # Mark ad breaks as sold
        db.ad_breaks.update_many(
            {"ad_break_id": {"$in": package_ids}},
            {"$set": {"is_sold": True, "media_buy_id": media_buy_id}}
        )
        
        return create_adcp_response(
            status="completed",
            data=serialize_doc(media_buy),
            message=f"Media buy '{request.name}' created successfully"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Media buy creation failed: {str(e)}"
        )

@app.get("/api/v1/adcp/media-buy/{media_buy_id}/delivery")
def adcp_get_delivery(media_buy_id: str):
    """
    ADCP Task: get_media_buy_delivery
    Get campaign performance metrics
    """
    try:
        media_buy = db.media_buys.find_one({"media_buy_id": media_buy_id})
        if not media_buy:
            raise HTTPException(status_code=404, detail="Media buy not found")
        
        # Get ad breaks for this campaign
        ad_breaks = list(db.ad_breaks.find({
            "media_buy_id": media_buy_id
        }))
        
        # Calculate metrics (placeholder - you'd have real metrics from ad server)
        total_spots = len(ad_breaks)
        delivered_spots = total_spots  # Assume all delivered for now
        
        delivery_data = {
            "media_buy_id": media_buy_id,
            "status": media_buy.get("status", "active"),
            "metrics": {
                "total_spots": total_spots,
                "delivered_spots": delivered_spots,
                "completion_rate": (delivered_spots / total_spots * 100) if total_spots > 0 else 0,
                "budget_spent": media_buy.get("budget", 0) * (delivered_spots / total_spots) if total_spots > 0 else 0,
                "budget_total": media_buy.get("budget", 0)
            },
            "placements": [serialize_doc(ab) for ab in ad_breaks]
        }
        
        return create_adcp_response(
            status="completed",
            data=delivery_data,
            message=f"Delivery data for {media_buy_id}"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Failed to retrieve delivery data: {str(e)}"
        )

# ==============================================
# ADCP ROUTES — Signals Activation Protocol
# ==============================================

@app.post("/api/v1/adcp/signals/discover")
def adcp_discover_signals(request: SignalDiscoveryRequest):
    """
    ADCP Task: discover_signals
    Natural language signal discovery
    """
    try:
        # Query audience data based on natural language
        # This is a simple implementation - in production, use ML/NLP
        query_keywords = request.query.lower().split()
        
        signals = []
        
        # Search in audience data
        audience_data = list(db.audience_data.find())
        
        for aud in audience_data:
            # Simple keyword matching (replace with proper NLP)
            signal = {
                "signal_id": f"sig_{aud.get('_id')}",
                "name": f"Audience: {aud.get('region', 'Unknown')}",
                "type": "audience",
                "description": f"Audience in {aud.get('region')} with demographics",
                "provider": "internal",
                "scale": aud.get("impressions", 0),
                "demographics": {
                    "age_range": aud.get("age_range", "18-65"),
                    "gender": aud.get("gender_split", {}),
                    "region": aud.get("region", "Unknown")
                },
                "metadata": serialize_doc(aud)
            }
            signals.append(signal)
        
        return create_adcp_response(
            status="completed",
            data={
                "signals": signals[:20],  # Limit to 20
                "total": len(signals),
                "query": request.query
            },
            message=f"Found {len(signals)} signals"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Signal discovery failed: {str(e)}"
        )

@app.post("/api/v1/adcp/signals/activate")
def adcp_activate_signal(request: SignalActivationRequest):
    """
    ADCP Task: activate_signal
    Activate signal on decisioning platforms
    """
    try:
        # Store activation record
        activation_id = f"act_{datetime.now().strftime('%Y%m%d%H%M%S')}"
        activation = {
            "activation_id": activation_id,
            "signal_id": request.signal_id,
            "platforms": [p["platform_id"] for p in request.platforms],
            "config": request.config,
            "status": "active",
            "activated_at": datetime.utcnow()
        }
        
        db.signal_activations.insert_one(activation)
        
        return create_adcp_response(
            status="completed",
            data=serialize_doc(activation),
            message=f"Signal activated on {len(request.platforms)} platforms"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Signal activation failed: {str(e)}"
        )

# ==============================================
# ADCP ROUTES — Creative Protocol
# ==============================================

@app.post("/api/v1/adcp/creatives/sync")
def adcp_sync_creatives(request: CreativeSyncRequest):
    """
    ADCP Task: sync_creatives
    Upload and assign creative assets
    """
    try:
        media_buy = db.media_buys.find_one({"media_buy_id": request.media_buy_id})
        if not media_buy:
            raise HTTPException(status_code=404, detail="Media buy not found")
        
        creative_ids = []
        for creative in request.creatives:
            creative_id = f"cr_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
            creative_doc = {
                "creative_id": creative_id,
                "media_buy_id": request.media_buy_id,
                "url": creative["url"],
                "assignments": request.assignments,
                "uploaded_at": datetime.utcnow()
            }
            db.creatives.insert_one(creative_doc)
            creative_ids.append(creative_id)
        
        return create_adcp_response(
            status="completed",
            data={"creative_ids": creative_ids, "total": len(creative_ids)},
            message=f"Synced {len(creative_ids)} creatives"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Creative sync failed: {str(e)}"
        )

# ==============================================
# ADCP ROUTES — Property Discovery
# ==============================================

@app.get("/api/v1/adcp/properties")
def adcp_get_properties(
    publisher_domain: Optional[str] = None,
    tags: Optional[str] = None
):
    """
    Get publisher-owned property definitions (AdCP v2.3.0)
    """
    try:
        query = {}
        if publisher_domain:
            query["publisher_domain"] = publisher_domain
        
        channels = list(db.channels.find(query))
        
        properties = []
        for channel in channels:
            prop = {
                "property_id": channel["channel_id"],
                "name": channel["channel_name"],
                "publisher_domain": channel.get("publisher_domain", "snrt.ma"),
                "type": "linear_tv",
                "language": channel.get("language", "ar"),
                "country": channel.get("country", "MA"),
                "tags": channel.get("tags", ["broadcast", "national"]),
                "reach": channel.get("reach", {}),
                "metadata": serialize_doc(channel)
            }
            properties.append(prop)
        
        return create_adcp_response(
            status="completed",
            data={"properties": properties, "total": len(properties)},
            message=f"Retrieved {len(properties)} properties"
        )
    
    except Exception as e:
        return create_adcp_response(
            status="failed",
            message=f"Property discovery failed: {str(e)}"
        )

# ==============================================
# ROOT ENDPOINT
# ==============================================

@app.get("/")
def root():
    return {
        "status": "ok",
        "api": "ADCP v2.3.0",
        "protocol": "adcp",
        "message": "Ad Context Protocol API is running 🚀",
        "endpoints": {
            "legacy": "/api/v1/*",
            "adcp_media_buy": "/api/v1/adcp/products, /api/v1/adcp/media-buy",
            "adcp_signals": "/api/v1/adcp/signals/*",
            "adcp_creatives": "/api/v1/adcp/creatives/*",
            "adcp_properties": "/api/v1/adcp/properties"
        }
    }