"""AgentBill Signal Builder

Single internal orchestrator for building and sending signal OTEL payloads.
All public methods (signal(), record_bulk()) delegate to _emit_signal() in this module.
delegate to _emit_signal() in this module.

v8.0.0: Created as part of Signals Revamp Phase 1.
"""
import hashlib
import json
import time
import uuid
import httpx
from typing import Optional, Dict, Any, List

from .attributes import (
    ATTR_EVENT_NAME,
    ATTR_IS_BUSINESS_EVENT,
    ATTR_IDEMPOTENCY_KEY,
    ATTR_DATA_SOURCE,
    ATTR_CUSTOMER_ID,
    ATTR_AGENT_ID,
    ATTR_REVENUE,
    ATTR_CURRENCY,
    ATTR_EVENT_TYPE,
    ATTR_EVENT_VALUE,
    ATTR_SESSION_ID,
    ATTR_ORDER_ID,
    ATTR_ORDER_EXTERNAL_ID,
    ATTR_PARENT_SPAN_ID,
    ATTR_METADATA,
    ATTR_MODEL,
    ATTR_PROVIDER,
    ATTR_PROMPT_TOKENS,
    ATTR_COMPLETION_TOKENS,
    ATTR_TOTAL_TOKENS,
    ATTR_LATENCY_MS,
    ATTR_PROMPT_HASH,
    ATTR_CACHE_HIT,
    ATTR_FROM_CACHE,
    ATTR_TOKENS_SAVED,
    ATTR_COST_SAVED,
    ATTR_COST_TRACING_ENABLED,
    ATTR_COST_PROVIDED,
    ATTR_COST_PROVIDER,
    ATTR_COST_AMOUNT,
    ATTR_COST_CURRENCY,
    ATTR_BULK_INDEX,
    ATTR_BULK_REQUEST,
    GROUPING_KEYS,
    RESOURCE_SERVICE_NAME,
    SDK_VERSION,
    SCOPE_NAME,
    SCOPE_NAME_BULK,
    SIGNAL_SPAN_NAME,
)
from .distributed import get_trace_context, set_trace_context


def _generate_idempotency_key(trace_id: str, event_name: str, timestamp_s: int) -> str:
    """Deterministic idempotency key for dedup at otel-collector.
    
    Note: otel-collector does NOT yet enforce idempotency — this is forward-compatible.
    """
    raw = f"{trace_id}:{event_name}:{timestamp_s}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]


