"""
Billing and Usage Tracking Hooks

Provides:
- Usage event webhooks
- Billing period aggregation
- Usage export for invoicing
- Metered billing support
"""
from __future__ import annotations

import asyncio
import json
import os
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from typing import Any, Dict, List, Optional

import httpx
import random

from app.observability.logging import get_logger


_logger = get_logger("billing")


# Configuration constants
BILLING_BATCH_SIZE = int(os.getenv("BILLING_BATCH_SIZE", "100"))
BILLING_BATCH_TIMEOUT = float(os.getenv("BILLING_BATCH_TIMEOUT", "5.0"))
BILLING_HTTP_TIMEOUT = float(os.getenv("BILLING_HTTP_TIMEOUT", "10.0"))
BILLING_QUEUE_MAX_SIZE = int(os.getenv("BILLING_QUEUE_MAX_SIZE", "1000"))
BILLING_MAX_RETRIES = int(os.getenv("BILLING_MAX_RETRIES", "3"))


class UsageEventType(str, Enum):
    VOICE_SESSION_START = "voice.session.start"
    VOICE_SESSION_END = "voice.session.end"
    VOICE_MINUTE = "voice.minute"
    API_REQUEST = "api.request"
    RATE_LIMIT_HIT = "rate_limit.hit"
    QUOTA_EXCEEDED = "quota.exceeded"


@dataclass
class UsageEvent:
    """A billable usage event."""
    event_type: UsageEventType
    org_id: str
    timestamp: int
    quantity: float
    unit: str
    metadata: Dict[str, Any]
    
    def to_dict(self) -> dict:
        return {
            "event_type": self.event_type.value,
            "org_id": self.org_id,
            "timestamp": self.timestamp,
            "quantity": self.quantity,
            "unit": self.unit,
            "metadata": self.metadata,
        }


class BillingWebhook:
    """
    Sends usage events to external billing systems.
    
    Supports multiple webhook endpoints for redundancy.
    """
    
    def __init__(self):
        self._endpoints = self._load_endpoints()
        self._client: Optional[httpx.AsyncClient] = None
        self._queue: asyncio.Queue[UsageEvent] = asyncio.Queue(maxsize=BILLING_QUEUE_MAX_SIZE)
        self._running = False
        self._task: Optional[asyncio.Task] = None
    
    def _load_endpoints(self) -> List[str]:
        """Load webhook endpoints from environment."""
        endpoints = []
        
        primary = os.getenv("BILLING_WEBHOOK_URL")
        if primary:
            endpoints.append(primary)
        
        # Support multiple endpoints
        for i in range(1, 5):
            url = os.getenv(f"BILLING_WEBHOOK_URL_{i}")
            if url:
                endpoints.append(url)
        
        return endpoints
    
    async def start(self) -> None:
        """Start the webhook sender background task."""
        if not self._endpoints:
            _logger.info("billing_webhook_disabled")
            return
        
        self._client = httpx.AsyncClient(timeout=BILLING_HTTP_TIMEOUT)
        self._running = True
        self._task = asyncio.create_task(self._sender_loop())
        _logger.info("billing_webhook_started", endpoints=len(self._endpoints))
    
    async def stop(self) -> None:
        """Stop the webhook sender."""
        self._running = False
        # Wait for background task to finish sending remaining batches
        if self._task:
            try:
                await self._task
            except Exception as e:
                _logger.error("billing_sender_stop_error", error=str(e))
        if self._client:
            await self._client.aclose()
    
    async def send_event(self, event: UsageEvent) -> None:
        """Queue an event for sending."""
        await self._queue.put(event)
    
    async def _sender_loop(self) -> None:
        """Background loop that sends queued events."""
        batch: List[UsageEvent] = []
        batch_timeout = BILLING_BATCH_TIMEOUT  # configurable
        last_send = time.time()
        
        while self._running:
            try:
                # Wait for events with timeout
                try:
                    event = await asyncio.wait_for(
                        self._queue.get(),
                        timeout=1.0,
                    )
                    batch.append(event)
                except asyncio.TimeoutError:
                    pass
                
                # Send batch if full or timeout reached
                should_send = (
                    len(batch) >= BILLING_BATCH_SIZE or
                    (batch and time.time() - last_send >= batch_timeout)
                )
                
                if should_send and batch:
                    await self._send_batch(batch)
                    batch = []
                    last_send = time.time()
                    
            except Exception as e:
                _logger.error("billing_sender_error", error=str(e))
                await asyncio.sleep(1)
        
        # Drain any remaining events from the queue before final send
        try:
            while True:
                try:
                    event = self._queue.get_nowait()
                    batch.append(event)
                except asyncio.QueueEmpty:
                    break
        except Exception as e:
            _logger.warning("billing_queue_drain_error", error=str(e))
        
        # Send remaining on shutdown
        if batch:
            await self._send_batch(batch)
    
    async def _send_batch(self, events: List[UsageEvent]) -> None:
        """Send a batch of events to webhook endpoints with retries."""
        if not self._client or not self._endpoints:
            return
        
        payload = {
            "events": [e.to_dict() for e in events],
            "batch_id": f"{int(time.time() * 1000)}",
            "sent_at": datetime.now(timezone.utc).isoformat(),
        }
        
        headers = {
            "Content-Type": "application/json",
            "X-Webhook-Secret": os.getenv("BILLING_WEBHOOK_SECRET", ""),
        }
        
        attempt = 0
        while attempt < BILLING_MAX_RETRIES:
            for endpoint in self._endpoints:
                try:
                    response = await self._client.post(
                        endpoint,
                        json=payload,
                        headers=headers,
                    )
                    if response.status_code < 300:
                        _logger.debug(
                            "billing_batch_sent",
                            endpoint=endpoint,
                            count=len(events),
                            attempt=attempt + 1,
                        )
                        return  # Success
                    else:
                        _logger.warning(
                            "billing_webhook_error",
                            endpoint=endpoint,
                            status=response.status_code,
                            attempt=attempt + 1,
                        )
                except Exception as e:
                    _logger.warning(
                        "billing_webhook_failed",
                        endpoint=endpoint,
                        error=str(e),
                        attempt=attempt + 1,
                    )
            # backoff before next attempt
            attempt += 1
            if attempt < BILLING_MAX_RETRIES:
                backoff = min(2 ** attempt, 10) + random.uniform(0, 0.5)
                await asyncio.sleep(backoff)
        
        # All attempts failed - log summary only
        sample = events[:3]
        _logger.error(
            "billing_batch_failed",
            count=len(events),
            sample=[{"event_type": e.event_type.value, "org_id": e.org_id} for e in sample],
        )


