import functools
import json
import logging
import os
import re
import sys
import time
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from http import HTTPStatus
from platform import python_version
from pprint import pprint
from typing import Any, Optional
from zoneinfo import ZoneInfo

import pymysql

try:
    pymysql.install_as_MySQLdb()
    print("PyMySQL successfully installed as MySQLdb")  # for debugging
except ImportError as e:
    import logging

    logging.error(f"Failed to install PyMySQL as MySQLdb: {e}")
    raise

import MySQLdb

import requests

from .auth import LoyaAuth
from .errors import (
    BadRequest,
    Forbidden,
    HTTPException,
    LoyaException,
    LoyaServerError,
    NotFound,
    TooManyRequests,
    Unauthorized,
)
from .models import (
    Account,
    Card,
    Client,
    ClientChild,
    ClientPet,
    Location,
    LocationGroup,
    Profile,
    RawClient,
    ResultSet,
)
from .parsers import JSONParser, ModelParser, Parser, RawParser
from appdirs import user_data_dir

log = logging.getLogger(__name__)


def get_version():
    from . import __version__  # Import inside function — avoids circular init

    return __version__


def ensure_auth(func):
    def wrapper(self, *args, **kwargs):
        if self.auth_client.sess_id and self.auth_client.is_session_expired():
            log.info("Сессия истекла → перелогиниваемся")
            self.auth()
        elif not self.auth_client.sess_id:
            self.auth()
        return func(self, *args, **kwargs)

    return wrapper


def payload(payload_type, **payload_kwargs):
    payload_list = payload_kwargs.get("list", False)

    def decorator(method):

        @functools.wraps(method)
        def wrapper(*args, **kwargs):
            kwargs["payload_list"] = payload_list
            kwargs["payload_type"] = payload_type

            return method(*args, **kwargs)

        wrapper.payload_list = payload_list
        wrapper.payload_type = payload_type

        return wrapper

    return decorator


SESSION_FILE = os.path.join(user_data_dir("loya-client"), "session.json")
os.makedirs(os.path.dirname(SESSION_FILE), exist_ok=True)
log.info(f"Используется путь для сессии: {SESSION_FILE}")


@dataclass
class RegistrationResult:
    """Результат регистрации с информацией о выполненных шагах"""

    client_id: int
    client_response: Optional["RawClient"] = None
    card_id: int | None = None
    account_id: int | None = None
    steps_completed: list[str] = field(default_factory=list)
    success: bool = True
    error: str | None = None

    def __post_init__(self):
        if self.steps_completed is None:
            self.steps_completed = []