def _build_attributes(
    *,
    event_name: str,
    customer_id: Optional[str] = None,
    agent_id: Optional[str] = None,
    revenue: Optional[float] = None,
    currency: str = "USD",
    event_type: Optional[str] = None,
    event_value: Optional[float] = None,
    model: Optional[str] = None,
    provider: Optional[str] = None,
    input_tokens: Optional[int] = None,
    output_tokens: Optional[int] = None,
    latency_ms: Optional[float] = None,
    prompt_hash: Optional[str] = None,
    session_id: Optional[str] = None,
    order_id: Optional[str] = None,
    order_external_id: Optional[str] = None,
    parent_span_id: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
    idempotency_key: Optional[str] = None,
    enable_cost_tracing: bool = False,
    group_by: Optional[Dict[str, str]] = None,
    data_source: Optional[str] = None,
    bulk_index: Optional[int] = None,
    # Cache attributes
    cache_hit: Optional[bool] = None,
    tokens_saved: Optional[int] = None,
    cost_saved: Optional[float] = None,
    # Manual cost (Phase 3)
    cost_data: Optional[Dict[str, Any]] = None,
    # v10.0.0: Business Context
    product_id: Optional[str] = None,
    feature_flag: Optional[str] = None,
    environment: Optional[str] = None,
    deployment_version: Optional[str] = None,
    region: Optional[str] = None,
    tenant_id: Optional[str] = None,
    # v10.0.0: Quality Metrics
    feedback_score: Optional[float] = None,
    user_satisfaction: Optional[float] = None,
    error_type: Optional[str] = None,
    error_message: Optional[str] = None,
    retry_count: Optional[int] = None,
    success_rate: Optional[float] = None,
    # v10.0.0: Categorization
    tags: Optional[list] = None,
    category: Optional[str] = None,
    priority: Optional[str] = None,
) -> List[Dict[str, Any]]:
    """Build OTEL-compliant span attributes list from kwargs.
    
    Uses canonical constants from attributes.py to prevent string drift.
    """
    attrs: List[Dict[str, Any]] = [
        {"key": ATTR_EVENT_NAME, "value": {"stringValue": event_name}},
        {"key": ATTR_IS_BUSINESS_EVENT, "value": {"boolValue": True}},
    ]

    if data_source:
        attrs.append({"key": ATTR_DATA_SOURCE, "value": {"stringValue": data_source}})

    if customer_id:
        attrs.append({"key": ATTR_CUSTOMER_ID, "value": {"stringValue": str(customer_id)}})
    if agent_id:
        attrs.append({"key": ATTR_AGENT_ID, "value": {"stringValue": str(agent_id)}})

    if revenue is not None:
        attrs.append({"key": ATTR_REVENUE, "value": {"doubleValue": revenue}})
        attrs.append({"key": ATTR_CURRENCY, "value": {"stringValue": currency}})
    if event_value is not None:
        attrs.append({"key": ATTR_EVENT_VALUE, "value": {"doubleValue": event_value}})
    if event_type:
        attrs.append({"key": ATTR_EVENT_TYPE, "value": {"stringValue": event_type}})

    # AI telemetry
    if model:
        attrs.append({"key": ATTR_MODEL, "value": {"stringValue": model}})
    if provider:
        attrs.append({"key": ATTR_PROVIDER, "value": {"stringValue": provider}})
    if input_tokens is not None:
        attrs.append({"key": ATTR_PROMPT_TOKENS, "value": {"intValue": input_tokens}})
    if output_tokens is not None:
        attrs.append({"key": ATTR_COMPLETION_TOKENS, "value": {"intValue": output_tokens}})
    if input_tokens is not None and output_tokens is not None:
        attrs.append({"key": ATTR_TOTAL_TOKENS, "value": {"intValue": input_tokens + output_tokens}})
    if latency_ms is not None:
        attrs.append({"key": ATTR_LATENCY_MS, "value": {"doubleValue": latency_ms}})
    if prompt_hash:
        attrs.append({"key": ATTR_PROMPT_HASH, "value": {"stringValue": prompt_hash}})

    # Cache attributes
    if cache_hit:
        attrs.append({"key": ATTR_CACHE_HIT, "value": {"boolValue": True}})
        attrs.append({"key": ATTR_FROM_CACHE, "value": {"boolValue": True}})
        if tokens_saved is not None:
            attrs.append({"key": ATTR_TOKENS_SAVED, "value": {"intValue": tokens_saved}})
        if cost_saved is not None:
            attrs.append({"key": ATTR_COST_SAVED, "value": {"doubleValue": cost_saved}})

    # Linking
    if session_id:
        attrs.append({"key": ATTR_SESSION_ID, "value": {"stringValue": session_id}})
    if order_id:
        attrs.append({"key": ATTR_ORDER_ID, "value": {"stringValue": order_id}})
    if order_external_id:
        attrs.append({"key": ATTR_ORDER_EXTERNAL_ID, "value": {"stringValue": order_external_id}})
    if parent_span_id:
        attrs.append({"key": ATTR_PARENT_SPAN_ID, "value": {"stringValue": parent_span_id}})

    # Idempotency
    if idempotency_key:
        attrs.append({"key": ATTR_IDEMPOTENCY_KEY, "value": {"stringValue": idempotency_key}})

    # Cost tracing flag
    if enable_cost_tracing:
        attrs.append({"key": ATTR_COST_TRACING_ENABLED, "value": {"boolValue": True}})

    # Business grouping (whitelisted keys only)
    if group_by:
        for key, value in group_by.items():
            if key in GROUPING_KEYS:
                attrs.append({"key": f"agentbill.{key}", "value": {"stringValue": value}})

    # Bulk index
    if bulk_index is not None:
        attrs.append({"key": ATTR_BULK_INDEX, "value": {"intValue": str(bulk_index)}})

    # Manual cost (Phase 3 – two-variant model)
    if cost_data:
        vendor = cost_data.get("vendor")
        if vendor:
            attrs.append({"key": ATTR_COST_PROVIDER, "value": {"stringValue": vendor}})
        cost_obj = cost_data.get("cost")
        if cost_obj:
            # Variant A: pre-calculated cost → server skips model_pricing
            attrs.append({"key": ATTR_COST_PROVIDED, "value": {"boolValue": True}})
            attrs.append({"key": ATTR_COST_AMOUNT, "value": {"doubleValue": cost_obj.get("amount", 0)}})
            attrs.append({"key": ATTR_COST_CURRENCY, "value": {"stringValue": cost_obj.get("currency", "USD")}})
        cost_attrs = cost_data.get("attributes")
        if cost_attrs:
            # Variant B: raw usage → server computes via model_pricing
            if cost_attrs.get("model"):
                attrs.append({"key": ATTR_MODEL, "value": {"stringValue": cost_attrs["model"]}})
            if cost_attrs.get("input_tokens") is not None:
                attrs.append({"key": ATTR_PROMPT_TOKENS, "value": {"intValue": cost_attrs["input_tokens"]}})
            if cost_attrs.get("output_tokens") is not None:
                attrs.append({"key": ATTR_COMPLETION_TOKENS, "value": {"intValue": cost_attrs["output_tokens"]}})

    # v10.0.0: Business Context (typed OTEL attributes)
    _biz_str = {"product_id": product_id, "feature_flag": feature_flag, "environment": environment,
                "deployment_version": deployment_version, "region": region, "tenant_id": tenant_id,
                "error_type": error_type, "error_message": error_message, "category": category, "priority": priority}
    for k, v in _biz_str.items():
        if v:
            attrs.append({"key": f"agentbill.{k}", "value": {"stringValue": v}})

    # v10.0.0: Quality Metrics (typed OTEL attributes)
    _biz_num = {"feedback_score": feedback_score, "user_satisfaction": user_satisfaction, "success_rate": success_rate}
    for k, v in _biz_num.items():
        if v is not None:
            attrs.append({"key": f"agentbill.{k}", "value": {"doubleValue": v}})
    if retry_count is not None:
        attrs.append({"key": "agentbill.retry_count", "value": {"intValue": str(retry_count)}})
    if tags:
        attrs.append({"key": "agentbill.tags", "value": {"stringValue": json.dumps(tags)}})
    if metadata:
        # Filter out cache-specific keys that are handled above
        clean_metadata = {k: v for k, v in metadata.items() 
                          if k not in ("cache_hit", "tokens_saved", "cost_saved")}
        if clean_metadata:
            attrs.append({"key": ATTR_METADATA, "value": {"stringValue": json.dumps(clean_metadata)}})

    return attrs


