"""Bitrix24 webhook request helpers."""

import copy
import time
from typing import Any
from urllib.parse import urlsplit

import requests

from .config import get_config, get_default_webhook_url
from .constants import (
    ERR_BITRIX_API,
    ERR_HTTP_STATUS,
    ERR_INVALID_JSON,
    ERR_INVALID_WEBHOOK_OR_METHOD,
    ERR_MISSING_METHOD,
    ERR_MISSING_WEBHOOK_URL,
    ERR_TIMEOUT,
    ERR_TRANSPORT,
    ERR_UNEXPECTED,
    ERR_UNKNOWN,
    ERROR_CODE_INVALID_JSON,
    ERROR_CODE_INVALID_WEBHOOK_OR_METHOD,
    ERROR_CODE_MISSING_METHOD,
    ERROR_CODE_MISSING_WEBHOOK_URL,
    ERROR_CODE_TIMEOUT,
    ERROR_CODE_UNKNOWN,
    ERROR_TYPE_BITRIX,
    ERROR_TYPE_CONFIGURATION,
    ERROR_TYPE_HTTP,
    ERROR_TYPE_INVALID_JSON,
    ERROR_TYPE_TIMEOUT,
    ERROR_TYPE_TRANSPORT,
    ERROR_TYPE_UNEXPECTED,
    LOG_API_ERROR,
    LOG_HTTP_ERROR,
    LOG_INVALID_JSON,
    LOG_REQUEST_COMPLETED,
    LOG_REQUEST_SKIPPED_MISSING_METHOD,
    LOG_REQUEST_SKIPPED_REASON,
    LOG_REQUEST_STARTED,
    LOG_TIMEOUT,
    LOG_TRANSPORT_ERROR,
    LOG_UNEXPECTED_ERROR,
    MAX_LOG_BODY_LENGTH,
    RETRY_STATUSES,
    )
from .logging_utils import auto_configure_logging

logger = auto_configure_logging()

_SENSITIVE_KEY_PARTS = (
    "phone",
    "email",
    "mail",
    "password",
    "token",
    "secret",
    "auth",
    "webhook",
    "cookie",
    "session",
    "userpic",
    )


def _build_method_url(webhook_url: str, method: str) -> str:
    url = (webhook_url or "").strip()
    method_name = (method or "").strip()
    if not url or not method_name:
        return ""
    if url.endswith("?"):
        url = url[:-1]
    if method_name in url and url.endswith(".json"):
        return url
    if url.endswith("/"):
        return f"{url}{method_name}.json"
    return f"{url}/{method_name}.json"


def _build_method_url_v3(webhook_url: str, method: str) -> str:
    url = (webhook_url or "").strip()
    method_name = (method or "").strip()
    if not url or not method_name:
        return ""
    if url.endswith("?"):
        url = url[:-1]
    if method_name in url and not url.endswith("/"):
        return url
    if "/rest/api/" not in url:
        url = url.replace("/rest/", "/rest/api/", 1)
    if url.endswith("/"):
        return f"{url}{method_name}"
    return f"{url}/{method_name}"


def _sanitize_portal(webhook_url: str) -> str:
    url = (webhook_url or "").strip()
    if not url:
        return ""
    parts = urlsplit(url)
    if parts.netloc:
        return parts.netloc
    if "/rest/" in url:
        return url.split("/rest/")[0].rstrip("/").split("//")[-1]
    return url.rstrip("/").split("//")[-1]


def _safe_payload_preview(parameters: dict[str, Any] | None) -> dict[str, Any] | None:
    if not parameters:
        return None
    return _sanitize_for_log(parameters)


def _is_sensitive_key(key: str) -> bool:
    lowered = (key or "").strip().lower()
    return any(part in lowered for part in _SENSITIVE_KEY_PARTS)


def _sanitize_scalar_for_log(value: Any) -> Any:
    if isinstance(value, str):
        if len(value) > 300:
            return _truncate_text(value)
        return value
    return value


def _sanitize_for_log(value: Any, parent_key: str = "") -> Any:
    if _is_sensitive_key(parent_key):
        return "***"

    if isinstance(value, dict):
        return {
            str(key): _sanitize_for_log(item, str(key))
            for key, item in value.items()
            }

    if isinstance(value, list):
        return [_sanitize_for_log(item, parent_key) for item in value]

    if isinstance(value, tuple):
        return tuple(_sanitize_for_log(item, parent_key) for item in value)

    return _sanitize_scalar_for_log(value)


