"""
EduBridge Utility Functions

Internal utilities for the EduBridge client.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING, TypeVar
from zoneinfo import ZoneInfo

from timeback_common import InputValidationError, ValidationIssue, normalize_boolean, normalize_date
from timeback_common.datetime import ensure_z_suffix

from .types.analytics import AggregatedMetrics, DailyActivityMap

if TYPE_CHECKING:
    from .types.users import User

# Re-export for backwards compatibility
__all__ = [
    "AggregatedMetrics",
    "aggregate_activity_metrics",
    "normalize_boolean",
    "normalize_date",
    "normalize_end_date",
    "normalize_start_date",
    "normalize_user",
]

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


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
# ═══════════════════════════════════════════════════════════════════════════════


def _bare_date_to_utc(bare_date: str, time: str, tz_name: str) -> str:
    """
    Convert a bare date + time-of-day in a given timezone to a UTC ISO string.

    Python's ``zoneinfo`` handles DST transitions correctly, so a single
    construction with ``tzinfo`` is sufficient (unlike the JS two-pass approach).

    Args:
        bare_date: Date in YYYY-MM-DD format
        time: Time of day as HH:MM:SS.mmm
        tz_name: IANA timezone name

    Returns:
        Full ISO 8601 datetime string in UTC
    """
    try:
        tz = ZoneInfo(tz_name)
    except (KeyError, ValueError):
        raise InputValidationError(
            "Invalid timezone",
            issues=[
                ValidationIssue(
                    path="timezone",
                    message=f'Must be a valid IANA timezone, got "{tz_name}"',
                ),
            ],
        )

    year_s, month_s, day_s = bare_date.split("-")
    h_s, m_s, sec_ms = time.split(":")
    sec_parts = sec_ms.split(".")
    s_s = sec_parts[0]
    ms = int(sec_parts[1]) if len(sec_parts) > 1 else 0

    local_dt = datetime(
        int(year_s),
        int(month_s),
        int(day_s),
        int(h_s),
        int(m_s),
        int(s_s),
        ms * 1000,
        tzinfo=tz,
    )
    utc_dt = local_dt.astimezone(UTC)
    return utc_dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{utc_dt.microsecond // 1000:03d}Z"


def normalize_start_date(date: str, tz: str | None = None) -> str:
    """
    Normalize a date string for use as a startDate query parameter.

    - Full ISO timestamps (containing ``T``) pass through unchanged.
    - Bare dates (``YYYY-MM-DD``) are expanded to the start of that day.
      When ``timezone`` is provided, "start of day" is computed in that
      timezone and converted to UTC. Otherwise UTC midnight is used.

    Args:
        date: Date string (bare or full ISO)
        tz: Optional IANA timezone name

    Returns:
        Full ISO 8601 datetime string in UTC
    """
    if "T" in date:
        return ensure_z_suffix(date)
    if not tz:
        return f"{date}T00:00:00.000Z"
    return _bare_date_to_utc(date, "00:00:00.000", tz)


def normalize_end_date(date: str, tz: str | None = None) -> str:
    """
    Normalize a date string for use as an endDate query parameter.

    - Full ISO timestamps (containing ``T``) pass through unchanged.
    - Bare dates (``YYYY-MM-DD``) are expanded to the end of that day
      (23:59:59.999). When ``timezone`` is provided, "end of day" is
      computed in that timezone and converted to UTC.

    Args:
        date: Date string (bare or full ISO)
        tz: Optional IANA timezone name

    Returns:
        Full ISO 8601 datetime string in UTC
    """
    if "T" in date:
        return ensure_z_suffix(date)
    if not tz:
        return f"{date}T23:59:59.999Z"
    return _bare_date_to_utc(date, "23:59:59.999", tz)


# ═══════════════════════════════════════════════════════════════════════════════
# 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: float = 0
    inactive_seconds: float = 0
    waste_seconds: float = 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,
    )
