"""
senzib.integrations.amocrm.client

Универсальный клиент для amoCRM API v4.

- Не знает про Flask / БД / конфиги.
- Работает только с доменом и access_token.
- Возвращает "сырые" dict/list из amoCRM (как есть из JSON).

Пример использования:

    from senzib.integrations.amocrm.client import AmoCRMClient

    client = AmoCRMClient(domain="example.amocrm.ru", access_token="...")

    user = client.get_user(123)
    pipelines = client.get_pipelines()
    leads = client.get_all_leads(limit=250, max_pages=10)

Для простых сценариев есть и функции-обёртки:
    from senzib.integrations.amocrm.client import get_pipelines
"""

from __future__ import annotations

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

import requests

logger = logging.getLogger(__name__)

JsonDict = Dict[str, Any]


def get_api_url(domain: str) -> str:
    """
    Строит базовый URL amoCRM API v4 для указанного домена.

    >>> get_api_url("example.amocrm.ru")
    'https://example.amocrm.ru/api/v4'
    """
    return f"https://{domain}/api/v4"


class AmoCRMError(Exception):
    """Общая ошибка работы с amoCRM API."""


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

    Хранит:
    - domain (example.amocrm.ru)
    - access_token (Bearer OAuth2)
    - базовые настройки таймаутов и пауз между запросами
    """

    def __init__(
        self,
        domain: str,
        access_token: str,
        *,
        request_timeout: int = 30,
        page_sleep: float = 3.0,
    ) -> None:
        self.domain = domain
        self.access_token = access_token
        self.request_timeout = request_timeout
        self.page_sleep = page_sleep

    # ------------------------------------------------------------------ #
    # ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
    # ------------------------------------------------------------------ #

    @property
    def base_url(self) -> str:
        return get_api_url(self.domain)

    @property
    def _default_headers(self) -> Dict[str, str]:
        return {
            "Authorization": f"Bearer {self.access_token}",
            "Content-Type": "application/json",
        }

    def _get(
        self,
        url: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        accept: Optional[str] = None,
    ) -> requests.Response:
        headers = dict(self._default_headers)
        if accept:
            headers["Accept"] = accept

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

        return resp

    def _patch(
        self,
        url: str,
        *,
        json_body: Dict[str, Any],
    ) -> requests.Response:
        headers = dict(self._default_headers)

        logger.debug("PATCH %s json=%s", url, json_body)
        try:
            resp = requests.patch(
                url,
                headers=headers,
                json=json_body,
                timeout=self.request_timeout,
            )
        except requests.RequestException as exc:
            raise AmoCRMError(f"Network error while PATCH {url}: {exc}") from exc

        return resp

    # ------------------------------------------------------------------ #
    # USERS
    # ------------------------------------------------------------------ #

    def get_user(self, user_id: int) -> Optional[JsonDict]:
        """
        GET /api/v4/users/{id}
        """
        url = f"{self.base_url}/users/{user_id}"
        params = {"with": "phone_number"}

        resp = self._get(url, params=params)

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("User %s not found", user_id)
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting user %s", user_id)
            return None

        logger.error("Error %s while getting user %s: %s", resp.status_code, user_id, resp.text)
        return None

    def get_users(self) -> Optional[JsonDict]:
        """
        GET /api/v4/users
        """
        url = f"{self.base_url}/users"
        params = {"with": "group"}

        resp = self._get(url, params=params)

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("Users not found")
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting users list")
            return None

        logger.error("Error %s while getting users: %s", resp.status_code, resp.text)
        return None

    # ------------------------------------------------------------------ #
    # PIPELINES
    # ------------------------------------------------------------------ #

    def get_pipelines(self) -> Optional[JsonDict]:
        """
        GET /api/v4/leads/pipelines
        """
        url = f"{self.base_url}/leads/pipelines"

        resp = self._get(
            url,
            accept="application/hal+json",
        )

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("Pipelines not found")
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting pipelines")
            return None

        logger.error("Error %s while getting pipelines: %s", resp.status_code, resp.text)
        return None

    # ------------------------------------------------------------------ #
    # LEADS (СПИСОК, ПАГИНАЦИЯ HAL)
    # ------------------------------------------------------------------ #

    def get_all_leads(
        self,
        *,
        limit: int = 250,
        extra_params: Optional[Dict[str, Any]] = None,
        max_pages: Optional[int] = 2,
        total_estimated: Optional[int] = None,
    ) -> List[JsonDict]:
        """
        Тянет все сделки из amoCRM, используя HAL-пагинацию по _links.next.href.

        :param limit: размер чанка (макс. 250)
        :param extra_params: любые доп. GET-параметры (например {'with': 'contacts,loss_reason'})
        :param max_pages: ограничить число страниц (опционально, None — без лимита)
        :param total_estimated: оценочное количество сделок (для логов)
        :return: список сделок (dict-объекты JSON)
        """
        base_url = f"{self.base_url}/leads"
        headers_accept = "application/hal+json"

        params = {"limit": min(limit, 250), "with": "contacts"}
        if extra_params:
            params.update(extra_params)

        all_leads: List[JsonDict] = []
        page_count = 0
        next_url = base_url

        while next_url:
            if max_pages is not None and page_count >= max_pages:
                logger.info("Reached max_pages=%s, stopping pagination", max_pages)
                break

            try:
                resp = requests.get(
                    next_url,
                    headers={
                        **self._default_headers,
                        "Accept": headers_accept,
                    },
                    params=params if next_url == base_url else None,
                    timeout=self.request_timeout,
                )
            except requests.RequestException as exc:
                raise AmoCRMError(f"Network error while GET {next_url}: {exc}") from exc

            # Rate limit 429
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", "1"))
                logger.warning("Received 429, sleeping %s seconds", retry_after)
                time.sleep(retry_after)
                resp = requests.get(
                    next_url,
                    headers={
                        **self._default_headers,
                        "Accept": headers_accept,
                    },
                    params=params if next_url == base_url else None,
                    timeout=self.request_timeout,
                )

            if resp.status_code == 200:
                data = resp.json() or {}

                embedded = data.get("_embedded", {}) or {}
                leads = embedded.get("leads") or embedded.get("items") or []
                all_leads.extend(leads)

                links = data.get("_links", {}) or {}
                next_link = links.get("next") or {}
                next_url = next_link.get("href")

                page_count += 1
                total_loaded = len(all_leads)

                if total_estimated:
                    logger.info(
                        "Loaded %s/%s leads (page %s)",
                        total_loaded,
                        total_estimated,
                        page_count,
                    )
                else:
                    logger.info(
                        "Loaded %s leads (page %s)", total_loaded, page_count
                    )

                if not next_url:
                    break

            elif resp.status_code == 204:
                logger.info("No more leads (204)")
                break
            elif resp.status_code == 401:
                raise AmoCRMError("Authorization error (401) while loading leads")
            elif resp.status_code == 402:
                raise AmoCRMError("Account is not paid (402)")
            else:
                raise AmoCRMError(
                    f"Error {resp.status_code} while loading leads: {resp.text}"
                )

            time.sleep(self.page_sleep)
            params = None  # чтобы не конфликтовать с query-параметрами в href

        return all_leads

    # ------------------------------------------------------------------ #
    # КАСТОМНЫЕ ПОЛЯ
    # ------------------------------------------------------------------ #

    def get_leads_custom_fields(self) -> Optional[JsonDict]:
        """
        GET /api/v4/leads/custom_fields
        """
        url = f"{self.base_url}/leads/custom_fields"

        resp = self._get(
            url,
            accept="application/hal+json",
        )

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("Lead custom fields not found")
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting lead custom fields")
            return None

        logger.error(
            "Error %s while getting lead custom fields: %s",
            resp.status_code,
            resp.text,
        )
        return None

    def get_contacts_custom_fields(self) -> Optional[JsonDict]:
        """
        GET /api/v4/contacts/custom_fields
        """
        url = f"{self.base_url}/contacts/custom_fields"

        resp = self._get(
            url,
            accept="application/hal+json",
        )

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("Contact custom fields not found")
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting contact custom fields")
            return None

        logger.error(
            "Error %s while getting contact custom fields: %s",
            resp.status_code,
            resp.text,
        )
        return None

    # ------------------------------------------------------------------ #
    # ЛИДЫ / КОНТАКТЫ (ПО ОДНОМУ)
    # ------------------------------------------------------------------ #

    def get_lead(self, lead_id: int) -> Optional[JsonDict]:
        """
        GET /api/v4/leads/{id}?with=contacts
        """
        url = f"{self.base_url}/leads/{lead_id}"
        params = {"with": "contacts"}

        resp = self._get(url, params=params)

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("Lead %s not found", lead_id)
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting lead %s", lead_id)
            return None

        logger.error("Error %s while getting lead %s: %s", resp.status_code, lead_id, resp.text)
        return None

    def get_contact(self, contact_id: int) -> Optional[JsonDict]:
        """
        GET /api/v4/contacts/{id}
        """
        url = f"{self.base_url}/contacts/{contact_id}"

        resp = self._get(url)

        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 204:
            logger.info("Contact %s not found", contact_id)
            return None
        if resp.status_code == 401:
            logger.error("Authorization error while getting contact %s", contact_id)
            return None

        logger.error(
            "Error %s while getting contact %s: %s", resp.status_code, contact_id, resp.text
        )
        return None

    # ------------------------------------------------------------------ #
    # ОБНОВЛЕНИЕ КАСТОМНЫХ ПОЛЕЙ СДЕЛКИ
    # ------------------------------------------------------------------ #

    def update_lead_fields(
        self,
        lead_id: int,
        data: Dict[str, Any],
        field_ids: Dict[str, int],
    ) -> requests.Response:
        """
        Обновление кастомных полей сделки.

        :param lead_id: ID сделки
        :param data: данные, где ключи совпадают с ключами field_ids
        :param field_ids: маппинг {ключ_в_data: field_id_в_amo}
        :return: Response от requests (даёт гибкость вызывающему коду)
        """
        url = f"{self.base_url}/leads/{lead_id}"

        custom_fields_payload: List[Dict[str, Any]] = []
        for key, field_id in field_ids.items():
            value = data.get(key)
            if value is not None:
                custom_fields_payload.append(
                    {
                        "field_id": int(field_id),
                        "values": [{"value": value}],
                    }
                )

        payload = {
            "custom_fields_values": custom_fields_payload,
        }

        resp = self._patch(url, json_body=payload)
        if resp.status_code not in (200, 202):
            logger.error(
                "Failed to update lead %s fields (%s): %s",
                lead_id,
                resp.status_code,
                resp.text,
            )
        return resp


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

def get_user(domain: str, access_token: str, user_id: int) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_user(user_id)


def get_users(domain: str, access_token: str) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_users()


def get_pipelines(domain: str, access_token: str) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_pipelines()


def get_all_leads(
    domain: str,
    access_token: str,
    *,
    limit: int = 250,
    extra_params: Optional[Dict[str, Any]] = None,
    max_pages: Optional[int] = 2,
    total_estimated: Optional[int] = None,
) -> List[JsonDict]:
    return AmoCRMClient(domain, access_token).get_all_leads(
        limit=limit,
        extra_params=extra_params,
        max_pages=max_pages,
        total_estimated=total_estimated,
    )


def get_leads_custom_fields(domain: str, access_token: str) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_leads_custom_fields()


def get_contacts_custom_fields(domain: str, access_token: str) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_contacts_custom_fields()


def get_lead(domain: str, access_token: str, lead_id: int) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_lead(lead_id)


def get_contact(domain: str, access_token: str, contact_id: int) -> Optional[JsonDict]:
    return AmoCRMClient(domain, access_token).get_contact(contact_id)


def update_lead_fields(
    domain: str,
    access_token: str,
    lead_id: int,
    data: Dict[str, Any],
    field_ids: Dict[str, int],
) -> requests.Response:
    return AmoCRMClient(domain, access_token).update_lead_fields(
        lead_id=lead_id,
        data=data,
        field_ids=field_ids,
    )