def _extract_items_key(result_dict: dict[str, Any], items_key: str | None = None) -> str | None:
    if items_key:
        return items_key
    if isinstance(result_dict, dict):
        for key, value in result_dict.items():
            if isinstance(value, list):
                return key
    return None


def _extract_next(response_data: dict[str, Any]) -> Any:
    for key in ("next", "next_id", "nextId"):
        if isinstance(response_data, dict) and key in response_data:
            return response_data.get(key)

    result = response_data.get("result") if isinstance(response_data, dict) else None
    if isinstance(result, dict):
        for key in ("next", "next_id", "nextId"):
            if key in result:
                return result.get(key)
    return None


def _truncate_text(value: str) -> str:
    text = (value or "").strip()
    if len(text) <= MAX_LOG_BODY_LENGTH:
        return text
    return text[:MAX_LOG_BODY_LENGTH] + "..."


def _error_dict(
        *,
        error_type: str,
        error_code: str,
        error_message: str,
        method: str,
        webhook_url: str,
        status_code: int | None = None,
        details: Any | None = None,
        ) -> dict[str, Any]:
    result: dict[str, Any] = {
        "error_result": True,
        "error_type": error_type,
        "error_code": error_code,
        "error_message": error_message,
        "method": method,
        "portal": _sanitize_portal(webhook_url),
        }
    if status_code is not None:
        result["status_code"] = status_code
    if details is not None:
        result["details"] = details
    return result


def request24(
        method: str,
        parameters: dict[str, Any] | None = None,
        webhook_url: str | None = None,
        *,
        all_iters: bool = False,
        timeout: int | None = None,
        ) -> dict[str, Any]:
    """Perform a Bitrix24 webhook request and always return a dict."""

    config = get_config()
    resolved_method = (method or "").strip()
    resolved_webhook = (webhook_url or "").strip() or get_default_webhook_url()
    resolved_timeout = timeout if timeout is not None else config.default_timeout
    full_url = _build_method_url(resolved_webhook, resolved_method)

    if not resolved_method:
        result = _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_MISSING_METHOD,
            error_message=ERR_MISSING_METHOD,
            method=resolved_method,
            webhook_url=resolved_webhook,
            )
        logger.warning(LOG_REQUEST_SKIPPED_MISSING_METHOD)
        return result

    if not resolved_webhook:
        result = _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_MISSING_WEBHOOK_URL,
            error_message=ERR_MISSING_WEBHOOK_URL,
            method=resolved_method,
            webhook_url=resolved_webhook,
            )
        logger.warning(LOG_REQUEST_SKIPPED_REASON, resolved_method, "missing_webhook_url")
        return result

    if not full_url:
        result = _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_INVALID_WEBHOOK_OR_METHOD,
            error_message=ERR_INVALID_WEBHOOK_OR_METHOD,
            method=resolved_method,
            webhook_url=resolved_webhook,
            )
        logger.warning(LOG_REQUEST_SKIPPED_REASON, resolved_method, "invalid_webhook_or_method")
        return result

    if all_iters:
        return _request24_all_iters(
            method=resolved_method,
            parameters=parameters,
            webhook_url=resolved_webhook,
            timeout=resolved_timeout,
            )

    return _request24_single(
        method=resolved_method,
        parameters=parameters,
        webhook_url=resolved_webhook,
        timeout=resolved_timeout,
        )


def req24(
        method: str,
        parameters: dict[str, Any] | None = None,
        webhook_url: str | None = None,
        *,
        all_iters: bool = False,
        timeout: int | None = None,
        ) -> dict[str, Any]:
    """Perform a Bitrix24 REST v3 webhook request and always return a dict."""

    config = get_config()
    resolved_method = (method or "").strip()
    resolved_webhook = (webhook_url or "").strip() or get_default_webhook_url()
    resolved_timeout = timeout if timeout is not None else config.default_timeout
    full_url = _build_method_url_v3(resolved_webhook, resolved_method)

    if not resolved_method:
        result = _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_MISSING_METHOD,
            error_message=ERR_MISSING_METHOD,
            method=resolved_method,
            webhook_url=resolved_webhook,
            )
        logger.warning(LOG_REQUEST_SKIPPED_MISSING_METHOD)
        return result

    if not resolved_webhook:
        result = _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_MISSING_WEBHOOK_URL,
            error_message=ERR_MISSING_WEBHOOK_URL,
            method=resolved_method,
            webhook_url=resolved_webhook,
            )
        logger.warning(LOG_REQUEST_SKIPPED_REASON, resolved_method, "missing_webhook_url")
        return result

    if not full_url:
        result = _error_dict(
            error_type=ERROR_TYPE_CONFIGURATION,
            error_code=ERROR_CODE_INVALID_WEBHOOK_OR_METHOD,
            error_message=ERR_INVALID_WEBHOOK_OR_METHOD,
            method=resolved_method,
            webhook_url=resolved_webhook,
            )
        logger.warning(LOG_REQUEST_SKIPPED_REASON, resolved_method, "invalid_webhook_or_method")
        return result

    if all_iters:
        return _request24_all_iters_v3(
            method=resolved_method,
            parameters=parameters,
            webhook_url=resolved_webhook,
            timeout=resolved_timeout,
            )

    return _request24_single_v3(
        method=resolved_method,
        parameters=parameters,
        webhook_url=resolved_webhook,
        timeout=resolved_timeout,
        )


