import asyncio
import inspect
import json
import traceback
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import TypeVar, Callable, List, Dict, Type, ParamSpec, Generic, Any, Literal, Union, cast, Awaitable, overload
from urllib.parse import urlparse, parse_qs

from pydantic import BaseModel, ValidationError, TypeAdapter, model_validator
import requests
import urllib3
import httpx
from functools import wraps
import logging
import time

# Отключаем логирование httpx и httpcore, чтобы не засорять вывод и избегать ошибок форматирования
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)

from rule import ResponseValidator, P, ValidateResponse, T, R, HttpResponse

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

from logger_final import logger
from curl_cffi.requests import AsyncSession



#
# # Настройка логгера
#
#
# logger = logging.getLogger(__name__)
# logging.basicConfig(
#     level=logging.INFO,
#     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s -  %(status_code)s - %(function)s'
# )
# 1. Определяем новый числовой уровень
SUCCESS_LEVEL_NUM = 25
logging.addLevelName(SUCCESS_LEVEL_NUM, "SUCCESS")

# 2. Добавляем метод success в класс Logger
def success(self, message, *args, **kws):
    if self.isEnabledFor(SUCCESS_LEVEL_NUM):
        self._log(SUCCESS_LEVEL_NUM, message, args, **kws)

logging.Logger.success = success



def _safe_path_segment(s: str, replace_dot: bool = False) -> str:
    """Безопасный сегмент пути: убираем : / \\ и опционально заменяем точку (для имён папок)."""
    for c in (":", "/", "\\", "\x00"):
        s = s.replace(c, "_")
    if replace_dot:
        s = s.replace(".", "_")
    return s or "unknown"


def _save_validation_error(
    exc: Exception,
    func_name: str,
    response: Any,
    output_dir: str,
) -> None:
    """
    Сохраняет данные об ошибке в JSON по пути:
    output_dir / {date} / {domain} / {function} / {time}-{exc_type}.json
    """
    exc_type = type(exc).__name__
    exc_msg = str(exc)
    if isinstance(exc, ValidationError):
        exc_msg = f"Pydantic validation error: {exc.json()}"

    try:
        full_url = None
        domain = None
        endpoint = None
        request_headers = None
        request_body = None
        response_headers = None
        response_body = None
        status_code = None
        redirect_history: list[str] = []
        cookies: dict[str, str] | None = None

        if response is not None:
            full_url = getattr(response, "url", None)
            if full_url is not None:
                try:
                    parsed = urlparse(str(full_url))
                    domain = parsed.netloc or None
                    endpoint = parsed.path or None
                except Exception:
                    domain = None
                    endpoint = None
            status_code = getattr(response, "status_code", None)
            response_headers = dict(getattr(response, "headers", {})) if getattr(response, "headers", None) else None
            try:
                rt = getattr(response, "text", None)
                response_body = rt if rt is None or isinstance(rt, str) else rt.decode("utf-8", errors="replace")
            except Exception:
                response_body = None
            history = getattr(response, "history", None)
            if history is not None:
                redirect_history = [str(getattr(r, "url", r)) for r in history]
            req = getattr(response, "request", None)
            if req is not None:
                request_headers = dict(getattr(req, "headers", {})) if getattr(req, "headers", None) else None
                cookie_header = None
                if request_headers:
                    cookie_header = request_headers.get("Cookie") or request_headers.get("cookie")
                if cookie_header:
                    cookies = {}
                    for part in cookie_header.split(";"):
                        part = part.strip()
                        if "=" in part:
                            k, v = part.split("=", 1)
                            cookies[k.strip()] = v.strip()
                else:
                    cookies = None
                rb = getattr(req, "body", None)
                if rb is None:
                    request_body = None
                elif isinstance(rb, str):
                    request_body = rb
                else:
                    try:
                        request_body = rb.decode("utf-8", errors="replace") if isinstance(rb, bytes) else str(rb)
                    except Exception:
                        request_body = f"<bytes len={len(rb) if rb else 0}>"
            else:
                request_body = None
                request_headers = None
                cookies = None

        exception_line = f"{exc_type} {exc_msg}".strip()
        domain_seg = _safe_path_segment(domain or "no_domain")
        func_seg = _safe_path_segment(func_name, replace_dot=True)
        now = datetime.now()
        date_str = now.strftime("%Y-%m-%d")
        stem = f"{now.strftime('%H-%M-%S')}-{exc_type}"
        date_exc = f"{stem}.json"
        dir_path = Path(output_dir) / date_str / domain_seg / func_seg
        dir_path.mkdir(parents=True, exist_ok=True)

        payload_request_body: str | dict | list | None = None
        payload_response_body: str | dict | list | None = None

        if request_body is not None:
            if isinstance(request_body, str) and request_body.strip().startswith(("{", "[")):
                try:
                    payload_request_body = json.loads(request_body)
                except (json.JSONDecodeError, TypeError):
                    payload_request_body = request_body
            else:
                payload_request_body = request_body if isinstance(request_body, str) else str(request_body)

        if response_body is not None:
            if isinstance(response_body, str) and response_body.strip().startswith(("{", "[")):
                try:
                    payload_response_body = json.loads(response_body)
                except (json.JSONDecodeError, TypeError):
                    resp_txt_name = f"{stem}.response_body.txt"
                    (dir_path / resp_txt_name).write_text(response_body, encoding="utf-8")
                    payload_response_body = resp_txt_name
            else:
                resp_txt_name = f"{stem}.response_body.txt"
                (dir_path / resp_txt_name).write_text(
                    response_body if isinstance(response_body, str) else str(response_body),
                    encoding="utf-8",
                )
                payload_response_body = resp_txt_name

        payload = {
            "exception_type": exception_line,
            "func": func_name,
            "full_url": full_url,
            "status_code": status_code,
            "request_body": payload_request_body,
            "response_body": payload_response_body,
            "redirect_history": redirect_history,
            "cookies": cookies,
            "request_headers": request_headers,
            "response_headers": response_headers,
        }

        file_path = dir_path / date_exc
        json_str = json.dumps(payload, ensure_ascii=False, indent=2)
        for key in ("exception_type", "func", "status_code", "redirect_history", "cookies", "request_headers", "response_headers"):
            json_str = json_str.replace(f'\n  "{key}"', f'\n\n  "{key}"')
        with open(file_path, "w", encoding="utf-8") as f:
            f.write(json_str)
    except Exception as save_err:
        logger.warning("Не удалось сохранить ошибку в JSON: %s", save_err)


