import json
import logging
from datetime import date, datetime
from typing import Generic, Optional, TypeVar, Union, get_args, get_origin

from pydantic import Field
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined

from .errors import LoyaException, NotFound

T = TypeVar("T")
log = logging.getLogger(__name__)


class ResultSet(list, Generic[T]):
    """A list like object that holds results from a Twitter API query."""

    def __init__(self, max_id=None, since_id=None):
        super().__init__()
        self._max_id = max_id
        self._since_id = since_id

    @property
    def max_id(self):
        if self._max_id:
            return self._max_id
        ids = self.ids()
        # Max_id is always set to the *smallest* id, minus one, in the set
        return (min(ids) - 1) if ids else None

    @property
    def since_id(self):
        if self._since_id:
            return self._since_id
        ids = self.ids()
        # Since_id is always set to the *greatest* id in the set
        return max(ids) if ids else None

    def ids(self):
        return [item.id for item in self if hasattr(item, "id")]

    def print_json(self, indent: int = 2):
        data = [item.to_dict() for item in self]
        print(json.dumps(data, indent=indent, ensure_ascii=False))


class Model:
    _api = None

    def __init__(self, api=None):
        self._api = api

    def __getstate__(self):
        pickle = self.__dict__.copy()
        pickle.pop("_api", None)
        return pickle

    def to_dict(self):
        """Рекурсивно конвертирует объект и вложенные модели в dict"""
        data = {}
        for k, v in vars(self).items():
            if k.startswith("_"):
                continue
            # Пропускаем FieldInfo — это метаданные, не данные
            if isinstance(v, FieldInfo):
                continue
            if isinstance(v, Model):
                data[k] = v.to_dict()
            elif isinstance(v, list):
                data[k] = [
                    item.to_dict() if isinstance(item, Model) else item for item in v
                ]
            elif isinstance(v, date):
                data[k] = v.strftime("%Y-%m-%d")
            elif isinstance(v, datetime):
                data[k] = v.strftime("%Y-%m-%d %H:%M:%S")
            elif isinstance(v, dict):
                data[k] = {
                    sk: sv.to_dict() if isinstance(sv, Model) else sv
                    for sk, sv in v.items()
                }
            else:
                data[k] = v
        return data

    def print_json(self, indent: int = 2):
        print(json.dumps(self.to_dict(), indent=indent, ensure_ascii=False))

    @classmethod
    def _get_default_value(cls, field_type, field_default):
        """
        Получает значение по умолчанию для поля.
        """
        if isinstance(field_default, FieldInfo):
            # Проверяем default в FieldInfo
            if (
                hasattr(field_default, "default") 
                and field_default.default is not PydanticUndefined  # ← Ключевое изменение!
                and field_default.default is not ...
                and field_default.default is not None
            ):
                return field_default.default
            
            # Проверяем default_factory
            if (
                hasattr(field_default, "default_factory") 
                and field_default.default_factory is not None
            ):
                return field_default.default_factory()
            
            # Нет default → используем типизированное значение
            return cls._get_type_default(field_type)
        
        # Проверяем на PydanticUndefined напрямую
        if field_default is PydanticUndefined or field_default is ...:
            return cls._get_type_default(field_type)
        
        if field_default is not None:
            return field_default
        
        return cls._get_type_default(field_type)

    @classmethod
    def _get_type_default(cls, field_type):
        """Возвращает значение по умолчанию для типа"""
        if field_type is None:
            return None
            
        origin = get_origin(field_type)
        args = get_args(field_type)
        
        # Optional[X] или Union[X, None]
        if origin is Union:
            non_none = [a for a in args if a is not type(None)]
            if non_none:
                return cls._get_type_default(non_none[0])
            return None
        
        if origin is list:
            return []
        if origin is dict:
            return {}
        if field_type is int:
            return 0
        if field_type is float:
            return 0.0
        if field_type is str:
            return ""
        if field_type is bool:
            return False
        
        return None

    @classmethod
    def parse(cls, api, json_data: dict):
        """Парсит один объект"""
        if not isinstance(json_data, dict):
            raise ValueError(f"Expected dict for {cls.__name__}, got {type(json_data)}")

        instance = cls(api)
        
        # Собираем аннотации из всей иерархии классов
        all_annotations = {}
        for klass in reversed(cls.__mro__):
            if hasattr(klass, "__annotations__"):
                all_annotations.update(klass.__annotations__)
        
        # ШАГ 1: Инициализируем все поля значениями по умолчанию
        for attr_name, attr_type in all_annotations.items():
            if attr_name.startswith("_"):
                continue
            
            # Ищем default в иерархии классов
            class_default = None
            for klass in cls.__mro__:
                if hasattr(klass, attr_name):
                    class_default = getattr(klass, attr_name)
                    break
            
            default_value = cls._get_default_value(attr_type, class_default)
            setattr(instance, attr_name, default_value)
        
        # ШАГ 2: Заполняем из JSON
        cls._populate_from_json(instance, json_data)
        
        return instance

    @classmethod
    def parse_list(cls, api, json_list) -> list:
        """Парсит список объектов (или один объект)"""
        results = []

        if isinstance(json_list, dict):
            results.append(cls.parse(api, json_list))
            return results

        if isinstance(json_list, list):
            for item in json_list:
                if item:
                    results.append(cls.parse(api, item))
            return results

        raise ValueError(
            f"Expected list or dict for {cls.__name__} list, got {type(json_list)}"
        )

    def __repr__(self):
        attrs = self.to_dict()
        items = [f"{k}={v!r}" for k, v in attrs.items()]
        return f"{self.__class__.__name__}({', '.join(items)})"

    @classmethod
    def _populate_from_json(cls, instance, json_data: dict):
        """Внутренний метод — заполняет объект атрибутами из JSON"""
        instance._json = json_data

        # Собираем аннотации из всей иерархии
        all_annotations = {}
        for klass in reversed(cls.__mro__):
            if hasattr(klass, "__annotations__"):
                all_annotations.update(klass.__annotations__)

        for key, value in json_data.items():
            if value is None:
                continue
                
            expected_type = all_annotations.get(key)

            if expected_type is None:
                setattr(instance, key, value)
                continue

            origin = get_origin(expected_type)
            args = get_args(expected_type)

            converted = cls._convert_value(value, expected_type, origin, args)
            setattr(instance, key, converted)

    @classmethod
    def _convert_value(cls, value, expected_type, origin, args):
        """Конвертирует значение в нужный тип"""
        if value is None:
            return None

        # datetime → из строки
        if expected_type in (datetime, date):
            if isinstance(value, str):
                if expected_type is datetime:
                    return datetime.fromisoformat(value.replace(" ", "T"))
                if expected_type is date:
                    return date.fromisoformat(value.split(" ")[0])
            return value

        # bool → иногда приходит как строка
        if expected_type is bool and isinstance(value, str):
            return value.lower() in ("true", "1", "yes", "on")

        # List[Something]
        if origin is list and args:
            item_type = args[0]
            if isinstance(value, list):
                return [
                    cls._convert_value(
                        item,
                        item_type,
                        get_origin(item_type),
                        get_args(item_type),
                    )
                    for item in value
                ]

        return value


