import click

from ffx.model.shifted_season import ShiftedSeason


class EpisodeOrderException(Exception):
    pass


class RangeOverlapException(Exception):
    pass


class ShiftedSeasonOwnerException(Exception):
    pass


class ShiftedSeasonController:

    def __init__(self, context):

        self.context = context
        self.Session = self.context['database']['session']  # convenience

    def _resolve_owner(self, showId=None, patternId=None):
        hasShow = showId is not None
        hasPattern = patternId is not None

        if hasShow == hasPattern:
            raise ShiftedSeasonOwnerException(
                "ShiftedSeason rules require exactly one owner: either showId or patternId."
            )

        if hasShow:
            if type(showId) is not int:
                raise ValueError(
                    "ShiftedSeasonController: Argument showId is required to be of type int"
                )
            return {
                'show_id': int(showId),
                'pattern_id': None,
                'label': f"show #{int(showId)}",
            }

        if type(patternId) is not int:
            raise ValueError(
                "ShiftedSeasonController: Argument patternId is required to be of type int"
            )
        return {
            'show_id': None,
            'pattern_id': int(patternId),
            'label': f"pattern #{int(patternId)}",
        }

    def _apply_owner_filter(self, query, owner):
        if owner['pattern_id'] is not None:
            return query.filter(ShiftedSeason.pattern_id == owner['pattern_id'])
        return query.filter(ShiftedSeason.show_id == owner['show_id'])

    def _normalize_shifted_season_fields(self, shiftedSeasonObj: dict):
        if type(shiftedSeasonObj) is not dict:
            raise ValueError(
                "ShiftedSeasonController: Argument shiftedSeasonObj is required to be of type dict"
            )

        fields = {
            'original_season': int(shiftedSeasonObj['original_season']),
            'first_episode': int(shiftedSeasonObj['first_episode']),
            'last_episode': int(shiftedSeasonObj['last_episode']),
            'season_offset': int(shiftedSeasonObj['season_offset']),
            'episode_offset': int(shiftedSeasonObj['episode_offset']),
        }

        firstEpisode = fields['first_episode']
        lastEpisode = fields['last_episode']
        if firstEpisode != -1 and lastEpisode != -1 and lastEpisode < firstEpisode:
            raise EpisodeOrderException(
                "ShiftedSeason last_episode must be greater than or equal to first_episode."
            )

        return fields

    def _ranges_overlap(self, firstEpisodeA, lastEpisodeA, firstEpisodeB, lastEpisodeB):
        startA = float('-inf') if int(firstEpisodeA) == -1 else int(firstEpisodeA)
        endA = float('inf') if int(lastEpisodeA) == -1 else int(lastEpisodeA)
        startB = float('-inf') if int(firstEpisodeB) == -1 else int(firstEpisodeB)
        endB = float('inf') if int(lastEpisodeB) == -1 else int(lastEpisodeB)
        return startA <= endB and startB <= endA

    def _ordered_query(self, session, owner):
        q = self._apply_owner_filter(session.query(ShiftedSeason), owner)
        return q.order_by(
            ShiftedSeason.original_season.asc(),
            ShiftedSeason.first_episode.asc(),
            ShiftedSeason.last_episode.asc(),
            ShiftedSeason.id.asc(),
        )

    def _find_matching_rule(self, session, owner, season: int, episode: int):
        for shiftedSeasonEntry in self._ordered_query(session, owner).all():
            if (
                season == shiftedSeasonEntry.getOriginalSeason()
                and (
                    shiftedSeasonEntry.getFirstEpisode() == -1
                    or episode >= shiftedSeasonEntry.getFirstEpisode()
                )
                and (
                    shiftedSeasonEntry.getLastEpisode() == -1
                    or episode <= shiftedSeasonEntry.getLastEpisode()
                )
            ):
                return shiftedSeasonEntry
        return None

    def checkShiftedSeason(
        self,
        showId: int | None = None,
        shiftedSeasonObj: dict | None = None,
        shiftedSeasonId: int = 0,
        patternId: int | None = None,
    ):
        """
        Check whether a shifted-season rule is valid within one owner scope.
        """

        session = None
        try:
            owner = self._resolve_owner(showId=showId, patternId=patternId)
            fields = self._normalize_shifted_season_fields(shiftedSeasonObj)
            session = self.Session()

            q = self._ordered_query(session, owner)
            if shiftedSeasonId:
                q = q.filter(ShiftedSeason.id != int(shiftedSeasonId))

            for siblingShiftedSeason in q.all():
                if fields['original_season'] != siblingShiftedSeason.getOriginalSeason():
                    continue

                if self._ranges_overlap(
                    fields['first_episode'],
                    fields['last_episode'],
                    siblingShiftedSeason.getFirstEpisode(),
                    siblingShiftedSeason.getLastEpisode(),
                ):
                    return False

            return True

        except (EpisodeOrderException, ShiftedSeasonOwnerException) as ex:
            raise click.ClickException(str(ex))
        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.checkShiftedSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def addShiftedSeason(
        self,
        showId: int | None = None,
        shiftedSeasonObj: dict | None = None,
        patternId: int | None = None,
    ):

        session = None
        try:
            owner = self._resolve_owner(showId=showId, patternId=patternId)
            fields = self._normalize_shifted_season_fields(shiftedSeasonObj)

            if not self.checkShiftedSeason(
                showId=owner['show_id'],
                patternId=owner['pattern_id'],
                shiftedSeasonObj=fields,
            ):
                raise RangeOverlapException(
                    f"ShiftedSeason rule overlaps with an existing rule for {owner['label']}."
                )

            session = self.Session()
            shiftedSeason = ShiftedSeason(
                show_id=owner['show_id'],
                pattern_id=owner['pattern_id'],
                original_season=fields['original_season'],
                first_episode=fields['first_episode'],
                last_episode=fields['last_episode'],
                season_offset=fields['season_offset'],
                episode_offset=fields['episode_offset'],
            )
            session.add(shiftedSeason)
            session.commit()
            return shiftedSeason.getId()

        except (EpisodeOrderException, RangeOverlapException, ShiftedSeasonOwnerException) as ex:
            raise click.ClickException(str(ex))
        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.addShiftedSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def updateShiftedSeason(self, shiftedSeasonId: int, shiftedSeasonObj: dict):

        if type(shiftedSeasonId) is not int:
            raise ValueError(
                "ShiftedSeasonController.updateShiftedSeason(): Argument shiftedSeasonId is required to be of type int"
            )

        session = None
        try:
            fields = self._normalize_shifted_season_fields(shiftedSeasonObj)
            session = self.Session()

            shiftedSeason = (
                session.query(ShiftedSeason)
                .filter(ShiftedSeason.id == int(shiftedSeasonId))
                .first()
            )

            if shiftedSeason is None:
                return False

            owner = self._resolve_owner(
                showId=shiftedSeason.getShowId(),
                patternId=shiftedSeason.getPatternId(),
            )
            if not self.checkShiftedSeason(
                showId=owner['show_id'],
                patternId=owner['pattern_id'],
                shiftedSeasonObj=fields,
                shiftedSeasonId=shiftedSeasonId,
            ):
                raise RangeOverlapException(
                    f"ShiftedSeason rule overlaps with an existing rule for {owner['label']}."
                )

            shiftedSeason.original_season = fields['original_season']
            shiftedSeason.first_episode = fields['first_episode']
            shiftedSeason.last_episode = fields['last_episode']
            shiftedSeason.season_offset = fields['season_offset']
            shiftedSeason.episode_offset = fields['episode_offset']

            session.commit()
            return True

        except (EpisodeOrderException, RangeOverlapException, ShiftedSeasonOwnerException) as ex:
            raise click.ClickException(str(ex))
        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.updateShiftedSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def findShiftedSeason(
        self,
        showId: int | None = None,
        originalSeason: int | None = None,
        firstEpisode: int | None = None,
        lastEpisode: int | None = None,
        patternId: int | None = None,
    ):

        if type(originalSeason) is not int:
            raise ValueError(
                "ShiftedSeasonController.findShiftedSeason(): Argument originalSeason is required to be of type int"
            )

        if type(firstEpisode) is not int:
            raise ValueError(
                "ShiftedSeasonController.findShiftedSeason(): Argument firstEpisode is required to be of type int"
            )

        if type(lastEpisode) is not int:
            raise ValueError(
                "ShiftedSeasonController.findShiftedSeason(): Argument lastEpisode is required to be of type int"
            )

        session = None
        try:
            owner = self._resolve_owner(showId=showId, patternId=patternId)
            session = self.Session()
            shiftedSeason = (
                self._apply_owner_filter(session.query(ShiftedSeason), owner)
                .filter(
                    ShiftedSeason.original_season == int(originalSeason),
                    ShiftedSeason.first_episode == int(firstEpisode),
                    ShiftedSeason.last_episode == int(lastEpisode),
                )
                .first()
            )

            return shiftedSeason.getId() if shiftedSeason is not None else None

        except ShiftedSeasonOwnerException as ex:
            raise click.ClickException(str(ex))
        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.findShiftedSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def getShiftedSeasonSiblings(
        self,
        showId: int | None = None,
        patternId: int | None = None,
    ):
        session = None

        try:
            owner = self._resolve_owner(showId=showId, patternId=patternId)
            session = self.Session()
            return self._ordered_query(session, owner).all()

        except ShiftedSeasonOwnerException as ex:
            raise click.ClickException(str(ex))
        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.getShiftedSeasonSiblings(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def getShiftedSeason(self, shiftedSeasonId: int):

        if type(shiftedSeasonId) is not int:
            raise ValueError(
                "ShiftedSeasonController.getShiftedSeason(): Argument shiftedSeasonId is required to be of type int"
            )

        session = None
        try:
            session = self.Session()
            return (
                session.query(ShiftedSeason)
                .filter(ShiftedSeason.id == int(shiftedSeasonId))
                .first()
            )

        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.getShiftedSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def deleteShiftedSeason(self, shiftedSeasonId):

        if type(shiftedSeasonId) is not int:
            raise ValueError(
                "ShiftedSeasonController.deleteShiftedSeason(): Argument shiftedSeasonId is required to be of type int"
            )

        session = None
        try:
            session = self.Session()
            shiftedSeason = (
                session.query(ShiftedSeason)
                .filter(ShiftedSeason.id == int(shiftedSeasonId))
                .first()
            )

            if shiftedSeason is not None:
                session.delete(shiftedSeason)
                session.commit()
                return True
            return False

        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.deleteShiftedSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()

    def shiftSeason(self, showId, season, episode, patternId=None):

        if season == -1 or episode == -1:
            return season, episode

        session = None
        try:
            session = self.Session()
            activeShift = None

            if patternId is not None:
                activeShift = self._find_matching_rule(
                    session,
                    self._resolve_owner(patternId=patternId),
                    season=int(season),
                    episode=int(episode),
                )

            if activeShift is None and showId is not None and showId != -1:
                activeShift = self._find_matching_rule(
                    session,
                    self._resolve_owner(showId=showId),
                    season=int(season),
                    episode=int(episode),
                )

            if activeShift is None:
                shiftedSeason = season
                shiftedEpisode = episode
                sourceLabel = "default"
            else:
                shiftedSeason = season + activeShift.getSeasonOffset()
                shiftedEpisode = episode + activeShift.getEpisodeOffset()
                sourceLabel = (
                    "pattern"
                    if activeShift.getPatternId() is not None
                    else "show"
                )

            self.context['logger'].info(
                f"Setting season shift {season}/{episode} -> {shiftedSeason}/{shiftedEpisode} from {sourceLabel}"
            )

            return shiftedSeason, shiftedEpisode

        except ShiftedSeasonOwnerException as ex:
            raise click.ClickException(str(ex))
        except Exception as ex:
            raise click.ClickException(
                f"ShiftedSeasonController.shiftSeason(): {repr(ex)}"
            )
        finally:
            if session is not None:
                session.close()