def request_logging(
    save_errors_to_file: bool = True,
    errors_output_dir: str = "output/validation_error",
):
    """
    Декоратор только для логирования: функция, URL, статус, время, при ошибке — тип и текст исключения.
    Поддерживает sync и async функции. При ошибке опционально сохраняет в JSON.
    """
    _log = logging.getLogger(__name__)

    def _log_success(result: Any, elapsed: float, function: str) -> None:
        response = result.response if hasattr(result, "response") else result
        url = getattr(response, "url", "N/A")
        status_code = getattr(response, "status_code", None)
        _log.success(url, extra={"elapsed": elapsed, "status_code": status_code, "function": function})

    def _log_error(e: Exception, elapsed: float, function: str, response: Any) -> None:
        # Пытаемся получить response из исключения, если он не передан явно
        if response is None:
            response = getattr(e, "response", None)
        
        url = getattr(response, "url", "N/A") if response else "N/A"
        status_code = getattr(response, "status_code", None) if response else None
        exc_type = type(e).__name__
        exc_msg = str(e)
        if isinstance(e, ValidationError):
            exc_msg = f"Pydantic validation error: {e.json()}"
        _log.error(f"{url} | {exc_type} - {exc_msg}", extra={"elapsed": elapsed, "status_code": status_code, "function": function})
        if save_errors_to_file:
            _save_validation_error(e, function, response, errors_output_dir)

    def decorator(func: Callable[P, Any]) -> Callable[P, Any]:
        if inspect.iscoroutinefunction(func):
            @wraps(func)
            async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
                start_time = time.time()
                function = func.__qualname__
                try:
                    result = await func(*args, **kwargs)
                    elapsed = time.time() - start_time
                    _log_success(result, elapsed, function)
                    return result
                except Exception as e:
                    elapsed = time.time() - start_time
                    response = getattr(e, "response", None)
                    _log_error(e, elapsed, function, response)
                    raise
            return async_wrapper  # type: ignore[return-value]
        else:
            @wraps(func)
            def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
                start_time = time.time()
                function = func.__qualname__
                try:
                    result = func(*args, **kwargs)
                    elapsed = time.time() - start_time
                    _log_success(result, elapsed, function)
                    return result
                except Exception as e:
                    elapsed = time.time() - start_time
                    response = getattr(e, "response", None)
                    _log_error(e, elapsed, function, response)
                    raise
            return sync_wrapper
    return decorator


