"""Scheduling algorithm and display formatting for cinema plans."""

from __future__ import annotations

from dataclasses import dataclass
import sys
from typing import TYPE_CHECKING, TextIO

if TYPE_CHECKING:
    from python_pkg.cinema_planner._cinema_parsing import Movie

# Ads duration before movie starts (Cinema City shows ~15 min of ads)
ADS_DURATION = 15

_SEPARATOR_WIDTH = 60


@dataclass
class Screening:
    """A specific screening of a movie at a particular time."""

    movie: str
    start: int  # minutes from midnight
    end: int  # minutes from midnight

    def overlaps(self, other: Screening, buffer: int = 0) -> bool:
        """Check if this screening overlaps with another, considering buffer."""
        # Account for ADS_DURATION grace period
        return not (
            self.end + buffer <= other.start + ADS_DURATION
            or other.end + buffer <= self.start + ADS_DURATION
        )

    def start_str(self) -> str:
        """Format start time as HH:MM."""
        return f"{self.start // 60:02d}:{self.start % 60:02d}"

    def end_str(self) -> str:
        """Format end time as HH:MM."""
        return f"{self.end // 60:02d}:{self.end % 60:02d}"


def find_best_schedule(
    movies: list[Movie],
    buffer: int,
) -> list[list[Screening]]:
    """Find ALL schedules that maximize number of movies watched."""
    movie_screenings: list[list[Screening]] = [
        [
            Screening(movie.name, start, start + movie.duration)
            for start in movie.start_times
        ]
        for movie in movies
    ]

    best_count = 0
    all_best_schedules: list[list[Screening]] = []

    def _backtrack(
        movie_idx: int,
        current_schedule: list[Screening],
    ) -> None:
        nonlocal best_count, all_best_schedules

        if movie_idx == len(movie_screenings):
            if len(current_schedule) > best_count:
                best_count = len(current_schedule)
                all_best_schedules = [current_schedule.copy()]
            elif len(current_schedule) == best_count and best_count > 0:
                all_best_schedules.append(current_schedule.copy())
            return

        # Pruning: can't beat the best
        remaining = len(movie_screenings) - movie_idx
        if len(current_schedule) + remaining < best_count:
            return

        # Try each screening of current movie
        for screening in movie_screenings[movie_idx]:
            conflicts = any(screening.overlaps(s, buffer) for s in current_schedule)
            if not conflicts:
                current_schedule.append(screening)
                _backtrack(movie_idx + 1, current_schedule)
                current_schedule.pop()

        # Also try skipping this movie
        _backtrack(movie_idx + 1, current_schedule)

    _backtrack(0, [])

    # Sort each schedule by start time and return
    return [sorted(schedule, key=lambda s: s.start) for schedule in all_best_schedules]


def _format_single_schedule(
    schedule: list[Screening],
    output: TextIO,
) -> None:
    """Format a single schedule to the output stream."""
    for i, screening in enumerate(schedule, 1):
        duration = screening.end - screening.start
        hours, mins = divmod(duration, 60)
        actual_start = screening.start + ADS_DURATION
        actual_start_str = f"{actual_start // 60:02d}:{actual_start % 60:02d}"
        output.write(
            f"  {i}. {screening.start_str()} - "
            f"{screening.end_str()}  {screening.movie}\n"
        )
        output.write(
            f"     Duration: {hours}h {mins}m (movie starts ~{actual_start_str})\n"
        )
        if i < len(schedule):
            gap = schedule[i].start - screening.end
            if gap > 0:
                output.write(f"     [{gap} min break]\n")
        output.write("\n")


def _format_schedules(
    schedules: list[list[Screening]],
    all_movies: list[str],
    date: str | None = None,
    max_display: int = 5,
    *,
    output: TextIO | None = None,
) -> None:
    """Format optimal schedules to the output stream."""
    if output is None:
        output = sys.stdout

    sep = "=" * _SEPARATOR_WIDTH
    thin_sep = "\u2500" * _SEPARATOR_WIDTH

    if not schedules or not schedules[0]:
        output.write("No movies can be scheduled!\n")
        return

    num_movies = len(schedules[0])
    num_schedules = len(schedules)

    output.write(f"\n{sep}\n")
    if date:
        output.write(f"  OPTIMAL CINEMA SCHEDULES - {date}\n")
    else:
        output.write("  OPTIMAL CINEMA SCHEDULES\n")
    output.write(f"  {num_movies} movies, {num_schedules} possible combination(s)\n")
    output.write(f"{sep}\n\n")

    display_count = min(num_schedules, max_display)
    for idx, schedule in enumerate(schedules[:display_count], 1):
        if num_schedules > 1:
            output.write(f"{thin_sep}\n")
            output.write(f"  OPTION {idx}:\n")
            output.write(f"{thin_sep}\n\n")
        _format_single_schedule(schedule, output)

    if num_schedules > display_count:
        output.write(f"{thin_sep}\n")
        output.write(f"  ... and {num_schedules - display_count} more combinations\n")
        output.write("  (use -n to show more, e.g., -n 10)\n")
        output.write("\n")

    # Show skipped movies (from first schedule as reference)
    scheduled_movies = {s.movie for s in schedules[0]}
    skipped = [m for m in all_movies if m not in scheduled_movies]
    if skipped and num_schedules == 1:
        output.write(f"{thin_sep}\n")
        output.write(f"  Skipped movies ({len(skipped)}):\n")
        for movie in skipped:
            output.write(f"    - {movie}\n")
        output.write("\n")


def _format_all_movies(
    movies: list[Movie],
    date: str | None = None,
    *,
    output: TextIO | None = None,
) -> None:
    """Format all parsed movies to the output stream."""
    if output is None:
        output = sys.stdout

    thin_sep = "\u2500" * _SEPARATOR_WIDTH

    output.write(f"\n{thin_sep}\n")
    if date:
        output.write(f"  Parsed {len(movies)} movies for {date}:\n")
    else:
        output.write(f"  Parsed {len(movies)} movies:\n")
    output.write(f"{thin_sep}\n")
    for movie in movies:
        times_str = ", ".join(
            f"{t // 60:02d}:{t % 60:02d}" for t in sorted(movie.start_times)
        )
        genre_str = f" [{', '.join(movie.genres)}]" if movie.genres else ""
        output.write(f"  {movie.name} ({movie.duration} min){genre_str}\n")
        output.write(f"    Times: {times_str}\n")
    output.write("\n")
