"""
senzib.integrations.profitbase.client

Клиент для Profitbase API v4 (JSON).

Особенности:
- Не знает про Flask, БД, app.config и т.п.
- Соблюдает ограничение Profitbase: не более 1 запроса в секунду (qps_delay).
- Хранит только account_number и access_token.
- Возвращает dict / list, максимально близкие к "сырым" ответам API.

Пример:

    from senzib.integrations.profitbase.client import ProfitbaseClient

    client = ProfitbaseClient(account_number="pb15930", access_token="...")

    statuses = client.get_statuses()
    houses = client.get_houses(is_archive=False)
    prop = client.get_property(property_id=123456)
"""

from __future__ import annotations

import logging
import time
from typing import Any, Dict, List, Optional, Sequence

import requests

logger = logging.getLogger(__name__)

JsonDict = Dict[str, Any]


class ProfitbaseAPIError(Exception):
    """Ошибка работы с Profitbase API."""


def get_api_url(account_number: str) -> str:
    """
    Базовый URL Profitbase API v4 JSON для указанного аккаунта.

    >>> get_api_url("pb15930")
    'https://pb15930.profitbase.ru/api/v4/json'
    """
    domain = "profitbase.ru"
    api = "api/v4/json"
    return f"https://{account_number}.{domain}/{api}"