class GenericError(Model):
    code: int = Field(..., description="Код ошибки")
    message: str = Field(..., description="Сообщение об ошибке")


class Location(Model):
    """Параметры магазина"""

    id: int = Field(..., description="ID магазина")
    partnerId: int = Field(..., description="ID партнера")
    name: str = Field(..., description="Название магазина")
    description: str = Field(..., description="Описание магазина")
    code: str = Field(..., description="Код магазина")
    address: str = Field(..., description="Адрес магазина")
    locationGroupIds: list[int] = Field(
        ...,
        description="Список ID локаций, в которые входит магазин",
    )

    # bySum, onlyCash, maxValue
    applyCashDiscount: str = Field(..., description="Применять скидки на кассе")


class LocationGroup(Model):
    """Параметры локации (группы магазинов)"""

    id: int = Field(..., description="ID локации")
    partnerId: int = Field(..., description="ID партнера")
    code: str = Field(..., description="Код локации")
    name: str = Field(..., description="Название локации")
    locationIds: list[int] = Field(
        ..., description="Список ID магазинов, входящих в локацию"
    )


class SubscriptionSbp(Model):
    """Реквизиты для оплаты по подписке СБП"""

    subscriptionToken: str = Field(
        ...,
        description="Токен безопасности (Токен СБП) покупателя, выданный НСПК",
        max_length=32,
    )

    subscriptionQrcId: str = Field(
        ...,
        description="Идентификатор QR кода в СБП (тип 03) по которому была подписка",
        max_length=32,
    )

    # "ACWP" – привязка счета Плательщика выполнена
    # "RJCT" – привязка счета Плательщика не выполнена
    state: str = Field(..., description="Статус оформления подписки")


class SystemParams(Model):
    """Системные параметры клиента"""

    registrationDateExpress: datetime = Field(
        ..., description="Дата экспресс регистрации"
    )
    fillFormDate: datetime = Field(..., description="Дата заполнения анкеты")
    merchantId: int = Field(..., description="Место регистрации (ID магазина)")
    userId: int = Field(..., description="Кто регистрировал (ID пользователя)")
    lastModifyUserId: int = Field(..., description="Кто редактировал (ID пользователя)")
    photoUrl: str = Field(..., description="URL фотографии")
    completeness: float = Field(..., description="Полнота данных")
    signsOfFraud: int = Field(..., description="Признаки мошенничества")
    categoryId: int = Field(..., description="Категория клиента")

    # Operator - зарегистрирован оператором (веб)
    # PersonalProfile - зарегистрирован через "Личный кабинет"
    # MobileApplication - зарегистрирован через "Мобильное приложение"
    # PartnerApplication - зарегистрирован через "Приложение-партнер"
    registrationSource: str = Field(..., description="Источник регистрации клиента")

    mobileAppExist: bool = Field(
        ..., description="Наличие мобильного приложения у клиента"
    )
    subscriptionSbp: SubscriptionSbp | None = None


