from __future__ import annotations

from dataclasses import dataclass, field
import datetime
import operator
from typing import Iterable, Iterator, List
from typing import Optional, Sequence, Union

from anchorpoint.textselectors import TextQuoteSelector

from nettlesome.terms import Comparable, ContextRegister, Explanation
from nettlesome.factors import Factor
from authorityspoke.holdings import Holding, HoldingGroup
from authorityspoke.opinions import Opinion, TextLinkDict
from authorityspoke.rules import Rule


@dataclass
class CAPCitation:
    cite: str
    reporter: Optional[str] = None
    case_ids: List[int] = field(default_factory=list)


class Decision(Comparable):
    r"""
    A court decision to resolve a step in litigation.

    Uses the model of a judicial decision from
    the `Caselaw Access Project API <https://api.case.law/v1/cases/>`_.
    One of these records may contain multiple :class:`.Opinion`\s.

    Typically one record will contain all
    the :class:`~authorityspoke.opinions.Opinion`\s
    from one appeal, but not necessarily from the whole lawsuit. One
    lawsuit may contain multiple appeals or other petitions, and
    if more then one of those generates published Opinions,
    the CAP API will divide those Opinions into a separate
    record for each appeal.

    The outcome of a decision may be determined by one majority
    :class:`~authorityspoke.opinions.Opinion` or by the combined
    effect of multiple Opinions.
    The lead opinion is commonly, but not always, the only
    Opinion that creates binding legal authority.
    Usually every :class:`~authorityspoke.rules.Rule` posited by the lead Opinion is
    binding, but some may not be, often because parts of the
    Opinion fail to command a majority of the panel
    of judges.


    :param name:
        full name of the opinion, e.g. "ORACLE AMERICA, INC.,
        Plaintiff-Appellant, v. GOOGLE INC., Defendant-Cross-Appellant"
    :param name_abbreviation:
        shorter name of the opinion, e.g. "Oracle America, Inc. v. Google Inc."
    :param citations:
        citations to the opinion, usually in the format
        ``[Volume Number] [Reporter Name Abbreviation] [Page Number]``
    :param first_page:
        the page where the opinion begins in its official reporter
    :param last_page:
        the page where the opinion ends in its official reporter
    :param decision_date:
        date when the opinion was first published by the court
        (not the publication date of the reporter volume)
    :param court:
        name of the court that published the opinion
    :param _id:
        unique ID from CAP API
    """

    def __init__(
        self,
        date: datetime.date,
        name: Optional[str] = None,
        name_abbreviation: Optional[str] = None,
        citations: Optional[Sequence[CAPCitation]] = None,
        first_page: Optional[int] = None,
        last_page: Optional[int] = None,
        court: Optional[str] = None,
        opinions: Optional[Union[Opinion, Sequence[Opinion]]] = None,
        jurisdiction: Optional[str] = None,
        cites_to: Optional[Sequence[CAPCitation]] = None,
        id: Optional[int] = None,
    ) -> None:
        self.date = date
        self.name = name
        self.name_abbreviation = name_abbreviation
        self.citations = citations
        self.first_page = first_page
        self.last_page = last_page
        self.court = court
        if isinstance(opinions, Opinion):
            self.opinions = [opinions]
        elif not opinions:
            self.opinions = []
        else:
            self.opinions = list(opinions)
        self.jurisdiction = jurisdiction
        self.cites_to = cites_to
        self._id = id

    def __str__(self):
        citation = self.citations[0].cite if self.citations else ""
        name = self.name_abbreviation or self.name
        return f"{name}, {citation} ({self.date})"

    @property
    def holdings(self) -> HoldingGroup:
        if self.majority is None:
            return HoldingGroup()
        return HoldingGroup(self.majority.holdings)

    def contradicts(self, other):
        if isinstance(other, Decision):
            if self.majority and other.majority:
                return self.majority.contradicts(other.majority)
            return False
        return self.majority.contradicts(other)

    def explain_contradiction(
        self, other: Union[Opinion, Holding, Rule]
    ) -> Optional[Explanation]:
        explanations = self.explanations_contradiction(other)
        try:
            explanation = next(explanations)
        except StopIteration:
            return None
        return explanation

    def explanations_contradiction(
        self,
        other: Union[Decision, Opinion, Holding, Rule],
    ) -> Iterator[Explanation]:
        if isinstance(other, Decision):
            if self.majority and other.majority:
                yield from self.majority.explanations_contradiction(other.majority)
        elif isinstance(other, (Rule, Holding, Opinion)):
            if self.majority:
                yield from self.majority.explanations_contradiction(other)
        else:
            raise TypeError(
                f"'Contradicts' test not implemented for types "
                f"{self.__class__} and {other.__class__}."
            )

    def explain_implication(
        self,
        other: Union[Opinion, Holding, Rule],
    ) -> Optional[Explanation]:
        explanations = self.explanations_implication(other)
        try:
            explanation = next(explanations)
        except StopIteration:
            return None
        return explanation

    def explanations_implication(
        self, other: Union[Decision, Opinion, Holding, Rule]
    ) -> Iterator[Explanation]:
        if isinstance(other, Decision):
            if self.majority and other.majority:
                yield from self.majority.explanations_implication(other.majority)
        elif isinstance(other, (Rule, Holding, Opinion)):
            if self.majority:
                yield from self.majority.explanations_implication(other)
        else:
            raise TypeError(
                f"'Implication' test not implemented for types "
                f"{self.__class__} and {other.__class__}."
            )

    def posit(
        self,
        holdings: Union[Holding, Iterable[Union[Holding, Rule]]],
        holding_anchors: Optional[
            List[Union[TextQuoteSelector, List[TextQuoteSelector]]]
        ] = None,
        named_anchors: Optional[TextLinkDict] = None,
        context: Optional[Sequence[Factor]] = None,
    ) -> None:
        """
        Add one or more Holdings to the majority Opinion of this Decision.
        """
        if self.majority is None:
            raise AttributeError(
                "Cannot posit Holding because this Decision has no known majority Opinion."
                " Try having an Opinion posit the Holding directly."
            )
        self.majority.posit(
            holdings=holdings,
            holding_anchors=holding_anchors,
            named_anchors=named_anchors,
            context=context,
        )

    def __ge__(self, other) -> bool:
        return self.implies(other)

    def __gt__(self, other) -> bool:
        return self.implies(other) and not self == other

    def implied_by_holding(
        self, other: Holding, context: Optional[ContextRegister] = None
    ) -> Iterator[Explanation]:
        if all(
            other.implies(self_holding, context=context)
            for self_holding in self.holdings
        ):
            yield Explanation(
                reasons=[(other, self_holding) for self_holding in self.holdings],
                operation=operator.ge,
            )

    def explanations_implied_by(
        self, other: Comparable, context: Optional[ContextRegister] = None
    ) -> Iterator[Explanation]:
        context = context or ContextRegister()
        if isinstance(other, Opinion):
            other = other.holdings
        if isinstance(other, HoldingGroup):
            yield from other.explanations_implication(
                self.holdings, context=context.reversed()
            )
        if isinstance(other, Rule):
            other = Holding(rule=other)
        if isinstance(other, Holding):
            yield from self.implied_by_holding(other, context)

    def implied_by(
        self, other: Optional[Comparable], context: Optional[ContextRegister] = None
    ) -> bool:
        if other is None:
            return False
        return any(self.explanations_implied_by(other, context))

    def implies_holding(
        self, other: Holding, context: Optional[ContextRegister] = None
    ) -> bool:
        return any(
            self_holding.implies(other, context=context)
            for self_holding in self.holdings
        )

    def implies_rule(
        self, other: Rule, context: Optional[ContextRegister] = None
    ) -> bool:
        return self.implies_holding(Holding(other), context=context)

    def implies(self, other, context: Optional[ContextRegister] = None) -> bool:
        if isinstance(other, (Decision, Opinion)):
            return self.holdings.implies(other.holdings)
        elif isinstance(other, Holding):
            return self.implies_holding(other, context=context)
        elif isinstance(other, Rule):
            return self.implies_rule(other, context=context)
        return False

    @property
    def majority(self) -> Optional[Opinion]:
        for opinion in self.opinions:
            if opinion.position == "majority":
                return opinion
        return None
