import uuid
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import List, Optional, Union

from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase

from .engine import codec_options
from .enums import ColumnType, InputDataType, PipelineStep, StepStatus
from .exceptions import InvalidSessionId


class BaseDataManager(ABC):
    __slots__ = ()

    @abstractmethod
    async def submit_data(
        self,
        *,
        biz_id: int,
        input_data: Union[str, bytes],
        input_data_type: InputDataType,
        parsed_input: List[List[str]],
    ) -> str:
        raise NotImplementedError()

    @abstractmethod
    async def show_preview(self, *, session_id: str, biz_id: int) -> dict:
        raise NotImplementedError()

    @abstractmethod
    async def submit_markup(
        self, *, session_id: str, biz_id: int, markup: dict
    ) -> None:
        raise NotImplementedError()

    @abstractmethod
    async def fetch_clients_creation_log(self, *, session_id: str, biz_id: int) -> dict:
        raise NotImplementedError()

    @abstractmethod
    async def submit_validated_clients(
        self,
        *,
        session_id: str,
        biz_id: int,
        valid_clients: List[dict],
        invalid_clients: List[dict],
        validation_step_status: StepStatus,
    ) -> None:
        raise NotImplementedError()

    @abstractmethod
    async def submit_error_file(
        self, *, session_id: str, biz_id: int, validation_errors_file_link: str
    ) -> None:
        raise NotImplementedError()

    @abstractmethod
    async def update_log_status(
        self,
        *,
        session_id: str,
        biz_id: int,
        pipeline_step: PipelineStep,
        status: StepStatus,
        failed_reason: Optional[str] = None,
    ) -> None:
        raise NotImplementedError()

    @abstractmethod
    async def submit_import_result(
        self, *, session_id: str, biz_id: int, import_result: dict
    ) -> None:
        raise NotImplementedError()

    @abstractmethod
    async def list_unvalidated_creation_entries(self) -> List[dict]:
        raise NotImplementedError()

    @abstractmethod
    async def list_unimported_creation_entries(self) -> List[dict]:
        raise NotImplementedError()