class Wallet(Model):
    """
    Кошелек
    """

    card: str = Field(..., description="Идентификатор карты в Wallet")
    url: str = Field(..., description="Cсылка для загрузки карты")
    inUse: bool = Field(..., description="клиент юзает/перестал юзать AppleWallet")


class RawCheckItemStorage(Model):
    skuName: str = Field(..., description="Название товара")
    categoryName: str = Field(..., description="Название категории")


class RawCheck(Model):
    id: int = Field(..., description="ID чека")
    checkId: str = Field(..., description="ID чека")
    originalId: int = Field(..., description="ID оригинального чека")
    number: str = Field(..., description="Номер чека")
    chType: int = Field(..., description="Тип чека")
    date: str = Field(..., description="Дата чека")
    dayId: int = Field(..., description="ID дня")
    monthId: int = Field(..., description="ID месяца")
    yearId: int = Field(..., description="ID года")
    merchantId: int = Field(..., description="ID магазина")
    partnerId: int = Field(..., description="ID партнера")
    clientId: int = Field(..., description="ID клиента")
    segmentId: int = Field(..., description="ID сегмента")
    total: float = Field(..., description="Сумма чека")
    positionCount: int = Field(..., description="Количество позиций")
    discount: float = Field(..., description="Скидка")
    awardBonus: float = Field(..., description="Бонус")
    usedBonus: float = Field(..., description="Использованный бонус")
    bonusStartDate: str = Field(..., description="Дата начала использования бонуса")
    state: str = Field(..., description="Статус чека")


class RawCheckStorage(Model):
    check: RawCheck = Field(..., description="Чек")
    # client: RawCheckClient = Field(..., description="Клиент")
    paidInCurrency: float = Field(..., description="Оплачено в валюте")
    usedBonusInCurrency: float = Field(..., description="Использованный бонус в валюте")
    merchantName: str = Field(..., description="Название магазина")
    merchantCode: str = Field(..., description="Код магазина")
    cardNumber: str = Field(..., description="Номер карты")
    hasResults: bool = Field(..., description="Есть ли результаты")
    # items: list[RawCheckItemStorage] = Field(..., description="Позиции в чеке")
    # awards: list[RawCheckAwards] = Field(..., description="Награды")
    # gifts: list[RawCheckGift] = Field(..., description="Подарки")

    @classmethod
    def parse(cls, api, json_data: dict):
        instance = super().parse(api, json_data)

        # Явно парсим вложенный check
        if "check" in json_data and isinstance(json_data["check"], dict):
            instance.check = RawCheck.parse(api, json_data["check"])

        # То же самое для client, если хотите
        if "client" in json_data and isinstance(json_data["client"], dict):
            instance.client = Client.parse(
                api, json_data["client"]
            )  # если Client подходит

        return instance


