"""
CSV2Notion Neo - Data Conversion Engine

This module provides the core data conversion engine for CSV2Notion Neo.
It handles the conversion of CSV/JSON data to Notion-compatible formats,
including complex data transformations, relation mapping, file handling,
AI integration, and comprehensive error handling with validation.
"""

import logging
from functools import partial
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from csv2notion_neo.utils_exceptions import NotionError

from csv2notion_neo.local_data import LocalData, CSVRowType
from csv2notion_neo.notion_convert_map import (
    map_checkbox,
    map_date,
    map_icon,
    map_notion_date,
    map_number,
    map_url_or_file,
)
from csv2notion_neo.notion_db import NotionDB
from csv2notion_neo.notion_type_guess import is_email
from csv2notion_neo.notion_uploader import NotionUploadRow
from csv2notion_neo.utils_exceptions import (
    NotionError,
    TypeConversionError,
    CriticalError,
)
from csv2notion_neo.utils_static import ConversionRules, FileType
from csv2notion_neo.utils_str import split_str

logger = logging.getLogger(__name__)


class NotionRowConverter(object):  # noqa:  WPS214
    def __init__(self, db: NotionDB, conversion_rules: ConversionRules):
        self.db = db
        self.rules = conversion_rules
        self._current_row = 0

    def convert_to_notion_rows(self, csv_data: LocalData) -> List[NotionUploadRow]:
        notion_rows = []
        # starting with 2nd row, because first is header
        self._current_row = 2
        self.csv_data = csv_data

        for row in csv_data:

            if self.rules.rename_notion_key_column:
                if (
                    self.rules.rename_notion_key_column[1]
                    == self.rules.rename_notion_key_column[0]
                ):
                    raise CriticalError(
                        f"Please do not provide same column name in rename-payload-key-column"
                    )
                new_id = self.rules.rename_notion_key_column[1]
                old_id = self.rules.rename_notion_key_column[0]
                row[new_id] = row[old_id]
                del row[old_id]

            try:
                notion_rows.append(self._convert_row(row))
            except NotionError as e:
                raise NotionError(f"CSV [{self._current_row}]: {e}")
            self._current_row += 1

        return notion_rows

    def _error(self, error: str) -> None:
        logger.error(f"CSV [{self._current_row}]: {error}")

        if self.rules.fail_on_conversion_error:
            raise NotionError("Error during conversion.")

    def _convert_row(self, row: CSVRowType) -> NotionUploadRow:
        properties = self._map_properties(row)
        columns = self._map_columns(row)

        return NotionUploadRow(columns=columns, properties=properties)

    def _map_properties(self, row: CSVRowType) -> Dict[str, Any]:
        properties = {}

        if self.rules.payload_key_column:
            if self.rules.rename_notion_key_column:
                new_id = self.rules.rename_notion_key_column[1]
                properties["payload_key_column"] = new_id
            else:
                properties["payload_key_column"] = self.rules.payload_key_column

        # ai features
        if self.rules.hugging_face_token:
            if self.rules.caption_column:
                properties["AI"] = {
                    "caption": {
                        "hftoken": self.rules.hugging_face_token,
                        "image_path": self.rules.csv_file.parent
                        / Path(row[self.rules.caption_column[0]]),
                        "caption_column": self.rules.caption_column[1],
                        "model_url": self.csv_data.model_url,
                    }
                }

        if self.rules.image_column_mode == "block":
            properties["cover_block"] = self._map_image(row)
            properties["cover_block_caption"] = self._map_image_caption(row)
        else:
            # properties["cover_block"] = self._map_image(row)
            # properties["cover_block_caption"] = self._map_image_caption(row)
            properties["cover"] = self._map_image(row)

        properties["icon"] = self._map_icon(row)

        properties["created_time"] = self._pop_column_type(row, "created_time")
        properties["last_edited_time"] = self._pop_column_type(row, "last_edited_time")

        return {k: v for k, v in properties.items() if v is not None}

    def _map_columns(self, row: CSVRowType) -> Dict[str, Any]:
        notion_row = {}

        for col_key, col_value in row.items():

            col_type = self.db.columns[col_key]["type"]

            notion_row[col_key] = self._map_column(col_key, col_value, col_type)

            self._raise_if_mandatory_empty(col_key, notion_row[col_key])

        return notion_row

    def _map_column(
        self, col_key: str, col_value: str, value_type: str
    ) -> Optional[Any]:
        conversion_map: Dict[str, Callable[[str], Any]] = {
            "relation": partial(self._map_relation, col_key),
            "checkbox": map_checkbox,
            "date": map_notion_date,
            "created_time": map_date,
            "last_edited_time": map_date,
            "multi_select": split_str,
            "number": map_number,
            "file": self._map_file,
            "person": self._map_person,
        }

        try:
            return conversion_map[value_type](col_value)
        except KeyError:
            return col_value
        except TypeConversionError as e:
            if not col_value.strip():
                return None

            self._error(str(e))
            return None

    def _pop_column_type(self, row: CSVRowType, col_type_to_pop: str) -> Optional[Any]:
        """Some column types can't have multiple values (like created_time)
        so we pop them out of the row leaving only the last non-empty one"""

        last_col_value = None

        for col_key, col_value in list(row.items()):
            col_type = self.db.columns[col_key]["type"]

            if col_type == col_type_to_pop:
                result_value = self._map_column(col_key, col_value, col_type)

                self._raise_if_mandatory_empty(col_key, result_value)

                if result_value is not None:
                    last_col_value = result_value

                row.pop(col_key)

        return last_col_value

    def _map_icon(self, row: CSVRowType) -> Optional[FileType]:
        icon: Optional[FileType] = None

        if self.rules.icon_column:
            icon = row.get(self.rules.icon_column, "").strip()
            if icon:
                icon = map_icon(icon)
                if isinstance(icon, Path):
                    icon = self._relative_path(icon)

            self._raise_if_mandatory_empty(self.rules.icon_column, icon)

            if not self.rules.icon_column_keep:
                row.pop(self.rules.icon_column, None)

        if not icon and self.rules.default_icon:
            icon = self.rules.default_icon

        return icon

    def _map_image(self, row: CSVRowType) -> List:

        image: Optional[FileType] = None
        images = []

        if self.rules.image_column:

            for image_column in self.rules.image_column:
                image = row.get(image_column, "").strip()
                if image:
                    image = map_url_or_file(image)

                    if isinstance(image, Path):
                        image = self._relative_path(image)

                self._raise_if_mandatory_empty(image_column, image)

                images.append(image)
                if not self.rules.image_column_keep:
                    row.pop(image_column, None)

        return images

    def _map_non_cover_images(self, row: CSVRowType) -> List:

        _ = None

        if self.rules.image_column:
            _ = self._remove_cover_image(self.rules.image_caption_column)

        if self.rules.image_column:
            pass

    def _remove_cover_image(self, image_column: List) -> List:

        if len(image_column) == 1:
            return None

        img_col_copy = image_column.copy()
        img_col_copy.pop(0)

        return img_col_copy

    def _mention_cover_image(self, image_column: List) -> List:

        if len(image_column) == 1:
            return image_column

        img_col_copy = image_column.copy()
        cover_image = img_col_copy.pop(0)
        img_col_copy.append(cover_image)

        return img_col_copy

    def _map_image_caption(self, row: CSVRowType) -> Optional[str]:
        image_caption = None

        if self.rules.image_caption_column:
            image_caption = row.get(self.rules.image_caption_column, "").strip()

            self._raise_if_mandatory_empty(
                self.rules.image_caption_column, image_caption
            )

            if not self.rules.image_caption_column_keep:
                row.pop(self.rules.image_caption_column, None)

        return image_caption

    def _map_file(self, s: str) -> List[FileType]:

        col_value = split_str(s)

        resolved_uris = []
        for v in col_value:
            file_uri = map_url_or_file(v)

            if isinstance(file_uri, Path):
                rel_file_uri = self._ensure_path(file_uri)

                if rel_file_uri is None:
                    continue

                file_uri = rel_file_uri

            if file_uri not in resolved_uris:
                resolved_uris.append(file_uri)

        return resolved_uris

    def _ensure_path(self, path: Path) -> Optional[Path]:
        ensured_path = self._relative_path(path)
        if ensured_path is None:
            return None

        if _is_banned_extension(ensured_path):
            self._error(
                f"File extension '*{ensured_path.suffix}' is not allowed"
                f" to upload on Notion."
            )
            return None

        return ensured_path

    def _relative_path(self, path: Path) -> Optional[Path]:
        # PATH
        search_path = self.rules.files_search_path

        if not path.is_absolute():
            path = search_path / path

        if not path.exists():
            self._error(f"File {path.name} does not exist.")
            return None

        return path

    def _map_relation(
        self, relation_column: str, col_value: str
    ) -> List[Dict[str, Any]]:
        col_values = split_str(col_value)

        resolved_relations = []
        for v in col_values:
            if v.startswith("https://www.notion.so/"):
                resolved_relation = self._resolve_relation_by_url(relation_column, v)
            else:
                resolved_relation = self._resolve_relation_by_key(relation_column, v)

            if resolved_relation and resolved_relation not in resolved_relations:
                resolved_relations.append(resolved_relation)

        return resolved_relations

    def _resolve_relation_by_key(
        self, relation_column: str, key: str
    ) -> Optional[Dict[str, Any]]:
        relation = self.db.relations[relation_column]

        try:
            return relation.rows[key]
        except KeyError:
            if self.rules.add_missing_relations:
                return relation.add_row_key(key)

            self._error(
                f"Value '{key}' for relation"
                f" '{relation_column} [column] -> {relation.name} [DB]'"
                f" is not a valid value."
            )

            return None

    def _resolve_relation_by_url(
        self, relation_column: str, url: str
    ) -> Optional[Dict[str, Any]]:
        block_id = self._extract_id(url)
        if block_id is None:
            return None

        relation = self.db.relations[relation_column]
        relation_rows = relation.rows.values()

        try:
            return next(r for r in relation_rows if r.get("id") == block_id)
        except StopIteration:
            self._error(
                f"Row with url '{url}' not found in relation"
                f" '{relation_column} [column] -> {relation.name} [DB]'."
            )

            return None

    def _extract_id(self, url: str) -> Optional[str]:
        try:
            from csv2notion_neo.notion_db import _extract_database_id_from_url
            return _extract_database_id_from_url(url)
        except Exception:
            self._error(f"'{url}' is not a valid Notion URL.")
            return None

    def _map_person(self, col_value: str) -> List[Dict[str, Any]]:
        col_values = split_str(col_value)

        resolved_persons = []
        for v in col_values:
            if is_email(v):
                resolved_person = self.db.users.get(v)

                if not resolved_person:
                    resolved_person = self.db.find_user(v)
            else:
                resolved_person = self.db.get_user_by_name(v)

            if resolved_person is None:
                self._error(f"Person '{v}' cannot be resolved.")
                continue

            if resolved_person not in resolved_persons:
                resolved_persons.append(resolved_person)

        return resolved_persons

    def _raise_if_mandatory_empty(self, col_key: str, col_value: Any) -> None:
        is_mandatory = col_key in self.rules.mandatory_column

        if is_mandatory and not col_value:
            raise NotionError(f"Mandatory column '{col_key}' is empty")


def _is_banned_extension(file_path: Path) -> bool:
    return file_path.suffix in {".exe", ".com", ".js"}
