import re

from loguru import logger
from sqlalchemy import desc

from flexget import plugin
from flexget.entry import Entry
from flexget.event import event
from flexget.manager import Session

from . import db

logger = logger.bind(name='next_series_episodes')


class NextSeriesEpisodes:
    """
    Emit next episode number from all series configured in this task.

    Supports only 'ep' and 'sequence' mode series.
    """

    schema = {
        'oneOf': [
            {'type': 'boolean'},
            {
                'type': 'object',
                'properties': {
                    'from_start': {'type': 'boolean', 'default': False},
                    'backfill': {'type': 'boolean', 'default': False},
                    'only_same_season': {'type': 'boolean', 'default': False},
                },
                'additionalProperties': False,
            },
        ]
    }

    def __init__(self):
        self.rerun_entries = []

    def ep_identifiers(self, season, episode):
        return ['S%02dE%02d' % (season, episode), '%dx%02d' % (season, episode)]

    def sequence_identifiers(self, episode):
        # Use a set to remove doubles, which will happen depending on number of digits in episode
        return {'%d' % episode, '%02d' % episode, '%03d' % episode}

    def search_entry(self, series, season, episode, task, rerun=True):
        # Extract the alternate names for the series
        alts = [alt.alt_name for alt in series.alternate_names]
        # Also consider series name without parenthetical (year, country) an alternate name
        paren_match = re.match(r'(.+?)( \(.+\))?$', series.name)
        if paren_match.group(2):
            alts.append(paren_match.group(1))
        if series.identified_by == 'ep':
            search_strings = [f'{series.name} {id}' for id in self.ep_identifiers(season, episode)]
            series_id = 'S%02dE%02d' % (season, episode)
            for alt in alts:
                search_strings.extend(
                    [f'{alt} {id}' for id in self.ep_identifiers(season, episode)]
                )
        else:
            search_strings = [f'{series.name} {id}' for id in self.sequence_identifiers(episode)]
            series_id = episode
            for alt in alts:
                search_strings.extend([f'{alt} {id}' for id in self.sequence_identifiers(episode)])
        entry = Entry(
            title=search_strings[0],
            url='',
            search_strings=search_strings,
            series_name=series.name,
            series_alternate_names=alts,  # Not sure if this field is useful down the road.
            series_season=season,
            series_episode=episode,
            season_pack_lookup=False,
            series_id=series_id,
            series_id_type=series.identified_by,
        )
        if rerun:
            entry.on_complete(
                self.on_search_complete, task=task, identified_by=series.identified_by
            )
        return entry

    def on_task_input(self, task, config):
        if not config:
            return
        if isinstance(config, bool):
            config = {}
        self.config = config
        if task.is_rerun:
            # Just return calculated next eps on reruns
            entries = self.rerun_entries
            self.rerun_entries = []
            return entries
        else:
            self.rerun_entries = []

        entries = []
        impossible = {}
        with Session() as session:
            for seriestask in (
                session.query(db.SeriesTask).filter(db.SeriesTask.name == task.name).all()
            ):
                series = seriestask.series
                logger.trace('evaluating {}', series.name)
                if not series:
                    # TODO: How can this happen?
                    logger.debug('Found SeriesTask item without series specified. Cleaning up.')
                    session.delete(seriestask)
                    continue

                if series.identified_by not in ['ep', 'sequence']:
                    logger.trace('unsupported identified_by scheme')
                    reason = series.identified_by or 'auto'
                    impossible.setdefault(reason, []).append(series.name)
                    continue

                low_season = 0 if series.identified_by == 'ep' else -1
                # Don't look for seasons older than begin ep
                if series.begin and series.begin.season and series.begin.season > 1:
                    # begin-1 or the range() loop will never get to the begin season
                    low_season = max(series.begin.season - 1, 0)

                new_season = None
                check_downloaded = not config.get('backfill')
                latest_season = db.get_latest_release(series, downloaded=check_downloaded)
                if latest_season:
                    if latest_season.season <= low_season:
                        latest_season = new_season = low_season + 1
                    elif latest_season.season in series.completed_seasons:
                        latest_season = new_season = latest_season.season + 1
                    else:
                        latest_season = latest_season.season
                else:
                    latest_season = low_season + 1

                for season in range(latest_season, low_season, -1):
                    if season in series.completed_seasons:
                        logger.debug('season {} is marked as completed, skipping', season)
                        continue
                    logger.trace(
                        'Evaluating episodes for series {}, season {}', series.name, season
                    )
                    latest = db.get_latest_release(
                        series, season=season, downloaded=check_downloaded
                    )
                    if (
                        series.begin
                        and season == series.begin.season
                        and (not latest or latest < series.begin)
                    ):
                        # In case series.begin season is already completed, look in next available season
                        lookup_season = series.begin.season
                        ep_number = series.begin.number
                        while lookup_season in series.completed_seasons:
                            lookup_season += 1
                            # If season number was bumped, start looking for ep 1
                            ep_number = 1
                        entries.append(self.search_entry(series, lookup_season, ep_number, task))
                    elif latest and not config.get('backfill'):
                        entries.append(
                            self.search_entry(series, latest.season, latest.number + 1, task)
                        )
                    elif latest:
                        start_at_ep = 1
                        episodes_this_season = (
                            session.query(db.Episode)
                            .filter(db.Episode.series_id == series.id)
                            .filter(db.Episode.season == season)
                        )
                        if series.identified_by == 'sequence':
                            # Don't look for missing too far back with sequence shows
                            start_at_ep = max(latest.number - 10, 1)
                            episodes_this_season = episodes_this_season.filter(
                                db.Episode.number >= start_at_ep
                            )
                        latest_ep_this_season = episodes_this_season.order_by(
                            desc(db.Episode.number)
                        ).first()
                        if latest_ep_this_season:
                            downloaded_this_season = (
                                episodes_this_season.join(db.Episode.releases)
                                .filter(db.EpisodeRelease.downloaded == True)
                                .all()
                            )
                            # Calculate the episodes we still need to get from this season
                            if series.begin and series.begin.season == season:
                                start_at_ep = max(start_at_ep, series.begin.number)
                            eps_to_get = list(range(start_at_ep, latest_ep_this_season.number + 1))
                            for ep in downloaded_this_season:
                                try:
                                    eps_to_get.remove(ep.number)
                                except ValueError:
                                    pass
                            entries.extend(
                                self.search_entry(series, season, x, task, rerun=False)
                                for x in eps_to_get
                            )
                            # If we have already downloaded the latest known episode, try the next episode
                            if latest_ep_this_season.releases:
                                entries.append(
                                    self.search_entry(
                                        series, season, latest_ep_this_season.number + 1, task
                                    )
                                )
                        else:
                            # No episode means that latest is a season pack, emit episode 1
                            entries.append(self.search_entry(series, season, 1, task))
                    # First iteration of a new season with no show begin and show has downloads
                    elif new_season and season == new_season:
                        entries.append(self.search_entry(series, season, 1, task))
                    else:
                        if config.get('from_start') or config.get('backfill'):
                            entries.append(self.search_entry(series, season, 1, task))
                        else:
                            logger.verbose(
                                'Series `{}` has no history. Set begin option, or use CLI `series begin` '
                                'subcommand to set first episode to emit',
                                series.name,
                            )
                            break
                    # Skip older seasons if we are not in backfill mode
                    if not config.get('backfill'):
                        logger.debug('backfill is not enabled; skipping older seasons')
                        break

        for reason, series in impossible.items():
            logger.verbose(
                'Series `{}` with identified_by value `{}` are not supported. ',
                ', '.join(sorted(series)),
                reason,
            )

        return entries

    def on_search_complete(self, entry, task=None, identified_by=None, **kwargs):
        """Decides whether we should look for next ep/season based on whether we found/accepted any episodes."""
        with Session() as session:
            series = (
                session.query(db.Series).filter(db.Series.name == entry['series_name']).first()
            )
            latest = db.get_latest_release(series)
            db_release = (
                session.query(db.EpisodeRelease)
                .join(db.EpisodeRelease.episode)
                .join(db.Episode.series)
                .filter(db.Series.name == entry['series_name'])
                .filter(db.Episode.season == entry['series_season'])
                .filter(db.Episode.number == entry['series_episode'])
                .first()
            )

            if entry.accepted:
                logger.debug(
                    '{} {} was accepted, rerunning to look for next ep.',
                    entry['series_name'],
                    entry['series_id'],
                )
                self.rerun_entries.append(
                    self.search_entry(
                        series, entry['series_season'], entry['series_episode'] + 1, task
                    )
                )
                # Increase rerun limit by one if we have matches, this way
                # we keep searching as long as matches are found!
                # TODO: this should ideally be in discover so it would be more generic
                task.max_reruns += 1
                task.rerun(plugin='next_series_episodes', reason='Look for next episode')
            elif db_release:
                # There are know releases of this episode, but none were accepted
                return
            elif latest:
                if latest.is_season:
                    # A season pack was picked up in the task, no need to look for more episodes
                    return
                elif (
                    not self.config.get('only_same_season')
                    and identified_by == 'ep'
                    and (
                        entry['series_season'] == latest.season
                        and entry['series_episode'] == latest.number + 1
                    )
                ):
                    # We searched for next predicted episode of this season unsuccessfully, try the next season
                    self.rerun_entries.append(
                        self.search_entry(series, latest.season + 1, 1, task)
                    )
                    logger.debug(
                        '{} {} not found, rerunning to look for next season',
                        entry['series_name'],
                        entry['series_id'],
                    )
                    task.rerun(plugin='next_series_episodes', reason='Look for next season')


@event('plugin.register')
def register_plugin():
    plugin.register(NextSeriesEpisodes, 'next_series_episodes', api_ver=2)
