# Copyright 2026 4f4d
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Utility functions for f1radio — fuzzy matching, time parsing, driver resolution."""

from __future__ import annotations

import re
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Tuple

from thefuzz import process as fuzz_process


# Session type abbreviation → full name mapping
SESSION_TYPES = {
    "R": "Race",
    "Q": "Qualifying",
    "S": "Sprint",
    "SQ": "Sprint Qualifying",
    "SS": "Sprint Shootout",
    "FP1": "Practice 1",
    "FP2": "Practice 2",
    "FP3": "Practice 3",
}

# Reverse mapping: full name → abbreviation
SESSION_TYPES_REVERSE = {v: k for k, v in SESSION_TYPES.items()}


def resolve_session_type(abbrev: str) -> str:
    """Resolve a session type abbreviation to its full name.

    Args:
        abbrev: Session type abbreviation (e.g., "R", "Q", "FP1").

    Returns:
        Full session type name (e.g., "Race", "Qualifying", "Practice 1").

    Raises:
        ValueError: If the abbreviation is not recognized.
    """
    key = abbrev.upper().strip()
    if key in SESSION_TYPES:
        return SESSION_TYPES[key]
    # Try matching against full names (case-insensitive)
    for full_name in SESSION_TYPES.values():
        if key == full_name.upper():
            return full_name
    raise ValueError(
        f"Unknown session type: '{abbrev}'. "
        f"Valid types: {', '.join(SESSION_TYPES.keys())}"
    )


def fuzzy_match_race(
    query: str,
    race_names: List[str],
    threshold: int = 60,
) -> Optional[str]:
    """Fuzzy match a race name query against a list of known race names.

    Supports partial matches like "Monza" → "Italian Grand Prix",
    "spa" → "Belgian Grand Prix", etc.

    Args:
        query: User's race name query.
        race_names: List of official race names to match against.
        threshold: Minimum match score (0-100) to accept.

    Returns:
        Best matching race name, or None if no match meets the threshold.
    """
    if not race_names:
        return None

    # Try exact match first (case-insensitive)
    query_lower = query.lower().strip()
    for name in race_names:
        if query_lower == name.lower():
            return name

    # Try substring match (e.g., "Monza" in "Italian Grand Prix... Monza")
    for name in race_names:
        if query_lower in name.lower():
            return name

    # Fall back to fuzzy matching
    result = fuzz_process.extractOne(query, race_names)
    if result and result[1] >= threshold:
        return result[0]

    return None


def parse_time(time_str: str) -> timedelta:
    """Parse a time string in HH:MM:SS or MM:SS format to a timedelta.

    Args:
        time_str: Time string like "01:32:15", "32:15", or "1:32.456".

    Returns:
        timedelta representing the time.

    Raises:
        ValueError: If the time string format is unrecognized.
    """
    time_str = time_str.strip()

    # Handle "HH:MM:SS" or "HH:MM:SS.mmm"
    match = re.match(r"^(\d+):(\d{2}):(\d{2})(?:\.(\d+))?$", time_str)
    if match:
        hours, minutes, seconds = int(match.group(1)), int(match.group(2)), int(match.group(3))
        microseconds = 0
        if match.group(4):
            frac = match.group(4).ljust(6, "0")[:6]
            microseconds = int(frac)
        return timedelta(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds)

    # Handle "MM:SS" or "MM:SS.mmm"
    match = re.match(r"^(\d+):(\d{2})(?:\.(\d+))?$", time_str)
    if match:
        minutes, seconds = int(match.group(1)), int(match.group(2))
        microseconds = 0
        if match.group(3):
            frac = match.group(3).ljust(6, "0")[:6]
            microseconds = int(frac)
        return timedelta(minutes=minutes, seconds=seconds, microseconds=microseconds)

    # Handle "M:SS.mmm" (lap time format like "1:32.456")
    match = re.match(r"^(\d+):(\d{2})\.(\d+)$", time_str)
    if match:
        minutes, seconds = int(match.group(1)), int(match.group(2))
        frac = match.group(3).ljust(6, "0")[:6]
        microseconds = int(frac)
        return timedelta(minutes=minutes, seconds=seconds, microseconds=microseconds)

    raise ValueError(f"Unrecognized time format: '{time_str}'")


def format_duration(seconds: float) -> str:
    """Format seconds as HH:MM:SS string.

    Args:
        seconds: Duration in seconds.

    Returns:
        Formatted string like "01:32:15".
    """
    total = int(seconds)
    hours = total // 3600
    minutes = (total % 3600) // 60
    secs = total % 60
    return f"{hours:02d}:{minutes:02d}:{secs:02d}"


def session_relative_time(absolute_date: str, session_start: str) -> str:
    """Calculate session-relative time from absolute ISO dates.

    Args:
        absolute_date: ISO 8601 date string of the event.
        session_start: ISO 8601 date string of the session start.

    Returns:
        Time string in "HH:MM:SS" format relative to session start.
    """
    event_dt = _parse_iso_datetime(absolute_date)
    start_dt = _parse_iso_datetime(session_start)
    delta = event_dt - start_dt
    total_seconds = max(0, delta.total_seconds())
    return format_duration(total_seconds)


def _parse_iso_datetime(dt_str: str) -> datetime:
    """Parse an ISO 8601 datetime string, handling various formats.

    Args:
        dt_str: ISO datetime string from OpenF1 API.

    Returns:
        timezone-aware datetime object.
    """
    # Handle the OpenF1 format: "2023-09-15T10:12:05.878000+00:00"
    dt_str = dt_str.strip()
    try:
        return datetime.fromisoformat(dt_str)
    except ValueError:
        pass

    # Try stripping microseconds
    try:
        # Remove fractional seconds for simpler parsing
        clean = re.sub(r"\.\d+", "", dt_str)
        return datetime.fromisoformat(clean)
    except ValueError:
        pass

    # Last resort: treat as UTC
    try:
        dt = datetime.strptime(dt_str[:19], "%Y-%m-%dT%H:%M:%S")
        return dt.replace(tzinfo=timezone.utc)
    except ValueError:
        raise ValueError(f"Cannot parse datetime: '{dt_str}'")


def driver_code_to_number(
    code: str,
    drivers_data: List[Dict],
) -> Optional[int]:
    """Look up a driver number from a 3-letter code.

    Args:
        code: Driver code like "VER", "HAM".
        drivers_data: List of driver dicts from OpenF1 API.

    Returns:
        Driver number, or None if not found.
    """
    code_upper = code.upper().strip()
    for driver in drivers_data:
        if driver.get("name_acronym", "").upper() == code_upper:
            return driver.get("driver_number")
    return None


def driver_number_to_info(
    number: int,
    drivers_data: List[Dict],
) -> Tuple[str, str, str]:
    """Look up driver info from a driver number.

    Args:
        number: Driver number.
        drivers_data: List of driver dicts from OpenF1 API.

    Returns:
        Tuple of (code, full_name, team_name). Returns ("???", "Unknown", "Unknown")
        if not found.
    """
    for driver in drivers_data:
        if driver.get("driver_number") == number:
            code = driver.get("name_acronym", "???")
            full_name = driver.get("full_name", "Unknown")
            team = driver.get("team_name", "Unknown")
            return code, full_name, team
    return "???", "Unknown", "Unknown"