@overload
def validate_response(
        model: Type[T] | TypeAdapter | None = None,
        validator: ResponseValidator | None = None,
        body_type: Literal["json", "html"] | None = None,
        raise_exceptions: bool = True,
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[ValidateResponse[T | None, R]]]]: ...

@overload
def validate_response(
        model: Type[T] | TypeAdapter | None = None,
        validator: ResponseValidator | None = None,
        body_type: Literal["json", "html"] | None = None,
        raise_exceptions: bool = True,
) -> Callable[[Callable[P, R]], Callable[P, ValidateResponse[T | None, R]]]: ...

def validate_response(
        model: Type[T] | TypeAdapter | None = None,
        validator: ResponseValidator | None = None,
        body_type: Literal["json", "html"] | None = None,
        raise_exceptions: bool = True,
) -> Any:
    """
    Декоратор только валидации (статус, заголовки, редиректы, тело ответа через модель).
    При переданном model обязательно указать body_type: "json" или "html".
    Логирование — отдельно через @request_logging().
    
    :param raise_exceptions: Если False, исключения валидации будут подавлены, 
                             а объект ошибки будет в result.error.
    """
    adapter = None

    if model is not None:
        if body_type is None or body_type not in ("json", "html"):
            raise TypeError(
                "При указании model необходимо задать body_type: Literal['json', 'html']"
            )
        if isinstance(model, TypeAdapter):
            adapter = model
        else:
            try:
                adapter = TypeAdapter(model)
            except Exception as e:
                raise TypeError(
                    f"Аргумент 'model' должен быть Pydantic моделью, Union или TypeAdapter. "
                    f"Ошибка создания адаптера: {e}"
                )

    def decorator(func: Callable[P, R]):
        # Внутренняя логика обработки ответа (вынесена для переиспользования)
        def process_response(response: R) -> ValidateResponse[T | None, R]:
            # Проверяем интерфейс (runtime check)
            if not isinstance(response, HttpResponse):
                raise TypeError(
                    f"Ошибка возвращаемого значения в '{func.__name__}': "
                    f"Ожидался объект, удовлетворяющий протоколу 'HttpResponse' (ответ requests/httpx), "
                    f"но получен '{type(response).__name__}': {response!r}"
                )

            error = None
            validated_data = None
            
            try:
                if validator:
                    validator._validate_response(response)

                if adapter:
                    body = response.json() if body_type == "json" else response.text
                    validated_data = adapter.validate_python(body)
            
            except Exception as e:
                # Всегда устанавливаем response в исключение для логирования
                if not hasattr(e, "response") or e.response is None:
                    e.response = response
                if raise_exceptions:
                    raise
                error = e

            return ValidateResponse(response=response, validate=validated_data, error=error)

        # ПРОВЕРКА: Асинхронная ли функция?
        if inspect.iscoroutinefunction(func):
            @wraps(func)
            async def async_wrapper(*args: P.args, **kwargs: P.kwargs):
                response = None
                try:
                    response = await func(*args, **kwargs) # Ждем результат
                    return process_response(response)
                except Exception as e:
                    # Убеждаемся, что response всегда установлен в исключении для логирования
                    if not hasattr(e, "response") or e.response is None:
                        e.response = response
                    raise
            return async_wrapper
        else:
            @wraps(func)
            def sync_wrapper(*args: P.args, **kwargs: P.kwargs):
                response = None
                try:
                    response = func(*args, **kwargs)
                    return process_response(response)
                except Exception as e:
                    # Убеждаемся, что response всегда установлен в исключении для логирования
                    if not hasattr(e, "response") or e.response is None:
                        e.response = response
                    raise
            return sync_wrapper

    return decorator