class Client(Model):
    """Данные клиента"""

    id: int = Field(..., description="ID клиента")
    accountId: int = Field(..., description="ID счёта")
    segmentId: int = Field(..., description="ID сегмента")
    surname: str = Field(..., description="Фамилия клиента")
    name: str = Field(..., description="Имя клиента")
    patronymic: str = Field(..., description="Отчество клиента")
    phone: str = Field(..., description="Номер телефона клиента")
    phoneConfirmed: bool = Field(..., description="Номер телефона клиента подтвержден")
    email: str = Field(..., description="E-mail адрес клиента")
    getEmail: bool = Field(..., description="Получать e-mail рассылки")
    getSms: bool = Field(..., description="Получать sms рассылку")
    preferChannelType: str = Field(..., description="Предпочитаемый канал связи")
    birthday: date = Field(..., description="День рождения клиента")
    locality: str = Field(..., description="Город проживания")
    street: str = Field(..., description="Улица проживания")
    login: str = Field(..., description="Логин")
    password: str = Field(..., description="Пароль")
    refusePaperCheck: bool = Field(..., description="Отказ от печати бумажного чека")
    SystemParams: SystemParams
    createdAt: datetime = Field(..., description="Дата-время создания клиента")
    updatedAt: datetime = Field(..., description="Дата редактирования анкеты")
    wallet: Wallet = Field(..., description="Данные кошелька")
    retailId: str = Field(..., description="ID магазина")
    state: str = Field(..., description="Статус клиента")
    getPush: bool = Field(..., description="Получать push рассылки")

    @staticmethod
    def get_last_id(api: "API") -> int:
        return api.get_last_client_id()

    @staticmethod
    def find_by_id(
        api: "API",
        client_id: int,
    ) -> Optional["Client"]:
        client = api.get_client(client_id)

        if client and client.client:
            phone = client.client.phone

            if (
                phone 
                and isinstance(phone, str) 
                and phone is not PydanticUndefined
            ):
                client.client.phone = (
                    client.client.phone.replace("+", "")
                    .replace(" ", "")
                    .replace("-", "")
                    .replace("(", "")
                    .replace(")", "")
                )

            return client.client
        return None

    @staticmethod
    def find_by_phone(
        api: "API",
        phone: str,
    ) -> "list[Client]":
        if not phone or not phone.strip():
            raise LoyaException("phone is required")

        phone = (
            phone.strip()
            .replace("+", "")
            .replace(" ", "")
            .replace("-", "")
            .replace("(", "")
            .replace(")", "")
        )

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

        # card = Card.find_by_phone(api, phone)
        # if card:
        #     return [Client.find_by_id(api, card.clientId)]

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

        if not client_ids:
            return []

        clients = []

        for client_id in client_ids:
            try:
                client = Client.find_by_id(api, 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

    def age(self) -> int:
        if not self.birthday or not isinstance(self.birthday, date):
            return 0
        return (date.today() - self.birthday).days // 365

    def profile(self) -> "Profile":
        profile = self._api.request(
            method="GET",
            api="1.1",
            payload_type="profile",
            endpoint=f"clientprofile/social/{self.id}",
        )

        if not profile or not isinstance(profile, Profile):
            raise LoyaException("Failed to get client profile")

        return profile

    def account(self) -> "Account":
        account = Account.find_by_id(self._api, self.accountId)

        if not account or not isinstance(account, Account):
            raise LoyaException("Failed to get client account")

        return account

    def balance(self) -> float:
        account = self.account()

        if not account or not isinstance(account, Account):
            raise LoyaException("Failed to get client balance")

        return account.balance

    def qr(self) -> str:
        qr = self._api.request(
            method="GET",
            payload_type="str",
            endpoint=f"qr/client/{self.id}",
        )
        if not qr or not isinstance(qr, str):
            raise LoyaException("Failed to get qr code")

        return qr

    def update(self) -> "Client":
        self.updatedAt = None
        self.createdAt = None

        client_response = self._api.request(
            method="POST",
            endpoint=f"client/{self.id}",
            json_payload=self.to_dict(),
            payload_type="rawclient",
            payload_list=False,
        )

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

        if not client_response.client:
            raise LoyaException("Failed to update client — no client in response")

        return client_response.client

    def save(self) -> "Client":
        return self.update()

    def transactions(self, limit: int = 10, drop: int = 0) -> list["RawCheckStorage"]:
        payload = {"clientId": self.id, "limit": limit, "drop": drop}

        raw = self._api.request(
            method="POST",
            endpoint="clienttransaction",
            json_payload=payload,
            payload_type="check",
            payload_list=True,
        )

        if not raw or not isinstance(raw, list):
            log.warning("Failed to get client transactions")
            return []

        return raw


class SocialStatus(Model):
    """Данные о социальном статусе"""

    id: int = Field(..., description="ID записи")
    name: str = Field(..., description="Наименование соц статуса")
    description: str = Field(..., description="Описание соц статуса")


class CustomField(Model):
    pass


class Social(Model):
    """Общие соц.-дем. данные клиента"""

    clientId: int = Field(..., description="ID клиента")
    birthday: date = Field(..., description="День рождения")
    age: int = Field(..., description="Возраст")
    hasChild: bool = Field(..., description="Есть дети")
    childCount: int = Field(..., description="Количество детей")
    hasPets: bool = Field(..., description="Есть животные")

    # unmarried - Не женат
    # married - Женат
    civilStatus: str = Field(..., description="Гражданский статус")

    # male - мужчина
    # female - женщина
    sex: str = Field(..., description="Пол человека", examples=["male", "female"])

    SocialStatuses: list[SocialStatus] = Field(
        ..., description="Список социальных статусов"
    )
    useAutomobile: bool = Field(..., description="Владение автомобилем")

    isPensioner: bool = Field(..., description="Пенсионер")
    isPensionerSince: date = Field(
        ..., description="Дата проставления признака пенсионер"
    )

    customFields: list[CustomField] = Field(..., description="Дополнительные поля")


class ClientPet(Model):
    """Данные о домашних животных"""

    id: int = Field(..., description="ID записи")
    clientId: int = Field(..., description="ID клиента")
    name: str = Field(..., description="Имя (кличка)")
    description: str = Field(..., description="Описание")

    # any - любое
    # dog - собака
    # cat - кошка
    # fish - рыбки
    # bird - птицы
    # none - нет животного
    petType: str = Field(..., description="Тип животного")

    def __init__(
        self, clientId: int, name: str, description: str, petType: str = "any"
    ):
        if not clientId:
            raise Exception("clientId is required")

        if not name:
            raise Exception("name is required")

        self.clientId = clientId
        self.name = name.capitalize()
        self.description = description
        self.petType = petType.lower()

    def to_dict(self):
        return {
            "clientId": self.clientId,
            "name": self.name,
            "description": self.description,
            "petType": self.petType,
        }


class ClientChild(Model):
    """Данные о детях"""

    id: int = Field(..., description="ID записи")
    clientId: int = Field(..., description="ID клиента")
    surname: str = Field(..., description="Фамилия")
    name: str = Field(..., description="Имя")
    patronymic: str = Field(..., description="Отчество")
    birthday: date = Field(..., description="День рождения")
    sex: str = Field(..., description="Пол человека")

    def __init__(
        self,
        clientId: int,
        surname: str,
        name: str,
        patronymic: str,
        birthday: str,
        sex: str = "male",
    ):
        if not clientId:
            raise Exception("clientId is required")
        if not name:
            raise Exception("name is required")

        self.surname = surname.capitalize()
        self.name = name.capitalize()
        self.patronymic = patronymic.capitalize()
        self.birthday = datetime.strptime(birthday, "%Y-%m-%d").date() or None
        self.sex = sex.lower()

    def to_dict(self):
        return {
            "clientId": self.clientId,
            "surname": self.surname,
            "name": self.name,
            "patronymic": self.patronymic,
            "birthday": self.birthday.isoformat(),
            "sex": self.sex,
        }


class Profile(Model):
    social: Social
    clientPets: list[ClientPet]
    clientChilds: list[ClientChild]


class Geo(Model):
    longitude: float = Field(..., description="Долгота")
    latitude: float = Field(..., description="Широта")


class Details(Model):
    merchantName: str = Field(..., description="Название магазина")
    userName: str = Field(..., description="Имя пользователя")


class Card(Model):
    """Данные карты"""

    id: int = Field(..., description="ID карты")
    number: str = Field(..., description="Номер карты")
    clientId: int = Field(default=0, description="ID клиента")
    userId: int = Field(..., description="Кто регистрировал (ID пользователя)")
    createDate: datetime = Field(..., description="Дата создания карты")
    activationDate: date = Field(..., description="Дата активации карты")

    # nonactive, active, blocked, archive
    state: str = Field(..., description="Статус карты")

    pin: str = Field(..., description="Пин карты")

    @staticmethod
    def create(
        api: "API",
        prefix: str = "551",
        generator: str = "EAN13",
        pin: str | None = None,
    ) -> "Card":
        """
        Статический метод создания новой карты.
        """
        if not prefix or len(prefix) > 3:
            raise ValueError("Card prefix must be 1-4 characters")

        params = f"generator={generator}&prefix={prefix}"
        payload = {
            "clientId": 0,
            "pin": pin,
        }

        card = api.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"Создана карта {card.id} (номер: {card.number})")

        return card

    @staticmethod
    def find_by_id(
        api: "API",
        card_id: int,
    ) -> Optional["Card"]:
        """
        Статический метод поиска карты по ID.
        Возвращает объект Card или None, если карта не найдена.
        """
        if card_id <= 0:
            raise ValueError("card_id is required")

        card = api.request(
            method="GET",
            endpoint=f"card/{card_id}",
            payload_type="card",
            payload_list=False,
        )

        if isinstance(card, Card) and card.id:
            return card
        return None

    @staticmethod
    def find_by_number(
        api: "API",
        card_number: str,
    ) -> Optional["Card"]:
        """
        Статический метод поиска карты по номеру.
        Возвращает объект Card или None, если карта не найдена.
        """
        if not card_number:
            raise ValueError("card_number is required")

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

        if isinstance(card, Card) and card.id:
            return card
        return None

    def is_clean(self) -> bool:
        """
        Проверка на чистоту карты (её можно привязать к клиенту).
        """
        client_id = getattr(self, "clientId", None)

        if (
            client_id is None
            or not isinstance(client_id, (int, float))
            or client_id <= 0
        ):
            return True

        try:
            client = Client.find_by_id(self._api, int(client_id))
            if client and client.name == self.number:
                return True
            return False
        except LoyaException as e:
            if "не найден" in str(e).lower():
                return True
            raise

    def set_status(self, status: str = "active") -> None:
        """
        Установка статуса карты.
        """
        if self.id <= 0:
            raise ValueError("Invalid card_id")

        if self._api is None:
            raise RuntimeError(
                f"API instance is not set on this {self.__class__.__name__} object"
            )

        valid_statuses = {"active", "nonactive", "blocked", "archive"}
        if status not in valid_statuses:
            raise ValueError(f"Status must be one of {valid_statuses}")

        from .parsers import RawParser

        self._api.request(
            method="PUT",
            endpoint=f"card/status/{self.id}/{status}",
            parser=RawParser(),
        )
        self.state = status
        log.info(f"Карта {self.number} (ID {self.id}) установлена в статус '{status}'")

    def activate(self) -> None:
        """
        Активация карты.
        """
        self.set_status("active")

    def deactivate(self) -> None:
        """
        Деактивация карты (установка статуса "Выдана").
        """
        self.set_status("nonactive")

    def block(self) -> None:
        """
        Блокирование карты.
        """
        self.set_status("blocked")

    def archive(self) -> None:
        """
        Архивирование карты.
        """
        self.set_status("archive")

    def attach_to_client(self, client_id: int) -> None:
        """
        Привязка карты к клиенту.
        """
        if self.id <= 0 or client_id <= 0:
            raise ValueError("Invalid card_id or client_id")

        if self._api is None:
            raise RuntimeError(
                f"API instance is not set on this {self.__class__.__name__} object"
            )

        from .parsers import RawParser

        self._api.request(
            method="PUT",
            endpoint=f"card/attach/{self.id}/{client_id}",
            parser=RawParser(),
        )
        log.info(f"Карта {self.number} (ID {self.id}) привязана к клиенту {client_id}")

    def get_client(self) -> Optional["Client"]:
        """
        Поиск клиента по ID карты
        """
        if self._api is None:
            raise RuntimeError(
                f"API instance is not set on this {self.__class__.__name__} object"
            )

        client = Client.find_by_id(self._api, self.clientId)
        if client is None or not isinstance(client, Client):
            log.debug("Клиент не найден для карты с ID %s", self.id)
            return None

        log.debug(f"Найден клиент {client.name} (ID {client.id})")

        return client