def _build_otel_payload(
    *,
    trace_id: str,
    span_id: str,
    parent_span_id: str,
    attributes: List[Dict[str, Any]],
    customer_id: str,
    agent_id: str,
    scope_name: str = SCOPE_NAME,
    is_bulk: bool = False,
) -> Dict[str, Any]:
    """Build OTEL resourceSpans payload envelope."""
    now_ns = int(time.time() * 1_000_000_000)

    resource_attrs = [
        {"key": "service.name", "value": {"stringValue": RESOURCE_SERVICE_NAME}},
        {"key": ATTR_CUSTOMER_ID, "value": {"stringValue": customer_id or ""}},
        {"key": ATTR_AGENT_ID, "value": {"stringValue": agent_id or ""}},
    ]

    if is_bulk:
        resource_attrs.append({"key": ATTR_BULK_REQUEST, "value": {"boolValue": True}})

    return {
        "resourceSpans": [{
            "resource": {"attributes": resource_attrs},
            "scopeSpans": [{
                "scope": {"name": scope_name, "version": SDK_VERSION},
                "spans": [{
                    "traceId": trace_id,
                    "spanId": span_id,
                    "parentSpanId": parent_span_id,
                    "name": SIGNAL_SPAN_NAME,
                    "kind": 1,
                    "startTimeUnixNano": str(now_ns),
                    "endTimeUnixNano": str(now_ns),
                    "attributes": attributes,
                    "status": {"code": 1},
                }],
            }],
        }],
    }