class APIError(Exception):
    """Базовое исключение для любых ошибок бизнес-логики API"""
    pass

class NotFoundError(APIError):
    """Ресурс или сущность не найдены"""
    pass




class AuthError(APIError):
    """Базовое исключение для любых ошибок бизнес-логики API"""

    def __init__(self, message="Произошла ошибка авторизации"):
        self.message = message
        super().__init__(self.message)

class AuthErrorCaptcha(AuthError):
    """Базовое исключение для любых ошибок бизнес-логики API"""

    def __init__(self, message="Требуется авторизация с каптчей"):
        self.message = message
        super().__init__(self.message)

class AuthErrorCreds(AuthError):
    """Базовое исключение для любых ошибок бизнес-логики API"""

    def __init__(self, message="Неверный логин или пароль"):
        self.message = message
        super().__init__(self.message)




# Модель для случая LOGON_FAILED (Успешный HTTP запрос, но ошибка логики)
class LogonFailedResponse(BaseModel):
    code: Literal["LOGON_FAILED", "LOGON_REQUIRED_WITH_CAPTCHA"]
    localizedMessage: str

    @model_validator(mode="after")
    def raise_on_failure(self) -> "LogonFailedResponse":
        if self.code == "LOGON_REQUIRED_WITH_CAPTCHA":
            raise AuthErrorCaptcha()

        elif self.code == "LOGON_FAILED":
            raise AuthErrorCreds()

        # Дефолтная ошибка авторизации
        raise AuthError(self.localizedMessage)

# Модель для SUCCESS
class SuccessResponse(BaseModel):
    code: Literal["SUCCESS"]
    localizedMessage: str = ""

# Объединяющая модель (Union)
# Pydantic посмотрит на поле 'code' и выберет нужную модель
RzdAuthResponse = Union[SuccessResponse, LogonFailedResponse]



proxies = {
    "http": "http://127.0.0.1:8080",
    "https": "http://127.0.0.1:8080",
}
# 1. Редирект на logonErr = ошибка авторизации (raise_as=AuthError)



