# -*- coding: utf-8 -*-
"""
Декораторы для HTTP-запросов: validate_response (валидация ответа и тела по Pydantic),
request_logging (логирование и опциональная запись ошибок в JSON-файл).
Используется только стандартный logging.
"""
import inspect
import json
import logging
import time
from datetime import datetime
from pathlib import Path
from typing import TypeVar, Callable, Type, Any, Literal, Awaitable, overload
from urllib.parse import urlparse
from functools import wraps

from pydantic import BaseModel, ValidationError, TypeAdapter

from .types import HttpResponse, ValidateResponse, T, R, P
from .validators import ResponseValidator

_log = logging.getLogger(__name__)


def _attach_response(exc: Exception, response: Any) -> None:
    """Безопасно прикрепляет response к исключению (пропускает __slots__ без 'response')."""
    try:
        if not hasattr(exc, "response") or getattr(exc, "response", None) is None:
            exc.response = response  # type: ignore[attr-defined]
    except (AttributeError, TypeError):
        pass


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
        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:
            _raw_url = getattr(response, "url", None)
            full_url = str(_raw_url) if _raw_url is not None else None
            if full_url is not None:
                try:
                    parsed = urlparse(full_url)
                    domain = parsed.netloc or None
                except Exception:
                    domain = None
            status_code = getattr(response, "status_code", None)
            _raw_headers = getattr(response, "headers", None)
            response_headers = {str(k): str(v) for k, v in dict(_raw_headers).items()} if _raw_headers 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:
                _raw_req_headers = getattr(req, "headers", None)
                request_headers = {str(k): str(v) for k, v in dict(_raw_req_headers).items()} if _raw_req_headers 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

        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": f"{exc_type} {exc_msg}".strip(),
            "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:
        _log.warning("Не удалось сохранить ошибку в JSON: %s", save_err)


def request_logging(
    save_errors_to_file: bool = True,
    errors_output_dir: str = "output/validation_error",
) -> Callable[[Callable[P, Any]], Callable[P, Any]]:
    """
    Декоратор логирования: URL, статус, время выполнения; при ошибке — тип и текст исключения.
    Поддерживает sync и async. При ошибке опционально сохраняет данные в JSON-файл.
    Использует стандартный logging (логгер request_pydantic_decorator.decorators).
    """
    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.info(
            "%s | %s | %.2fс",
            str(url),
            status_code if status_code is not None else "—",
            elapsed,
            extra={"function": function},
        )

    def _log_error(e: Exception, elapsed: float, function: str, response: Any) -> None:
        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(
            "%s | %s | %.2fс | %s - %s",
            str(url),
            status_code if status_code is not None else "—",
            elapsed,
            exc_type,
            exc_msg,
            extra={"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:
    """
    Декоратор валидации ответа: статус, заголовки, редиректы, тело по Pydantic-модели.
    При указании 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]) -> Any:
        def process_response(response: R) -> ValidateResponse[T | None, R]:
            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:
                _attach_response(e, 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) -> Any:
                response = None
                try:
                    response = await func(*args, **kwargs)
                    return process_response(response)
                except Exception as e:
                    _attach_response(e, response)
                    raise
            return async_wrapper
        else:
            @wraps(func)
            def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
                response = None
                try:
                    response = func(*args, **kwargs)
                    return process_response(response)
                except Exception as e:
                    _attach_response(e, response)
                    raise
            return sync_wrapper
    return decorator
