from datetime import datetime
from typing import Dict, List, Optional, Union

import requests
from requests import RequestException

from tonic_textual.classes.common_api_responses.base_file import BaseFile
from tonic_textual.classes.common_api_responses.single_detection_result import (
    SingleDetectionResult,
)
from tonic_textual.classes.enums.file_type import FileTypeEnum
from tonic_textual.classes.file_content.csv_document import CsvDocument
from tonic_textual.classes.file_content.docx_document import DocxDocument
from tonic_textual.classes.file_content.pdf_document import PdfDocument
from tonic_textual.classes.file_content.raw_document import RawDocument
from tonic_textual.classes.file_content.xlsx_document import XlsxDocument
from tonic_textual.classes.httpclient import HttpClient
from tonic_textual.classes.table import Table
from tonic_textual.classes.tonic_exception import DownloadResultFileError
from tonic_textual.enums.pii_state import PiiState
from tonic_textual.markdown_utils import split_markdown
from tonic_textual.generator_utils import (
    filter_entities_by_config,
    make_utf_compatible_entities,
)


class FileParseResult(object):
    """A class that represents the result of a parsed file.

    Parameters
    ----------
    response: Dict
        The response from the API.

    client: HttpClient
        The HTTP client to use.

    lazy_load_content: bool
        Whether to lazy load the content of the file. Default is False.
    """

    def __init__(
        self,
        response: Dict,
        client: HttpClient,
        lazy_load_content=False,
        document: Dict = None,
    ):
        self.id: str = response["id"]
        self.file: BaseFile = BaseFile(response["file"])
        self.file_parse_job_id: str = response["fileParseJobId"]
        self.parsed_file_path: str = response["parsedFilePath"]
        self.created_date: datetime = response["createdDate"]
        self.last_modified_date: datetime = response["lastModifiedDate"]
        self.__is_content_retrieved = False
        self.client = client

        if document is not None:
            self.content = self.__convert_document_json(document)
            self.__is_content_retrieved = True

        if not lazy_load_content and self.__is_content_retrieved is False:
            self.__get_content()
        self.metadata = {}

    def __convert_document_json(
        self, doc_json: Dict
    ) -> Union[RawDocument, CsvDocument, DocxDocument, XlsxDocument, PdfDocument]:
        if self.file.fileType == FileTypeEnum.csv:
            return CsvDocument(self.client, doc_json)
        elif self.file.fileType == FileTypeEnum.raw:
            return RawDocument(self.client, doc_json)
        elif self.file.fileType == FileTypeEnum.xlsx:
            return XlsxDocument(self.client, doc_json)
        elif self.file.fileType == FileTypeEnum.docX:
            return DocxDocument(self.client, doc_json)
        elif (
            self.file.fileType == FileTypeEnum.pdf
            or self.file.fileType == FileTypeEnum.png
            or self.file.fileType == FileTypeEnum.jpg
            or self.file.fileType == FileTypeEnum.tif
        ):
            return PdfDocument(self.client, doc_json)
        else:
            raise Exception("Unknown file type " + self.file.fileType)

    def __get_content(self):
        if not self.__is_content_retrieved:
            with requests.Session() as session:
                response = self.client.http_get(
                    f"/api/parsejob/files/{self.id}/content", session=session
                )

                self.content = self.__convert_document_json(response)
                self.__is_content_retrieved = True

    def get_json(self) -> Dict:
        """Returns the raw JSON generated by Tonic Textual.

        Returns
        -------
        Dict
            The raw JSON that Textal generates when it parses the file, in the form of a dictionary.
        """

        self.__get_content()
        return self.content.get_json()

    def get_markdown(
        self,
        generator_config: Dict[str, PiiState] = dict(),
        generator_default: PiiState = PiiState.Off,
        random_seed: Optional[int] = None
    ) -> str:
        """Returns the file in Markdown format. In the file, the entities are redacted or synthesized based on the specified configuration.

        Parameters
        ----------
        generator_config: Dict[str, PiiState]
            A dictionary of sensitive data entity types. For each entity type, indicates whether
            to redact, synthesize, or ignore the detected entities.
            Values must be one of "Redaction", "Synthesis", or "Off".

        generator_default: PiiState = PiiState.Redaction
            The default redaction to use for all entity types that not specified in generator_config.
            Value must be one of "Redaction", "Synthesis", or "Off".
        
        random_seed: Optional[int] = None
            An optional value to use to override Textual's default random number
            seeding. Can be used to ensure that different API calls use the same or
            different random seeds.
            
        Returns
        -------
        str
            The file in Markdown format. In the file, the entities are redacted or synthesized based on
            generator_config and generator_default.
        """
        self.__get_content()
        rawtext = self.content.get_markdown()
        filtered_entities = filter_entities_by_config(
            self.content.get_all_entities(),
            generator_config,
            generator_default,
        )
        if len(filtered_entities) == 0:
            return rawtext
        utf_compatible_filtered_entities = make_utf_compatible_entities(
            rawtext, filtered_entities
        )

        if random_seed is not None:
            additional_headers = {"textual-random-seed": str(random_seed)}
        else:
            additional_headers = {}

        response = self.client.http_post(
            "/api/redact/known_entities",
            data={
                "knownEntities": utf_compatible_filtered_entities,
                "text": rawtext,
                "generatorConfig": generator_config,
                "generatorDefault": generator_default,
            },
            additional_headers=additional_headers
        )
        return response["redactedText"]

    def describe(self) -> str:
        """Returns the parsed file path."""
        return f"{self.parsed_file_path}"

    def get_all_entities(self) -> List[SingleDetectionResult]:
        """Returns a list of all of the detected entities in the file.

        Returns
        -------
        List[SingleDetectionResult]
            A list of detected entities in the file.
        """
        self.__get_content()
        return self.content.get_all_entities()

    def get_entities(
        self,
        generator_config: Dict[str, PiiState] = dict(),
        generator_default: PiiState = PiiState.Redaction,
        allow_overlap: bool = False,
    ) -> List[SingleDetectionResult]:
        """Returns a list of entities in the document. The entities are filtered by the
        generator_default configuration.

        Parameters
        ----------
        generator_default: PiiState
            The default redaction to use for all entity types that not specified in generator_config.
            Value must be one of "Redaction", "Synthesis", or "Off".

        generator_config: Dict[str, PiiState]
            A dictionary of sensitive data entity types. For each entity type, indicates whether
            to redact, synthesize, or ignore the detected entities.
            Values must be one of "Redaction", "Synthesis", or "Off".

        Returns
        -------
        List[SingleDetectionResult]
            A list of the detected entities. Each item in list contains the entity type,
            source start index, source end index, the entity text, and replacement text.
        """
        self.__get_content()
        all_entities = self.content.get_all_entities()
        rawtext = self.content.get_markdown()
        filtered_entities = filter_entities_by_config(
            all_entities, generator_config, generator_default
        )
        if allow_overlap:
            return filtered_entities
        utf_compatible_filtered_entities = make_utf_compatible_entities(
            rawtext, filtered_entities
        )
        response = self.client.http_post(
            "/api/redact/known_entities",
            data={"knownEntities": utf_compatible_filtered_entities},
        )
        return [
            SingleDetectionResult(
                x["start"], x["end"], x["label"], x["text"], x["score"]
            )
            for x in list(response["deIdentifyResults"])
        ]

    def is_sensitive(
        self, sensitive_entity_types: List[str], start: int = 0, end: int = -1
    ) -> bool:
        """Returns True if the element contains sensitive data. Otherwise returns False.

        Parameters
        ----------
        sensitive_entity_types: List[str]
            A list of sensitive entity types to check for.

        start: int = 0
            The start index to check for sensitive data.

        end: int = -1
            The end index to check for sensitive data.

        Returns
        -------
        bool
            Returns True if the element contains sensitive data. Otherwise returns False.
        """
        all_entities = self.get_entities()
        entities = [
            ent for ent in all_entities if ent["label"] in sensitive_entity_types
        ]
        filtered_entities = self._find_intersecting_entites(start, end, entities)
        return len(filtered_entities) > 0

    def get_tables(self) -> List[Table]:
        """Returns a list of tables found in the document. Applies to CSV, XLSX, PDF, and image files.

        Parameters
        ----------
        sensitive_entity_types: List[str]
            A list of sensitive entity types to check for.

        start: int = 0
            The start index to check for sensitive data.

        end: int = -1
            The end index to check for sensitive data.

        Returns
        -------
        bool
            Returns True if the element contains sensitive data. Otherwise returns False.
        """
        self.__get_content()

        if self.file.fileType not in (
            FileTypeEnum.csv,
            FileTypeEnum.xlsx,
            FileTypeEnum.pdf,
            FileTypeEnum.png,
            FileTypeEnum.tif,
            FileTypeEnum.jpg,
        ):
            return []

        return self.content.tables

    @staticmethod
    def _make_entity_metadata(entity_list: List[Dict]) -> Dict[str, List[str]]:
        """Flattens a list of entity spans into a dictionary of entities per type"""
        entity_dict = {}
        for entity in entity_list:
            entity_type = entity["label"]
            entity_list = entity_dict.get(entity_type, [])
            entity_text = entity["text"]
            if entity_text not in entity_list:
                entity_list.append(entity_text)
            entity_dict[entity_type] = entity_list
        return entity_dict

    @staticmethod
    def _find_intersecting_entites(
        start: int, end: int, entities: List[SingleDetectionResult]
    ) -> List[Dict]:
        """Finds entities that intersect with the given start and end gindices
        Parameters
        ----------
        start: int
            The start index to check for intersecting entities.
        end: int
            The end index to check for intersecting entities. If -1, returns all
            entities that occur after start.
        Returns
        -------
        List[Dict]
            A list of entities that intersect with the given start and end indices.
        """
        intersecting_entities = []
        for entity in entities:
            entity_start = entity["start"]
            entity_end = entity["end"]
            if (entity_start <= end or end == -1) and entity_end >= start:
                intersecting_entities.append(entity)
        return intersecting_entities

    def get_chunks(
        self,
        max_chars=15_000,
        generator_config: Dict[str, PiiState] = {},
        generator_default: PiiState = PiiState.Off,
        metadata_entities: List[str] = [],
        include_metadata=True,
    ) -> List:
        """Returns a list of chunks of text from the document. The chunks are filtered
        by the generator_default configuration.

        Parameters
        ----------
        max_chars: int = 15_000
            The maximum number of characters in each chunk.

        generator_config: Dict[str, PiiState]
            A dictionary of sensitive data entity types. For each entity type, indicates
            whether to redact, synthesize, or ignore the detected entities.
            Values must be one of "Redaction", "Synthesis", or "Off".

        generator_default: PiiState = PiiState.Redaction
            The default redaction to use for all entity types that are not specified in
            generator_config.
            Value must be one of "Redaction", "Synthesis", or "Off".


        include_metadata: bool = True
            If True, the metadata is included in the chunk.
        Returns:
            List[str]
                A list of strings that contain the chunks of text.
        """
        text = self.get_markdown(generator_config, generator_default)
        all_entities = self.get_all_entities()
        entities = [ent for ent in all_entities if ent["label"] in metadata_entities]
        output = []
        for chunk in split_markdown(text, max_chars):
            start, end = chunk["indices"]
            headers = chunk["headers"]
            sensitive_entity_types = [
                label for label, key in generator_config.items() if key != PiiState.Off
            ]

            chunk_text = text[start:end]
            is_sensitive = self.is_sensitive(
                sensitive_entity_types,
                start,
                end,  # type: ignore
            )
            chunk_dict = {"text": chunk_text, "is_sensitive": is_sensitive}
            if include_metadata:
                entity_metdata = self._make_entity_metadata(  # type: ignore
                    self._find_intersecting_entites(
                        start,
                        end,
                        entities,  # type: ignore
                    )
                )

                metadata = {"headers": headers, "entities": entity_metdata}
                metadata.update(self.metadata)
                chunk_dict["metadata"] = metadata
            output.append(chunk_dict)
        return output

    def download_results(self) -> str:
        """Downloads the results file.

        Returns
        -------
        string
            The results file.
        """
        try:
            with requests.Session() as session:
                return self.client.http_get_file(
                    f"/api/parsejob/files/{self.id}/content", session=session
                ).decode("utf-8")
        except RequestException as req_err:
            if hasattr(req_err, "response") and req_err.response is not None:
                status_code = req_err.response.status_code
                error_message = req_err.response.text
                raise DownloadResultFileError(f"Error {status_code}: {error_message}")
            else:
                raise req_err
