import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import List, Optional

from maps_adv.common.mds import MDSClient
from maps_adv.geosmb.doorman.client import DoormanClient, Source
from maps_adv.geosmb.harmonist.server.lib.data_manager import BaseDataManager
from maps_adv.geosmb.harmonist.server.lib.domain.file_parsers import (
    CsvParser,
    XLSXParser,
)
from maps_adv.geosmb.harmonist.server.lib.enums import (
    ColumnType,
    FileExtension,
    InputDataType,
    PipelineStep,
    StepStatus,
)
from maps_adv.geosmb.harmonist.server.lib.exceptions import (
    ClientsAlreadyWereImported,
    InvalidSessionId,
    MarkUpAlreadySubmitted,
    ValidationNotFinished,
)

from .lock_manager import LockManager
from .validator import Validator

FILE_PARSERS = {
    FileExtension.CSV: (CsvParser, InputDataType.CSV_FILE),
    FileExtension.XLSX: (XLSXParser, InputDataType.XLSX_FILE),
}


class Domain:
    __slots__ = "_dm", "_validator", "_mds_client", "_doorman_client", "_lock_manager"

    _dm: BaseDataManager
    _validator: Validator
    _mds_client: MDSClient
    _doorman_client: DoormanClient
    _lock_manager: LockManager

    def __init__(
        self,
        dm: BaseDataManager,
        mds_client: MDSClient,
        doorman_client: DoormanClient,
        lock_manager: LockManager,
    ):
        self._dm = dm
        self._mds_client = mds_client
        self._doorman_client = doorman_client
        self._validator = Validator()
        self._lock_manager = lock_manager

    async def submit_data(
        self,
        biz_id: int,
        text: Optional[str] = None,
        file_content: Optional[bytes] = None,
        file_extension: Optional[FileExtension] = None,
    ) -> str:
        # TODO add logging for parsing data
        if text:
            return await self._dm.submit_data(
                biz_id=biz_id,
                input_data=text,
                input_data_type=InputDataType.TEXT,
                parsed_input=CsvParser.parse(text),
            )
        elif file_content:
            parser, input_type = FILE_PARSERS[file_extension]

            return await self._dm.submit_data(
                biz_id=biz_id,
                input_data=file_content,
                input_data_type=input_type,
                parsed_input=parser.parse(file_content),
            )
        else:
            raise AssertionError("One of data fields must be set: text, file")

    async def show_preview(self, session_id: str, biz_id: int) -> dict:
        return await self._dm.show_preview(session_id=session_id, biz_id=biz_id)

    async def submit_markup(self, session_id: str, biz_id: int, markup: dict) -> None:
        creation_log = await self._dm.fetch_clients_creation_log(
            session_id=session_id, biz_id=biz_id
        )

        if not creation_log:
            raise InvalidSessionId(
                f"Session not found: session_id={session_id}, biz_id={biz_id}"
            )

        if creation_log.get("markup"):
            raise MarkUpAlreadySubmitted(
                f"Markup for session_id {session_id} already submitted."
            )

        await self._dm.submit_markup(
            session_id=session_id, biz_id=biz_id, markup=markup
        )

        asyncio.create_task(
            self._validate_data(
                session_id=session_id,
                biz_id=biz_id,
                markup=markup,
                parsed_input=creation_log["parsed_input"],
            )
        )

    async def import_clients(self, session_id: str, biz_id: int) -> None:
        creation_log = await self._dm.fetch_clients_creation_log(
            session_id=session_id, biz_id=biz_id
        )

        self._check_if_clients_are_ready_for_import(
            creation_log, session_id=session_id, biz_id=biz_id
        )

        clients_for_import = creation_log.get("valid_clients")
        if clients_for_import:
            await self._dm.update_log_status(
                session_id=session_id,
                biz_id=biz_id,
                pipeline_step=PipelineStep.IMPORTING_CLIENTS,
                status=StepStatus.IN_PROGRESS,
            )
            asyncio.create_task(
                self._import_clients(
                    session_id=session_id,
                    biz_id=biz_id,
                    segment=creation_log.get("markup", {}).get("segment"),
                    clients=clients_for_import,
                )
            )
        else:
            await self._dm.submit_import_result(
                session_id=session_id,
                biz_id=biz_id,
                import_result={"created_amount": 0, "updated_amount": 0},
            )

    async def process_unvalidated(self) -> None:
        for entry in await self._dm.list_unvalidated_creation_entries():
            try:
                await self._validate_data(**entry)
            except Exception as exc:
                logging.getLogger(__name__).exception(
                    f"Failed to validate_data for session_id {entry['session_id']}",
                    exc_info=exc,
                )

    async def process_unimported(self) -> None:
        for entry in await self._dm.list_unimported_creation_entries():
            await self._import_clients(**entry)

    async def _validate_data(
        self,
        *,
        session_id: str,
        biz_id: int,
        markup: dict,
        parsed_input: List[List[str]],
    ) -> None:
        async with self._lock_manager.try_lock_creation_entry(session_id) as success:
            if not success:
                return

            clients_data = iter(parsed_input)
            if markup["ignore_first_line"]:
                next(clients_data)

            valid_clients, invalid_clients = self._validator.validate_data(
                parsed_data=clients_data,
                markup=[
                    mk
                    for mk in markup["column_type_map"]
                    if mk["column_type"] != ColumnType.DO_NOT_IMPORT
                ],
            )

            await self._dm.submit_validated_clients(
                session_id=session_id,
                biz_id=biz_id,
                valid_clients=valid_clients,
                invalid_clients=invalid_clients,
                validation_step_status=StepStatus.IN_PROGRESS
                if invalid_clients
                else StepStatus.FINISHED,
            )

            if invalid_clients:
                await self._upload_file_with_validation_errors(
                    invalid_clients=invalid_clients,
                    session_id=session_id,
                    biz_id=biz_id,
                )

    async def _upload_file_with_validation_errors(
        self, *, invalid_clients: List[dict], session_id: str, biz_id: int
    ) -> None:
        file_content = "\n".join(
            ";".join((ic["row"], str(ic["reason"]))) for ic in invalid_clients
        )
        try:
            upload_result = await self._mds_client.upload_file(
                file_content=file_content.encode("utf-8"),
                file_name=f"{int(datetime.now(tz=timezone.utc).timestamp())}.csv",
                expire=timedelta(days=1),
            )
            await self._dm.submit_error_file(
                session_id=session_id,
                biz_id=biz_id,
                validation_errors_file_link=upload_result.download_link,
            )
        except Exception as exc:
            logging.getLogger(__name__).exception(
                f"Failed to upload errors file for session_id {session_id}",
                exc_info=exc,
            )
            await self._dm.update_log_status(
                session_id=session_id,
                biz_id=biz_id,
                pipeline_step=PipelineStep.VALIDATING_DATA,
                status=StepStatus.FAILED,
                failed_reason="Failed to create errors file.",
            )

    @staticmethod
    def _check_if_clients_are_ready_for_import(
        creation_log: Optional[dict], session_id: str, biz_id: int
    ) -> None:
        if not creation_log:
            raise InvalidSessionId(
                f"Session not found: session_id={session_id}, biz_id={biz_id}"
            )

        if "import_result" in creation_log:
            raise ClientsAlreadyWereImported(
                f"Clients for session_id {session_id} already imported."
            )

        last_log = creation_log["log_history"][-1]
        if (
            last_log["step"] != PipelineStep.VALIDATING_DATA
            or last_log["status"] != StepStatus.FINISHED
        ):
            raise ValidationNotFinished(
                f"Validation for session_id {session_id} hasn't finished yet."
            )

    async def _import_clients(
        self, session_id: str, biz_id: int, segment: Optional[str], clients: List[dict]
    ) -> None:
        async with self._lock_manager.try_lock_creation_entry(session_id) as success:
            if not success:
                return

            try:
                (
                    created_amount,
                    updated_amount,
                ) = await self._doorman_client.create_clients(
                    biz_id=biz_id,
                    source=Source.CRM_INTERFACE,
                    label=segment,
                    clients=clients,
                )
                await self._dm.submit_import_result(
                    session_id=session_id,
                    biz_id=biz_id,
                    import_result={
                        "new_clients_amount": created_amount,
                        "updated_clients_amount": updated_amount,
                    },
                )
            except Exception as exc:
                logging.getLogger(__name__).exception(
                    f"Failed to import clients for session_id {session_id}",
                    exc_info=exc,
                )
                await self._dm.update_log_status(
                    session_id=session_id,
                    biz_id=biz_id,
                    pipeline_step=PipelineStep.IMPORTING_CLIENTS,
                    status=StepStatus.FAILED,
                    failed_reason="Failed to import clients",
                )

    async def fetch_pipeline_status(self, session_id: str, biz_id: int) -> dict:
        creation_log = await self._dm.fetch_clients_creation_log(
            session_id=session_id, biz_id=biz_id
        )

        if not creation_log:
            raise InvalidSessionId(
                f"Session not found: session_id={session_id}, biz_id={biz_id}"
            )

        current_log_state = creation_log["log_history"][-1]
        last_finished_step = self._find_last_finished_step(
            log_history=creation_log["log_history"]
        )
        pipeline_status = dict(
            step=current_log_state["step"], step_status=current_log_state["status"]
        )

        if current_log_state["status"] == StepStatus.FAILED:
            pipeline_status["failed_reason"] = current_log_state["failed_reason"]

        elif last_finished_step == PipelineStep.PARSING_DATA:
            pipeline_status["preview"] = self._compose_preview(creation_log)

        elif last_finished_step == PipelineStep.VALIDATING_DATA:
            pipeline_status["validation_result"] = self._compose_validation_result(
                creation_log
            )
        elif last_finished_step == PipelineStep.IMPORTING_CLIENTS:
            pipeline_status["import_result"] = creation_log["import_result"]

        return pipeline_status

    @staticmethod
    def _find_last_finished_step(log_history: List[dict]) -> PipelineStep:
        for log in reversed(log_history):
            if log["status"] == StepStatus.FINISHED:
                return log["step"]

    @staticmethod
    def _compose_preview(creation_log: dict) -> dict:
        preview = {"rows": creation_log["parsed_input"][:10]}

        if creation_log.get("markup"):
            preview["markup"] = creation_log["markup"]

        return preview

    @staticmethod
    def _compose_validation_result(creation_log: dict) -> dict:
        validation_result = {"valid_clients_amount": len(creation_log["valid_clients"])}
        if creation_log["invalid_clients"]:
            validation_result["invalid_clients"] = {
                "invalid_clients_amount": len(creation_log["invalid_clients"]),
                "report_link": creation_log["validation_errors_file_link"],
            }

        return validation_result
