"""
Event Transformer for LWAI Platform

Transforms Beyond TimebackProfile events to LWAI AggregationProfile format.
Handles:
- Profile name translation (TimebackProfile → AggregationProfile)
- Field name casing (PascalCase for LWAI)
- Platform-specific field mappings
- Datetime normalization to LWAI's expected .SSSZ format
"""

import uuid
from datetime import UTC, datetime
from typing import Any, Literal

from .base import EventPayloadTransformer

# LWAI magic session value — tells the platform to attach the session automatically.
_SESSION_AUTO_ATTACH = "urn:tag:auto-attach"


# Caliper type constants
class CaliperType:
    """Standard Caliper entity and collection types."""

    PERSON = "Person"
    ASSIGNABLE_DIGITAL_RESOURCE = "AssignableDigitalResource"
    ACTIVITY_METRIC = "ActivityMetric"
    TIME_SPENT_METRIC = "TimeSpentMetric"
    ACTIVITY_METRICS_COLLECTION = "ActivityMetricsCollection"
    TIME_SPENT_METRICS_COLLECTION = "TimeSpentMetricsCollection"
    COURSE_OFFERING = "CourseOffering"
    SOFTWARE_APPLICATION = "SoftwareApplication"


# Event types that expect learning activity/resource objects
LEARNING_ACTIVITY_EVENT_TYPES = {
    "ActivityEvent",
    "TimeSpentEvent",
    "AssignableEvent",
    "ViewEvent",
}


def _normalize_datetime(dt_str: str | None) -> str | None:
    """
    Normalize datetimes to LWAI's preferred ISO 8601 shape.

    LWAI expects UTC datetimes with exactly three fractional digits:
    YYYY-MM-DDTHH:mm:ss.SSSZ
    """
    if not dt_str:
        return dt_str

    normalized_input = dt_str[:-1] + "+00:00" if dt_str.endswith("Z") else dt_str

    try:
        parsed = datetime.fromisoformat(normalized_input)
    except ValueError:
        return dt_str

    if parsed.tzinfo is not None:
        parsed = parsed.astimezone(UTC)

    return parsed.strftime("%Y-%m-%dT%H:%M:%S.%f")[:23] + "Z"


def transform_for_lwai(envelope_dict: dict[str, Any]) -> dict[str, Any]:
    """
    Transform Caliper envelope for LWAI platform.

    Changes:
    1. Profile: TimebackProfile → AggregationProfile
    2. Field casing: PascalCase for specific fields
    3. Type names: Timeback* → standard Caliper types where applicable

    Args:
        envelope_dict: Caliper envelope as dictionary

    Returns:
        Transformed envelope for LWAI
    """
    if "data" not in envelope_dict or not isinstance(envelope_dict["data"], list):
        return envelope_dict

    transformed_data = []
    for event in envelope_dict["data"]:
        if isinstance(event, dict):
            transformed_event = _transform_event(event)
            transformed_data.append(transformed_event)
        else:
            transformed_data.append(event)

    # Return new envelope with transformed data and normalized sendTime.
    result = {**envelope_dict, "data": transformed_data}
    if "sendTime" in result:
        result["sendTime"] = _normalize_datetime(result["sendTime"])
    return result


class LwaiEventTransformer:
    """LWAI-specific event transformer."""

    def transform(self, envelope: dict[str, Any]) -> dict[str, Any]:
        return transform_for_lwai(envelope)


def _transform_event(event: dict[str, Any]) -> dict[str, Any]:
    """Transform a single event for LWAI."""
    transformed = event.copy()

    # 0. Ensure @context is preserved (required by Caliper 1.2)
    if "@context" not in transformed:
        transformed["@context"] = "http://purl.imsglobal.org/ctx/caliper/v1p2"

    # 1. Profile translation
    if transformed.get("profile") == "TimebackProfile":
        transformed["profile"] = "AggregationProfile"

    # 2. Transform actor (TimebackUser → Person with PascalCase)
    # Only transform when the actor is explicitly typed as TimebackUser
    if (
        "actor" in transformed
        and isinstance(transformed["actor"], dict)
        and transformed["actor"].get("type") == "TimebackUser"
    ):
        transformed["actor"] = _transform_actor(transformed["actor"])

    # 3. Transform object (TimebackActivityContext → AssignableDigitalResource)
    # Only transform for event types where object is a learning activity/resource
    if "object" in transformed and isinstance(transformed["object"], dict):
        event_type = transformed.get("type")
        transformed["object"] = _transform_object(transformed["object"], event_type)

    # 4. Transform generated (metrics collection for ActivityEvent and TimeSpentEvent)
    if "generated" in transformed and isinstance(transformed["generated"], dict):
        event_type = transformed.get("type")
        if event_type == "ActivityEvent":
            transformed["generated"] = _transform_metrics(transformed["generated"])
        elif event_type == "TimeSpentEvent":
            transformed["generated"] = _transform_time_spent_metrics(transformed["generated"])

    # 5. Normalize eventTime to LWAI's preferred .SSSZ format.
    if "eventTime" in transformed:
        transformed["eventTime"] = _normalize_datetime(transformed["eventTime"])

    # 6. Auto-inject session when missing (LWAI requires session for event attribution)
    if not transformed.get("session"):
        transformed["session"] = _SESSION_AUTO_ATTACH

    return transformed