class API:
    def __init__(
        self,
        auth: LoyaAuth,
        *,
        host="http://127.0.0.1",
        loya_database_host="127.0.0.1",
        loya_database="coper",
        loya_database_user="root",
        loya_database_password="",
        parser=None,
        retry_count=0,
        retry_delay=0,
        retry_errors=None,
        timeout=60,
        user_agent=None,
        wait_on_rate_limit=False,
    ):
        self.host = host

        self.email = auth.email
        self.password = auth.password
        self.apikey = auth.apikey

        self.loya_database_host = loya_database_host
        self.loya_database = loya_database
        self.loya_database_user = loya_database_user
        self.loya_database_password = loya_database_password

        self.auth_client = auth

        if parser is None:
            parser = ModelParser()
        self.parser = parser

        self.retry_count = retry_count
        self.retry_delay = retry_delay
        self.retry_errors = retry_errors
        self.timeout = timeout

        if user_agent is None:
            user_agent = f"Python/{python_version()} LoyaClient/{get_version()}"
        self.user_agent = user_agent

        self.wait_on_rate_limit = wait_on_rate_limit

        if not isinstance(self.parser, Parser):
            raise TypeError(
                "parser should be an instance of Parser, not " + str(type(self.parser)),
            )

        self.session = requests.Session()

    @staticmethod
    def validate_phone(phone: str) -> str:
        """
        Валидация и нормализация российского номера телефона.

        Примеры:
        - "+7 900 123-45-67" → "79001234567"
        - "8 (900) 123-45-67" → "79001234567"
        - "9001234567" → "79001234567"

        :raises ValueError: если номер некорректный
        :return: нормализованный номер (11 цифр, начинается с 7)
        """
        if not phone:
            raise ValueError("Phone number is required")

        # Убираем всё кроме цифр
        digits = re.sub(r"\D", "", str(phone))

        if len(digits) < 10:
            raise ValueError("Phone number too short (minimum 10 digits)")

        # 8 → 7 (российский формат)
        if digits.startswith("8"):
            digits = "7" + digits[1:]

        # Если не 7 в начале — добавляем (берём последние 10 цифр)
        if not digits.startswith("7"):
            digits = "7" + digits[-10:]

        if len(digits) != 11 or not digits.startswith("7"):
            raise ValueError(
                "Invalid Russian phone format (expected 11 digits starting with 7)"
            )

        return digits

    @staticmethod
    def validate_email(email: str | None) -> str | None:
        """Валидация email (простая, но надёжная)"""
        if not email:
            return None

        email = email.strip().lower()
        if len(email) > 254 or not re.match(
            r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
            email,
        ):
            raise ValueError(f"Invalid email format: {email}")
        return email

    @staticmethod
    def validate_date(value: str | None, field_name: str = "date") -> date | None:
        """Парсит YYYY-MM-DD → date или None"""
        if not value:
            return None
        try:
            return datetime.strptime(value.strip(), "%Y-%m-%d").date()
        except ValueError as e:
            raise ValueError(
                f"Invalid {field_name} format (use YYYY-MM-DD): {value}"
            ) from e

    def _get_by_id(
        self, id: int, payload_type: str, endpoint_template: str, api="1.0"
    ) -> Any:
        """
        Универсальный метод для получения объекта по ID.

        :param id: ID объекта
        :param payload_type: тип для декоратора @payload (например, 'location')
        :param endpoint_template: шаблон эндпоинта, например "location/{id}"
        :param model_class: класс модели (для аннотации возврата, не обязателен)
        :raises LoyaException: если id не передан или не положительный
        """
        if not id:
            raise LoyaException("id is required")
        if not isinstance(id, int) or id <= 0:
            raise LoyaException("id must be a positive integer")
        return self.request(
            method="GET",
            endpoint=endpoint_template.format(id=id),
            payload_type=payload_type,
            payload_list=False,
            api=api,
        )

    def _get_list(
        self,
        payload_type: str,
        endpoint: str,
        limit: int = 10,
        drop: int = 0,
        **kwargs,
    ) -> ResultSet | None:
        """
        Универсальный метод для получения списка объектов с пагинацией.

        :param payload_type: тип для @payload (например, 'location')
        :param endpoint: эндпоинт без параметров (например, "location")
        :param limit: количество записей
        :param drop: смещение
        :return: ResultSet с объектами
        """
        if limit < 0 or drop < 0:
            raise LoyaException("limit and drop must be non-negative")

        params = {"limit": limit, "drop": drop}

        return self.request(
            method="GET",
            endpoint=endpoint,
            params=params,
            payload_type=payload_type,
            payload_list=True,
            **kwargs,
        )

    def _get_simple_list(
        self,
        endpoint: str,
        params: dict | None = None,
        **kwargs,
    ) -> list[int] | None:
        """
        Для эндпоинтов, которые возвращают простые списки (int, str и т.д.)
        """
        if params is None:
            params = {}

        return self.request(
            method="GET",
            endpoint=endpoint,
            params=params,
            parser=JSONParser(),  # ← Просто JSON, без моделей
            **kwargs,
        )

    def convert_date(self, date_str: str) -> datetime:
        if "[" in date_str:
            date_str = date_str.split("[", 1)[0]
        date_str = date_str.replace("Z", "+00:00")

        # Парсим строку с таймзоной
        exp_dt = datetime.fromisoformat(date_str)
        if exp_dt.tzinfo is None:
            exp_dt = exp_dt.replace(tzinfo=ZoneInfo("Europe/Moscow"))

        return exp_dt

    def save_session(self):
        """Сохраняет текущую сессию (куку) в файл"""
        sess_id = self.session.cookies.get("PLAY2AUTH_SESS_ID")
        if not sess_id:
            log.warning("Нет активной сессии для сохранения")
            return

        # Декодируем куку, чтобы взять время истечения
        try:
            payload = self.auth_client.decode_play_session_cookie()
            last_used_dt = self.convert_date(payload["lastUsedDateTime"])
            expiration_dt = (
                last_used_dt + timedelta(seconds=payload["cookieMaxAge"] - 60)
                if last_used_dt
                else None
            )
        except Exception as e:
            log.warning(f"Не удалось извлечь время истечения сессии: {e}")
            expiration_dt = None

        session_data = {
            "PLAY2AUTH_SESS_ID": sess_id,
            "expiration": expiration_dt.isoformat() if expiration_dt else None,
            "saved_at": datetime.now(ZoneInfo("Europe/Moscow")).isoformat(),
        }

        try:
            with open(SESSION_FILE, "w", encoding="utf-8") as f:
                json.dump(session_data, f, ensure_ascii=False, indent=2)
            log.info(f"Сессия сохранена в {SESSION_FILE}")
        except Exception as e:
            log.error(f"Ошибка сохранения сессии: {e}")

    def load_session(self) -> bool:
        """Загружает сессию из файла, если она валидна"""
        if not os.path.exists(SESSION_FILE):
            log.debug("Файл сессии не найден")
            return False

        try:
            with open(SESSION_FILE, "r", encoding="utf-8") as f:
                session_data = json.load(f)

            sess_id = session_data.get("PLAY2AUTH_SESS_ID")
            expiration_str = session_data.get("expiration")

            if not sess_id:
                log.debug("В файле сессии нет куки")
                return False

            # Проверяем, не истекла ли сессия
            if expiration_str:
                expiration_dt = datetime.fromisoformat(expiration_str)
                now = datetime.now().astimezone()
                if now >= expiration_dt:
                    log.info("Сессия из файла истекла — нужен новый логин")
                    os.remove(SESSION_FILE)  # удаляем просроченную
                    return False

            # Загружаем куку в сессию
            self.session.cookies.set("PLAY2AUTH_SESS_ID", sess_id)
            self.auth_client.apply_auth(sess_id)

            log.info(
                f"Сессия загружена из {SESSION_FILE} (действительна до {expiration_str})"
            )
            return True

        except Exception as e:
            log.error(f"Ошибка загрузки сессии: {e}")
            return False

    def request(
        self,
        method,
        endpoint,
        *,
        params=None,
        headers=None,
        json_payload=None,
        parser=None,
        post_data=None,
        require_auth=True,
        api="1.0",
        **kwargs,
    ):
        if require_auth and not self.auth_client.sess_id:
            raise LoyaException("Authentication is required!")

        if headers is None:
            headers = {}
        headers["User-Agent"] = self.user_agent

        if json_payload is not None:
            headers["Content-Type"] = "application/json"

        path = f"/api/{api}/{endpoint}"
        url = self.host + path

        if params is None:
            params = {}

        log.debug("PARAMS: %r", params)

        if parser is None:
            parser = self.parser

        try:
            retries_performed = 0
            while retries_performed <= self.retry_count:
                auth = None
                # if self.auth:
                #     auth = self.auth.apply_auth()

                try:
                    resp = self.session.request(
                        method,
                        url,
                        params=params,
                        headers=headers,
                        data=post_data,
                        json=json_payload,
                        timeout=self.timeout,
                        auth=auth,
                    )
                except Exception as e:
                    raise LoyaException(f"Failed to send request: {e}").with_traceback(
                        sys.exc_info()[2],
                    )

                if HTTPStatus.OK <= resp.status_code < HTTPStatus.MULTIPLE_CHOICES:
                    result = parser.parse(
                        resp.text,
                        api=self,
                        payload_list=kwargs.get("payload_list", False),
                        payload_type=kwargs.get("payload_type"),
                    )
                    return result

                error_json = None
                if resp.headers.get("Content-Type", "").startswith("application/json"):
                    try:
                        error_json = resp.json()
                    except json.JSONDecodeError:
                        raise Exception("Невозможно преобразовать ответ в JSON")

                if error_json and isinstance(error_json, dict):
                    api_code = error_json.get("code")
                    message = error_json.get("message") or ""

                    # Преобразуем "фейковые" ошибки в правильные
                    if (
                        api_code == HTTPStatus.NOT_FOUND
                        or "не найден" in message.lower()
                    ):
                        raise NotFound(resp, response_json=error_json) from None
                    if (
                        api_code == HTTPStatus.UNAUTHORIZED
                        or "авторизация" in message.lower()
                    ):
                        self.auth(force=True)
                        log.info("Перелогиниваемся")
                        raise Unauthorized(resp, response_json=error_json) from None
                    if (
                        api_code == HTTPStatus.FORBIDDEN
                        or "заблокирован" in message.lower()
                    ):
                        raise Forbidden(resp, response_json=error_json) from None

                    # Общее исключение для остальных
                    raise LoyaServerError(resp, response_json=error_json) from None

                self.last_response = resp

                # Check if the response is due to a timeout or internal server error
                if (
                    HTTPStatus.GATEWAY_TIMEOUT
                    <= resp.status_code
                    >= HTTPStatus.INTERNAL_SERVER_ERROR
                    or resp.status_code == HTTPStatus.REQUEST_TIMEOUT
                ) and retries_performed < self.retry_count:
                    retries_performed += 1
                    time.sleep(self.retry_delay)
                    continue

                if resp.status_code == HTTPStatus.BAD_REQUEST:
                    raise BadRequest(
                        resp, response_json={"message": resp.text or "Bad Request"}
                    )

                if resp.status_code == HTTPStatus.UNAUTHORIZED:
                    self.auth(force=True)
                    log.info("Перелогиниваемся")
                    raise Unauthorized(resp, response_json={"message": "Unauthorized"})

                if resp.status_code == HTTPStatus.FORBIDDEN:
                    raise Forbidden(resp, response_json={"message": "Forbidden"})

                if resp.status_code == HTTPStatus.NOT_FOUND:
                    raise NotFound(resp, response_json={"message": "Not Found"})

                if resp.status_code == HTTPStatus.TOO_MANY_REQUESTS:
                    raise TooManyRequests(
                        resp, response_json={"message": "Too Many Requests"}
                    )

                if resp.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
                    raise LoyaServerError(
                        resp,
                        response_json={"message": resp.text or "Server Error"},
                    )

                raise HTTPException(resp, response_json={"message": "Unknown error"})
        finally:
            self.session.close()

    def auth(self, force=False):
        self.session.cookies.clear()
        if not force:
            if self.load_session():
                return True

        auth_payload = {
            "email": self.email,
            "password": self.password,
            "apikey": self.apikey,
        }

        response = self.request(
            method="POST",
            endpoint="login",
            json_payload=auth_payload,
            require_auth=False,
            parser=RawParser(),
        )

        resp_text = response.strip() if isinstance(response, str) else ""
        if resp_text != "login success":
            raise LoyaException(f"Ожидали 'login success', а получили: {response!r}")

        if resp_text == "login success":
            cookies = self.session.cookies
            sess_id = cookies.get("PLAY2AUTH_SESS_ID")
            self.auth_client.apply_auth(sess_id)
            log.debug("Authentication successful")
            pprint(self.auth_client.decode_play_session_cookie())
            self.save_session()

            return True

        raise LoyaException("Authentication failed")

    @ensure_auth
    def get_client_count(self, **kwargs) -> int:
        try:
            connect = MySQLdb.connect(
                host=self.loya_database_host,
                user=self.loya_database_user,
                passwd=self.loya_database_password,
                db=self.loya_database,
                charset="utf8",
                use_unicode=True,
            )
            if not connect:
                log.error("Не удалось подключиться к базе данных Loya")

                return 0

            if connect:
                cursor = connect.cursor()
                cursor.execute("SELECT COUNT(*) FROM client")
                result = cursor.fetchone()
                cursor.close()
                connect.close()

                if not result:
                    log.error("Не удалось получить последний ID клиента")

                    return 0

                return result[0]
        except Exception as e:
            log.error(f"Не удалось получить последний ID клиента: {e}")

        return 0

    @ensure_auth
    @payload("location", list=True)
    def get_locations(
        self, limit: int = 10, drop: int = 0, **kwargs
    ) -> None | ResultSet[Location]:
        return self._get_list(
            payload_type="location",
            endpoint="location",
            limit=limit,
            drop=drop,
        )

    @ensure_auth
    def get_location(self, id: int, **kwargs) -> Location | None:
        return self._get_by_id(
            id=id,
            payload_type="location",
            endpoint_template="location/{id}",
        )

    @ensure_auth
    @payload("locationgroup", list=True)
    def get_location_groups(
        self,
        limit: int = 10,
        drop: int = 0,
        **kwargs,
    ) -> None | ResultSet[LocationGroup]:
        return self._get_list(
            payload_type="locationgroup",
            endpoint="location/group",
            limit=limit,
            drop=drop,
        )

    @ensure_auth
    def get_location_group(self, id: int, **kwargs) -> None | LocationGroup:
        return self._get_by_id(
            id=id,
            payload_type="locationgroup",
            endpoint_template="location/group/{id}",
        )

    @ensure_auth
    @payload("rawclient", list=True)
    def get_clients(
        self, limit: int = 10, drop: int = 0, **kwargs
    ) -> None | list[RawClient]:
        return self._get_list(
            payload_type="rawclient",
            endpoint="client",
            limit=limit,
            drop=drop,
        )

    @ensure_auth
    def get_client(self, id: int, **kwargs) -> None | RawClient:
        client = self._get_by_id(
            id=id,
            payload_type="rawclient",
            endpoint_template="client/{id}",
        )

        return client

    @ensure_auth
    @payload("profile")
    def get_clientprofile(self, id: int, **kwargs) -> None | Profile:
        return self._get_by_id(
            id=id,
            api="1.1",
            payload_type="profile",
            endpoint_template="clientprofile/social/{id}",
        )

    @ensure_auth
    def get_client_by_phone(self, phone: str, **kwargs) -> list[RawClient]:
        if not phone or not phone.strip():
            raise LoyaException("phone is required")

        phone = phone.strip()

        if not phone.isdigit() or len(phone) < 10:
            raise LoyaException("invalid phone number format")

        client_ids: list[int] | None = self._get_simple_list(
            endpoint="clientbyphone",
            params={"phone": phone},
        )

        if not client_ids:
            return []

        clients = []

        for client_id in client_ids:
            try:
                client = self.get_client(client_id)
                if client:
                    clients.append(client)
            except NotFound:
                log.warning(
                    f"Клиент с ID {client_id} не найден (хотя был в поиске по телефону)"
                )
            except Exception as e:
                log.error(f"Ошибка при загрузке клиента {client_id}: {e}")

        return clients

    @ensure_auth
    def get_client_by_card(self, card_number: str, **kwargs) -> RawClient:
        if not card_number or not card_number.strip():
            raise LoyaException("card is required")

        card_number = card_number.strip()

        if not card_number.isdigit() or len(card_number) != 13:
            raise LoyaException("invalid card number format")

        card = self.get_card_by_number(card_number)

        if not card:
            raise LoyaException("card not found")

        if not isinstance(card, Card) or not card.clientId:
            raise LoyaException("client id not found")

        client = self.get_client(card.clientId)

        if not client:
            raise LoyaException("client not found")

        return client

    @ensure_auth
    def get_card_by_number(self, card_number: str, **kwargs) -> None | Card:
        if card_number is None:
            raise LoyaException("card_number is required")

        card = self.request(
            method="GET",
            endpoint=f"card/number/{card_number}",
            payload_type="card",
            payload_list=False,
        )

        log.debug(f"card (API): {card}")

        if isinstance(card, Card):
            return card

        return None

    @ensure_auth
    def create_base_client(
        self,
        surname: str,
        name: str,
        phone: str,
        *,
        email: str | None = None,
        patronymic: str | None = None,
        birthday: str | None = None,
        get_sms: bool = True,
        get_email: bool = True,
        segment_id: int = 0,
        refuse_paper_check: bool = False,
        state: str | None = "active",
        **extra_fields,
    ) -> "Client":
        """
        Создание базового клиента через PUT /client/express.

        Используйте этот метод для создания клиента без карты/счёта.

        :return: Созданный объект Client
        """
        if not surname or not surname.strip():
            raise ValueError("Surname is required and cannot be empty")
        if not name or not name.strip():
            raise ValueError("Name is required and cannot be empty")

        phone = self.validate_phone(phone)
        email = self.validate_email(email)
        birthday_date = self.validate_date(birthday, "birthday")

        payload = {
            "surname": surname.strip(),
            "name": name.strip(),
            "patronymic": (patronymic or "").strip(),
            "phone": phone,
            "email": email or "",
            "phoneConfirmed": False,
            "getSms": get_sms,
            "getEmail": get_email,
            "getPush": True,
            "birthday": birthday_date.isoformat() if birthday_date else None,
            "segmentId": segment_id,
            "refusePaperCheck": refuse_paper_check,
            "state": state,
            **extra_fields,
        }

        client = self.request(
            method="PUT",
            endpoint="client/express",
            json_payload=payload,
            payload_type="client",
            payload_list=False,
        )

        if not isinstance(client, Client) or not getattr(client, "id", None):
            raise LoyaException("Failed to create client — invalid response or no ID")

        log.info(f"Created client {client.id}: {client.surname} {client.name}")
        return client

    @ensure_auth
    def client_update(
        self,
        client_id: int,
        client: Client,
    ) -> Client | None:
        log.debug(f"Updating client {client_id}")

        if client_id <= 0:
            raise ValueError("Invalid client_id")

        if not isinstance(client, Client) or not getattr(client, "id", None):
            raise ValueError("Invalid client object")

        payload_client = client.to_dict()
        del payload_client["updatedAt"]
        del payload_client["createdAt"]

        try:
            client_response = self.request(
                method="POST",
                endpoint=f"client/{client_id}",
                json_payload=payload_client,
                payload_type="client",
                payload_list=False,
            )
        except Exception as e:
            raise LoyaException(f"Failed to update client: {e}")

        if not isinstance(client_response, Client):
            raise LoyaException("Failed to update client — invalid response or no ID")

        log.info(f"Updated client {client.id}")

        return client_response

    @ensure_auth
    def update_social_profile(
        self,
        client_id: int,
        *,
        birthday: str | None = None,
        sex: str | None = None,
        has_child: bool | None = None,
        has_pets: bool | None = None,
        child_count: int | None = None,
        civil_status: str | None = None,
        use_automobile: bool | None = None,
        is_pensioner: bool | None = None,
        custom_fields: dict | None = None,
        pets: list[ClientPet] | None = None,
        children: list[ClientChild] | None = None,
    ) -> None:
        """
        Обновление социального профиля клиента.

        POST /clientprofile/social/{client_id}
        """
        if client_id <= 0:
            raise ValueError("Invalid client_id")

        social: dict[str, str | int | None] = {"clientId": client_id}

        birthday_date = None
        age = 0
        if birthday is not None:
            birthday_date = self.validate_date(birthday, "birthday")
            if birthday_date:
                age = birthday_date and datetime.now().year - birthday_date.year
                social["birthday"] = birthday_date.strftime("%Y-%m-%d")
                social["age"] = age

        for key, value in {
            "sex": sex.lower() if sex else None,
            "hasChild": has_child,
            "hasPets": has_pets,
            "childCount": child_count,
            "civilStatus": civil_status.lower() if civil_status else None,
            "useAutomobile": use_automobile,
            "isPensioner": is_pensioner,
            "customFields": custom_fields,
        }.items():
            if value is not None:
                social[key] = value

        payload: dict[str, Any] = {"social": social}

        if pets is not None:
            payload["clientPets"] = [
                {
                    "name": pet.name,
                    "description": pet.description,
                    "petType": pet.petType,
                }
                for pet in pets
            ]

        if children is not None:
            payload["clientChilds"] = [
                {
                    "surname": child.surname,
                    "name": child.name,
                    "patronymic": child.patronymic,
                    "birthday": child.birthday.isoformat() if child.birthday else None,
                    "sex": child.sex.lower() if child.sex else None,
                }
                for child in children
            ]

        if len(payload) == 1 and len(social) == 1:
            log.debug(f"No fields to update for client {client_id}")
            return

        self.request(
            method="POST",
            api="1.1",
            endpoint=f"clientprofile/social/{client_id}",
            json_payload=payload,
            payload_type="json",
            payload_list=False,
        )
        log.info(f"Social profile updated for client {client_id}")

    def get_social_profile(
        self,
        client_id: int,
    ) -> Profile | None:
        """
        Получение социального профиля клиента.

        GET /clientprofile/social/{client_id}
        """
        log.info(f"Social profile fetch for client {client_id}")
        if client_id <= 0:
            raise ValueError("Invalid client_id")

        profile = self.request(
            method="GET",
            api="1.1",
            endpoint=f"clientprofile/social/{client_id}",
            parser=JSONParser(),
            payload_list=False,
        )

        if not profile:
            raise LoyaException("Failed to get social profile")

        return profile

    @ensure_auth
    def create_card(
        self,
        client_id: int = 0,
        *,
        prefix: str = "551",
        generator: str = "EAN13",
        pin: str | None = None,
    ) -> Card | None:
        """Генерация новой карты для клиента"""

        # if client_id <= 0:
        #     raise ValueError("Invalid client_id")

        if not prefix or len(prefix) > 4:
            raise ValueError("Card prefix must be 1-4 characters")

        params = f"generator={generator}&prefix={prefix}"
        payload = {
            "clientId": client_id,
            "pin": pin,  # если нужен PIN
        }

        card = self.request(
            method="PUT",
            endpoint=f"card?{params}",
            json_payload=payload,
            payload_type="card",
            payload_list=False,
        )

        if not isinstance(card, Card) or not card.id:
            raise LoyaException("Card creation failed — no ID returned")

        log.info(
            f"Created card {card.id} for client {client_id}, number: {card.number}"
        )
        return card

    @ensure_auth
    def attach_card_to_client(self, card_id: int, client_id: int) -> None:
        """Привязка существующей карты к клиенту"""
        if card_id <= 0 or client_id <= 0:
            raise ValueError("Invalid card_id or client_id")

        self.request(
            method="PUT",
            endpoint=f"card/attach/{card_id}/{client_id}",
            parser=RawParser(),
        )
        log.info(f"Attached card {card_id} to client {client_id}")

    @ensure_auth
    def activate_card(self, card_id: int, status: str = "active") -> None:
        """Активация карты"""
        if card_id <= 0:
            raise ValueError("Invalid card_id")
        if status not in ("active", "nonactive", "blocked", "archive"):
            raise ValueError(
                "Status must be 'active', 'nonactive', 'blocked' or 'archive'"
            )

        self.request(
            method="PUT",
            endpoint=f"card/status/{card_id}/{status}",
            parser=RawParser(),
        )
        log.info(f"Set card {card_id} status to '{status}'")

    @ensure_auth
    def create_card_full(
        self,
        client_id: int = 0,
        prefix: str = "551",
        pin: str | None = None,
    ) -> Card:
        """
        Полный цикл: создание → привязка → активация карты.

        Удобный метод для быстрого создания активной карты.
        """

        card = self.create_card(client_id, prefix=prefix, pin=pin)
        if card is None:
            raise LoyaException("Failed to create card")

        self.attach_card_to_client(card.id, client_id)
        self.activate_card(card.id, "active")

        return card

    @ensure_auth
    def create_account(
        self,
        *,
        award_amount: float = 0.0,
        sales_amount: float = 0.0,
        burnt_amount: float = 0.0,
    ) -> Account | None:
        """Создание счёта"""

        payload = {
            "awardAmount": float(award_amount),
            "salesAmount": float(sales_amount),
            "burntAmount": float(burnt_amount),
            "state": "active",
        }

        account = self.request(
            method="PUT",
            endpoint="account",
            json_payload=payload,
            payload_type="account",
            payload_list=False,
        )

        if not isinstance(account, Account) or not account.id:
            raise LoyaException("Failed to create account")

        log.info(f"Created account {account.id}")

        return account

    @ensure_auth
    def attach_account_to_client(self, client_id: int, account_id: int) -> None:
        """Привязка счёта к клиенту"""

        if client_id <= 0 or account_id <= 0:
            raise ValueError("Invalid client_id or account_id")

        self.request(
            method="PUT",
            endpoint=f"client/attach/{client_id}/{account_id}",
            parser=RawParser(),
        )

        log.info(f"Attached account {account_id} to client {client_id}")