class DataManager(BaseDataManager):
    __slots__ = ["_client", "_db"]

    _client: AsyncIOMotorClient
    _db: AsyncIOMotorDatabase

    def __init__(self, motor_client: AsyncIOMotorClient, db_name: str):
        self._client = motor_client
        self._db = self._client.get_database(name=db_name, codec_options=codec_options)

    async def submit_data(
        self,
        *,
        biz_id: int,
        input_data: Union[str, bytes],
        input_data_type: InputDataType,
        parsed_input: List[List[str]],
    ) -> str:
        now = datetime.now(tz=timezone.utc)
        document = {
            "_id": str(uuid.uuid4()),
            "biz_id": biz_id,
            "input_data": input_data,
            "input_data_type": input_data_type,
            "parsed_input": parsed_input,
            "created_at": now,
            "log_history": [
                {
                    "step": PipelineStep.PARSING_DATA,
                    "status": StepStatus.FINISHED,
                    "created_at": now,
                },
            ],
        }

        uploaded_data = await self._db.clients_creation_logs.insert_one(document)

        return str(uploaded_data.inserted_id)

    async def show_preview(self, *, session_id: str, biz_id: int) -> dict:
        preview = await self._db.clients_creation_logs.find_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {"_id": 0, "markup": 1, "rows": {"$slice": ["$parsed_input", 10]}},
        )

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

        # TODO amstir: deal with codecs
        if preview.get("markup"):
            self._compose_markup(preview)

        return preview

    async def submit_markup(
        self, *, session_id: str, biz_id: int, markup: dict
    ) -> None:
        await self._db.clients_creation_logs.update_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {
                "$set": {"markup": markup},
                "$push": {
                    "log_history": {
                        "step": PipelineStep.VALIDATING_DATA,
                        "status": StepStatus.IN_PROGRESS,
                        "created_at": datetime.now(tz=timezone.utc),
                    },
                },
            },
        )

    async def fetch_clients_creation_log(
        self, *, session_id: str, biz_id: int
    ) -> Optional[dict]:
        creation_log = await self._db.clients_creation_logs.find_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {"input_data": 0},
        )

        if not creation_log:
            return

        for history_record in creation_log["log_history"]:
            history_record["step"] = PipelineStep[history_record["step"]]
            history_record["status"] = StepStatus[history_record["status"]]

        if creation_log.get("markup"):
            self._compose_markup(creation_log)

        return creation_log

    @staticmethod
    def _compose_markup(creation_log: dict) -> None:
        creation_log["markup"]["column_type_map"] = [
            dict(
                column_number=item["column_number"],
                column_type=ColumnType[item["column_type"]],
            )
            for item in creation_log["markup"]["column_type_map"]
        ]

    async def submit_validated_clients(
        self,
        *,
        session_id: str,
        biz_id: int,
        valid_clients: List[dict],
        invalid_clients: List[dict],
        validation_step_status: StepStatus,
    ) -> None:
        await self._db.clients_creation_logs.update_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {
                "$set": {
                    "valid_clients": valid_clients,
                    "invalid_clients": invalid_clients,
                },
                "$push": {
                    "log_history": {
                        "step": PipelineStep.VALIDATING_DATA,
                        "status": validation_step_status,
                        "created_at": datetime.now(tz=timezone.utc),
                    },
                },
            },
        )

    async def submit_error_file(
        self, *, session_id: str, biz_id: int, validation_errors_file_link: str
    ) -> None:
        await self._db.clients_creation_logs.update_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {
                "$set": {
                    "validation_errors_file_link": validation_errors_file_link,
                },
                "$push": {
                    "log_history": {
                        "step": PipelineStep.VALIDATING_DATA,
                        "status": StepStatus.FINISHED,
                        "created_at": datetime.now(tz=timezone.utc),
                    },
                },
            },
        )

    async def update_log_status(
        self,
        *,
        session_id: str,
        biz_id: int,
        pipeline_step: PipelineStep,
        status: StepStatus,
        failed_reason: Optional[str] = None,
    ) -> None:
        log_record = {
            "step": pipeline_step,
            "status": status,
            "created_at": datetime.now(tz=timezone.utc),
        }

        if failed_reason:
            log_record["failed_reason"] = failed_reason

        await self._db.clients_creation_logs.update_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {"$push": {"log_history": log_record}},
        )

    async def submit_import_result(
        self, *, session_id: str, biz_id: int, import_result: dict
    ) -> None:
        await self._db.clients_creation_logs.update_one(
            {"$and": [{"_id": session_id}, {"biz_id": biz_id}]},
            {
                "$set": {"import_result": import_result},
                "$push": {
                    "log_history": {
                        "step": PipelineStep.IMPORTING_CLIENTS,
                        "status": StepStatus.FINISHED,
                        "created_at": datetime.now(tz=timezone.utc),
                    },
                },
            },
        )

    async def list_unvalidated_creation_entries(self) -> List[dict]:
        entries = await self._db.clients_creation_logs.aggregate(
            pipeline=[
                {
                    "$match": {
                        "$and": [
                            {"markup": {"$exists": 1}},
                            {"valid_clients": {"$exists": 0}},
                        ]
                    }
                },
                {
                    "$sort": {"created_at": 1},
                },
                {
                    "$project": {
                        "session_id": "$_id",
                        "_id": 0,
                        "biz_id": 1,
                        "parsed_input": 1,
                        "markup": 1,
                    }
                },
            ],
        ).to_list(None)

        for entry in entries:
            for column_markup in entry["markup"]["column_type_map"]:
                column_markup["column_type"] = ColumnType[column_markup["column_type"]]

        return entries

    async def list_unimported_creation_entries(self) -> List[dict]:
        return await self._db.clients_creation_logs.aggregate(
            pipeline=[
                {
                    "$match": {
                        "$and": [
                            {"valid_clients": {"$exists": 1}},
                            {"valid_clients.1": {"$exists": 1}},
                            {"import_result": {"$exists": 0}},
                            {
                                "$and": [
                                    {
                                        "$expr": {
                                            "$eq": [
                                                {
                                                    "$arrayElemAt": [
                                                        "$log_history.step",
                                                        -1,
                                                    ]
                                                },
                                                "IMPORTING_CLIENTS",
                                            ]
                                        }
                                    },
                                    {
                                        "$expr": {
                                            "$eq": [
                                                {
                                                    "$arrayElemAt": [
                                                        "$log_history.status",
                                                        -1,
                                                    ]
                                                },
                                                "IN_PROGRESS",
                                            ]
                                        }
                                    },
                                ]
                            },
                        ]
                    }
                },
                {
                    "$sort": {"created_at": 1},
                },
                {
                    "$project": {
                        "session_id": "$_id",
                        "_id": 0,
                        "biz_id": 1,
                        "clients": "$valid_clients",
                        "segment": {"$ifNull": ["$markup.segment", None]},
                    }
                },
            ],
        ).to_list(None)