class BonusBurnSaveDate(Model):
    """
    День месяца для ограничения сжигания бонусов(опционально, только если сжигают раз в год), если бонусы начислены позже этого дня, то они в дату сжигания не сжигаются, сжигаются в след году
    """

    bonusBurnSaveDay: int = Field(..., description="День сохранения бонусов")
    bonusBurnSaveMonth: int = Field(..., description="Месяц сохранения бонусов")


class AccountBonusValiditySettings(Model):
    """
    Доплнительные параметры бонусов
    """

    bonusValidity: int = Field(..., description="Срок действия бонусов (кол-во)")

    # day, month, week, year
    bonusPeriod: str = Field(..., description="Период дествия бонусов")

    # atonce - бонусы начинают действовать сразу
    # withdaydelay - указана задержка в днях
    # monthday - бонусы начинают действовать в определенное число месяца
    bonusStartMode: str = Field(..., description="Режим начала действия бонусов")

    bonusStartValue: int = Field(
        ...,
        description="Через сколько дней после начисления начинают действовать баллы. Округляем в меньшую сторону, т.е. если баллы начислены 01.01.2015 в 16:00 и bonusStartDelay = 1 (день), то бонусы будут перечислены на счет 02.01.2015 в ~ 00:00 (актором, по расписанию)",
    )

    # unlimited - у бонусов нет даты окончания действия
    # duration - указано время жизни бонусов (в днях / месяцах) после начала действия
    # annually - сжигать баллы раз в год в указанный день указанного месяца
    # monthday - сжигать баллы в указанное число каждого месяца
    bonusBurnMode: str = Field(
        ...,
        description="Режим сжигания баллов, стандартный либо один из пользовательских",
    )

    bonusBurnDay: int = Field(
        ...,
        description="День, в который нужно сжигать бонусы (опционально, один раз в год)",
    )
    bonusBurnMonth: int = Field(
        ...,
        description="Месяц, в который нужно сжигать бонусы (опционально, один раз в год)",
    )

    bonusBurnSaveDate: BonusBurnSaveDate = Field(
        ...,
        description="День месяца для ограничения сжигания бонусов(опционально, только если сжигают раз в год), если бонусы начислены позже этого дня, то они в дату сжигания не сжигаются, сжигаются в след году",
    )
    bonusWaitConfirmDaysCount: int = Field(
        ...,
        description="Кол-во дней, которое ожидаем confirm, если по их прошествии нет confirm, то возвращаем на счет списанные бонусы",
    )