class ProfitbaseClient:
    """
    Высокоуровневый клиент для Profitbase API.

    :param account_number: поддомен Profitbase (без .profitbase.ru), например "pb15930".
    :param access_token: токен доступа Profitbase.
    :param request_timeout: таймаут HTTP-запросов в секундах.
    :param qps_delay: задержка между запросами в секундах
                      (по правилам Profitbase — не более 1 запроса/сек).
    """

    def __init__(
        self,
        account_number: str,
        access_token: str,
        *,
        request_timeout: int = 30,
        qps_delay: float = 1.0,
    ) -> None:
        self.account_number = account_number
        self.access_token = access_token
        self.request_timeout = request_timeout
        self.qps_delay = qps_delay
        self._base_url = get_api_url(account_number)

    # ------------------------------------------------------------------ #
    # ВНУТРЕННИЕ МЕТОДЫ
    # ------------------------------------------------------------------ #

    def _sleep(self) -> None:
        """
        Соблюдаем ограничение по QPS (1 запрос/сек).
        """
        if self.qps_delay > 0:
            time.sleep(self.qps_delay)

    def _get(
        self,
        path: str,
        params: Optional[Dict[str, Any]] = None,
    ) -> Any:
        """
        Внутренний GET-запрос с автоматическим добавлением access_token.
        Возвращает распарсенный JSON (dict/list).

        При сетевой ошибке или HTTP != 200 кидает ProfitbaseAPIError.
        """
        self._sleep()

        url = f"{self._base_url}/{path.lstrip('/')}"
        final_params: Dict[str, Any] = {"access_token": self.access_token}
        if params:
            final_params.update(params)

        logger.debug("Profitbase GET %s params=%s", url, final_params)
        try:
            resp = requests.get(url, params=final_params, timeout=self.request_timeout)
        except requests.RequestException as exc:
            raise ProfitbaseAPIError(
                f"Network error while GET {url}: {exc}"
            ) from exc

        if resp.status_code != 200:
            raise ProfitbaseAPIError(
                f"Profitbase GET {url} failed: HTTP {resp.status_code}, body={resp.text}"
            )

        try:
            return resp.json()
        except ValueError as exc:
            raise ProfitbaseAPIError(
                f"Failed to parse JSON from Profitbase GET {url}: {exc}"
            ) from exc

    def _post(
        self,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Dict[str, Any]] = None,
        timeout: Optional[int] = None,
    ) -> Any:
        """
        Внутренний POST-запрос с автоматическим добавлением access_token.
        Возвращает распарсенный JSON.

        При сетевой ошибке или HTTP != 200 кидает ProfitbaseAPIError.
        """
        self._sleep()

        url = f"{self._base_url}/{path.lstrip('/')}"
        final_params: Dict[str, Any] = {"access_token": self.access_token}
        if params:
            final_params.update(params)

        logger.debug("Profitbase POST %s params=%s json=%s", url, final_params, json_body)
        try:
            resp = requests.post(
                url,
                params=final_params,
                json=json_body,
                timeout=timeout or self.request_timeout,
            )
        except requests.RequestException as exc:
            raise ProfitbaseAPIError(
                f"Network error while POST {url}: {exc}"
            ) from exc

        if resp.status_code != 200:
            # Для некоторых методов (crm/addPropertyDeal/...) Profitbase
            # возвращает текст или "error" в JSON — но HTTP всё равно 200.
            raise ProfitbaseAPIError(
                f"Profitbase POST {url} failed: HTTP {resp.status_code}, body={resp.text}"
            )

        try:
            return resp.json()
        except ValueError as exc:
            raise ProfitbaseAPIError(
                f"Failed to parse JSON from Profitbase POST {url}: {exc}"
            ) from exc

    # ------------------------------------------------------------------ #
    # СТАТУСЫ, ТИПЫ, ДОМА, ПОМЕЩЕНИЯ
    # ------------------------------------------------------------------ #

    def get_statuses(self) -> List[JsonDict]:
        """
        Список статусов (custom-status/list).

        Возвращает список словарей статусов или пустой список.

        Исходный endpoint:
            GET /custom-status/list
        """
        data = self._get("custom-status/list")
        return data.get("data") or []

    def get_property_types(self) -> Any:
        """
        Список типов помещений (property-types).

        Возвращает сырой JSON, который отдаёт Profitbase.
        """
        return self._get("property-types")

    def get_property_plan(self, property_id: int, *, full: bool = True) -> Optional[JsonDict]:
        """
        Получить preset (планировка) конкретного помещения.

        Исходный endpoint:
            GET /property?access_token=...&id={property_id}&full=true

        Возвращает dict preset или None, если данных нет.
        """
        data = self._get(
            "property",
            params={
                "id": int(property_id),
                "full": "true" if full else "false",
            },
        )
        if data.get("status") == "success" and data.get("data"):
            prop = data["data"][0]
            return prop.get("preset")
        return None

    def get_house_id_by_property(self, property_id: int) -> Optional[int]:
        """
        Получить house_id по ID помещения.

        Исходный endpoint:
            GET /property?access_token=...&id={property_id}
        """
        data = self._get(
            "property",
            params={"id": int(property_id)},
        )
        if "data" in data and isinstance(data["data"], list) and data["data"]:
            house_id = data["data"][0].get("house_id")
            return int(house_id) if house_id is not None else None
        return None

    def get_floor_raw(self, house_id: int) -> Any:
        """
        Вернуть "сырые" данные по этажам дома.

        Исходный endpoint:
            GET /floor?access_token=...&houseId={house_id}

        Profitbase обычно возвращает список.
        """
        return self._get(
            "floor",
            params={"houseId": int(house_id)},
        )

    def get_floor_coordinates(
        self,
        house_id: int,
        property_id: int,
    ) -> Optional[Dict[str, Any]]:
        """
        Найти координаты и размеры нужного помещения на поэтажном плане дома.

        Возвращает dict:

            {
                "image_source": <url>,
                "coordinates": <list>,
                "height": <originalLayoutHeight>,
                "width": <originalLayoutWidth>,
            }

        или None, если нужное помещение не найдено / формат неожиданен.
        """
        data = self.get_floor_raw(house_id)

        # Ожидаем список floor_data
        if not isinstance(data, list):
            return None

        for floor_data in data:
            areas = floor_data.get("areas") or []
            for area in areas:
                if area.get("propertyId") == int(property_id):
                    image_source = (floor_data.get("images") or {}).get("source")
                    coordinates = area.get("coordinates")
                    original_layout_height = floor_data.get("originalLayoutHeight")
                    original_layout_width = floor_data.get("originalLayoutWidth")

                    if (
                        image_source
                        and coordinates
                        and original_layout_height
                        and original_layout_width
                    ):
                        return {
                            "image_source": image_source,
                            "coordinates": coordinates,
                            "height": original_layout_height,
                            "width": original_layout_width,
                        }

        return None

    def get_house(self, house_id: int) -> Optional[JsonDict]:
        """
        Получить информацию о доме по ID.

        Исходный endpoint:
            GET /house?access_token=...&id={house_id}

        Возвращает dict house_data или None.
        """
        data = self._get(
            "house",
            params={"id": int(house_id)},
        )
        house_list = data.get("data") or []
        if not house_list:
            return None
        return house_list[0]

    def get_houses(self, *, is_archive: bool = False) -> List[JsonDict]:
        """
        Получить список домов.

        :param is_archive: фильтр архивных домов (isArchive).

        Исходный endpoint:
            GET /house?access_token=...&isArchive={0/1}
        """
        data = self._get(
            "house",
            params={
                "isArchive": bool(is_archive),
            },
        )
        return data.get("data") or []

    def get_all_house_properties(
        self,
        house_id: int,
        *,
        full: bool = False,
        statuses: Optional[Sequence[int]] = None,
    ) -> List[JsonDict]:
        """
        Возвращает все помещения по дому, пагинируя по limit/offset.

        full=True  -> детальные данные (limit=100)
        full=False -> "короткие" данные (limit=1000)

        Исходный endpoint:
            GET /property?access_token=...&houseId=...&limit=...&offset=...
        """
        url_path = "property"
        page_size = 100 if full else 1000
        offset = 0
        all_items: List[JsonDict] = []

        while True:
            params: Dict[str, Any] = {
                "houseId": int(house_id),
                "full": "true" if full else "false",
                "limit": page_size,
                "offset": offset,
            }
            if statuses:
                # Profitbase принимает status[]=...
                params["status[]"] = list(statuses)

            data = self._get(url_path, params=params)

            if data.get("status") != "success" or data.get("data") is None:
                break

            batch = data.get("data", [])
            if not batch:
                break

            all_items.extend(batch)

            if len(batch) < page_size:
                # последняя страница
                break

            offset += page_size

        return all_items

    def get_property(
        self,
        property_id: int,
        *,
        full: bool = True,
    ) -> Optional[JsonDict]:
        """
        Получить полную информацию о помещении.

        Исходный endpoint:
            GET /property?access_token=...&id={property_id}&full=true

        Возвращает dict property_data или None, если нет данных.
        """
        data = self._get(
            "property",
            params={
                "id": int(property_id),
                "full": "true" if full else "false",
            },
        )

        if data.get("status") == "success" and data.get("data"):
            return data["data"][0]
        return None

    # ------------------------------------------------------------------ #
    # CRM: addPropertyDeal / deals / removePropertyDeal
    # ------------------------------------------------------------------ #

    def add_property_deal(
        self,
        deal_id: int,
        property_id: int,
    ) -> Dict[str, Any]:
        """
        Привязать помещение к сделке в Profitbase CRM.

        Исходный endpoint:
            POST /crm/addPropertyDeal

        На выходе нормализованный dict:
            {"success": True}
        или
            {"success": False, "error": "..."}
        """
        try:
            deal_id_int = int(deal_id)
            property_id_int = int(property_id)
        except (TypeError, ValueError):
            return {
                "success": False,
                "error": f"Invalid IDs. deal_id={deal_id}, property_id={property_id}",
            }

        if deal_id_int <= 0 or property_id_int <= 0:
            return {
                "success": False,
                "error": f"IDs must be positive. deal_id={deal_id_int}, property_id={property_id_int}",
            }

        payload = {
            "propertyId": property_id_int,
            "dealId": deal_id_int,
        }

        try:
            data = self._post("crm/addPropertyDeal", json_body=payload, timeout=10)
        except ProfitbaseAPIError as exc:
            return {"success": False, "error": str(exc)}

        # УСПЕХ
        if isinstance(data, dict) and data.get("success") is True:
            return {"success": True}

        # Ошибки в разных форматах
        err = data.get("error") if isinstance(data, dict) else None
        if isinstance(err, dict):
            code = err.get("code", "")
            msg = err.get("message", "Unknown error")
            return {"success": False, "error": f"{code}: {msg}"}
        if isinstance(err, str):
            return {"success": False, "error": err}

        return {"success": False, "error": str(data)}

    def get_property_deal(self, deal_id: int) -> Dict[str, Any]:
        """
        Получить связку помещения и сделки в Profitbase CRM.

        Исходный endpoint:
            GET /crm/deals?dealId={id}

        Нормализованный ответ:
            {"success": True, "data": <obj>}
        или
            {"success": False, "error": "...", "data": []}
        """
        try:
            deal_id_int = int(deal_id)
        except (TypeError, ValueError):
            return {"success": False, "error": f"Invalid ID. deal_id={deal_id}"}

        if deal_id_int <= 0:
            return {
                "success": False,
                "error": f"ID must be positive. deal_id={deal_id_int}",
            }

        try:
            data = self._get(
                "crm/deals",
                params={"dealId": deal_id_int},
            )
        except ProfitbaseAPIError as exc:
            return {"success": False, "error": str(exc)}

        # По доке в ответе массив объектов или dict
        if isinstance(data, list):
            if not data:
                return {
                    "success": False,
                    "error": f"Deal with id={deal_id_int} not found",
                    "data": [],
                }
            return {"success": True, "data": data[0]}

        if isinstance(data, dict):
            err = data.get("error")
            if isinstance(err, dict):
                code = err.get("code", "")
                msg = err.get("message", "Unknown error")
                return {"success": False, "error": f"{code}: {msg}"}
            if isinstance(err, str):
                return {"success": False, "error": err}
            return {"success": True, "data": data}

        return {
            "success": False,
            "error": f"Unexpected response format: {type(data)}",
        }

    def remove_property_deal(self, deal_id: int) -> Dict[str, Any]:
        """
        Удалить привязку помещений к сделке в Profitbase CRM.

        Исходный endpoint:
            POST /crm/removePropertyDeal

        Возвращает:
            {"success": True}
        или
            {"success": False, "error": "..."}
        """
        try:
            deal_id_int = int(deal_id)
            if deal_id_int <= 0:
                raise ValueError
        except (TypeError, ValueError):
            return {
                "success": False,
                "error": f"Invalid dealId: {deal_id}",
            }

        payload = {"dealId": deal_id_int}

        try:
            data = self._post("crm/removePropertyDeal", json_body=payload, timeout=10)
        except ProfitbaseAPIError as exc:
            return {"success": False, "error": str(exc)}

        if isinstance(data, dict):
            if data.get("success") is True:
                return {"success": True}

            err = data.get("error")
            if isinstance(err, dict):
                code = err.get("code", "")
                msg = err.get("message", "Unknown error")
                return {"success": False, "error": f"{code}: {msg}"}
            if isinstance(err, str):
                return {"success": False, "error": err}

        return {"success": False, "error": str(data)}


# ---------------------------------------------------------------------- #
# ФУНКЦИИ-ОБЁРТКИ (на случай, если разработчикам так удобнее)
# ---------------------------------------------------------------------- #

def get_statuses(account_number: str, access_token: str) -> List[JsonDict]:
    return ProfitbaseClient(account_number, access_token).get_statuses()


def get_property_types(account_number: str, access_token: str) -> Any:
    return ProfitbaseClient(account_number, access_token).get_property_types()


def get_property(
    account_number: str,
    access_token: str,
    property_id: int,
    *,
    full: bool = True,
) -> Optional[JsonDict]:
    return ProfitbaseClient(account_number, access_token).get_property(
        property_id=property_id,
        full=full,
    )


def get_houses(
    account_number: str,
    access_token: str,
    *,
    is_archive: bool = False,
) -> List[JsonDict]:
    return ProfitbaseClient(account_number, access_token).get_houses(
        is_archive=is_archive
    )
