"""
Events Resource

Methods for sending and retrieving Caliper events.
"""

from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING, Any
from urllib.parse import quote

from timeback_common import (
    ValidationIssue,
    create_input_validation_error,
    utc_iso_timestamp,
    validate_non_empty_string,
    validate_offset_list_params,
)

from ..constants import CALIPER_DATA_VERSION
from ..event_factories import (
    create_activity_event,
    create_question_answered_event,
    create_question_graded_event,
    create_question_seen_event,
    create_time_spent_event,
)
from ..exceptions import APIError, UnsupportedOperationError
from ..lib.pagination import Paginator
from ..types import (
    timeback as timeback_types,  # noqa: TC001 — runtime import needed for get_type_hints()
)
from ..types.api import (
    CaliperEnvelope,
    ListEventsResult,
    Pagination,
    SendEventsResult,
    StoredEvent,
    ValidationResult,
)

if TYPE_CHECKING:
    from ..lib.transport import Transport
    from ..types.timeback import TimebackEvent
    from .event_transformers import EventPayloadTransformer


def _parse_stored_event(raw: Any) -> StoredEvent:
    return StoredEvent(**raw) if isinstance(raw, dict) else raw


class EventsResource:
    """
    Events resource for sending and retrieving Caliper events.

    Access via `client.events`.

    Example:
        ```python
        # Send events
        result = await client.events.send(
            sensor_id="https://example.edu/sensors/1",
            events=[event1, event2],
        )

        # Validate before sending
        validation = await client.events.validate(envelope)

        # List events
        events_page = await client.events.list(limit=50)

        # Get specific event
        event = await client.events.get("urn:uuid:...")
        ```
    """

    def __init__(
        self,
        transport: Transport,
        event_transformer: EventPayloadTransformer | None = None,
    ) -> None:
        self._transport = transport
        self._event_transformer = event_transformer

    @staticmethod
    def _normalize_optional_non_empty_string(value: str | None, field: str) -> str | None:
        """Trim optional string filters and reject empty/whitespace-only values."""
        if value is None:
            return None
        validate_non_empty_string(value, field)
        return value.strip()

    async def send(
        self,
        sensor_id: str,
        events: list[TimebackEvent],
    ) -> SendEventsResult:
        """
        Send raw Caliper events.

        Wraps events in an envelope and sends to the API.
        Events are validated against the IMS Caliper specification
        and queued for async processing.

        Args:
            sensor_id: Sensor identifier (IRI format)
            events: List of Caliper events to send

        Returns:
            Result containing job_id for tracking processing status
        """
        validate_non_empty_string(sensor_id, "sensor_id")
        sensor_id = sensor_id.strip()
        if not isinstance(events, list) or len(events) == 0:
            raise create_input_validation_error(
                "Invalid send data",
                [ValidationIssue(path="events", message="Must be a non-empty list")],
            )

        envelope = CaliperEnvelope(
            sensor=sensor_id,
            sendTime=utc_iso_timestamp(),
            dataVersion=CALIPER_DATA_VERSION,
            data=[_serialize_event(e) for e in events],
        )

        return await self.send_envelope(envelope)

    async def send_envelope(
        self,
        envelope: CaliperEnvelope,
    ) -> SendEventsResult:
        """
        Send a raw Caliper envelope.

        Use this when you need full control over the envelope structure.
        For most cases, prefer `send()` which builds the envelope for you.

        Args:
            envelope: Caliper envelope containing events

        Returns:
            Result containing job_id for tracking processing status

        Example:
            ```python
            from timeback_caliper.types import CaliperEnvelope
            from datetime import datetime, UTC

            envelope = CaliperEnvelope(
                sensor="https://example.edu/sensors/1",
                send_time=utc_iso_timestamp(),
                data_version="http://purl.imsglobal.org/ctx/caliper/v1p2",
                data=[event1_dict, event2_dict],
            )
            result = await client.events.send_envelope(envelope)
            ```
        """
        send_path = self._transport.paths.send
        payload = envelope.model_dump(mode="json", by_alias=True)
        if isinstance(payload.get("sensor"), str):
            payload["sensor"] = payload["sensor"].strip()
        _validate_send_envelope_payload(payload)

        if self._event_transformer:
            payload = self._event_transformer.transform(payload)

        response = await self._transport.post(send_path, payload)

        if not response:
            return SendEventsResult(jobId=None)

        return SendEventsResult(**response)

    async def validate(
        self,
        envelope: CaliperEnvelope,
    ) -> ValidationResult:
        """
        Validate Caliper events without storing them.

        Use this to check if events conform to the IMS Caliper specification
        before sending them for processing.

        Note:
            This operation is only available on the BEYOND_AI platform.

        Args:
            envelope: Caliper envelope containing events to validate

        Returns:
            Validation result with status and any errors

        Raises:
            UnsupportedOperationError: If not supported on current platform

        Example:
            ```python
            result = await client.events.validate(envelope)
            if result.status == "success":
                print("Events are valid!")
            else:
                print("Validation errors:", result.errors)
            ```
        """
        validate_path = self._transport.paths.validate
        if validate_path is None:
            raise UnsupportedOperationError("validate")

        payload = envelope.model_dump(mode="json", by_alias=True)
        response = await self._transport.post(validate_path, payload)
        return ValidationResult(**response)

    async def list(
        self,
        *,
        limit: int = 100,
        offset: int = 0,
        sensor: str | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        actor_id: str | None = None,
        actor_email: str | None = None,
    ) -> ListEventsResult:
        """
        List Caliper events with optional filtering.

        Note:
            This operation is only available on the BEYOND_AI platform.

        Args:
            limit: Maximum events per page (default: 100)
            offset: Starting offset for pagination
            sensor: Filter by sensor ID
            start_date: Filter by start date (ISO format)
            end_date: Filter by end date (ISO format)
            actor_id: Filter by actor ID
            actor_email: Filter by actor email

        Returns:
            Events array and pagination metadata

        Raises:
            UnsupportedOperationError: If not supported on current platform
            InputValidationError: If limit or offset are invalid

        Example:
            ```python
            result = await client.events.list(
                limit=50,
                actor_email="student@example.edu",
                start_date="2024-01-01",
            )
            print(f"Total: {result.pagination.total}")
            for event in result.events:
                print(event.id)
            ```
        """
        validate_offset_list_params(limit=limit, offset=offset)
        sensor = self._normalize_optional_non_empty_string(sensor, "sensor")
        actor_id = self._normalize_optional_non_empty_string(actor_id, "actor_id")

        list_path = self._transport.paths.list
        if list_path is None:
            raise UnsupportedOperationError("list")

        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if sensor is not None:
            params["sensor"] = sensor
        if start_date:
            params["startDate"] = start_date
        if end_date:
            params["endDate"] = end_date
        if actor_id is not None:
            params["actorId"] = actor_id
        if actor_email:
            params["actorEmail"] = actor_email

        response = await self._transport.get(list_path, params=params)

        events_data = response.get("events", [])
        events = [StoredEvent(**e) for e in events_data]

        pagination_data = response.get("pagination", {})
        pagination = Pagination(**pagination_data)

        return ListEventsResult(events=events, pagination=pagination)

    def stream(
        self,
        *,
        limit: int = 100,
        offset: int = 0,
        sensor: str | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        actor_id: str | None = None,
        actor_email: str | None = None,
        max_items: int | None = None,
    ) -> Paginator[StoredEvent]:
        """
        Stream Caliper events with automatic pagination.

        Returns a paginator that lazily fetches pages as you iterate.
        Use this for large datasets or when you need all matching events.

        Note:
            This operation is only available on the BEYOND_AI platform.

        Args:
            limit: Events per page (default: 100)
            offset: Starting offset for pagination (default: 0)
            sensor: Filter by sensor ID
            start_date: Filter by start date (ISO format)
            end_date: Filter by end date (ISO format)
            actor_id: Filter by actor ID
            actor_email: Filter by actor email
            max_items: Maximum total events to fetch

        Returns:
            Async iterator for streaming events

        Raises:
            UnsupportedOperationError: If not supported on current platform
            InputValidationError: If limit, offset, or max_items are invalid

        Example:
            ```python
            # Iterate over all events
            async for event in client.events.stream(start_date="2024-01-01"):
                print(event.id)

            # Collect all events into a list
            all_events = await client.events.stream(start_date="2024-01-01").to_list()

            # Limit total events fetched
            events = await client.events.stream(max_items=1000).to_list()

            # Start from a specific offset
            events = await client.events.stream(offset=100).to_list()

            # Get first page with metadata
            page = await client.events.stream(start_date="2024-01-01").first_page()
            ```
        """
        validate_offset_list_params(limit=limit, offset=offset, max_items=max_items)
        sensor = self._normalize_optional_non_empty_string(sensor, "sensor")
        actor_id = self._normalize_optional_non_empty_string(actor_id, "actor_id")

        list_path = self._transport.paths.list
        if list_path is None:
            raise UnsupportedOperationError("stream")

        params: dict[str, Any] = {}
        if sensor is not None:
            params["sensor"] = sensor
        if start_date:
            params["startDate"] = start_date
        if end_date:
            params["endDate"] = end_date
        if actor_id is not None:
            params["actorId"] = actor_id
        if actor_email:
            params["actorEmail"] = actor_email

        return Paginator(
            self._transport,
            list_path,
            params=params,
            limit=limit,
            max_items=max_items,
            offset=offset,
            transform=_parse_stored_event,
        )

    async def get(self, external_id: str) -> StoredEvent:
        """
        Get a specific event by its external ID.

        Note:
            This operation is only available on the BEYOND_AI platform.

        Args:
            external_id: The event's external ID (URN UUID format, e.g., 'urn:uuid:...')

        Returns:
            The stored event

        Raises:
            UnsupportedOperationError: If not supported on current platform
            InputValidationError: If external_id is empty
            APIError: If the event is not found

        Important:
            Use `external_id` (URN UUID), not the internal numeric `id`.
            The `StoredEvent` returned from `list()` contains both.

        Example:
            ```python
            event = await client.events.get("urn:uuid:c51570e4-f8ed-4c18-bb3a-dfe51b2cc594")
            print(event.event_type, event.actor_id)
            ```
        """
        validate_non_empty_string(external_id, "external_id")

        get_path = self._transport.paths.get
        if get_path is None:
            raise UnsupportedOperationError("get")

        # Use {id} template like TS does
        path = get_path.replace("{id}", quote(external_id, safe=""))
        response = await self._transport.get(path)

        event_data = response.get("event")
        if not event_data:
            raise APIError(f"Event not found: {external_id}", status_code=404)

        return StoredEvent(**event_data)

    async def send_activity(
        self,
        sensor_id: str,
        input: timeback_types.ActivityCompletedInput,
    ) -> SendEventsResult:
        """
        Send an activity completed event.

        Auto-generates id, eventTime, and metrics collection id if not provided.

        Args:
            sensor_id: Sensor identifier (IRI format)
            input: Activity completion input

        Returns:
            Result containing job_id for tracking processing status

        Example:
            ```python
            result = await client.events.send_activity(
                sensor_id="https://myapp.example.com/sensors/main",
                input=ActivityCompletedInput(
                    actor=TimebackUser(id="...", type="TimebackUser", email="student@example.edu"),
                    object=TimebackActivityContext(
                        id="...",
                        type="TimebackActivityContext",
                        subject="Math",
                        app=TimebackApp(name="My Learning App"),
                    ),
                    metrics=[
                        TimebackActivityMetric(type="totalQuestions", value=10),
                        TimebackActivityMetric(type="correctQuestions", value=8),
                    ],
                ),
            )
            ```
        """
        event = create_activity_event(input)
        return await self.send(sensor_id, [event])

    async def send_time_spent(
        self,
        sensor_id: str,
        input: timeback_types.TimeSpentInput,
    ) -> SendEventsResult:
        """
        Send a time spent event.

        Auto-generates id, eventTime, and metrics collection id if not provided.

        Args:
            sensor_id: Sensor identifier (IRI format)
            input: Time spent input

        Returns:
            Result containing job_id for tracking processing status

        Example:
            ```python
            result = await client.events.send_time_spent(
                sensor_id="https://myapp.example.com/sensors/main",
                input=TimeSpentInput(
                    actor=TimebackUser(id="...", type="TimebackUser", email="student@example.edu"),
                    object=TimebackActivityContext(
                        id="...",
                        type="TimebackActivityContext",
                        subject="Reading",
                        app=TimebackApp(name="My Learning App"),
                    ),
                    metrics=[
                        TimeSpentMetric(type="active", value=1800),  # 30 min
                        TimeSpentMetric(type="inactive", value=300),  # 5 min
                    ],
                ),
            )
            ```
        """
        event = create_time_spent_event(input)
        return await self.send(sensor_id, [event])

    # ═══════════════════════════════════════════════════════════════════════════
    # ASSESSMENT PROFILE CONVENIENCE METHODS
    # ═══════════════════════════════════════════════════════════════════════════

    async def send_question_seen(
        self,
        sensor_id: str,
        input: timeback_types.QuestionSeenInput,
    ) -> SendEventsResult:
        """
        Send a "question seen" event (AssessmentItemEvent.Started).

        Records when a question is presented to a student.

        Args:
            sensor_id: Sensor identifier (IRI format)
            input: Question seen data

        Returns:
            Result containing job_id for tracking processing status

        Example:
            ```python
            result = await client.events.send_question_seen(
                sensor_id="https://myapp.example.com/sensors/main",
                input=QuestionSeenInput(
                    actor="urn:uuid:student-123",
                    object=QuestionObjectInput(id="urn:uuid:q-456", name="What is 2+2?"),
                    ed_app="https://myapp.example.com",
                ),
            )
            ```
        """
        event = create_question_seen_event(input)
        return await self.send(sensor_id, [event])

    async def send_question_answered(
        self,
        sensor_id: str,
        input: timeback_types.QuestionAnsweredInput,
    ) -> SendEventsResult:
        """
        Send a "question answered" event (AssessmentItemEvent.Completed).

        Records when a student submits an answer to a question.

        Args:
            sensor_id: Sensor identifier (IRI format)
            input: Question answered data

        Returns:
            Result containing job_id for tracking processing status

        Example:
            ```python
            result = await client.events.send_question_answered(
                sensor_id="https://myapp.example.com/sensors/main",
                input=QuestionAnsweredInput(
                    actor="urn:uuid:student-123",
                    object=QuestionObjectInput(id="urn:uuid:q-456", name="What is 2+2?"),
                    ed_app="https://myapp.example.com",
                    generated=ResponseGeneratedInput(
                        id="urn:uuid:response-789",
                        attempt="urn:uuid:attempt-101",
                    ),
                ),
            )
            ```
        """
        event = create_question_answered_event(input)
        return await self.send(sensor_id, [event])

    async def send_question_graded(
        self,
        sensor_id: str,
        input: timeback_types.QuestionGradedInput,
    ) -> SendEventsResult:
        """
        Send a "question graded" event (GradeEvent.Graded).

        Records when a question response is scored. The Score entity is
        auto-filled with score_type="QUESTION_RESULT".

        Args:
            sensor_id: Sensor identifier (IRI format)
            input: Question graded data

        Returns:
            Result containing job_id for tracking processing status

        Example:
            ```python
            result = await client.events.send_question_graded(
                sensor_id="https://myapp.example.com/sensors/main",
                input=QuestionGradedInput(
                    actor="urn:uuid:student-123",
                    object="urn:uuid:attempt-101",
                    generated=ScoreGeneratedInput(
                        score_given=1, max_score=1, attempt="urn:uuid:attempt-101"
                    ),
                    ed_app="https://myapp.example.com",
                ),
            )
            ```
        """
        event = create_question_graded_event(input)
        return await self.send(sensor_id, [event])