def _request24_single(
        *,
        method: str,
        parameters: dict[str, Any] | None,
        webhook_url: str,
        timeout: int,
        ) -> dict[str, Any]:
    payload_preview = _safe_payload_preview(parameters)
    full_url = _build_method_url(webhook_url, method)
    config = get_config()
    attempt_count = max(1, config.retry_count + 1)
    last_error: dict[str, Any] | None = None

    for attempt in range(1, attempt_count + 1):
        try:
            logger.debug(
                LOG_REQUEST_STARTED,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                timeout,
                payload_preview,
                )

            if parameters:
                response = requests.post(full_url, json=parameters, timeout=timeout)
            else:
                response = requests.post(full_url, timeout=timeout)

            if response.status_code == 200:
                try:
                    response_data = response.json()
                except ValueError:
                    last_error = _error_dict(
                        error_type=ERROR_TYPE_INVALID_JSON,
                        error_code=ERROR_CODE_INVALID_JSON,
                        error_message=ERR_INVALID_JSON,
                        method=method,
                        webhook_url=webhook_url,
                        status_code=response.status_code,
                        details=_truncate_text(response.text),
                        )
                    logger.error(
                        LOG_INVALID_JSON,
                        method,
                        _sanitize_portal(webhook_url),
                        )
                    return last_error

                if isinstance(response_data, dict) and response_data.get("error"):
                    last_error = {
                        **response_data,
                        **_error_dict(
                            error_type=ERROR_TYPE_BITRIX,
                            error_code=str(response_data.get("error")),
                            error_message=str(
                                response_data.get("error_description") or ERR_BITRIX_API
                                ),
                            method=method,
                            webhook_url=webhook_url,
                            status_code=response.status_code,
                            ),
                        }
                    logger.warning(
                        LOG_API_ERROR,
                        method,
                        _sanitize_portal(webhook_url),
                        response_data.get("error"),
                        )
                    return last_error

                logger.debug(
                    LOG_REQUEST_COMPLETED,
                    method,
                    _sanitize_portal(webhook_url),
                    )
                if isinstance(response_data, dict):
                    return response_data
                return {"result": response_data}

            details: Any
            try:
                details = response.json()
            except ValueError:
                details = _truncate_text(response.text)

            last_error = _error_dict(
                error_type=ERROR_TYPE_HTTP,
                error_code=str(response.status_code),
                error_message=ERR_HTTP_STATUS.format(status_code=response.status_code),
                method=method,
                webhook_url=webhook_url,
                status_code=response.status_code,
                details=details,
                )
            logger.error(
                LOG_HTTP_ERROR,
                method,
                _sanitize_portal(webhook_url),
                response.status_code,
                attempt,
                payload_preview,
                )

            if response.status_code in RETRY_STATUSES and attempt < attempt_count:
                time.sleep(config.retry_delay)
                continue
            return last_error

        except requests.Timeout as exc:
            last_error = _error_dict(
                error_type=ERROR_TYPE_TIMEOUT,
                error_code=ERROR_CODE_TIMEOUT,
                error_message=ERR_TIMEOUT,
                method=method,
                webhook_url=webhook_url,
                details=str(exc),
                )
            logger.error(
                LOG_TIMEOUT,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                )
        except requests.RequestException as exc:
            last_error = _error_dict(
                error_type=ERROR_TYPE_TRANSPORT,
                error_code=exc.__class__.__name__,
                error_message=ERR_TRANSPORT,
                method=method,
                webhook_url=webhook_url,
                details=str(exc),
                )
            logger.error(
                LOG_TRANSPORT_ERROR,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                exc,
                )
        except Exception as exc:
            last_error = _error_dict(
                error_type=ERROR_TYPE_UNEXPECTED,
                error_code=exc.__class__.__name__,
                error_message=ERR_UNEXPECTED,
                method=method,
                webhook_url=webhook_url,
                details=str(exc),
                )
            logger.exception(
                LOG_UNEXPECTED_ERROR,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                )

        if attempt < attempt_count:
            time.sleep(config.retry_delay)

    return last_error or _error_dict(
        error_type=ERROR_TYPE_UNEXPECTED,
        error_code=ERROR_CODE_UNKNOWN,
        error_message=ERR_UNKNOWN,
        method=method,
        webhook_url=webhook_url,
        )


