from pushtunes.models.track import Track
from pushtunes.models.push_status import PushStatus
from pushtunes.services.music_service import MusicService
from pushtunes.utils.filters import TrackFilter
from pushtunes.utils.similarity import get_best_match
from pushtunes.utils.logging import get_logger
from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class TrackResult:
    track: Track
    status: PushStatus
    message: str = ""
    found_track: Track | None = None


@dataclass
class TrackPusher:
    tracks: list[Track]
    service: MusicService
    filter: TrackFilter | None = None
    min_similarity: float = 0.8
    mappings: "MappingsManager | None" = None

    def push_tracks(self) -> list[TrackResult]:
        """Push tracks from a music source to one or more services

        Args:
            source: The music source to sync from
            track_filter: Optional TrackFilter to filter which tracks to sync

        Returns:
            Dictionary with TrackResults

        Raises:
            Exception: If authentication or cache loading fails
        """

        log = get_logger()
        log.info(f"Got {len(self.tracks)} tracks to push")

        track_results: list[TrackResult] = []
        for track in self.tracks:
            if self.filter and self.filter.matches(track):
                add_result(
                    track_results, TrackResult(track=track, status=PushStatus.filtered)
                )
                continue

            # This uses the track cache to discard perfect matches before we even send a search query
            # If authentication fails, this will raise an exception and abort the entire operation
            try:
                is_in_library = self.service.is_track_in_library(track)
            except Exception as e:
                log.error(
                    f"Failed to check library for track {track.artist} - {track.title}: {e}"
                )
                log.error(
                    "Authentication or library access failed. Aborting track push operation."
                )
                raise

            if is_in_library:
                add_result(
                    track_results,
                    TrackResult(track, status=PushStatus.already_in_library),
                )
                continue

            # Check if track has a service ID for the target service in extra_data (from CSV)
            best_match = None
            if track.extra_data:
                service_id_key = f"{self.service.service_name}_id"
                service_id = track.extra_data.get(service_id_key)
                if service_id:
                    # Create a track object with the service ID
                    best_match = Track(
                        artists=track.artists,
                        title=track.title,
                        album=track.album,
                        year=track.year,
                        service_id=service_id,
                        service_name=self.service.service_name,
                    )
                    log.info(
                        f"Using CSV service_id for {track.artist} - {track.title} -> {self.service.service_name} ID {service_id}"
                    )

            # Check if there's a mapping for this track (if no service ID from CSV)
            if not best_match and self.mappings:
                mapped_track = self.mappings.get_track_mapping(
                    track, self.service.service_name
                )
                if mapped_track:
                    # If the mapping has a service_id, use it directly
                    if mapped_track.service_id:
                        best_match = mapped_track
                        log.info(
                            f"Using mapping for {track.artist} - {track.title} -> ID {mapped_track.service_id}"
                        )
                    else:
                        # If the mapping has metadata, search for it
                        log.info(
                            f"Using mapping for {track.artist} - {track.title} -> {mapped_track.artist} - {mapped_track.title}"
                        )
                        search_results = self.service.search_tracks(mapped_track)
                        if search_results:
                            best_match, _ = get_best_match(
                                source=mapped_track,
                                candidates=search_results,
                                min_similarity=self.min_similarity,
                            )

            # If no service ID or mapping, do normal search
            if not best_match:
                search_results: list[Track] = self.service.search_tracks(track)
                if not search_results:
                    add_result(
                        track_results, TrackResult(track=track, status=PushStatus.not_found)
                    )
                    continue

                best_match, _ = get_best_match(
                    source=track,
                    candidates=search_results,
                    min_similarity=self.min_similarity,
                )

            if best_match:
                # A suitable match was found on the target service.
                # The is_track_in_library check should have already caught this,
                # but we'll double-check here just in case.
                try:
                    is_best_match_in_library = self.service.is_track_in_library(
                        best_match
                    )
                except Exception as e:
                    log.error(
                        f"Failed to check library for best match {best_match.artist} - {best_match.title}: {e}"
                    )
                    log.error(
                        "Authentication or library access failed. Aborting track push operation."
                    )
                    raise

                if is_best_match_in_library:
                    add_result(
                        track_results,
                        TrackResult(
                            track,
                            found_track=best_match,
                            status=PushStatus.already_in_library,
                        ),
                    )
                else:
                    # It's a good match and it's not in the library, so add it.
                    success = self.service.add_track(best_match)
                    if success:
                        add_result(
                            track_results,
                            TrackResult(
                                track,
                                found_track=best_match,
                                status=PushStatus.added,
                            ),
                        )
                    else:
                        add_result(
                            track_results,
                            TrackResult(
                                track,
                                found_track=best_match,
                                status=PushStatus.error,
                            ),
                        )
            else:  # No suitable match was found in the search results.
                add_result(
                    track_results,
                    TrackResult(
                        track,
                        found_track=None,
                        status=PushStatus.similarity_too_low,
                    ),
                )
        return track_results


def pretty_print_result(result: TrackResult):
    match result.status:
        case PushStatus.error:
            return f"Failed to add {result.track.artist} - {result.track.title}"
        case PushStatus.not_found:
            return f"Could not find a match for {result.track.artist} - {result.track.title}"
        case PushStatus.already_in_library:
            return f"Skipping {result.track.artist} - {result.track.title} (already in library)"
        case PushStatus.filtered:
            return f"Skipping {result.track.artist} - {result.track.title} (filtered)"
        case PushStatus.similarity_too_low:
            return f"Skipping {result.track.artist} - {result.track.title} (similarity too low)"
        case PushStatus.mapped:
            return f"Added {result.track.artist} - {result.track.title} -> Mapped to {result.found_track.artist} - {result.found_track.title}"
        case PushStatus.added:
            return f"Added {result.track.artist} - {result.track.title} -> Found {result.found_track.artist} - {result.found_track.title}"
        case PushStatus.deleted:
            return f"Deleted {result.track.artist} - {result.track.title} from target library"
        case _:
            return f"Something unknown happened while adding {result.track.artist} - {result.track.title}"


def add_result(results: list[TrackResult], result: TrackResult) -> None:
    """Add a result and send it to the logger at the same time"""
    log = get_logger(__name__)
    results.append(result)
    if result.status == PushStatus.error:
        log.error(pretty_print_result(result))
    else:
        log.info(pretty_print_result(result))