class AccountUpdateRecord(Model):
    """
    Данные для корректировки счёта
    """

    amount: float = Field(..., description="Сумма для корректировки")
    description: str = Field(..., description="Описание операции", max_length=254)

    # credit, debit
    operType: str = Field(..., description="Тип операции")

    partnerId: int = Field(
        ...,
        description="ID партнера, от имени которого будет сделана корректировка",
    )
    bonusValiditySettings: AccountBonusValiditySettings = Field(
        ...,
        description="Доплнительные параметры бонусов",
    )


class Account(Model):
    """
    Данные счёта
    """

    id: int = Field(..., description="ID счета")
    number: str = Field(..., description="Номер счета")
    balance: float = Field(..., description="Активный баланс счёта")
    balanceInactive: float = Field(..., description="Неактивный баланс счёта")
    awardAmount: float = Field(
        ..., description="Сколько было выдано бонусов за всю историю"
    )
    salesAmount: float = Field(
        ..., description="Сколько было потрачено бонусов за всю историю"
    )
    burntAmount: float = Field(
        ..., description="Сколько всего сгорело бонусов за всю историю"
    )
    userId: int = Field(..., description="ID пользователя, сощдавшего счёт")
    createDate: datetime = Field(..., description="Дата создания счёта")

    # active, archive
    state: str = Field(..., description="Статус счёта")

    @staticmethod
    def create(
        api: "API",
        state: str = "active",
        award_amount: float = 0.0,
        sales_amount: float = 0.0,
        burnt_amount: float = 0.0,
    ) -> "Account":
        """Создание счёта"""

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

        account = api.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

    @staticmethod
    def find_by_id(
        api: "API",
        account_id: int,
        with_history: bool = False,
        limit: int = 5,
        drop: int = 0,
        sort_field: str = "date",
        sort_order: str = "desc",
    ) -> Union[None, "Account", "AccountWithHistory"]:
        """
        Статический метод поиска карты по ID.
        Возвращает объект Card или None, если карта не найдена.
        """
        if account_id <= 0:
            raise ValueError("account_id is required")

        accountWithHistory = api.request(
            method="GET",
            endpoint=f"account/{account_id}",
            payload_type="accountWithHistory",
            params={
                "limit": limit,
                "drop": drop,
                "sortField": sort_field,
                "sortOrder": sort_order,
            },
            payload_list=False,
        )

        if (
            isinstance(accountWithHistory, AccountWithHistory)
            and accountWithHistory.account["id"]
        ):
            log.info(f"Found account {account_id}")

            if accountWithHistory.history and with_history:
                log.info(f"Found history for account {account_id}")

                return accountWithHistory

            account = Account.parse(api, accountWithHistory.account)

            if account and isinstance(account, Account):
                log.info(f"Found account {account.id}")
                return account
            log.info(f"Account {account_id} not found")
        else:
            log.info(f"Account {account_id} not found")

        return None

    def history(
        self,
        limit: int = 10,
        drop: int = 0,
        sort_field: str = "date",
        sort_order: str = "desc",
    ) -> list["AccountHistory"]:
        if self._api is None:
            raise RuntimeError(
                f"API instance is not set on this {self.__class__.__name__} object"
            )

        raw = self._api.request(
            method="GET",
            endpoint=f"account/{self.id}",
            payload_type="accountWithHistory",
            params={
                "pager.limit": limit,
                "pager.drop": drop,
                "sorting.field": sort_field,
                "sorting.order": sort_order,
            },
            payload_list=False,
        )

        if not raw or not isinstance(raw, AccountWithHistory):
            return []

        parsed_history = []
        for item in raw.history or []:
            if isinstance(item, dict):
                # Создаём объект AccountHistory из словаря
                record_dict = item.get("record", {})
                user_dict = item.get("user", {})

                record = (
                    AccountOperationRecord.parse(self._api, record_dict)
                    if record_dict
                    else None
                )
                user = User.parse(self._api, user_dict) if user_dict else None

                hist_item = AccountHistory()
                hist_item.record = record
                hist_item.user = user
                hist_item._api = self._api

                parsed_history.append(hist_item)
            else:
                # уже объект — оставляем как есть
                parsed_history.append(item)

        return parsed_history

    def attach_to_client(self, client_id: int) -> None:
        """
        Привязка счёта к клиенту.
        """
        if self.id <= 0 or client_id <= 0:
            raise ValueError("Невалидные ID счета или клиента")

        if self._api is None:
            raise RuntimeError(
                f"API instance is not set on this {self.__class__.__name__} object"
            )

        from .parsers import RawParser

        self._api.request(
            method="PUT",
            endpoint=f"client/attach/{client_id}/{self.id}",
            parser=RawParser(),
        )
        log.info(f"Счёт {self.number} (ID {self.id}) привязан к клиенту {client_id}")