def _serialize_event(event: Any) -> dict[str, Any]:
    """Serialize event model or pass through dict payloads."""
    if hasattr(event, "model_dump"):
        return event.model_dump(mode="json", by_alias=True, exclude_none=True)
    if isinstance(event, dict):
        return event
    raise create_input_validation_error(
        "Invalid send data",
        [ValidationIssue(path="events", message="Each event must be a dict-like payload")],
    )


def _validate_send_envelope_payload(payload: dict[str, Any]) -> None:
    """Validate runtime envelope constraints mirrored from fixture expectations."""
    issues: list[ValidationIssue] = []

    sensor = payload.get("sensor")
    if not isinstance(sensor, str) or sensor.strip() == "":
        issues.append(ValidationIssue(path="sensor", message="Must be a non-empty string"))

    send_time = payload.get("sendTime")
    if not isinstance(send_time, str) or send_time.strip() == "":
        issues.append(ValidationIssue(path="sendTime", message="Must be a non-empty string"))
    else:
        try:
            datetime.fromisoformat(send_time.replace("Z", "+00:00"))
        except ValueError:
            issues.append(ValidationIssue(path="sendTime", message="Must be a valid ISO datetime"))

    data_version = payload.get("dataVersion")
    if data_version != CALIPER_DATA_VERSION:
        issues.append(
            ValidationIssue(
                path="dataVersion",
                message=f"Must equal {CALIPER_DATA_VERSION}",
            )
        )

    data = payload.get("data")
    if not isinstance(data, list) or len(data) == 0:
        issues.append(ValidationIssue(path="data", message="Must be a non-empty list"))

    if issues:
        raise create_input_validation_error("Invalid envelope data", issues)
