import logging

from portion import closed

from juice_scheduler.model.interval_list import DateStrInterval, IntervalList
from juice_scheduler.model.segmentation import Segmentation

logger = logging.getLogger(__name__)

class RuleStrategy:
    def __init__(self, name):
        self.rule = name
        self.feasible = False

    def apply(self, opnav, segment):

        pass

    @staticmethod
    def get_rule_strategy(rule_name, all_maneouvres, all_downlink):
        if rule_name == AdjacentToManeouvre.name:
            return AdjacentToManeouvre(all_maneouvres)
        if rule_name == Anywhere.name:
            return Anywhere(all_maneouvres, all_downlink)
        if rule_name == AnywhereDownlink.name:
            return AnywhereDownlink(all_maneouvres, all_downlink)
        raise ValueError(f"Unknown rule: {rule_name}")


class AdjacentToManeouvre(RuleStrategy):
    name = "adjacent_to_man_1h30m"
    def __init__(self, all_maneouvres, duration=3600 * 1.5):
        super().__init__(AdjacentToManeouvre.name)
        self.duration = duration
        self.all_maneouvres = all_maneouvres


    def apply(self, slot):
        return self.check_adjacent_maneouvres(slot, self.all_maneouvres, self.duration)

    def check_adjacent_maneouvres(self, slot, all_maneouvres, min_duration):
        """
        Check if a given slot is adjacent to any of the given maneuvres slots.

        Parameters
        ----------
        slot : DateStrInterval
            The slot to check.
        all_maneouvres : list of Prime
            The list of prime segments containing the maneuvres slots.
        min_duration : int
            The minimum duration of the adjacent slot.

        Returns
        -------
        adjacent : DateStrInterval or None
            The adjacent slot if found, None otherwise.
        name : str or None
            The name of the maneuvre if found, None otherwise.
        """
        for maneuvre in all_maneouvres:
            for maneuvre_slot in maneuvre.instances:
                nc = slot.intersection(maneuvre_slot)
                if nc:
                    parts = slot - maneuvre_slot
                    for part in parts:
                        feasible, adjacent = self.cut_adjacent(part, maneuvre_slot,min_duration)
                        if feasible:
                            return adjacent, maneuvre.name
        return None, None


    def cut_adjacent(self, part, maneuvre_slot, min_duration):
        """
        Check if a given part is adjacent to a given maneuvre slot and
        cut it if necessary to respect a minimum duration.

        Parameters
        ----------
        part : DateStrInterval
            The part to check
        maneuvre_slot : DateStrInterval
            The maneuvre slot to check against
        min_duration : timedelta
            The minimum duration to respect

        Returns
        -------
        bool
            True if the part is adjacent to the maneuvre slot, False otherwise
        DateStrInterval or None
            The resulting part if it is adjacent, None otherwise
        """
        duration = part.upper - part.lower
        if duration < min_duration:
            return False, None
        if part.upper <= maneuvre_slot.lower:
            return True, DateStrInterval(closed(maneuvre_slot.lower - min_duration, maneuvre_slot.lower))
        return True, DateStrInterval(closed(maneuvre_slot.upper, maneuvre_slot.upper + min_duration))



class Anywhere(RuleStrategy):
    name = "anywhere_2h"
    def __init__(self, all_maneouvres, all_downlink, duration=3600 * 2):
        super().__init__(Anywhere.name)
        self.duration = duration
        self.all_maneouvres = all_maneouvres
        self.all_downlink = all_downlink

    def apply(self, slot):
        return self.check_anywhere(slot, self.all_maneouvres, self.duration)

    def check_anywhere(self, slot, all_maneouvres, min_duration):
        maneouvres_slots = IntervalList.get_all_slots(all_maneouvres)
        downlink_slots = IntervalList.get_all_slots(self.all_downlink)
        part = slot - maneouvres_slots - downlink_slots
        if part:
            feasible, adjacent = self.cut_inside(part, min_duration)
            if feasible:
                return adjacent, None
        return None, None

    def cut_inside(self, part, min_duration):
        duration = part.upper - part.lower
        if duration < min_duration:
            return False, None
        return True, DateStrInterval(closed(part.lower, part.lower + min_duration))

class AnywhereDownlink(RuleStrategy):
    name = "anywhere_cutting_DL_1h30m"
    def __init__(self, all_maneouvres, all_downlink, duration=3600 * 1.5):
        super().__init__(AnywhereDownlink.name)
        self.duration = duration
        self.all_maneouvres = all_maneouvres
        self.all_downlink = all_downlink


    def apply(self, slot):
        maneouvres_slots = IntervalList.get_all_slots(self.all_maneouvres)
        part = slot - maneouvres_slots
        if not part:
            return None, None

        for downlink in self.all_downlink:
            for downlink_slot in downlink.instances:
                nc = slot.intersection(downlink_slot)
                if nc:
                    feasible, adjacent = self.cut_inside(part)
                    if feasible:
                        return adjacent, downlink.name
        return None, None

    def cut_inside(self, part):
        duration = part.upper - part.lower
        if duration < self.duration:
            return False, None
        return True, DateStrInterval(closed(part.lower, part.lower + self.duration))



class Rule:
    def __init__(self, segment_ids: list[str], rule_strategies: list[str]):
        self.segment_ids = segment_ids
        self.rule_strategies = rule_strategies

    @staticmethod
    def from_configuration(configuration):
        return [ Rule(item.get("segment_ids"), item.get("rule_strategies")) for item in configuration]

class RuleApplicator:
    def __init__(self, configuration, segmentation: Segmentation):
        self.rules = Rule.from_configuration(configuration)
        self.segmentation = segmentation

        self.all_opnav = self.segmentation.select_segment("OPNAV_*")
        self.all_maneouvres = self.segmentation.select_segment(".*_FD_.*")
        self.all_downlink = self.segmentation.select_segment("DL.*")

    def apply(self):

        if not self.rules:
            logger.info("No rules to apply")
            return {}

        opnav_all = set()
        for opnav in self.all_opnav:
            opnav_name = opnav.name
            for opnav_index, _ in enumerate(opnav.instances):
                opnav_id = f"{opnav_name}-{opnav_index:03d}"
                opnav_all.add(opnav_id)

        for rule in self.rules:
            all_segment = self.segmentation.select_segment(rule.segment_ids)
            solved = {}
            for segment in all_segment:
                logger.debug("Processing segment %s", segment.name)
                self.apply_to_segment(rule, segment, solved)

        non_solved = opnav_all - set(solved.keys())
        logger.debug("OPNAV non solved: %d/%d : %s", len(non_solved), len(opnav_all), non_solved)
        return solved


    def apply_to_segment(self, rule, segment, solved):
        for opnav in self.all_opnav:
            opnav_name = opnav.name
            for opnav_index, opnav_slot in enumerate(opnav.instances):
                opnav_id = f"{opnav_name}-{opnav_index:03d}"
                if opnav_id in solved:
                    continue
                candidates = opnav_slot.intersection(segment.instances)
                for slot in candidates:
                    for strategy_name in rule.rule_strategies:
                        strategy = RuleStrategy.get_rule_strategy(strategy_name, self.all_maneouvres, self.all_downlink)
                        part, name = strategy.apply(slot)
                        if part:
                            solved[opnav_id] = (name, part, strategy_name + "_" + segment.name)