def _build_otel_payload_multi_span(
    *,
    spans: List[Dict[str, Any]],
    customer_id: str,
    agent_id: str,
    is_bulk: bool = True,
) -> Dict[str, Any]:
    """Build OTEL resourceSpans payload with multiple spans (for bulk)."""
    resource_attrs = [
        {"key": "service.name", "value": {"stringValue": RESOURCE_SERVICE_NAME}},
        {"key": ATTR_CUSTOMER_ID, "value": {"stringValue": customer_id or ""}},
        {"key": ATTR_AGENT_ID, "value": {"stringValue": agent_id or ""}},
    ]
    if is_bulk:
        resource_attrs.append({"key": ATTR_BULK_REQUEST, "value": {"boolValue": True}})

    return {
        "resourceSpans": [{
            "resource": {"attributes": resource_attrs},
            "scopeSpans": [{
                "scope": {"name": SCOPE_NAME_BULK, "version": SDK_VERSION},
                "spans": spans,
            }],
        }],
    }


def _emit_signal(
    *,
    event_name: str,
    config: Dict[str, Any],
    # Identity
    customer_id: Optional[str] = None,
    agent_id: Optional[str] = None,
    # Trace correlation
    trace_id: Optional[str] = None,
    span_id: Optional[str] = None,
    parent_span_id: Optional[str] = None,
    # Business data
    revenue: Optional[float] = None,
    currency: str = "USD",
    event_type: Optional[str] = None,
    event_value: Optional[float] = None,
    # AI telemetry
    model: Optional[str] = None,
    provider: Optional[str] = None,
    input_tokens: Optional[int] = None,
    output_tokens: Optional[int] = None,
    latency_ms: Optional[float] = None,
    prompt_hash: Optional[str] = None,
    # Cache
    cache_hit: Optional[bool] = None,
    tokens_saved: Optional[int] = None,
    cost_saved: Optional[float] = None,
    # Linking
    order_id: Optional[str] = None,
    order_external_id: Optional[str] = None,
    session_id: Optional[str] = None,
    # Business grouping
    group_by: Optional[Dict[str, str]] = None,
    # Metadata
    metadata: Optional[Dict[str, Any]] = None,
    # Idempotency
    idempotency_key: Optional[str] = None,
    # Data source
    data_source: Optional[str] = None,
    # Bulk index (for bulk operations)
    bulk_index: Optional[int] = None,
    # Override: update trace context (for wrappers that manage context)
    update_trace_context: bool = False,
    # Manual cost (Phase 3)
    cost_data: Optional[Dict[str, Any]] = None,
    # v10.0.0: Business Context
    product_id: Optional[str] = None,
    feature_flag: Optional[str] = None,
    environment: Optional[str] = None,
    deployment_version: Optional[str] = None,
    region: Optional[str] = None,
    tenant_id: Optional[str] = None,
    # v10.0.0: Quality Metrics
    feedback_score: Optional[float] = None,
    user_satisfaction: Optional[float] = None,
    error_type: Optional[str] = None,
    error_message: Optional[str] = None,
    retry_count: Optional[int] = None,
    success_rate: Optional[float] = None,
    # v10.0.0: Categorization
    tags: Optional[list] = None,
    category: Optional[str] = None,
    priority: Optional[str] = None,
) -> Dict[str, Any]:
    """
    SINGLE internal function for building and sending signal OTEL payloads.
    All public methods (signal(), record_bulk()) delegate here.
    
    Key behaviors:
    - Auto-detects trace_id/span_id from distributed.get_trace_context() if not provided
    - Always generates unique span_id for the signal (never reuses context span_id)
    - Auto-generates idempotency_key if not provided
    - Reads enable_cost_tracing from config and injects attribute
    - Injects group_by keys as agentbill.{key} attributes (whitelisted)
    - Uses SDK_VERSION from attributes.py (single source of truth)
    - Sends to otel-collector via httpx
    """
    api_key = config.get("api_key")
    if not api_key:
        raise ValueError("AgentBill not initialized. Call AgentBill.init() first or use agentbill_tracing context.")

    # Resolve identity
    effective_customer_id = customer_id or config.get("customer_id", "")
    effective_agent_id = agent_id or config.get("agent_id") or config.get("agent_external_id", "")

    # Auto-detect trace context
    ctx = get_trace_context()
    resolved_trace_id = trace_id
    resolved_parent_span_id = parent_span_id

    if ctx:
        resolved_trace_id = resolved_trace_id or ctx.get("trace_id")
        if not resolved_parent_span_id:
            resolved_parent_span_id = ctx.get("span_id")

    # Always generate unique IDs for the signal span
    generated_trace_id = resolved_trace_id or uuid.uuid4().hex
    generated_span_id = span_id or uuid.uuid4().hex[:16]

    # Optionally update trace context (used by wrappers that manage context)
    if update_trace_context:
        set_trace_context(generated_trace_id, generated_span_id, resolved_parent_span_id)

    # Auto-generate idempotency key
    if not idempotency_key:
        timestamp_s = int(time.time())
        idempotency_key = _generate_idempotency_key(generated_trace_id, event_name, timestamp_s)

    # Read cost tracing flag from config
    enable_cost_tracing = config.get("enable_cost_tracing", False)

    # Build attributes
    attributes = _build_attributes(
        event_name=event_name,
        customer_id=effective_customer_id,
        agent_id=effective_agent_id,
        revenue=revenue,
        currency=currency,
        event_type=event_type,
        event_value=event_value,
        model=model,
        provider=provider,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
        latency_ms=latency_ms,
        prompt_hash=prompt_hash,
        session_id=session_id,
        order_id=order_id,
        order_external_id=order_external_id,
        parent_span_id=resolved_parent_span_id,
        metadata=metadata,
        idempotency_key=idempotency_key,
        enable_cost_tracing=enable_cost_tracing,
        group_by=group_by,
        data_source=data_source,
        bulk_index=bulk_index,
        cache_hit=cache_hit,
        tokens_saved=tokens_saved,
        cost_saved=cost_saved,
        cost_data=cost_data,
        # v10.0.0: Business Context
        product_id=product_id,
        feature_flag=feature_flag,
        environment=environment,
        deployment_version=deployment_version,
        region=region,
        tenant_id=tenant_id,
        # v10.0.0: Quality Metrics
        feedback_score=feedback_score,
        user_satisfaction=user_satisfaction,
        error_type=error_type,
        error_message=error_message,
        retry_count=retry_count,
        success_rate=success_rate,
        # v10.0.0: Categorization
        tags=tags,
        category=category,
        priority=priority,
    )

    # Build payload
    payload = _build_otel_payload(
        trace_id=generated_trace_id,
        span_id=generated_span_id,
        parent_span_id=resolved_parent_span_id or "",
        attributes=attributes,
        customer_id=effective_customer_id,
        agent_id=effective_agent_id,
    )

    # Send to otel-collector
    base_url = config.get("base_url", "https://api.agentbill.io")
    url = f"{base_url}/functions/v1/otel-collector"
    debug = config.get("debug", False)

    try:
        with httpx.Client(timeout=10) as client:
            response = client.post(
                url,
                json=payload,
                headers={"x-api-key": api_key, "Content-Type": "application/json"},
            )

            response_data = {}
            body_success = True
            try:
                response_data = response.json()
                body_success = response_data.get("success", True)
            except Exception:
                pass

            is_success = response.status_code == 200 and body_success

            if debug:
                if is_success:
                    print(f"[AgentBill] ✓ Signal '{event_name}' tracked via OTEL")
                    if revenue is not None:
                        print(f"[AgentBill]   Revenue: ${revenue:.2f} {currency}")
                    print(f"[AgentBill]   Trace ID: {generated_trace_id}")
                else:
                    error_msg = response_data.get("error", response.text)
                    errors = response_data.get("errors", [])
                    print(f"[AgentBill] ⚠️ Signal tracking failed: {error_msg}")
                    if errors:
                        for err in errors:
                            print(f"[AgentBill]   - {err}")

            return {
                "success": is_success,
                "status_code": response.status_code,
                "trace_id": generated_trace_id,
                "span_id": generated_span_id,
                "error": response_data.get("error") if not is_success else None,
                "errors": response_data.get("errors") if not is_success else None,
            }

    except Exception as e:
        if debug:
            print(f"[AgentBill] ⚠️ Signal tracking error: {e}")

        return {
            "success": False,
            "error": str(e),
            "trace_id": generated_trace_id,
        }


