from __future__ import unicode_literals, division, absolute_import
import logging
import re

from sqlalchemy import desc

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

log = logging.getLogger('emit_series')

try:
    from flexget.plugins.filter.series import SeriesTask, Series, Episode, Release, get_latest_release, AlternateNames
except ImportError as e:
    log.error(e.message)
    raise plugin.DependencyError(issued_by='emit_series', missing='series')


class EmitSeries(object):
    """
    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}
                },
                'additionalProperties': False
            }
        ]
    }

    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 set(['%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 = ['%s %s' % (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(['%s %s' % (alt, id) for id in self.ep_identifiers(season, episode)])
        else:
            search_strings = ['%s %s' % (series.name, id) for id in self.sequence_identifiers(episode)]
            series_id = episode
            for alt in alts:
                search_strings.extend(['%s %s' % (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,
                      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 = {}

        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 = []
        with Session() as session:
            for seriestask in session.query(SeriesTask).filter(SeriesTask.name == task.name).all():
                series = seriestask.series
                if not series:
                    # TODO: How can this happen?
                    log.debug('Found SeriesTask item without series specified. Cleaning up.')
                    session.delete(seriestask)
                    continue

                if series.identified_by not in ['ep', 'sequence']:
                    log.verbose('Can only emit ep or sequence based series. '
                                '`%s` is identified_by %s' %
                                (series.name, series.identified_by or 'auto'))
                    continue

                low_season = 0 if series.identified_by == 'ep' else -1

                check_downloaded = not config.get('backfill')
                latest_season = get_latest_release(series, downloaded=check_downloaded)
                if latest_season:
                    latest_season = latest_season.season
                else:
                    latest_season = low_season + 1

                for season in xrange(latest_season, low_season, -1):
                    log.debug('Adding episodes for season %d' % season)
                    latest = get_latest_release(series, season=season, downloaded=check_downloaded)
                    if series.begin and (not latest or latest < series.begin):
                        entries.append(self.search_entry(series, series.begin.season, series.begin.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(Episode).
                                                filter(Episode.series_id == series.id).
                                                filter(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(Episode.number >= start_at_ep)
                        latest_ep_this_season = episodes_this_season.order_by(desc(Episode.number)).first()
                        downloaded_this_season = (episodes_this_season.join(Episode.releases).
                                                filter(Release.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 = 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:
                        if config.get('from_start') or config.get('backfill'):
                            entries.append(self.search_entry(series, season, 1, task))
                        else:
                            log.verbose('Series `%s` 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'):
                        break
                    # Don't look for seasons older than begin ep
                    if series.begin and series.begin.season >= season:
                        break

        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(Series).filter(Series.name == entry['series_name']).first()
            latest = get_latest_release(series)
            db_release = (session.query(Release).join(Release.episode).join(Episode.series).
                       filter(Series.name == entry['series_name']).
                       filter(Episode.season == entry['series_season']).
                       filter(Episode.number == entry['series_episode']).first())

            if entry.accepted:
                log.debug('%s %s 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))
                task.rerun()
            elif db_release:
                # There are know releases of this episode, but none were accepted
                return
            elif latest 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))
                log.debug('%s %s not found, rerunning to look for next season' %
                          (entry['series_name'], entry['series_id']))
                task.rerun()


@event('plugin.register')
def register_plugin():
    plugin.register(EmitSeries, 'emit_series', api_ver=2)