def _request24_single_v3(
        *,
        method: str,
        parameters: dict[str, Any] | None,
        webhook_url: str,
        timeout: int,
        ) -> dict[str, Any]:
    payload_preview = _safe_payload_preview(parameters)
    full_url = _build_method_url_v3(webhook_url, method)
    config = get_config()
    attempt_count = max(1, config.retry_count + 1)
    last_error: dict[str, Any] | None = None
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
        }

    for attempt in range(1, attempt_count + 1):
        try:
            logger.debug(
                LOG_REQUEST_STARTED,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                timeout,
                payload_preview,
                )

            if parameters:
                response = requests.post(full_url, json=parameters, headers=headers, timeout=timeout)
            else:
                response = requests.get(full_url, headers=headers, timeout=timeout)

            if response.status_code == 200:
                try:
                    response_data = response.json()
                except ValueError:
                    last_error = _error_dict(
                        error_type=ERROR_TYPE_INVALID_JSON,
                        error_code=ERROR_CODE_INVALID_JSON,
                        error_message=ERR_INVALID_JSON,
                        method=method,
                        webhook_url=webhook_url,
                        status_code=response.status_code,
                        details=_truncate_text(response.text),
                        )
                    logger.error(
                        LOG_INVALID_JSON,
                        method,
                        _sanitize_portal(webhook_url),
                        )
                    return last_error

                if isinstance(response_data, dict) and response_data.get("error"):
                    error_data = response_data.get("error")
                    error_code = ""
                    error_message = ERR_BITRIX_API
                    if isinstance(error_data, dict):
                        error_code = str(error_data.get("code") or "")
                        error_message = str(error_data.get("message") or ERR_BITRIX_API)
                    else:
                        error_code = str(error_data)

                    last_error = {
                        **response_data,
                        **_error_dict(
                            error_type=ERROR_TYPE_BITRIX,
                            error_code=error_code,
                            error_message=error_message,
                            method=method,
                            webhook_url=webhook_url,
                            status_code=response.status_code,
                            ),
                        }
                    logger.warning(
                        LOG_API_ERROR,
                        method,
                        _sanitize_portal(webhook_url),
                        error_code,
                        )
                    return last_error

                logger.debug(
                    LOG_REQUEST_COMPLETED,
                    method,
                    _sanitize_portal(webhook_url),
                    )
                if isinstance(response_data, dict):
                    return response_data
                return {"result": response_data}

            details: Any
            try:
                details = response.json()
            except ValueError:
                details = _truncate_text(response.text)

            last_error = _error_dict(
                error_type=ERROR_TYPE_HTTP,
                error_code=str(response.status_code),
                error_message=ERR_HTTP_STATUS.format(status_code=response.status_code),
                method=method,
                webhook_url=webhook_url,
                status_code=response.status_code,
                details=details,
                )
            logger.error(
                LOG_HTTP_ERROR,
                method,
                _sanitize_portal(webhook_url),
                response.status_code,
                attempt,
                payload_preview,
                )

            if response.status_code in RETRY_STATUSES and attempt < attempt_count:
                time.sleep(config.retry_delay)
                continue
            return last_error

        except requests.Timeout as exc:
            last_error = _error_dict(
                error_type=ERROR_TYPE_TIMEOUT,
                error_code=ERROR_CODE_TIMEOUT,
                error_message=ERR_TIMEOUT,
                method=method,
                webhook_url=webhook_url,
                details=str(exc),
                )
            logger.error(
                LOG_TIMEOUT,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                )
        except requests.RequestException as exc:
            last_error = _error_dict(
                error_type=ERROR_TYPE_TRANSPORT,
                error_code=exc.__class__.__name__,
                error_message=ERR_TRANSPORT,
                method=method,
                webhook_url=webhook_url,
                details=str(exc),
                )
            logger.error(
                LOG_TRANSPORT_ERROR,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                exc,
                )
        except Exception as exc:
            last_error = _error_dict(
                error_type=ERROR_TYPE_UNEXPECTED,
                error_code=exc.__class__.__name__,
                error_message=ERR_UNEXPECTED,
                method=method,
                webhook_url=webhook_url,
                details=str(exc),
                )
            logger.exception(
                LOG_UNEXPECTED_ERROR,
                method,
                _sanitize_portal(webhook_url),
                attempt,
                )

        if attempt < attempt_count:
            time.sleep(config.retry_delay)

    return last_error or _error_dict(
        error_type=ERROR_TYPE_UNEXPECTED,
        error_code=ERROR_CODE_UNKNOWN,
        error_message=ERR_UNKNOWN,
        method=method,
        webhook_url=webhook_url,
        )


