"""
EduBridge Utility Functions

Internal utilities for the EduBridge client.
"""

from __future__ import annotations

import re
from datetime import datetime
from typing import TYPE_CHECKING, TypeVar

from timeback_common import ensure_z_suffix

from .types.analytics import AggregatedMetrics, DailyActivityMap

if TYPE_CHECKING:
    from .types.users import User

# ═══════════════════════════════════════════════════════════════════════════════
# TYPE NORMALIZATION
# ═══════════════════════════════════════════════════════════════════════════════


def normalize_boolean(value: bool | str) -> bool:
    """
    Normalize a boolean-like value to a boolean.

    The Beyond-AI API sometimes returns boolean fields as strings (`"true"`/`"false"`).
    This normalizes them to actual booleans for a stable, predictable API.

    Args:
        value: Boolean-like value from the API

    Returns:
        Normalized boolean

    Example:
        >>> normalize_boolean("true")
        True
        >>> normalize_boolean("false")
        False
        >>> normalize_boolean(True)
        True
    """
    if isinstance(value, bool):
        return value
    return value == "true"


T = TypeVar("T", bound="User")


def normalize_user[T: "User"](user: T) -> T:
    """
    Normalize a User object from the API.

    Applies all necessary field normalizations to ensure consistent types.
    Currently normalizes:
    - `enabled_user`: string → boolean

    Args:
        user: Raw user from the API

    Returns:
        User with normalized fields

    Note:
        This function modifies the user object in-place and returns it.
        The Pydantic model's `enabled_user` field may be typed as `Literal["true", "false"]`
        from the API, but after normalization it will be a boolean.
    """
    # Access the underlying attribute to normalize it
    # Since Pydantic models are mutable by default, we can modify in place
    object.__setattr__(user, "enabled_user", normalize_boolean(user.enabled_user))
    return user


# ═══════════════════════════════════════════════════════════════════════════════
# DATE NORMALIZATION
# ═══════════════════════════════════════════════════════════════════════════════


# Regex for date-only format: YYYY-MM-DD
_DATE_ONLY_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def normalize_date(date: str | datetime | None) -> str | None:
    """
    Normalize a date string to full ISO 8601 format.

    The Edubridge API requires full ISO 8601 datetime format (e.g., `2025-12-25T00:00:00.000Z`).
    This function accepts multiple input formats for developer convenience:
    - Date-only: `2025-12-25` → `2025-12-25T00:00:00.000Z`
    - Full ISO: `2025-12-25T10:30:00.000Z` → passed through unchanged
    - Date object: `datetime()` → `.isoformat()` + 'Z'

    Args:
        date: Date string or datetime object

    Returns:
        Full ISO 8601 datetime string, or None if input is None

    Example:
        >>> normalize_date("2025-12-25")
        '2025-12-25T00:00:00.000Z'
        >>> normalize_date("2025-12-25T10:30:00.000Z")
        '2025-12-25T10:30:00.000Z'
        >>> normalize_date(None)
        None
    """
    if date is None:
        return None

    if isinstance(date, datetime):
        # Format datetime to ISO 8601 with milliseconds and Z suffix
        return date.strftime("%Y-%m-%dT%H:%M:%S.") + f"{date.microsecond // 1000:03d}Z"

    # If the date string is already in full ISO 8601 format
    # (i.e., contains a 'T' character), normalize the timezone suffix.
    # Python's isoformat() produces "+00:00" but API expects "Z".
    if "T" in date:
        return ensure_z_suffix(date)

    # Handles date-only format (YYYY-MM-DD) by appending time at midnight UTC.
    # Converts '2025-12-25' → '2025-12-25T00:00:00.000Z'
    if _DATE_ONLY_PATTERN.match(date):
        return f"{date}T00:00:00.000Z"

    # If the date string is not in a valid format, return it as-is;
    # this will be caught by the API validation error.
    return date


# ═══════════════════════════════════════════════════════════════════════════════
# METRICS AGGREGATION
# ═══════════════════════════════════════════════════════════════════════════════


def aggregate_activity_metrics(data: DailyActivityMap) -> AggregatedMetrics:
    """
    Aggregate activity metrics from a DailyActivityMap.

    Sums up all metrics across dates and subjects into a single totals object.

    Args:
        data: Activity data grouped by date and subject

    Returns:
        Aggregated totals

    Example:
        ```python
        activity = await client.analytics.get_activity(...)
        totals = aggregate_activity_metrics(activity)
        print(f"Total XP: {totals.total_xp}")
        ```
    """
    total_xp: float = 0
    total_questions: int = 0
    correct_questions: int = 0
    mastered_units: int = 0
    active_seconds: int = 0
    inactive_seconds: int = 0
    waste_seconds: int = 0
    subject_count: int = 0

    dates = list(data.keys())
    day_count = len(dates)

    for date in dates:
        subjects = data.get(date)
        if subjects is None:
            continue

        for subject_key in subjects:
            metrics = subjects.get(subject_key)
            if metrics is None:
                continue

            subject_count += 1
            total_xp += metrics.activity_metrics.xp_earned
            total_questions += metrics.activity_metrics.total_questions
            correct_questions += metrics.activity_metrics.correct_questions
            mastered_units += metrics.activity_metrics.mastered_units
            active_seconds += metrics.time_spent_metrics.active_seconds
            inactive_seconds += metrics.time_spent_metrics.inactive_seconds
            waste_seconds += metrics.time_spent_metrics.waste_seconds

    return AggregatedMetrics(
        totalXp=total_xp,
        totalQuestions=total_questions,
        correctQuestions=correct_questions,
        masteredUnits=mastered_units,
        activeSeconds=active_seconds,
        inactiveSeconds=inactive_seconds,
        wasteSeconds=waste_seconds,
        dayCount=day_count,
        subjectCount=subject_count,
    )