async def _emit_signal_async(
    *,
    event_name: str,
    config: Dict[str, Any],
    customer_id: Optional[str] = None,
    agent_id: Optional[str] = None,
    trace_id: Optional[str] = None,
    span_id: Optional[str] = None,
    parent_span_id: Optional[str] = None,
    model: Optional[str] = None,
    provider: Optional[str] = None,
    input_tokens: Optional[int] = None,
    output_tokens: Optional[int] = None,
    latency_ms: Optional[float] = None,
    prompt_hash: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
    update_trace_context: bool = False,
    **kwargs,
) -> Dict[str, Any]:
    """Async version of _emit_signal for async wrappers.
    
    Mirrors _emit_signal but uses aiohttp for non-blocking HTTP.
    """
    import aiohttp

    api_key = config.get("api_key")
    if not api_key:
        raise ValueError("AgentBill not initialized.")

    effective_customer_id = customer_id or config.get("customer_id", "")
    effective_agent_id = agent_id or config.get("agent_id", "")

    ctx = get_trace_context()
    resolved_trace_id = trace_id
    resolved_parent_span_id = parent_span_id

    if ctx:
        resolved_trace_id = resolved_trace_id or ctx.get("trace_id")
        if not resolved_parent_span_id:
            resolved_parent_span_id = ctx.get("span_id")

    generated_trace_id = resolved_trace_id or uuid.uuid4().hex
    generated_span_id = span_id or uuid.uuid4().hex[:16]

    if update_trace_context:
        set_trace_context(generated_trace_id, generated_span_id, resolved_parent_span_id)

    if not kwargs.get("idempotency_key"):
        timestamp_s = int(time.time())
        kwargs["idempotency_key"] = _generate_idempotency_key(generated_trace_id, event_name, timestamp_s)

    enable_cost_tracing = config.get("enable_cost_tracing", False)

    attributes = _build_attributes(
        event_name=event_name,
        customer_id=effective_customer_id,
        agent_id=effective_agent_id,
        model=model,
        provider=provider,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
        latency_ms=latency_ms,
        prompt_hash=prompt_hash,
        parent_span_id=resolved_parent_span_id,
        metadata=metadata,
        idempotency_key=kwargs.get("idempotency_key"),
        enable_cost_tracing=enable_cost_tracing,
    )

    payload = _build_otel_payload(
        trace_id=generated_trace_id,
        span_id=generated_span_id,
        parent_span_id=resolved_parent_span_id or "",
        attributes=attributes,
        customer_id=effective_customer_id,
        agent_id=effective_agent_id,
    )

    base_url = config.get("base_url", "https://api.agentbill.io")
    url = f"{base_url}/functions/v1/otel-collector"
    debug = config.get("debug", False)

    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(
                url,
                json=payload,
                headers={"x-api-key": api_key, "Content-Type": "application/json"},
                timeout=aiohttp.ClientTimeout(total=10),
            ) as response:
                response_data = {}
                try:
                    response_data = await response.json()
                except Exception:
                    pass

                is_success = response.status == 200 and response_data.get("success", True)

                if debug:
                    if is_success:
                        print(f"[AgentBill] ✓ Signal '{event_name}' tracked via OTEL (async)")
                    else:
                        print(f"[AgentBill] ⚠️ Signal tracking failed: {response_data.get('error')}")

                return {
                    "success": is_success,
                    "status_code": response.status,
                    "trace_id": generated_trace_id,
                    "span_id": generated_span_id,
                }
    except Exception as e:
        if debug:
            print(f"[AgentBill] ⚠️ Async signal tracking error: {e}")
        return {"success": False, "error": str(e), "trace_id": generated_trace_id}