def _request24_all_iters(
        *,
        method: str,
        parameters: dict[str, Any] | None,
        webhook_url: str,
        timeout: int,
        ) -> dict[str, Any]:
    config = get_config()
    params = copy.deepcopy(parameters) if parameters else {}
    start = params.get("start", 0)
    page_size = 50
    all_items: list[Any] = []
    last_response: dict[str, Any] | None = None
    items_key: str | None = None
    result_is_list = False

    while True:
        if start is not None:
            params["start"] = start

        response = _request24_single(
            method=method,
            parameters=params,
            webhook_url=webhook_url,
            timeout=timeout,
            )
        last_response = response

        if "error_result" in response:
            return response

        result = response.get("result")
        if isinstance(result, list):
            result_is_list = True
            all_items.extend(result)
        elif isinstance(result, dict):
            items_key = _extract_items_key(result, items_key)
            if not items_key:
                return response

            items = result.get(items_key, [])
            if not isinstance(items, list):
                return response
            all_items.extend(items)
        else:
            return response

        next_value = _extract_next(response)
        if next_value is not None:
            start = next_value
            if config.request_delay > 0:
                time.sleep(config.request_delay)
            continue

        total_value = response.get("total")
        if isinstance(total_value, int) and len(all_items) < total_value:
            start = (start or 0) + page_size
            if config.request_delay > 0:
                time.sleep(config.request_delay)
            continue

        break

    final_response = dict(last_response) if isinstance(last_response, dict) else {"result": {}}
    if result_is_list:
        final_response["result"] = all_items
    else:
        final_result = dict(final_response.get("result", {}))
        if items_key:
            final_result[items_key] = all_items
        final_result.pop("next", None)
        final_result.pop("next_id", None)
        final_result.pop("nextId", None)
        final_response["result"] = final_result

    final_response["total"] = len(all_items)
    final_response.pop("next", None)
    final_response.pop("next_id", None)
    final_response.pop("nextId", None)
    return final_response


def _request24_all_iters_v3(
        *,
        method: str,
        parameters: dict[str, Any] | None,
        webhook_url: str,
        timeout: int,
        ) -> dict[str, Any]:
    config = get_config()
    params = copy.deepcopy(parameters) if parameters else {}
    start = params.get("start", 0)
    page_size = 50
    all_items: list[Any] = []
    last_response: dict[str, Any] | None = None
    items_key: str | None = None
    result_is_list = False

    while True:
        if start is not None:
            params["start"] = start

        response = _request24_single_v3(
            method=method,
            parameters=params,
            webhook_url=webhook_url,
            timeout=timeout,
            )
        last_response = response

        if "error_result" in response:
            return response

        result = response.get("result")
        if isinstance(result, list):
            result_is_list = True
            all_items.extend(result)
        elif isinstance(result, dict):
            items_key = _extract_items_key(result, items_key)
            if not items_key:
                return response

            items = result.get(items_key, [])
            if not isinstance(items, list):
                return response
            all_items.extend(items)
        else:
            return response

        next_value = _extract_next(response)
        if next_value is not None:
            start = next_value
            if config.request_delay > 0:
                time.sleep(config.request_delay)
            continue

        total_value = response.get("total")
        if isinstance(total_value, int) and len(all_items) < total_value:
            start = (start or 0) + page_size
            if config.request_delay > 0:
                time.sleep(config.request_delay)
            continue

        break

    final_response = dict(last_response) if isinstance(last_response, dict) else {"result": {}}
    if result_is_list:
        final_response["result"] = all_items
    else:
        final_result = dict(final_response.get("result", {}))
        if items_key:
            final_result[items_key] = all_items
        final_result.pop("next", None)
        final_result.pop("next_id", None)
        final_result.pop("nextId", None)
        final_response["result"] = final_result

    final_response["total"] = len(all_items)
    final_response.pop("next", None)
    final_response.pop("next_id", None)
    final_response.pop("nextId", None)
    return final_response
