#! /usr/bin/env python3
"""CoLRev status operation: Display the project status."""
from __future__ import annotations

import io
import typing
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

import yaml

import colrev.env.utils
import colrev.operation
import colrev.record
from colrev.constants import Colors
from colrev.constants import Fields
from colrev.constants import FieldValues


class Status(colrev.operation.Operation):
    """Determine the status of the project"""

    def __init__(self, *, review_manager: colrev.review_manager.ReviewManager) -> None:
        super().__init__(
            review_manager=review_manager,
            operations_type=colrev.operation.OperationsType.check,
        )

    def get_analytics(self) -> dict:
        """Get status analytics"""

        analytics_dict = {}
        git_repo = self.review_manager.dataset.get_repo()

        revlist = list(
            (
                commit.hexsha,
                commit.author.name,
                commit.committed_date,
                (commit.tree / "status.yaml").data_stream.read(),
            )
            for commit in git_repo.iter_commits(paths="status.yaml")
        )
        for ind, (commit_id, commit_author, committed_date, filecontents) in enumerate(
            revlist
        ):
            try:
                var_t = io.StringIO(filecontents.decode("utf-8"))

                # TBD: we could simply include the whole status.yaml
                # (to create a general-purpose status analyzer)
                # -> flatten nested structures (e.g., overall/currently)
                # -> integrate with get_status (current data) -
                # and get_prior? (levels: aggregated_statistics vs. record-level?)

                data_loaded = yaml.safe_load(var_t)
                analytics_dict[len(revlist) - ind] = {
                    "atomic_steps": data_loaded["atomic_steps"],
                    "completed_atomic_steps": data_loaded["completed_atomic_steps"],
                    "commit_id": commit_id,
                    "commit_author": commit_author,
                    "committed_date": committed_date,
                    "search": data_loaded["overall"]["md_retrieved"],
                    "included": data_loaded["overall"]["rev_included"],
                }
            except (IndexError, KeyError):
                pass

        # keys = list(analytics_dict.values())[0].keys()
        # with open("analytics.csv", "w", newline="", encoding="utf8") as output_file:
        #     dict_writer = csv.DictWriter(output_file, keys)
        #     dict_writer.writeheader()
        #     dict_writer.writerows(reversed(analytics_dict.values()))

        return analytics_dict

    def get_review_status_report(
        self, *, records: Optional[dict] = None, colors: bool = True
    ) -> str:
        """Get the review status report"""

        status_stats = self.review_manager.get_status_stats(records=records)

        template = colrev.env.utils.get_template(
            template_path="template/ops/status.txt"
        )

        if colors:
            content = template.render(status_stats=status_stats, colors=Colors)
        else:
            content = template.render(status_stats=status_stats, colors=None)

        return content


