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

"""f1radio — F1 team radio with race context.

Data sourced from OpenF1 (https://openf1.org) under CC BY-NC-SA 4.0.
Audio files are hosted by Formula One Management.
"""

from __future__ import annotations

import logging
import warnings
from pathlib import Path
from typing import Any, Dict, List, Optional

from f1radio.cache import CacheManager, set_cache_dir, get_cache_dir
from f1radio.client import OpenF1Client, stats
from f1radio.context import ContextEnricher
from f1radio.exceptions import (
    F1RadioError,
    SessionNotFoundError,
    APIError,
    CacheError,
    PlaybackError,
)
from f1radio.models import Clip, ClipContext, ClipList, Session
from f1radio.utils import (
    driver_number_to_info,
    fuzzy_match_race,
    resolve_session_type,
    session_relative_time,
)

__version__ = "1.0.3"
__all__ = [
    "load",
    "clear_cache",
    "cache_info",
    "set_cache_dir",
    "stats",
    "Session",
    "Clip",
    "ClipList",
    "ClipContext",
    "F1RadioError",
    "SessionNotFoundError",
    "APIError",
    "CacheError",
    "PlaybackError",
    "__version__",
]

logger = logging.getLogger("f1radio")


def _resolve_session(
    client: OpenF1Client,
    year: int,
    race: str,
    session_abbrev: str,
) -> Dict[str, Any]:
    """Resolve year + race name + session type to a session_key.

    Uses fuzzy matching on race names and returns the session metadata dict.
    """
    session_type = resolve_session_type(session_abbrev)

    # First, get all meetings for the year to find matching race
    meetings = client.get_meetings(year=year)
    if not meetings:
        raise SessionNotFoundError(year, race, session_type)

    # Build race name list from meetings
    race_names = []
    meeting_map: Dict[str, Dict] = {}
    for m in meetings:
        name = m.get("meeting_name", "") or m.get("meeting_official_name", "")
        country = m.get("country_name", "")
        circuit = m.get("circuit_short_name", "")

        # Create multiple searchable variants
        for variant in [name, country, circuit]:
            if variant and variant not in race_names:
                race_names.append(variant)
                meeting_map[variant] = m

    # Fuzzy match the race name
    matched_name = fuzzy_match_race(race, race_names)
    if not matched_name:
        available = ", ".join(sorted(set(
            m.get("meeting_name", "Unknown") for m in meetings
        )))
        raise SessionNotFoundError(
            year, race, session_type,
        )

    matched_meeting = meeting_map[matched_name]
    meeting_key = matched_meeting.get("meeting_key")

    # Now find the specific session within this meeting
    sessions = client.get_sessions(year=year, session_type=session_type)
    if not sessions:
        raise SessionNotFoundError(year, race, session_type)

    # Filter to the matched meeting
    matching = [
        s for s in sessions
        if s.get("meeting_key") == meeting_key
    ]

    if not matching:
        # Fallback: try direct country_name search
        country = matched_meeting.get("country_name", "")
        if country:
            sessions_by_country = client.get_sessions(
                year=year, country_name=country, session_type=session_type
            )
            if sessions_by_country:
                matching = sessions_by_country

    if not matching:
        raise SessionNotFoundError(year, race, session_type)

    # Return the first (and usually only) match
    result = matching[0]
    result["_meeting"] = matched_meeting
    result["_race_name"] = matched_meeting.get("meeting_name", race)
    return result