class RequestRZD():




    @request_logging()
    @validate_response(
        validator=ResponseValidator().status_code().whitelist([200])
    )
    async def check_ip_async(self):
        """Асинхронная проверка IP."""
        async with httpx.AsyncClient(verify=False) as client:
            return await client.get("https://httpbin.org/ip")
            
    @request_logging()
    @validate_response(
        model=RzdAuthResponse,
        body_type="json",
        validator=ResponseValidator()
        .status_code().whitelist([200])

    )
    def login_rzd(self, super:int):
        data = {
            'j_username': 'richard.sanchez2963',
            'j_password': '11111111aA1',
        }
        headers = {
            'Host': 'www.rzd.ru',
            # 'Content-Length': '52',
            'Sec-Ch-Ua-Platform': '"macOS"',
            'X-Requested-With': 'XMLHttpRequest',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
            'Accept': '*/*',
            'Sec-Ch-Ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"',
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'Sec-Ch-Ua-Mobile': '?0',
            'Origin': 'https://www.rzd.ru',
            'Sec-Fetch-Site': 'same-origin',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Dest': 'empty',
            'Referer': 'https://www.rzd.ru/',
            # 'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
            'Priority': 'u=1, i',
            'Connection': 'keep-alive',
            # 'Cookie': 'session-cookie=189244027cff348f97c8aabc6940ac72a7d1ba4fbf1e2a5357326a73c4569ec55f549c34f74d336810778f8e686aeead; _ym_uid=1770552389554787225; _ym_d=1770552389; _ym_visorc=b; accessible=false; lang=ru; _ym_isad=2; _at_uid=2e54b3f7-1889-4563-8565-0e6d03eb1875; _at_session_id=9b8a4992-cae4-4e1c-baa2-f85f74369697; acceptCookies=1; WASReqURL=https:///selfcare/successLogon/ru/json; ClientUid=bylxP1NpyaKphQV15bkqIfuXtLpqTcOz; AuthFlag=false; JSESSIONID=00001VC5dzzOS8XpbTrszvRvZbp:17obqbi3l',
        }

        cookies = {
            'session-cookie': '189244027cff348f97c8aabc6940ac72a7d1ba4fbf1e2a5357326a73c4569ec55f549c34f74d336810778f8e686aeead',
            '_ym_uid': '1770552389554787225',
            '_ym_d': '1770552389',
            '_ym_visorc': 'b',
            'accessible': 'false',
            'lang': 'ru',
            '_ym_isad': '2',
            '_at_uid': '2e54b3f7-1889-4563-8565-0e6d03eb1875',
            '_at_session_id': '9b8a4992-cae4-4e1c-baa2-f85f74369697',
            'acceptCookies': '1',
            'WASReqURL': 'https:///selfcare/successLogon/ru/json',
            'ClientUid': 'bylxP1NpyaKphQV15bkqIfuXtLpqTcOz',
            'AuthFlag': 'false',
            'JSESSIONID': '00001VC5dzzOS8XpbTrszvRvZbp:17obqbi3l',
        }


        # with httpx.Client(verify=False, proxy="http://127.0.0.1:8080", follow_redirects=True) as client:
        #     res =  client.post(
        #         'https://www.rzd.ru/selfcare/j_security_check/json',
        #         headers=headers,
        #         data=data,
        #         cookies=cookies,
        #     )
        #     return res

        response = requests.post(
            'https://www.rzd.ru/selfcare/j_security_check/json',
            headers=headers,
            data=data,
            cookies=cookies,
            verify=False,
            proxies=proxies,
            allow_redirects=True,
        )
        return response

    @request_logging()
    @validate_response(
        model=RzdAuthResponse,
        body_type="json",
        validator=ResponseValidator()
        .status_code().whitelist([200])

    )
    async def login_rzd_async(self, super: int):
        """Асинхронный метод: внутри используется sync httpx (запрос выполняется в потоке)."""
        data = {
            'j_username': 'richard.sanchez2963',
            'j_password': '11111111aA1',
        }
        headers = {
            'Host': 'www.rzd.ru',
            'Sec-Ch-Ua-Platform': '"macOS"',
            'X-Requested-With': 'XMLHttpRequest',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
            'Accept': '*/*',
            'Sec-Ch-Ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"',
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'Sec-Ch-Ua-Mobile': '?0',
            'Origin': 'https://www.rzd.ru',
            'Sec-Fetch-Site': 'same-origin',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Dest': 'empty',
            'Referer': 'https://www.rzd.ru/',
            'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
            'Priority': 'u=1, i',
            'Connection': 'keep-alive',
        }
        cookies = {
            'session-cookie': '189244027cff348f97c8aabc6940ac72a7d1ba4fbf1e2a5357326a73c4569ec55f549c34f74d336810778f8e686aeead',
            '_ym_uid': '1770552389554787225',
            'lang': 'ru',
            'acceptCookies': '1',
            'JSESSIONID': '00001VC5dzzOS8XpbTrszvRvZbp:17obqbi3l',
        }



        async with AsyncSession(verify=False, proxy="http://127.0.0.1:8080", allow_redirects=True) as client:
            res = await client.post(
                'https://www.rzd.ru/selfcare/j_security_check/json',
                headers=headers,
                data=data,
                cookies=cookies,
            )

            return res








async def main() -> None:

    rzd = RequestRZD()
    proxy = "http://127.0.0.1:8080"

    client_settings = httpx.AsyncClient(verify=False, proxy=proxy, follow_redirects=True)
    async with client_settings as client:
        pass


    try:
        # Проверка IP
        ip_res = await rzd.check_ip_async()
        if ip_res.validate:
            print(f"ASYNC IP: {ip_res.validate.origin}")


        # Асинхронный метод (внутри sync httpx в потоке)
        result = await rzd.login_rzd_async(1)


        # print("Async result status:", result.status_code, result.url)

    except Exception as e:
        logger.error(f"Неожиданная ошибка: {type(e).__name__}: {e}")


if __name__ == "__main__":
    asyncio.run(main())