class User(Model):
    name: str = Field(..., description="Имя пользователя")
    description: str = Field(..., description="Описание пользователя")


class AccountOperationRecord(Model):
    """
    Описание проводки по счёту
    """

    id: int = Field(..., description="ID проводки")
    accountId: int = Field(..., description="ID счета")
    date: datetime = Field(..., description="Дата проводки")
    amount: float = Field(..., description="Сумма проводки")
    amountRest: float = Field(..., description="Остаток по проводке")
    bonusStartDate: datetime = Field(..., description="Дата начала действия бонусов")
    bonusEndDate: datetime = Field(..., description="Дата окончания действия бонусов")
    balance: float = Field(..., description="Активный баланс счёта после проводки")
    balanceInactive: float = Field(
        ..., description="Неактивный баланс счёта после проводки"
    )
    locationId: int = Field(
        ..., description="ID магазина, в котором была осуществлена проводка"
    )
    campaignId: int = Field(
        ...,
        description="ID кампании, от лица которой была осуществела проводка",
    )
    storageCheckId: int = Field(..., description="ID чека")
    checkNumber: str = Field(..., description="Номер чека")
    userId: int = Field(
        ...,
        description="Пользователь, от лица которого была осуществлена проводка",
    )
    clientId: int = Field(
        ..., description="Клиент, от лица которого была осуществлена проводка"
    )
    segmentId: int = Field(..., description="ID сегмента, в который входил клиент")
    description: str = Field(..., description="Комментарий к проводке")

    # credit - начисление
    # debit - списание
    # burning - сжигание
    # returnCreditActive - возврат начисления на активный счет
    # returnCreditInactive - возврат начисления на неактивный счет (списание с неактивного счета)
    # returnDebit - возврат списания (начисление)
    # transfer - перевод отложенных начислений
    operType: str = Field(..., description="Тип проводки")

    # auto, manual
    recType: str = Field(..., description="Тип проводки")

    # created, confirmed, lockedToTransfer, transferred, lockedToSpread, archived
    status: str = Field(..., description="Статус операции")

    def check(self) -> RawCheckStorage:
        if self._api is None:
            raise RuntimeError(
                f"API instance is not set on this {self.__class__.__name__} object"
            )

        if self.checkNumber is None or self.checkNumber == "":
            raise Exception("Check number is required")

        raw = self._api.request(
            method="GET",
            endpoint=f"check/{self.storageCheckId}",
            payload_type="check",
            payload_list=True,
        )

        if raw is None:
            raise Exception("Check not found")

        if not isinstance(raw, list) or not raw:
            raise ValueError(f"Ожидался список чеков, получено: {type(raw)}")

        first_check_data = raw[0]

        if isinstance(first_check_data, RawCheckStorage):
            return first_check_data

        if isinstance(first_check_data, dict):
            check_obj = RawCheckStorage.parse(self._api, first_check_data)

            if check_obj is None:
                raise ValueError("Не удалось распарсить чек")

            return check_obj

        raise ValueError(f"Неожиданный тип элемента в списке: {type(first_check_data)}")


