from pathlib import Path
from typing import List, Dict

from metacarp.camel_gene_detection.app.camel import Camel
from metacarp.camel_gene_detection.app.components.genedetection.genedetectionkmahit import GeneDetectionKMAHit
from metacarp.camel_gene_detection.app.error.invalidinputspecificationerror import InvalidInputSpecificationError
from metacarp.camel_gene_detection.app.io.tooliovalue import ToolIOValue
from metacarp.camel_gene_detection.app.loggers import logger
from metacarp.camel_gene_detection.app.tools.tool import Tool


class KMAGeneDetectionHitExtractor(Tool):
    """
    Extracts Gene detection hits from the KMA output.
    """

    def __init__(self, camel: Camel) -> None:
        """
        Initializes this tool.
        :param camel: CAMEL instance
        """
        super().__init__('KMA gene detection hit extractor', '1.0', camel)

    def _check_input(self) -> None:
        """
        Checks if the provided input is valid.
        :return: None
        """
        if 'TSV' not in self._tool_inputs:
            raise InvalidInputSpecificationError('TSV input is required')
        super()._check_input()

    def _execute_tool(self) -> None:
        """
        Executes this tool.
        :return: None
        """
        hits = self.__parse_kma_output_file(Path(self._tool_inputs['TSV'][0].path))
        hits_filtered = self.__filter_hits(hits)
        hits_by_cluster = self.__cluster_hits(hits_filtered)
        hits_selected = self.__get_best_hit_per_cluster(hits_by_cluster)
        self._tool_outputs['VAL_hits'] = [ToolIOValue(h) for h in hits_selected]

    def __parse_kma_output_file(self, path_output: Path) -> List[GeneDetectionKMAHit]:
        """
        Parses the output file generated by KMA.
        :param path_output: Output file path
        :return: List of hits
        """
        hits = []
        with path_output.open() as handle:
            lines = handle.readlines()
            if len(lines) <= 1:
                return []
            header_indices = {h: i for i, h in enumerate(lines[0].strip().split('\t'))}
            for line in lines[1:]:
                parts = [x.strip() for x in line.split('\t')]
                seq_id = parts[header_indices['#Template']].split('__')[2]
                hit = GeneDetectionKMAHit(
                    parts[header_indices['#Template']].split('__')[1],
                    seq_id,
                    None,
                    parts[header_indices['#Template']],
                    int(parts[header_indices['Score']]),
                    int(parts[header_indices['Template_length']]),
                    float(parts[header_indices['Template_Identity']]),
                    float(parts[header_indices['Template_Coverage']]),
                    float(parts[header_indices['Depth']])
                )
                hits.append(hit)
        return hits

    def __filter_hits(self, hits: List[GeneDetectionKMAHit]) -> List[GeneDetectionKMAHit]:
        """
        Filters the detected hits based on the tool parameters.
        :param hits: List of input hits
        :return: List of filtered hits
        """
        # Percent identity filtering
        min_percent_identity = float(self._parameters['min_percent_identity'].value)
        hits_filtered_identity = [h for h in hits if h.percent_identity >= min_percent_identity]
        logger.info(f"{len(hits_filtered_identity)}/{len(hits)} hits passed identity filtering")

        # Percent coverage filtering
        min_percent_coverage = float(self._parameters['min_percent_coverage'].value)
        hits_filtered_coverage = [h for h in hits_filtered_identity if h.subject_coverage >= min_percent_coverage]
        logger.info(f"{len(hits_filtered_coverage)}/{len(hits_filtered_identity)} hits passed identity filtering")

        return hits_filtered_coverage

    @staticmethod
    def __cluster_hits(hits: List[GeneDetectionKMAHit]) -> Dict[str, List[GeneDetectionKMAHit]]:
        """
        Groups hits based on the database cluster they belong to.
        :param hits: List of input hits
        :return: Dictionary of hits per cluster
        """
        hits_by_cluster = {}
        for hit in hits:
            cluster = hit.subject.split('__')[1]
            if cluster not in hits_by_cluster:
                hits_by_cluster[cluster] = []
            hits_by_cluster[cluster].append(hit)
        logger.info(f"{len(hits_by_cluster)} clusters with hits found")
        return hits_by_cluster

    @staticmethod
    def __get_best_hit_per_cluster(hits_by_cluster: Dict[str, List[GeneDetectionKMAHit]]) -> List[GeneDetectionKMAHit]:
        """
        Returns the best hit for each cluster.
        :param hits_by_cluster: Dictionary of hits per cluster
        :return: List of best hit for each cluster
        """
        reported_hits = []
        for _, hits in hits_by_cluster.items():
            if len(hits) < 1:
                continue
            reported_hits.append(sorted(hits, key=lambda x: -x.score)[0])
        return reported_hits