def _fetch_all_context(
    client: OpenF1Client,
    cache: CacheManager,
    session_key: int,
    year: int,
    race: str,
    session_type: str,
    force_refresh: bool,
    verbose: bool,
) -> Dict[str, List[Dict]]:
    """Fetch all context data endpoints, using cache when available.

    Returns a dict mapping endpoint name to list of result dicts.
    """
    endpoints = {
        "drivers": lambda: client.get_drivers(session_key),
        "position": lambda: client.get_position(session_key),
        "intervals": lambda: client.get_intervals(session_key),
        "stints": lambda: client.get_stints(session_key),
        "laps": lambda: client.get_laps(session_key),
        "race_control": lambda: client.get_race_control(session_key),
        "overtakes": lambda: client.get_overtakes(session_key),
        "pit": lambda: client.get_pit(session_key),
    }

    context: Dict[str, List[Dict]] = {}

    if verbose:
        from tqdm import tqdm as tqdm_bar
        it = tqdm_bar(endpoints.items(), desc="Fetching context", unit="endpoint")
    else:
        it = endpoints.items()

    for name, fetcher in it:
        # Try cache first
        if not force_refresh:
            cached = cache.load_context(year, race, session_type, name)
            if cached is not None:
                context[name] = cached
                continue

        # Fetch from API
        data = fetcher()
        context[name] = data

        # Save to cache
        try:
            cache.save_context(year, race, session_type, name, data)
        except CacheError as e:
            logger.warning(f"Failed to cache {name}: {e}")

    return context


def _build_clips(
    radio_data: List[Dict],
    context: Dict[str, List[Dict]],
    cache_audio_dir: Path,
    session_start: Optional[str],
) -> ClipList:
    """Build enriched Clip objects from raw radio data + context.

    Args:
        radio_data: Raw team_radio API response.
        context: Pre-fetched context data from all endpoints.
        cache_audio_dir: Directory where MP3s are/will be stored.
        session_start: ISO datetime of session start for relative time calc.
    """
    drivers_data = context.get("drivers", [])

    # Build context enricher
    enricher = ContextEnricher(
        position_data=context.get("position", []),
        interval_data=context.get("intervals", []),
        stint_data=context.get("stints", []),
        lap_data=context.get("laps", []),
        race_control_data=context.get("race_control", []),
        overtake_data=context.get("overtakes", []),
        pit_data=context.get("pit", []),
        drivers_data=drivers_data,
    )

    clips = []
    for radio in sorted(radio_data, key=lambda r: r.get("date", "")):
        driver_number = radio.get("driver_number")
        date_str = radio.get("date", "")
        recording_url = radio.get("recording_url", "")

        if not driver_number or not recording_url:
            continue

        # Resolve driver info
        code, full_name, team = driver_number_to_info(driver_number, drivers_data)

        # Calculate session-relative time
        if session_start and date_str:
            rel_time = session_relative_time(date_str, session_start)
        else:
            rel_time = "00:00:00"

        # Determine local MP3 path
        safe_time = rel_time.replace(":", "")
        filename = f"{code}_{safe_time}.mp3"
        local_path = str(cache_audio_dir / filename)

        # Enrich with context
        clip_context = enricher.enrich(date_str, driver_number)

        clip = Clip(
            driver=code,
            driver_number=driver_number,
            driver_name=full_name,
            team=team,
            time=rel_time,
            date=date_str,
            lap=clip_context.lap_number,
            recording_url=recording_url,
            local_path=local_path,
            context=clip_context,
        )
        clips.append(clip)

    return ClipList(clips)