@dataclass
class StatusStats:
    """Data class for status statistics"""

    # pylint: disable=too-many-instance-attributes
    atomic_steps: int
    nr_curated_records: int
    currently: StatusStatsCurrently
    overall: StatusStatsOverall
    completed_atomic_steps: int
    completeness_condition: bool

    def __init__(
        self,
        *,
        review_manager: colrev.review_manager.ReviewManager,
        records: Optional[dict] = None,
    ) -> None:
        self.review_manager = review_manager
        colrev.operation.CheckOperation(review_manager=review_manager)

        if records:
            self.records = records
        else:
            self.records = self.review_manager.dataset.load_records_dict()

        self.status_list = [x[Fields.STATUS] for x in self.records.values()]
        self.screening_criteria = [
            x[Fields.SCREENING_CRITERIA]
            for x in self.records.values()
            if x.get(Fields.SCREENING_CRITERIA, "") not in ["", "NA"]
        ]

        self.md_duplicates_removed = 0
        for item in self.records.values():
            self.md_duplicates_removed += (
                len([o for o in item[Fields.ORIGIN] if not o.startswith("md_")]) - 1
            )

        origin_list = [x[Fields.ORIGIN] for x in self.records.values()]
        self.record_links = 0
        for origin in origin_list:
            self.record_links += len([o for o in origin if not o.startswith("md_")])

        criteria = list(review_manager.settings.screen.criteria.keys())
        self.screening_statistics = {crit: 0 for crit in criteria}
        for screening_case in self.screening_criteria:
            for criterion in screening_case.split(";"):
                criterion_name, decision = criterion.split("=")
                if decision == "out":
                    self.screening_statistics[criterion_name] += 1

        self.currently = self.StatusStatsCurrently(status_stats=self)
        self.overall = self.StatusStatsOverall(status_stats=self)

        self.completed_atomic_steps = 0
        self.nr_incomplete = 0

        self.__overall_stats_backward_calculation()

        self.currently.non_processed = (
            self.currently.md_imported
            + self.currently.md_retrieved
            + self.currently.md_needs_manual_preparation
            + self.currently.md_prepared
        )

        self.currently.md_retrieved = self.overall.md_retrieved - self.record_links

        self.completeness_condition = (
            (0 == self.nr_incomplete)
            and (0 == self.currently.md_retrieved)
            and self.overall.md_retrieved > 0
        )

        self.currently.exclusion = self.screening_statistics

        self.overall.rev_screen = self.overall.pdf_prepared

        self.overall.rev_prescreen = self.overall.md_processed
        self.currently.pdf_needs_retrieval = self.currently.rev_prescreen_included

        colrev_masterdata_items = [
            x[Fields.MD_PROV] for x in self.records.values() if Fields.MD_PROV in x
        ]

        self.nr_curated_records = len(
            [x for x in colrev_masterdata_items if FieldValues.CURATED in x]
        )
        if review_manager.settings.is_curated_masterdata_repo():
            self.nr_curated_records = self.overall.md_processed

        self.atomic_steps = (
            # initially, all records have to pass 8 operations
            8 * self.overall.md_retrieved
            # for removed duplicates, 5 operations are no longer needed
            - 5 * self.currently.md_duplicates_removed
            # for rev_prescreen_excluded, 4 operations are no longer needed
            - 4 * self.currently.rev_prescreen_excluded
            - 3 * self.currently.pdf_not_available
            - self.currently.rev_excluded
        )

        self.perc_curated = 0
        denominator = (
            self.overall.md_processed
            + self.currently.md_prepared
            + self.currently.md_needs_manual_preparation
            + self.currently.md_imported
        )

        if denominator > 0:
            self.perc_curated = int((self.nr_curated_records / (denominator)) * 100)

    def __overall_stats_backward_calculation(self) -> None:
        """Calculate the state_x overall stats (based on backward calculation)"""
        # self.review_manager.logger.debug(
        #     "Set overall colrev_status statistics (going backwards)"
        # )
        visited_states = []
        current_state = colrev.record.RecordState.rev_synthesized  # start with the last
        atomic_step_number = 0
        while True:
            # self.review_manager.logger.debug(
            #     "current_state: %s with %s",
            #     current_state,
            #     getattr(self.overall, str(current_state)),
            # )
            if colrev.record.RecordState.md_prepared == current_state:
                overall_md_prepared = (
                    getattr(self.overall, str(current_state))
                    + self.md_duplicates_removed
                )
                getattr(self.overall, str(current_state), overall_md_prepared)

            states_to_consider = [current_state]
            predecessors: list[dict[str, typing.Any]] = [
                {
                    "trigger": "init",
                    "source": colrev.record.RecordState.md_imported,
                    "dest": colrev.record.RecordState.md_imported,
                }
            ]
            # Go backward through the process model
            predecessor = None
            while predecessors:
                predecessors = [
                    t
                    for t in colrev.record.RecordStateModel.transitions
                    if t["source"] in states_to_consider
                    and t["dest"] not in visited_states
                ]
                for predecessor in predecessors:
                    # self.review_manager.logger.debug(
                    #     " add %s from %s (predecessor transition: %s)",
                    #     getattr(self.overall, str(predecessor["dest"])),
                    #     str(predecessor["dest"]),
                    #     predecessor["trigger"],
                    # )
                    setattr(
                        self.overall,
                        str(current_state),
                        (
                            getattr(self.overall, str(current_state))
                            + getattr(self.overall, str(predecessor["dest"]))
                        ),
                    )
                    visited_states.append(predecessor["dest"])
                    if predecessor["dest"] not in states_to_consider:
                        states_to_consider.append(predecessor["dest"])
                if len(predecessors) > 0:
                    if predecessors[0]["trigger"] != "init":
                        # ignore _man versions to avoid double-counting:
                        if "_man" not in predecessors[0]["trigger"]:
                            self.completed_atomic_steps += getattr(
                                self.overall, str(predecessor["dest"])
                            )
                        # Note : load is not a predecessor so we need to
                        # correct for a missing step (same number like prep)
                        if predecessors[0]["trigger"] == "prep":
                            self.completed_atomic_steps += getattr(
                                self.overall, str(predecessor["dest"])
                            )

            atomic_step_number += 1
            # Note : the following does not consider multiple parallel steps.
            for trans_for_completeness in [
                t
                for t in colrev.record.RecordStateModel.transitions
                if current_state == t["dest"]
            ]:
                self.nr_incomplete += getattr(
                    self.currently, str(trans_for_completeness["source"])
                )

            t_list = [
                t
                for t in colrev.record.RecordStateModel.transitions
                if current_state == t["dest"]
            ]
            transition: dict = t_list.pop()
            if current_state == colrev.record.RecordState.md_imported:
                break
            current_state = transition["source"]  # go a step back
            self.currently.non_completed += getattr(self.currently, str(current_state))

    def get_active_metadata_operation_info(self) -> str:
        """Get active metadata operation info (convenience function for status printing)"""
        infos = []
        if self.currently.md_retrieved > 0:
            infos.append(f"{self.currently.md_retrieved} to load")
        if self.currently.md_imported > 0:
            infos.append(f"{self.currently.md_imported} to prepare")
        if self.currently.md_needs_manual_preparation > 0:
            infos.append(
                f"{self.currently.md_needs_manual_preparation} to prepare manually"
            )
        if self.currently.md_prepared > 0:
            infos.append(f"{self.currently.md_prepared} to deduplicate")
        return ", ".join(infos)

    def get_active_pdf_operation_info(self) -> str:
        """Get active PDF operation info (convenience function for status printing)"""
        infos = []
        if self.currently.rev_prescreen_included > 0:
            infos.append(f"{self.currently.rev_prescreen_included} to retrieve")
        if self.currently.pdf_needs_manual_retrieval > 0:
            infos.append(
                f"{self.currently.pdf_needs_manual_retrieval} to retrieve manually"
            )
        if self.currently.pdf_imported > 0:
            infos.append(f"{self.currently.pdf_imported} to prepare")
        if self.currently.pdf_needs_manual_preparation > 0:
            infos.append(
                f"{self.currently.pdf_needs_manual_preparation} to prepare manually"
            )
        return ", ".join(infos)

    def get_transitioned_records(
        self, current_origin_states_dict: dict
    ) -> list[typing.Dict]:
        """Get the transitioned records"""

        committed_origin_states_dict = (
            self.review_manager.dataset.get_committed_origin_state_dict()
        )
        transitioned_records = []
        for (
            committed_origin,
            committed_colrev_status,
        ) in committed_origin_states_dict.items():
            transitioned_record = {
                "origin": committed_origin,
                "source": committed_colrev_status,
                "dest": current_origin_states_dict.get(
                    committed_origin, "no_source_state"
                ),
            }

            operations_type = [
                x["trigger"]
                for x in colrev.record.RecordStateModel.transitions
                if x["source"] == transitioned_record["source"]
                and x["dest"] == transitioned_record["dest"]
            ]
            if (
                len(operations_type) == 0
                and transitioned_record["source"] != transitioned_record["dest"]
            ):
                transitioned_record["operations_type"] = "invalid_transition"

            if len(operations_type) > 0:
                transitioned_record["operations_type"] = operations_type[0]
                transitioned_records.append(transitioned_record)

        return transitioned_records

    def get_priority_operations(self, *, current_origin_states_dict: dict) -> list:
        """Get the priority operations"""

        # get "earliest" states (going backward)
        earliest_state = []
        search_states = [colrev.record.RecordState.rev_synthesized]
        while True:
            if any(
                search_state in current_origin_states_dict.values()
                for search_state in search_states
            ):
                earliest_state = [
                    search_state
                    for search_state in search_states
                    if search_state in current_origin_states_dict.values()
                ]
            search_states = [
                x["source"]  # type: ignore
                for x in colrev.record.RecordStateModel.transitions
                if x["dest"] in search_states
            ]
            if [] == search_states:
                break
        # print(f'earliest_state: {earliest_state}')

        # next: get the priority transition for the earliest states
        priority_transitions = [
            x["trigger"]
            for x in colrev.record.RecordStateModel.transitions
            if x["source"] in earliest_state
        ]

        priority_operations = list(set(priority_transitions))

        self.review_manager.logger.debug(f"priority_operations: {priority_operations}")
        return priority_operations

    def get_active_operations(self, *, current_origin_states_dict: dict) -> list:
        """Get the active processing functions"""

        active_operations: typing.List[str] = []
        for state in set(current_origin_states_dict.values()):
            valid_transitions = colrev.record.RecordStateModel.get_valid_transitions(
                state=state
            )
            active_operations.extend(valid_transitions)

        self.review_manager.logger.debug(f"active_operations: {set(active_operations)}")
        return active_operations

    def get_operation_in_progress(self, *, transitioned_records: list) -> list:
        """Get the operation currently in progress"""

        in_progress_operation = list(
            {x["operations_type"] for x in transitioned_records}
        )
        self.review_manager.logger.debug(
            f"in_progress_operation: {in_progress_operation}"
        )
        return in_progress_operation

    @dataclass
    class StatusStatsParent:
        """Parent class for StatusStatsCurrently and StatusStatsOverall"""

        # pylint: disable=too-many-instance-attributes
        # Note : StatusStatsCurrently and StatusStatsOverall start with the same frequencies
        def __init__(
            self,
            *,
            status_stats: StatusStats,
        ) -> None:
            self.status_stats = status_stats

            self.md_retrieved = self.__get_freq(colrev.record.RecordState.md_retrieved)

            self.md_imported = self.__get_freq(colrev.record.RecordState.md_imported)
            self.md_needs_manual_preparation = self.__get_freq(
                colrev.record.RecordState.md_needs_manual_preparation
            )
            self.md_prepared = self.__get_freq(colrev.record.RecordState.md_prepared)
            self.md_processed = self.__get_freq(colrev.record.RecordState.md_processed)
            self.rev_prescreen_excluded = self.__get_freq(
                colrev.record.RecordState.rev_prescreen_excluded
            )
            self.rev_prescreen_included = self.__get_freq(
                colrev.record.RecordState.rev_prescreen_included
            )
            self.pdf_needs_manual_retrieval = self.__get_freq(
                colrev.record.RecordState.pdf_needs_manual_retrieval
            )
            self.pdf_imported = self.__get_freq(colrev.record.RecordState.pdf_imported)
            self.pdf_not_available = self.__get_freq(
                colrev.record.RecordState.pdf_not_available
            )
            self.pdf_needs_manual_preparation = self.__get_freq(
                colrev.record.RecordState.pdf_needs_manual_preparation
            )
            self.pdf_prepared = self.__get_freq(colrev.record.RecordState.pdf_prepared)
            self.rev_excluded = self.__get_freq(colrev.record.RecordState.rev_excluded)
            self.rev_included = self.__get_freq(colrev.record.RecordState.rev_included)
            self.rev_synthesized = self.__get_freq(
                colrev.record.RecordState.rev_synthesized
            )
            self.md_duplicates_removed = self.status_stats.md_duplicates_removed

        def __get_freq(self, colrev_status: colrev.record.RecordState) -> int:
            return len([x for x in self.status_stats.status_list if colrev_status == x])

    @dataclass
    class StatusStatsCurrently(StatusStatsParent):
        """The current status statistics"""

        # pylint: disable=too-many-instance-attributes
        md_retrieved: int
        md_imported: int
        md_prepared: int
        md_needs_manual_preparation: int
        md_duplicates_removed: int
        md_processed: int
        non_processed: int
        rev_prescreen_excluded: int
        rev_prescreen_included: int
        pdf_needs_retrieval: int
        pdf_needs_manual_retrieval: int
        pdf_not_available: int
        pdf_imported: int
        pdf_needs_manual_preparation: int
        pdf_prepared: int
        rev_excluded: int
        rev_included: int
        rev_synthesized: int
        non_completed: int
        exclusion: dict

        def __init__(
            self,
            *,
            status_stats: StatusStats,
        ) -> None:
            self.exclusion: typing.Dict[str, int] = {}
            self.non_completed = 0
            self.non_processed = 0
            super().__init__(status_stats=status_stats)
            self.pdf_needs_retrieval = self.rev_prescreen_included

    @dataclass
    class StatusStatsOverall(StatusStatsParent):
        """The overall-status statistics (records currently/previously in each state)"""

        # pylint: disable=too-many-instance-attributes
        md_retrieved: int
        md_imported: int
        md_needs_manual_preparation: int
        md_prepared: int
        md_processed: int
        rev_prescreen: int
        rev_prescreen_excluded: int
        rev_prescreen_included: int
        pdf_needs_manual_retrieval: int
        pdf_imported: int
        pdf_not_available: int
        pdf_needs_manual_preparation: int
        pdf_prepared: int
        rev_excluded: int
        rev_included: int
        rev_screen: int
        rev_synthesized: int

        def __init__(
            self,
            *,
            status_stats: StatusStats,
        ) -> None:
            self.rev_screen = 0
            self.rev_prescreen = 0
            super().__init__(status_stats=status_stats)
            self.md_retrieved = self.__get_nr_search(
                search_dir=self.status_stats.review_manager.search_dir
            )

        def __get_nr_search(self, *, search_dir: Path) -> int:
            if not search_dir.is_dir():
                return 0
            bib_files = search_dir.glob("*.bib")
            number_search = 0
            for search_file in bib_files:
                # Note : skip md-prep sources
                if str(search_file.name).startswith("md_"):
                    continue
                number_search += self.status_stats.review_manager.dataset.get_nr_in_bib(
                    file_path=search_file
                )
            return number_search