# Global webhook instance
_webhook = BillingWebhook()


async def init_billing() -> None:
    """Initialize billing system."""
    await _webhook.start()


async def shutdown_billing() -> None:
    """Shutdown billing system."""
    await _webhook.stop()


async def track_voice_session_start(
    org_id: str,
    session_id: str,
    plan: str,
) -> None:
    """Track voice session start for billing."""
    event = UsageEvent(
        event_type=UsageEventType.VOICE_SESSION_START,
        org_id=org_id,
        timestamp=int(time.time()),
        quantity=1,
        unit="session",
        metadata={
            "session_id": session_id,
            "plan": plan,
        },
    )
    await _webhook.send_event(event)


async def track_voice_session_end(
    org_id: str,
    session_id: str,
    duration_seconds: float,
) -> None:
    """Track voice session end with duration for billing."""
    event = UsageEvent(
        event_type=UsageEventType.VOICE_SESSION_END,
        org_id=org_id,
        timestamp=int(time.time()),
        quantity=duration_seconds,
        unit="seconds",
        metadata={
            "session_id": session_id,
            "duration_seconds": duration_seconds,
            "duration_minutes": round(duration_seconds / 60, 2),
        },
    )
    await _webhook.send_event(event)


async def track_voice_minute(
    org_id: str,
    session_id: str,
) -> None:
    """Track each voice minute for metered billing."""
    event = UsageEvent(
        event_type=UsageEventType.VOICE_MINUTE,
        org_id=org_id,
        timestamp=int(time.time()),
        quantity=1,
        unit="minute",
        metadata={
            "session_id": session_id,
        },
    )
    await _webhook.send_event(event)


async def track_api_request(
    org_id: str,
    endpoint: str,
    method: str,
    status_code: int,
    duration_ms: int,
) -> None:
    """Track API request for usage reporting."""
    event = UsageEvent(
        event_type=UsageEventType.API_REQUEST,
        org_id=org_id,
        timestamp=int(time.time()),
        quantity=1,
        unit="request",
        metadata={
            "endpoint": endpoint,
            "method": method,
            "status_code": status_code,
            "duration_ms": duration_ms,
        },
    )
    await _webhook.send_event(event)


async def track_rate_limit_hit(
    org_id: str,
    limit_type: str,
    limit: int,
    current: int,
) -> None:
    """Track rate limit hits for monitoring."""
    event = UsageEvent(
        event_type=UsageEventType.RATE_LIMIT_HIT,
        org_id=org_id,
        timestamp=int(time.time()),
        quantity=1,
        unit="hit",
        metadata={
            "limit_type": limit_type,
            "limit": limit,
            "current": current,
        },
    )
    await _webhook.send_event(event)