def load(
    year: int,
    race: str,
    session: str = "R",
    verbose: bool = True,
    force_refresh: bool = False,
) -> Session:
    """Load an F1 session with enriched team radio clips.

    On first call, fetches all radio clips + context data from OpenF1 API
    and downloads MP3 audio files to local cache. Subsequent calls load
    from cache instantly.

    Args:
        year: Season year (2023 onwards).
        race: Race name — supports fuzzy matching (e.g., "Monza", "Italian").
        session: Session type abbreviation: R, Q, FP1, FP2, FP3, S, SQ.
        verbose: Show progress bars and status messages.
        force_refresh: Re-download even if cached.

    Returns:
        Session object with enriched clips.

    Raises:
        SessionNotFoundError: If the session cannot be found.
        F1RadioError: On other errors.
    """
    if verbose:
        print(f"📻 f1radio — Loading {year} {race} ({session})")

    client = OpenF1Client()
    cache_mgr = CacheManager(client)

    # Auto-cleanup stale sessions (untouched for 30+ days)
    try:
        cleaned = cache_mgr.cleanup_stale()
        if cleaned > 0 and verbose:
            print(f"  🧹 Auto-cleaned {cleaned} stale session(s)")
    except Exception:
        pass  # Never let cleanup failure block loading

    # Step 1: Resolve session
    if verbose:
        print("  Resolving session...")
    session_meta = _resolve_session(client, year, race, session)
    session_key = session_meta.get("session_key")
    meeting = session_meta.get("_meeting", {})
    race_name = session_meta.get("_race_name", race)
    session_type = resolve_session_type(session)
    meeting_key = meeting.get("meeting_key")
    session_start = session_meta.get("date_start")

    if verbose:
        print(f"  Found: {race_name} — {session_type} (key={session_key})")

    # Step 2: Fetch team radio data
    cache_path = cache_mgr.get_cache_path(year, race_name, session_type)
    audio_dir = cache_path / "audio"
    audio_dir.mkdir(parents=True, exist_ok=True)

    if not force_refresh:
        cached_radio = cache_mgr.load_context(year, race_name, session_type, "team_radio")
    else:
        cached_radio = None

    if cached_radio is not None:
        radio_data = cached_radio
        if verbose:
            print(f"  Radio clips: {len(radio_data)} (cached)")
    else:
        if verbose:
            print("  Fetching radio clips...")
        radio_data = client.get_team_radio(session_key)
        try:
            cache_mgr.save_context(year, race_name, session_type, "team_radio", radio_data)
        except CacheError:
            pass
        if verbose:
            print(f"  Radio clips: {len(radio_data)} found")

    if not radio_data:
        warnings.warn(
            f"No team radio data available for {year} {race_name} ({session_type}). "
            "The session may not have radio recordings.",
            stacklevel=2,
        )

    # Step 3: Fetch all context data
    if verbose:
        print("  Fetching context data...")
    context = _fetch_all_context(
        client, cache_mgr, session_key,
        year, race_name, session_type,
        force_refresh, verbose,
    )

    # Step 4: Build enriched clips
    if verbose:
        print("  Enriching clips with context...")
    clips = _build_clips(radio_data, context, audio_dir, session_start)

    # Step 5: Download MP3 files
    downloads = [
        (clip.recording_url, clip.local_path)
        for clip in clips
        if clip.recording_url and clip.local_path
    ]
    if downloads:
        if verbose:
            print(f"  Downloading {len(downloads)} MP3 files...")
        success = cache_mgr.download_all_mp3s(downloads, verbose=verbose)
        if verbose:
            print(f"  Downloaded: {success}/{len(downloads)} files")

    # Step 6: Build event log
    event_log = sorted(
        context.get("race_control", []),
        key=lambda e: e.get("date", ""),
    )

    # Save metadata
    try:
        cache_mgr.save_metadata(year, race_name, session_type, {
            "year": year,
            "race": race_name,
            "session_type": session_type,
            "session_key": session_key,
            "meeting_key": meeting_key,
            "total_clips": len(clips),
        })
    except CacheError:
        pass

    session_obj = Session(
        year=year,
        race=race_name,
        session_type=session_type,
        session_key=session_key,
        meeting_key=meeting_key,
        clips=clips,
        event_log=event_log,
    )

    if verbose:
        print(f"\n✓ {session_obj}")
        print(f"  📊 {stats.summary()}")

    return session_obj


def clear_cache(
    year: Optional[int] = None,
    race: Optional[str] = None,
    session_type: Optional[str] = None,
) -> int:
    """Clear cached session data.

    Args:
        year: If all three provided, clear specific session. Otherwise clear all.
        race: Race name.
        session_type: Session type.

    Returns:
        Number of sessions cleared.
    """
    cache_mgr = CacheManager()
    return cache_mgr.clear_cache(year, race, session_type)


def cache_info() -> Dict[str, Any]:
    """Get information about cached sessions.

    Returns:
        Dict with cache directory, session list, and total size.
    """
    cache_mgr = CacheManager()
    return cache_mgr.cache_info()