def _transform_actor(actor: dict[str, Any]) -> dict[str, Any]:
    """Transform actor from TimebackUser to LWAI Person format."""
    # Type: TimebackUser → Person (LWAI uses standard Caliper Person)
    # Note: email field excluded (not in Person schema strict mode)
    return {"type": CaliperType.PERSON, "id": actor.get("id")}


def _transform_object(obj: dict[str, Any], event_type: str | None) -> dict[str, Any]:
    """
    Transform object based on source type and event type.

    Only transforms TimebackActivityContext → AssignableDigitalResource for event types
    that expect a learning activity/resource as the object.

    Event types with learning activity objects:
    - ActivityEvent: completion of a learning activity
    - TimeSpentEvent: time spent on a learning activity
    - AssignableEvent: working on an assignable learning resource
    - ViewEvent: viewing a learning resource

    Event types with other object types (NOT transformed):
    - AssessmentItemEvent: object is AssessmentItem (not a resource)
    - SessionEvent: object is SoftwareApplication (the app itself)
    - NavigationEvent: varies by context
    - GradeEvent: object is Attempt (not a resource)
    - Others: preserve original type
    """
    obj_type = obj.get("type")

    # Only transform TimebackActivityContext for events that expect learning resources
    should_transform = (
        obj_type == "TimebackActivityContext" and event_type in LEARNING_ACTIVITY_EVENT_TYPES
    )

    if not should_transform:
        # Don't transform - preserve original object
        # This handles: AssessmentItem, SoftwareApplication, Attempt, etc.
        return obj.copy()

    # Transform TimebackActivityContext → AssignableDigitalResource
    transformed = {"type": CaliperType.ASSIGNABLE_DIGITAL_RESOURCE, "id": obj.get("id")}

    # Add optional name field
    if obj.get("name"):
        transformed["name"] = obj["name"]

    # Note: 'subject' field removed - not in AssignableDigitalResource schema (strict mode)

    # Map course → isPartOf (CourseOffering), omit null fields
    if "course" in obj and isinstance(obj["course"], dict):
        course = obj["course"]
        is_part_of = {"type": CaliperType.COURSE_OFFERING, "id": course.get("id")}
        # Only add name if it's not None
        if course.get("name"):
            is_part_of["name"] = course["name"]
        transformed["isPartOf"] = is_part_of

    # Transform app to extensions (LWAI doesn't have TimebackApp)
    if "app" in obj and isinstance(obj["app"], dict):
        transformed["extensions"] = {
            "app": {
                "name": obj["app"].get("name"),
                "type": obj["app"].get("type", CaliperType.SOFTWARE_APPLICATION),
            }
        }

    return transformed


def _transform_metrics_collection(
    metrics_collection: dict[str, Any],
    collection_type: Literal["ActivityMetricsCollection", "TimeSpentMetricsCollection"],
    item_type: Literal["ActivityMetric", "TimeSpentMetric"],
    default_metric_type: str = "",
) -> dict[str, Any]:
    """
    Transform Timeback metrics collection to Caliper standard format.

    Handles both ActivityMetricsCollection and TimeSpentMetricsCollection transformations.

    Args:
        metrics_collection: Source metrics collection
        collection_type: Target collection type
        item_type: Target item type
        default_metric_type: Default if metric type is empty

    Returns:
        Transformed metrics collection with proper schema
    """
    # BaseEntity requires an id field; preserve source id when provided.
    has_source_id = "id" in metrics_collection
    source_id = metrics_collection.get("id")
    transformed: dict[str, Any] = {
        "id": source_id if has_source_id else f"urn:uuid:{uuid.uuid4()}",
        "type": collection_type,
    }

    # Transform items to proper metric schema
    if "items" in metrics_collection:
        transformed["items"] = []
        for item in metrics_collection["items"]:
            metric_type = item.get("type", "")
            metric_value = item.get("value")

            # Capitalize first letter: xpEarned → XpEarned, timeSpentMetric → TimeSpentMetric
            pascal_type = (
                metric_type[0].upper() + metric_type[1:] if metric_type else default_metric_type
            )

            # Each metric is a BaseEntity and requires proper schema
            transformed["items"].append(
                {
                    "id": f"urn:uuid:{uuid.uuid4()}",  # Required by BaseEntity
                    "type": item_type,  # Required literal type
                    "metricType": pascal_type,  # PascalCase metric name
                    "metricValue": metric_value,  # Value of the metric
                }
            )

    # Copy any other fields from original
    for key, value in metrics_collection.items():
        if key not in ("id", "type", "items"):
            transformed[key] = value

    return transformed


def _transform_metrics(metrics_collection: dict[str, Any]) -> dict[str, Any]:
    """
    Transform TimebackActivityMetricsCollection → ActivityMetricsCollection.
    """
    return _transform_metrics_collection(
        metrics_collection,
        CaliperType.ACTIVITY_METRICS_COLLECTION,
        CaliperType.ACTIVITY_METRIC,
        default_metric_type="",
    )


def _transform_time_spent_metrics(metrics_collection: dict[str, Any]) -> dict[str, Any]:
    """
    Transform TimebackTimeSpentMetricsCollection → TimeSpentMetricsCollection.
    """
    return _transform_metrics_collection(
        metrics_collection,
        CaliperType.TIME_SPENT_METRICS_COLLECTION,
        CaliperType.TIME_SPENT_METRIC,
        default_metric_type="Active",
    )


__all__ = [
    "EventPayloadTransformer",
    "LwaiEventTransformer",
    "transform_for_lwai",
]