async def get_usage_for_period(
    redis,
    org_id: str,
    start_timestamp: int,
    end_timestamp: int,
) -> Dict[str, Any]:
    """
    Get aggregated usage for a billing period.
    
    Used for generating invoices.
    """
    usage = {
        "org_id": org_id,
        "period_start": datetime.fromtimestamp(start_timestamp, tz=timezone.utc).isoformat(),
        "period_end": datetime.fromtimestamp(end_timestamp, tz=timezone.utc).isoformat(),
        "voice_seconds": 0.0,
        "voice_sessions": 0,
        "api_requests": 0,
        "daily_breakdown": [],
    }
    
    # Aggregate daily usage
    current_day = start_timestamp // 86400
    end_day = end_timestamp // 86400
    
    pipe = redis.pipeline()
    
    while current_day <= end_day:
        pipe.get(f"usage:{org_id}:voice:{current_day}")
        pipe.get(f"ratelimit:{org_id}:day:{current_day}")
        current_day += 1
    
    results = await pipe.execute()
    
    current_day = start_timestamp // 86400
    idx = 0
    
    while current_day <= end_day:
        voice_seconds = float(results[idx] or 0)
        api_requests = int(results[idx + 1] or 0)
        
        day_date = datetime.fromtimestamp(current_day * 86400, tz=timezone.utc)
        
        usage["daily_breakdown"].append({
            "date": day_date.strftime("%Y-%m-%d"),
            "voice_seconds": voice_seconds,
            "api_requests": api_requests,
        })
        
        usage["voice_seconds"] += voice_seconds
        usage["api_requests"] += api_requests
        
        current_day += 1
        idx += 2
    
    usage["voice_minutes"] = round(usage["voice_seconds"] / 60, 2)
    
    return usage


def calculate_invoice_amount(
    usage: Dict[str, Any],
    plan: str,
    pricing: Optional[Dict[str, float]] = None,
) -> Dict[str, Any]:
    """
    Calculate invoice amount based on usage and pricing.
    
    Default pricing (can be overridden):
    - Voice: $0.02 per minute
    - API requests: $0.0001 per request (after free tier)
    """
    if pricing is None:
        pricing = {
            "voice_per_minute": os.getenv("PRICE_VOICE_PER_MINUTE", "0.02"),
            "api_per_request": os.getenv("PRICE_API_PER_REQUEST", "0.0001"),
            "free_api_requests": os.getenv("FREE_API_REQUESTS", "10000"),
        }
    
    # Normalize pricing to Decimal/int
    voice_rate = Decimal(str(pricing["voice_per_minute"]))
    api_rate = Decimal(str(pricing["api_per_request"]))
    free_api_requests = int(pricing["free_api_requests"]) if not isinstance(pricing["free_api_requests"], int) else pricing["free_api_requests"]
    
    # Plan-based discounts
    discounts = {
        "free": Decimal("0.0"),
        "starter": Decimal("0.0"),
        "pro": Decimal("0.10"),  # 10% discount
        "enterprise": Decimal("0.20"),  # 20% discount
    }
    discount = discounts.get(plan, Decimal("0.0"))
    
    # Calculate voice charges
    voice_minutes_raw = usage.get("voice_minutes", 0)
    voice_minutes = Decimal(str(voice_minutes_raw))
    voice_charge = voice_minutes * voice_rate
    
    # Calculate API charges (with free tier)
    api_requests = int(usage.get("api_requests", 0))
    billable_requests = max(0, api_requests - free_api_requests)
    api_charge = Decimal(billable_requests) * api_rate
    
    # Apply discount
    subtotal = voice_charge + api_charge
    discount_amount = subtotal * discount
    total = subtotal - discount_amount
    
    q = Decimal("0.01")
    return {
        "org_id": usage.get("org_id"),
        "period_start": usage.get("period_start"),
        "period_end": usage.get("period_end"),
        "plan": plan,
        "line_items": [
            {
                "description": f"Voice minutes ({voice_minutes:.2f} min)",
                "quantity": float(voice_minutes.quantize(q)),
                "unit_price": float(voice_rate.quantize(q)),
                "amount": float(voice_charge.quantize(q)),
            },
            {
                "description": f"API requests ({billable_requests:,} billable)",
                "quantity": billable_requests,
                "unit_price": float(api_rate.quantize(q)),
                "amount": float(api_charge.quantize(q)),
            },
        ],
        "subtotal": float(subtotal.quantize(q)),
        "discount_percent": float((discount * Decimal(100)).quantize(q)),
        "discount_amount": float(discount_amount.quantize(q)),
        "total": float(total.quantize(q)),
        "currency": "USD",
    }