class AccountHistory(Model):
    record: AccountOperationRecord = Field(..., description="Данные проводки")
    user: User = Field(..., description="Пользователь, осуществивший проводку")

    def __init__(self, record=None, user=None, **kwargs):
        super().__init__(**kwargs)
        if record is None:
            record = AccountOperationRecord()
        self.record = record

        if user is None:
            user = User()
        self.user = user

    def __repr__(self):
        return f"AccountHistory(" f"record={self.record!r}, " f"user={self.user!r}" f")"


class AccountWithHistory(Model):
    """
    Данные счёта с историей транзакций
    """

    account: Account = Field(..., description="Счет")
    clients: list[Client] = Field(..., description="Клиенты, которые владеют счетом")
    user: User = Field(..., description="Пользователь, сощдавший счёт")
    history: list[AccountHistory] = Field(..., description="История счета")
    bindsAccount: list[Account] = Field(
        ..., description="Список объединённых с этим счётом счетов"
    )


class RawClient(Model):
    client: Client = Field(..., description="Данные клиента")
    wallet: Wallet | None = Field(default=None, description="Данные кошелька")
    cards: list[Card] = Field(..., description="Данные карт")
    geo: Geo | None = Field(..., description="Данные геолокации")
    details: Details = Field(..., description="Данные детей")

    @classmethod
    def parse(cls, api, json_data: dict):
        if not isinstance(json_data, dict):
            raise ValueError(f"Expected dict for RawClient, got {type(json_data)}")

        instance = cls(api)

        cls._populate_from_json(instance, json_data)

        # Парсим client
        client_data = json_data.get("client")
        if client_data:
            instance.client = Client.parse(api, client_data)

            # Системные параметры
            system_params_data = client_data.get("systemParams")
            if system_params_data:
                instance.client.SystemParams = SystemParams.parse(
                    api, system_params_data
                )

                # Подписка СБП — может быть null
                sbp_data = system_params_data.get("subscriptionSbp")
                if sbp_data is not None:  # ← ключевое изменение
                    instance.client.SystemParams.subscriptionSbp = (
                        SubscriptionSbp.parse(
                            api,
                            sbp_data,
                        )
                    )
                else:
                    instance.client.SystemParams.subscriptionSbp = (
                        None  # или можно создать пустой объект
                    )
        else:
            instance.client = Client()

        # Парсим cards
        cards_data = json_data.get("cards", [])
        instance.cards = Card.parse_list(api, cards_data)

        return instance

    @classmethod
    def parse_list(cls, api, json_list) -> list:
        results = []

        if isinstance(json_list, dict):
            results.append(cls.parse(api, json_list))
        elif isinstance(json_list, list):
            for item in json_list:
                if item:
                    results.append(cls.parse(api, item))
        else:
            raise ValueError("Expected list or dict")
        return results


class JSONModel:
    @classmethod
    def parse(cls, api, json):
        return json


class ModelFactory:
    location = Location
    locationgroup = LocationGroup
    client = Client
    card = Card
    rawclient = RawClient
    profile = Profile
    account = Account
    accountWithHistory = AccountWithHistory
    check = RawCheckStorage
    pet = ClientPet
    child = ClientChild
    str = JSONModel

    json = JSONModel
